[
  {
    "path": ".github/FUNDING.yml",
    "content": "# Supported platforms for funding\nopen_collective: n1_loader\ngithub: djezzzl\n"
  },
  {
    "path": ".github/copilot-instructions.md",
    "content": "# Copilot Instructions\n\nEvery PR must have no errors from:\n\n```\nbundle exec rubocop\nbundle exec rspec spec/n1_loader_spec.rb\nbundle exec rspec spec/n1_loader_spec.rb spec/activerecord_spec.rb\nbundle exec rspec spec/n1_loader_spec.rb spec/activerecord_spec.rb spec/ar_lazy_preload_spec.rb\n```\n"
  },
  {
    "path": ".github/workflows/copilot-setup-steps.yml",
    "content": "name: Copilot Setup Steps\n\n# Automatically run the setup steps when they are changed to allow for easy validation, and\n# allow manual testing through the repository's \"Actions\" tab\non:\n  workflow_dispatch:\n  push:\n    paths:\n      - .github/workflows/copilot-setup-steps.yml\n  pull_request:\n    paths:\n      - .github/workflows/copilot-setup-steps.yml\n      \njobs:\n  copilot-setup-steps:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n\n    env:\n      ACTIVERECORD_GEMFILE: 'ar_8_latest'\n      AR_LAZY_PRELOAD_GEMFILE: 'ar_lazy_preload_master'\n\n    steps:\n      - uses: actions/checkout@v3\n\n      - name: Set up Ruby\n        uses: ruby/setup-ruby@v1\n        with:\n          ruby-version: head\n\n      - name: Install dependencies\n        run: bundle install\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\n\non:\n  workflow_dispatch:\n    inputs:\n      version:\n        description: 'Version to release (e.g. 2.1.4)'\n        required: true\n\njobs:\n  release:\n    runs-on: ubuntu-latest\n    if: github.actor == 'djezzzl'\n    permissions:\n      contents: write\n\n    steps:\n      - uses: actions/checkout@v3\n\n      - name: Set up Ruby\n        uses: ruby/setup-ruby@v1\n        with:\n          ruby-version: '3.2'\n          bundler-cache: true\n\n      - name: Install dependencies\n        run: bundle install\n\n      - name: Validate version format\n        run: |\n          if ! echo \"${{ github.event.inputs.version }}\" | grep -qE '^[0-9]+\\.[0-9]+\\.[0-9]+$'; then\n            echo \"Invalid version format: ${{ github.event.inputs.version }}. Expected semver (e.g. 1.2.3)\"\n            exit 1\n          fi\n\n      - name: Bump version\n        run: |\n          sed -i 's/VERSION = \".*\"/VERSION = \"${{ github.event.inputs.version }}\"/' lib/n1_loader/version.rb\n\n      - name: Update changelog\n        env:\n          VERSION: ${{ github.event.inputs.version }}\n        run: |\n          DATE=$(date +%Y/%m/%d)\n          sed -i.bak -e \"s|^## \\[Unreleased\\]|## [$VERSION] - $DATE|\" CHANGELOG.md\n          rm -f CHANGELOG.md.bak\n\n      - name: Git status\n        run: git status\n\n      - name: Commit version bump\n        run: |\n          git config user.name \"Evgenii Demin\"\n          git config user.email \"lawliet.djez@gmail.com\"\n          git add lib/n1_loader/version.rb CHANGELOG.md\n          git commit -m \"Release v${{ github.event.inputs.version }}\"\n          git push origin HEAD\n\n      - name: Release gem\n        env:\n          GEM_HOST_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }}\n        run: bundle exec rake release\n"
  },
  {
    "path": ".github/workflows/rubocop.yml",
    "content": "name: Rubocop\n\non:\n  pull_request:\n  schedule:\n    - cron: '0 0 * * 0'\n\njobs:\n  rubocop:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v3\n\n      - name: Set up Ruby\n        uses: ruby/setup-ruby@v1\n        with:\n          ruby-version: 2.7\n\n      - name: Install dependencies\n        run: bundle install\n\n      - name: Run Rubocop\n        run: bundle exec rubocop\n"
  },
  {
    "path": ".github/workflows/tests.yml",
    "content": "name: RSpec tests\n\non:\n  pull_request:\n  schedule:\n    - cron: '0 0 * * 0'\n\njobs:\n  tests:\n    runs-on: ubuntu-latest\n    continue-on-error: true\n    strategy:\n      matrix:\n        include:\n          - ruby-version: '2.7'\n            activerecord-gemfile: 'ar_5_latest'\n            ar_lazy_preload-gemfile: 'ar_lazy_preload_0.6.1'\n\n          - ruby-version: '2.7'\n            activerecord-gemfile: 'ar_6_latest'\n            ar_lazy_preload-gemfile: 'ar_lazy_preload_0.6.1'\n\n          - ruby-version: '3.0'\n            activerecord-gemfile: 'ar_6_latest'\n            ar_lazy_preload-gemfile: 'ar_lazy_preload_0.6.1'\n\n          - ruby-version: '3.0'\n            activerecord-gemfile: 'ar_7_latest'\n            ar_lazy_preload-gemfile: 'ar_lazy_preload_master'\n\n          - ruby-version: 'head'\n            activerecord-gemfile: 'ar_7_latest'\n            ar_lazy_preload-gemfile: 'ar_lazy_preload_master'\n\n          - ruby-version: 'head'\n            activerecord-gemfile: 'ar_8_latest'\n            ar_lazy_preload-gemfile: 'ar_lazy_preload_master'\n    env:\n      ACTIVERECORD_GEMFILE: ${{ matrix.activerecord-gemfile }}\n      AR_LAZY_PRELOAD_GEMFILE: ${{ matrix.ar_lazy_preload-gemfile }}\n\n    steps:\n      - uses: actions/checkout@v3\n\n      - name: Set up Ruby ${{ matrix.ruby-version }}\n        uses: ruby/setup-ruby@v1\n        with:\n          ruby-version: ${{ matrix.ruby-version }}\n\n      - name: Install dependencies\n        run: bundle install\n\n      - name: Run Core tests\n        run: bundle exec rspec spec/n1_loader_spec.rb\n\n      - name: Run ActiveRecord tests\n        run: bundle exec rspec spec/n1_loader_spec.rb spec/activerecord_spec.rb\n\n      - name: Run ArLazyPreload tests\n        run: bundle exec rspec spec/n1_loader_spec.rb spec/activerecord_spec.rb spec/ar_lazy_preload_spec.rb\n\n      - name: Run Goldiloader tests\n        run: bundle exec rspec spec/n1_loader_spec.rb spec/activerecord_spec.rb spec/ar_lazy_preload_spec.rb\n"
  },
  {
    "path": ".gitignore",
    "content": "/.bundle/\n/.yardoc\n/_yardoc/\n/coverage/\n/doc/\n/pkg/\n/spec/reports/\n/tmp/\n/.idea\n.rspec_status\nGemfile.lock\n.ruby-version\n/vendor\n"
  },
  {
    "path": ".rspec",
    "content": "--format documentation\n--color\n--require spec_helper\n"
  },
  {
    "path": ".rubocop.yml",
    "content": "AllCops:\n  TargetRubyVersion: 2.5\n  Exclude:\n    - examples/**/*\n    - vendor/bundle/**/*\n\nStyle/StringLiterals:\n  Enabled: true\n  EnforcedStyle: double_quotes\n\nStyle/StringLiteralsInInterpolation:\n  Enabled: true\n  EnforcedStyle: double_quotes\n\nLayout/LineLength:\n  Max: 120\n\nMetrics/BlockLength:\n  Exclude:\n    - spec/**/*\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "## [3.0.0] - 2026/03/18\n\n- Add support of [Goldiloader](https://github.com/KentaaNL/goldiloader)\n\n## [2.2.1] - 2026/03/17\n\n- Fix a rare thread-safety issue for setting context \n- Raise errors if `n1_bind_to` received unexpected arguments\n- Fix context propagation to loaded objects when using `n1_bind_to`\n\n## [2.2.0] - 2026/03/16\n\n- Support both identity and equality comparison in `Loader#for`. Identity lookup (via `object_id`) is tried first, with equality lookup as a fallback. Thanks [Alfonso Uceda](https://github.com/AlfonsoUceda) for reporting the issue!\n- Clear binding when `n1_clear_cache` is called, so objects load independently after a cache reset.\n\n## [2.1.0] - 2026/03/07\n\n- Add `n1_bind_to` to support context sharing for plain Ruby objects without ActiveRecord, enabling N+1-free batch loading. Nested loading through N1Loader automatically propagates the shared context. Thanks [Pawel Pacana](https://github.com/paneq) for the feature request!\n\n## [2.0.1] - 2025/12/28\n\n- Add support of Rails 8.\n\n## [2.0.0] - 2025/12/28\n\n- Make loader thread-safe. \n- Make loader idempotent.\n\n## [1.7.4] - 2023/12/18\n\n- Fix nested association does not preload properly. Thanks [dannyongtey](https://github.com/dannyongtey) for reporting and fixing the issue!\n\n## [1.7.3] - 2023/08/04\n\n- Decrease the package size by 60%. \n\n## [1.7.2] - 2023/08/04\n\n- Refactor core that ended up with speed boost.\n\n## [1.7.1] - 2023/07/30\n\n- Fix interface discrepancy for `N1LoaderReflection`. Thanks [Denis Talakevich](https://github.com/senid231) for suggesting it!\n\n## [1.7.0] - 2023/07/30\n\nExtend the flexibility of loading data comparison. Thanks [Nazar Matus](https://github.com/FunkyloverOne) for suggesting it! \n\n**BREAKING CHANGES:**\n\nLoose comparison of loaded data. Before loaded data was initialized with identity comparator in mind:\n\n```ruby\n@loaded = {}.compare_by_identity\n```\n\nNow it will be:\n\n```ruby\n@loaded = {}\n```\n\nThis might bring unwanted results for cases when strict comparison was wanted. \n\nOn the other hand, it gives more flexibility for many other cases, especially with isolated loader.\nFor example, this will work now, when it wasn't working before.\n\n```ruby\n# ActiveRecord object\nobject = Entity.first\n\n# Initialize isolated loader\ninstance = loader.new([object])\n\n# This was working before because the loaded object is identical to passed object by `#object_id`\ninstance.for(object)\n\n# This wasn't working before because the loaded object is not identical to passed one by `#object_id`\n# \n# But it will be working now, because object == Entity.find(object.id)\ninstance.for(Entity.find(object.id))\n```\n\nIf you need strict comparison support, please feel free to open the issue or the PR.\n\n## [1.6.6] - 2023/07/30\n\n- Fix naive check of required arguments. Thanks [Nazar Matus](https://github.com/FunkyloverOne) for the issue!\n\n## [1.6.5] - 2023/07/30\n\n- Fix nested preloading for ActiveRecord 7. Thanks [Igor Gonchar](https://github.com/gigorok) for the issue!\n\n## [1.6.4] - 2023/07/30\n\n- Add support of `n1_optimized` ending with `?` (question mark). Thanks [Ilya Kamenko](https://github.com/Galathius) for the suggestion!\n\n## [1.6.3] - 2022/12/30\n\n- Performance optimization: avoid unnecessary calls. Thanks [Nazar Matus](https://github.com/FunkyloverOne) for the [contribution](https://github.com/djezzzl/n1_loader/pull/33).\n\n## [1.6.2] - 2022/11/23\n\n- Add fund metadata\n\n## [1.6.1] - 2022/10/29\n\n- Fix ArLazyPreload context setup when using isolated loaders for objects without the context.\n\n## [1.6.0] - 2022/10/24\n\n- Add support of ArLazyPreload context for isolated loaders.\n\n## [1.5.1] - 2022/09/20\n\n- Fix support of falsey value of arguments. Thanks [Aitor Lopez Beltran](https://github.com/aitorlb) for the [contribution](https://github.com/djezzzl/n1_loader/pull/23)!\n\n## [1.5.0] - 2022/05/01\n\n- Add support of Rails 7\n\n## [1.4.4] - 2022/04/29\n\n- Inject `N1Loader::Loadable` to `ActiveRecord::Base` automatically\n- Make `reload` to call `n1_clear_cache`\n\n## [1.4.3] - 2022-04-13\n\n- Add `default` support to arguments\n\n## [1.4.2] - 2022-03-01\n\n- Add n1_clear_cache method which is useful for cases like reload in ActiveRecord\n\n## [1.4.1] - 2022-02-24\n\n- Fix preloading of invalid objects\n\n## [1.4.0] - 2022-02-22\n\n- add support of optional arguments\n\nBREAKING CHANGES:\n- rework arguments to use single definition through `argument <name>` only\n- use keyword arguments\n\n## [1.3.0] - 2022-02-22\n\n- add support of named arguments with `argument <name>`\n\nBREAKING CHANGES:\n- rename `n1_load` to `n1_optimized`\n- rework `def self.arguments_key` to `cache_key`\n\n## [1.2.0] - 2022-01-14\n\n- Introduce arguments support.\n\n## [1.1.0] - 2021-12-27\n\n- Introduce `fulfill` method to abstract the storage.\n\n## [1.0.0] - 2021-12-26\n\n- Various of great features.\n\n## [0.1.0] - 2021-12-16\n\n- Initial release.\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.\n\nWe pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.\n\n## Our Standards\n\nExamples of behavior that contributes to a positive environment for our community include:\n\n* Demonstrating empathy and kindness toward other people\n* Being respectful of differing opinions, viewpoints, and experiences\n* Giving and gracefully accepting constructive feedback\n* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience\n* Focusing on what is best not just for us as individuals, but for the overall community\n\nExamples of unacceptable behavior include:\n\n* The use of sexualized language or imagery, and sexual attention or\n  advances of any kind\n* Trolling, insulting or derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or email\n  address, without their explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Enforcement Responsibilities\n\nCommunity leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.\n\nCommunity leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.\n\n## Scope\n\nThis Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at TODO: Write your email address. All complaints will be reviewed and investigated promptly and fairly.\n\nAll community leaders are obligated to respect the privacy and security of the reporter of any incident.\n\n## Enforcement Guidelines\n\nCommunity leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:\n\n### 1. Correction\n\n**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.\n\n**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.\n\n### 2. Warning\n\n**Community Impact**: A violation through a single incident or series of actions.\n\n**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.\n\n### 3. Temporary Ban\n\n**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.\n\n**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.\n\n### 4. Permanent Ban\n\n**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior,  harassment of an individual, or aggression toward or disparagement of classes of individuals.\n\n**Consequence**: A permanent ban from any sort of public interaction within the community.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,\navailable at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.\n\nCommunity Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see the FAQ at\nhttps://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.\n"
  },
  {
    "path": "Gemfile",
    "content": "# frozen_string_literal: true\n\nsource \"https://rubygems.org\"\n\n# Specify your gem's dependencies in n1_loader.gemspec\ngemspec\n\n# Hack to make Github work with Circle CI job names with slashes\ngemfiles = []\ngemfiles << \"activerecord-gemfiles/#{ENV[\"ACTIVERECORD_GEMFILE\"]}.gemfile\" if ENV[\"ACTIVERECORD_GEMFILE\"]\ngemfiles << \"ar_lazy_preload-gemfiles/#{ENV[\"AR_LAZY_PRELOAD_GEMFILE\"]}.gemfile\" if ENV[\"AR_LAZY_PRELOAD_GEMFILE\"]\ngemfiles << \"goldiloader-gemfiles/#{ENV[\"GOLDILOADER_GEMFILE\"]}.gemfile\" if ENV[\"GOLDILOADER_GEMFILE\"]\n\ngemfiles.each do |path|\n  eval(File.read(path)) # rubocop:disable Security/Eval\nend\n"
  },
  {
    "path": "LICENSE.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2021 TODO: Write your name\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# N1Loader\n\n[![Gem Version][3]][4]\n[![][11]][12]\n[![][13]][14]\n[![][9]][10]\n\nN1Loader is designed to provide a simple way for avoiding [N+1 issues][7] of any kind. \nFor example, it can help with resolving N+1 for:\n- database querying (most common case)\n- 3rd party service calls\n- complex calculations\n- and many more\n\n> If the project helps you or your organization, I would be very grateful if you [contribute][15] or [donate][10].  \n> Your support is an incredible motivation and the biggest reward for my hard work.\n\n___Support:___ ActiveRecord 5, 6, 7, and 8.\n\nFollow me and stay tuned for the updates:\n- [LinkedIn](https://www.linkedin.com/in/evgeniydemin/)\n- [Medium](https://evgeniydemin.medium.com/)\n- [Twitter](https://twitter.com/EvgeniyDemin/)\n- [GitHub](https://github.com/djezzzl)\n\n## Killer feature for GraphQL API\n\nN1Loader in combination with [ArLazyPreload][6] or [Goldiloader][16] is a killer feature for your GraphQL API. \nGive it a try now and see incredible results instantly! Check out the [example](examples/graphql.rb) and start benefiting from it in your projects!\n\n```ruby\ngem 'n1_loader', require: 'n1_loader/ar_lazy_preload'\n# or\ngem 'n1_loader', require: 'n1_loader/goldiloader'\n```\n\n## Enhance [ActiveRecord][5]\n\nAre you working with well-known Rails application? Try it out and see how well N1Loader fulfills missing gaps when you can't define ActiveRecord associations!\nCheck out the detailed [guide](guides/enhanced-activerecord.md) with examples or its [short version](examples/active_record_integration.rb).\n\n```ruby\ngem 'n1_loader', require: 'n1_loader/active_record'\n```\n\nAre you ready to forget about N+1 once and for all? Install [ArLazyPreload][6] or [Goldiloader][16] and see dreams come true!\n\n```ruby\ngem 'n1_loader', require: 'n1_loader/ar_lazy_preload'\n# or\ngem 'n1_loader', require: 'n1_loader/goldiloader'\n```\n\n## Standalone mode\n\nAre you not working with [ActiveRecord][5]? N1Loader is ready to be used as standalone solution! ([full snippet](examples/core.rb))\n\n```ruby\ngem 'n1_loader'\n```\n\nWant lazy, N+1-free loading without explicit preloading? Use `n1_bind_to` to share context across a collection of plain Ruby objects. ([full snippet](examples/n1_bind_to.rb))\n\n```ruby\nusers = [User.new, User.new, User.new]\n\n# Bind users to the collection — lazy access is now automatically batched\nusers.each { |user| user.n1_bind_to(users) }\n\nusers.map(&:optimized_call) # loads all in a single batch, no N+1\n```\n\n## How to use it?\n\nN1Loader provides DSL that allows you to define N+1 ready loaders that can \nbe injected into your objects in a way that you can avoid N+1 issues.\n\n> _Disclaimer_: examples below are working but designed to show N1Loader potentials only.\nIn real live applications, N1Loader can be applied anywhere and in more [elegant way](examples/isolated_loader.rb).  \n\nLet's look at simple example below ([full snippet](examples/active_record_integration.rb)):\n```ruby\nclass User < ActiveRecord::Base\n  has_many :payments\n\n  n1_optimized :payments_total do |users|\n    total_per_user = \n      Payment.group(:user_id)\n        .where(user: users)\n        .sum(:amount)\n        .tap { |h| h.default = 0 }\n\n    users.each do |user|\n      total = total_per_user[user.id]\n      fulfill(user, total)\n    end\n  end\nend\n\nclass Payment < ActiveRecord::Base\n  belongs_to :user\n\n  validates :amount, presence: true\nend\n\n# A user has many payments. \n# Assuming, we want to know for group of users, what is a total of their payments, we can do the following:\n\n# Has N+1 issue\np User.all.map { |user| user.payments.sum(&:amount) }\n\n# Has no N+1 but we load too many data that we don't actually need\np User.all.includes(:payments).map { |user| user.payments.sum(&:amount) }\n\n# Has no N+1 and we load only what we need\np User.all.includes(:payments_total).map { |user| user.payments_total }\n```\n\nLet's assume now, that we want to calculate the total of payments for the given period for a group of users. \nN1Loader can do that as well! ([full snippet](examples/arguments_support.rb)) \n\n```ruby\nclass User < ActiveRecord::Base\n  has_many :payments\n\n  n1_optimized :payments_total do\n    argument :from\n    argument :to\n\n    def perform(users)\n      total_per_user =\n        Payment\n          .group(:user_id)\n          .where(created_at: from..to)\n          .where(user: users)\n          .sum(:amount)\n          .tap { |h| h.default = 0 }\n\n      users.each do |user|\n        total = total_per_user[user.id]\n        fulfill(user, total)\n      end\n    end\n  end\nend\n\nclass Payment < ActiveRecord::Base\n  belongs_to :user\n\n  validates :amount, presence: true\nend\n\n# Has N+1\np User.all.map { |user| user.payments.select { |payment| payment.created_at >= from && payment.created_at <= to }.sum(&:amount) }\n\n# Has no N+1 but we load too many data that we don't need\np User.all.includes(:payments).map { |user| user.payments.select { |payment| payment.created_at >= from && payment.created_at <= to }.sum(&:amount) }\n\n# Has no N+1 and calculation is the most efficient\np User.all.includes(:payments_total).map { |user| user.payments_total(from: from, to: to) }\n```\n\n## Features and benefits\n\n- N1Loader doesn't use Promises which means it's easy to debug\n- Doesn't require injection to objects, can be used in [isolation](examples/isolated_loader.rb)\n- Loads data [lazily](examples/lazy_loading.rb)\n- Loaders can be [shared](examples/shared_loader.rb) between multiple classes\n- Loaded data can be [re-fetched](examples/reloading.rb)\n- Loader can be optimized for [single cases](examples/single_case.rb)\n- Loader support [arguments](examples/arguments_support.rb)\n- Has [integration](examples/active_record_integration.rb) with [ActiveRecord][5] which makes it brilliant\n- Has [integration](examples/ar_lazy_integration.rb) with [ArLazyPreload][6] which makes it excellent\n- Has [integration](examples/goldiloader_integration.rb) with [Goldiloader][16] which makes it excellent\n- Supports [context sharing](examples/n1_bind_to.rb) for plain Ruby objects without ActiveRecord\n\n### Feature killer for [ArLazyPreload][6] and [Goldiloader][16] integration with isolated loaders\n\nIn [version 1.6.0](CHANGELOG.md#160---20221019) isolated loaders were integrated with [ArLazyPreload][6] context.\nThis means, it isn't required to inject `N1Loader` into your [ActiveRecord][5] models to avoid N+1 issues out of the box.\nIt is especially great as many engineers are trying to avoid extra coupling between their models/services when it's possible.\nAnd this feature was designed exactly for this without losing an out of a box solution for N+1.\n\nWithout further ado, please have a look at the [example](examples/ar_lazy_integration_with_isolated_loader.rb) for ArLazyPreload or the [example](examples/goldiloader_integration_with_isolated_loader.rb) for Goldiloader.\n\n_Spoiler:_ as soon as you have your loader defined, it will be as simple as `Loader.for(element)` to get your data efficiently and without N+1.\n\n### Context sharing for plain Ruby objects with `n1_bind_to`\n\nIn [version 2.1.0](CHANGELOG.md#210---20260307) context sharing was added for plain Ruby objects.\nThis allows you to get lazy, N+1-free loading without [ActiveRecord][5] or explicit preloading.\n\nBy calling `n1_bind_to(collection)` on each element, you bind them to their shared collection.\nWhen any loader is triggered on a bound element, it automatically batch-loads for the entire collection — and nested loaders propagate the context automatically as well.\n\nHave a look at the [example](examples/n1_bind_to.rb) to see how simple it is.\n\n## Funding\n\n### Open Collective Backers\n\nYou're an individual who wants to support the project with a monthly donation. Your logo will be available on the Github page. [[Become a backer](https://opencollective.com/n1_loader#backer)]\n\n<a href=\"https://opencollective.com/n1_loader/backer/0/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/backer/0/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/backer/1/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/backer/1/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/backer/2/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/backer/2/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/backer/3/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/backer/3/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/backer/4/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/backer/4/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/backer/5/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/backer/5/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/backer/6/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/backer/6/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/backer/7/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/backer/7/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/backer/8/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/backer/8/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/backer/9/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/backer/9/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/backer/10/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/backer/10/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/backer/11/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/backer/11/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/backer/12/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/backer/12/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/backer/13/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/backer/13/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/backer/14/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/backer/14/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/backer/15/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/backer/15/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/backer/16/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/backer/16/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/backer/17/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/backer/17/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/backer/18/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/backer/18/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/backer/19/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/backer/19/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/backer/20/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/backer/20/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/backer/21/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/backer/21/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/backer/22/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/backer/22/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/backer/23/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/backer/23/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/backer/24/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/backer/24/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/backer/25/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/backer/25/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/backer/26/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/backer/26/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/backer/27/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/backer/27/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/backer/28/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/backer/28/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/backer/29/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/backer/29/avatar.svg\"></a>\n\n### Open Collective Sponsors\n\nYou're an organization that wants to support the project with a monthly donation. Your logo will be available on the Github page. [[Become a sponsor](https://opencollective.com/n1_loader#sponsor)]\n\n<a href=\"https://opencollective.com/n1_loader/sponsor/0/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/sponsor/0/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/sponsor/1/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/sponsor/1/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/sponsor/2/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/sponsor/2/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/sponsor/3/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/sponsor/3/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/sponsor/4/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/sponsor/4/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/sponsor/5/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/sponsor/5/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/sponsor/6/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/sponsor/6/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/sponsor/7/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/sponsor/7/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/sponsor/8/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/sponsor/8/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/sponsor/9/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/sponsor/9/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/sponsor/10/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/sponsor/10/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/sponsor/11/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/sponsor/11/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/sponsor/12/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/sponsor/12/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/sponsor/13/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/sponsor/13/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/sponsor/14/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/sponsor/14/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/sponsor/15/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/sponsor/15/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/sponsor/16/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/sponsor/16/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/sponsor/17/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/sponsor/17/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/sponsor/18/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/sponsor/18/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/sponsor/19/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/sponsor/19/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/sponsor/20/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/sponsor/20/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/sponsor/21/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/sponsor/21/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/sponsor/22/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/sponsor/22/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/sponsor/23/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/sponsor/23/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/sponsor/24/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/sponsor/24/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/sponsor/25/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/sponsor/25/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/sponsor/26/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/sponsor/26/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/sponsor/27/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/sponsor/27/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/sponsor/28/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/sponsor/28/avatar.svg\"></a>\n<a href=\"https://opencollective.com/n1_loader/sponsor/29/website\" target=\"_blank\"><img src=\"https://opencollective.com/n1_loader/sponsor/29/avatar.svg\"></a>\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at https://github.com/djezzzl/n1_loader. \nThis project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](CODE_OF_CONDUCT.md).\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).\n\n## Code of Conduct\n\nEveryone interacting in the N1Loader project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](CODE_OF_CONDUCT.md).\n\n## Changelog\n\n*N1Loader*'s changelog is available [here](CHANGELOG.md).\n\n## Copyright\n\nCopyright (c) Evgeniy Demin. See [LICENSE.txt](LICENSE.txt) for further details.\n\n[3]: https://badge.fury.io/rb/n1_loader.svg\n[4]: https://badge.fury.io/rb/n1_loader\n[5]: https://github.com/rails/rails/tree/main/activerecord\n[6]: https://github.com/DmitryTsepelev/ar_lazy_preload\n[7]: https://stackoverflow.com/questions/97197/what-is-the-n1-selects-problem-in-orm-object-relational-mapping\n[8]: https://github.com/djezzzl/n1_loader\n[9]: https://opencollective.com/n1_loader/tiers/badge.svg\n[10]: https://opencollective.com/n1_loader#support\n[11]: https://github.com/djezzzl/n1_loader/actions/workflows/tests.yml/badge.svg?branch=master\n[12]: https://github.com/djezzzl/n1_loader/actions/workflows/tests.yml?query=event%3Aschedule\n[13]: https://github.com/djezzzl/n1_loader/actions/workflows/rubocop.yml/badge.svg?branch=master\n[14]: https://github.com/djezzzl/n1_loader/actions/workflows/rubocop.yml?query=event%3Aschedule\n[15]: https://github.com/djezzzl/n1_loader#contributing\n[16]: https://github.com/KentaaNL/goldiloader\n"
  },
  {
    "path": "Rakefile",
    "content": "# frozen_string_literal: true\n\nrequire \"bundler/gem_tasks\"\nrequire \"rspec/core/rake_task\"\n\nRSpec::Core::RakeTask.new(:spec)\n\nrequire \"rubocop/rake_task\"\n\nRuboCop::RakeTask.new\n\ntask default: %i[spec rubocop]\n"
  },
  {
    "path": "activerecord-gemfiles/ar_5_latest.gemfile",
    "content": "# frozen_string_literal: true\n\ngem \"activerecord\", \"~> 5\"\ngem \"concurrent-ruby\", \"= 1.3.4\"\n"
  },
  {
    "path": "activerecord-gemfiles/ar_6_latest.gemfile",
    "content": "# frozen_string_literal: true\n\ngem \"activerecord\", \"~> 6\"\ngem \"benchmark\"\ngem \"bigdecimal\"\ngem \"concurrent-ruby\", \"= 1.3.4\"\ngem \"sqlite3\", \"~> 1.4\"\n"
  },
  {
    "path": "activerecord-gemfiles/ar_7_latest.gemfile",
    "content": "# frozen_string_literal: true\n\ngem \"activerecord\", \"~> 7\"\ngem \"benchmark\"\ngem \"bigdecimal\"\ngem \"concurrent-ruby\", \"= 1.3.4\"\ngem \"sqlite3\", \"< 2.4.0\"\n"
  },
  {
    "path": "activerecord-gemfiles/ar_8_latest.gemfile",
    "content": "# frozen_string_literal: true\n\ngem \"activerecord\", \"~> 8\"\ngem \"benchmark\"\ngem \"bigdecimal\"\ngem \"sqlite3\", \"< 2.4.0\"\n"
  },
  {
    "path": "ar_lazy_preload-gemfiles/ar_lazy_preload_0.6.1.gemfile",
    "content": "# frozen_string_literal: true\n\ngem \"ar_lazy_preload\", \"= 0.6.1\"\n"
  },
  {
    "path": "ar_lazy_preload-gemfiles/ar_lazy_preload_master.gemfile",
    "content": "# frozen_string_literal: true\n\ngem \"ar_lazy_preload\", git: \"https://github.com/DmitryTsepelev/ar_lazy_preload\", branch: \"master\"\n"
  },
  {
    "path": "bin/console",
    "content": "#!/usr/bin/env ruby\n# frozen_string_literal: true\n\nrequire \"bundler/setup\"\nrequire \"n1_loader\"\n\n# You can add fixtures and/or initialization code here to make experimenting\n# with your gem easier. You can also use a different console, if you like.\n\n# (If you use this, don't forget to add pry to your Gemfile!)\n# require \"pry\"\n# Pry.start\n\nrequire \"irb\"\nIRB.start(__FILE__)\n"
  },
  {
    "path": "bin/setup",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\nIFS=$'\\n\\t'\nset -vx\n\nbundle install\n\n# Do any other automated setup that you need to do here\n"
  },
  {
    "path": "examples/active_record_integration.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"n1_loader/active_record\"\n\nrequire_relative 'context/setup_database'\n\nclass User < ActiveRecord::Base\n  has_many :payments\n\n  n1_optimized :payments_total do |users|\n    total_per_user = Payment.group(:user_id).where(user: users).sum(:amount).tap { |h| h.default = 0 }\n\n    users.each do |user|\n      total = total_per_user[user.id]\n      fulfill(user, total)\n    end\n  end\nend\n\nclass Payment < ActiveRecord::Base\n  belongs_to :user\n\n  validates :amount, presence: true\nend\n\nfill_database\n\n# Has N+1\np User.all.map { |user| user.payments.sum(&:amount) }\n# Has no N+1 but we load too many data that we don't need\np User.all.includes(:payments).map { |user| user.payments.sum(&:amount) }\n# Has no N+1 and calculation is the most efficient\np User.all.includes(:payments_total).map(&:payments_total)\n"
  },
  {
    "path": "examples/ar_lazy_integration.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"n1_loader/ar_lazy_preload\"\n\nrequire_relative 'context/setup_ar_lazy'\nrequire_relative 'context/setup_database'\n\nclass User < ActiveRecord::Base\n  has_many :payments\n\n  n1_optimized :payments_total do |users|\n    total_per_user = Payment.group(:user_id).where(user: users).sum(:amount).tap { |h| h.default = 0 }\n\n    users.each do |user|\n      total = total_per_user[user.id]\n      fulfill(user, total)\n    end\n  end\nend\n\nclass Payment < ActiveRecord::Base\n  belongs_to :user\n\n  validates :amount, presence: true\nend\n\nfill_database\n\n# Has N+1\np User.all.map { |user| user.payments.sum(&:amount) }\n\n# Has no N+1 and loads only required data\np User.preload_associations_lazily.map(&:payments_total)\n# or\nArLazyPreload.config.auto_preload = true\nUser.all.map(&:payments_total)\n"
  },
  {
    "path": "examples/ar_lazy_integration_with_isolated_loader.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"n1_loader/ar_lazy_preload\"\n\nrequire_relative 'context/setup_ar_lazy'\nrequire_relative 'context/setup_database'\n\nclass Loader < N1Loader::Loader\n  def perform(users)\n    total_per_user = Payment.group(:user_id).where(user: users).sum(:amount).tap { |h| h.default = 0 }\n\n    users.each do |user|\n      total = total_per_user[user.id]\n      fulfill(user, total)\n    end\n  end\nend\n\nclass User < ActiveRecord::Base\n  has_many :payments\nend\n\nclass Payment < ActiveRecord::Base\n  belongs_to :user\n\n  validates :amount, presence: true\nend\n\nfill_database\n\n# Has N+1 and loads redundant data\np User.all.map { |user| user.payments.sum(&:amount) }\n\n# Has no N+1 and loads only required data\np User.preload_associations_lazily.all.map { |user| Loader.for(user) }\n\n# or\nArLazyPreload.config.auto_preload = true\np User.all.map { |user| Loader.for(user) }\n"
  },
  {
    "path": "examples/arguments_support.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"n1_loader/active_record\"\n\nrequire_relative 'context/setup_database'\n\nclass User < ActiveRecord::Base\n  has_many :payments\n\n  n1_optimized :payments_total do\n    # Arguments can be:\n    # argument :something, optional: true\n    # argument :something, default: -> { 100 }\n    #\n    # Note: do not use mutable (mostly timing related) defaults like:\n    # argument :from, default -> { 2.minutes.from_now }\n    # because such values will be unique for every loader call which will make N+1 issue stay\n    argument :from\n    argument :to\n\n    # This is used to define logic how loaders are compared to each other\n    # default is:\n    # cache_key { *arguments.map(&:object_id) }\n    cache_key { [from, to] }\n\n    def perform(users)\n      total_per_user =\n        Payment\n        .group(:user_id)\n        .where(created_at: from..to)\n        .where(user: users)\n        .sum(:amount)\n        .tap { |h| h.default = 0 }\n\n      users.each do |user|\n        total = total_per_user[user.id]\n        fulfill(user, total)\n      end\n    end\n  end\nend\n\nclass Payment < ActiveRecord::Base\n  belongs_to :user\n\n  validates :amount, presence: true\nend\n\nfill_database\n\nfrom = 2.days.ago\nto = 1.day.ago\n\n# Has N+1\np User.all.map { |user|\n  user.payments.select do |payment|\n    payment.created_at >= from && payment.created_at <= to\n  end.sum(&:amount)\n}\n# Has no N+1 but we load too many data that we don't need\np User.all.includes(:payments).map { |user|\n  user.payments.select do |payment|\n    payment.created_at >= from && payment.created_at <= to\n  end.sum(&:amount)\n}\n# Has no N+1 and calculation is the most efficient\np User.all.includes(:payments_total).map { |user| user.payments_total(from: from, to: to) }\n"
  },
  {
    "path": "examples/context/service.rb",
    "content": "# 3rd party service, or database, or anything else that can perform in batches\nclass Service\n  def self.count\n    @count ||= 0\n  end\n\n  def self.increase!\n    @count = (@count || 0) + 1\n  end\n\n  def self.receive(*users)\n    increase!\n\n    users.flatten.map(&:object_id)\n  end\n\n  def self.single(user)\n    user.object_id\n  end\nend"
  },
  {
    "path": "examples/context/setup_ar_lazy.rb",
    "content": "ActiveSupport.on_load(:active_record) do\n  ActiveRecord::Base.include(ArLazyPreload::Base)\n\n  ActiveRecord::Relation.prepend(ArLazyPreload::Relation)\n  ActiveRecord::AssociationRelation.prepend(ArLazyPreload::AssociationRelation)\n  ActiveRecord::Relation::Merger.prepend(ArLazyPreload::Merger)\n\n  [\n    ActiveRecord::Associations::CollectionAssociation,\n    ActiveRecord::Associations::Association\n  ].each { |klass| klass.prepend(ArLazyPreload::Association) }\n\n  ActiveRecord::Associations::CollectionAssociation.prepend(ArLazyPreload::CollectionAssociation)\n  ActiveRecord::Associations::CollectionProxy.prepend(ArLazyPreload::CollectionProxy)\nend"
  },
  {
    "path": "examples/context/setup_database.rb",
    "content": "require \"sqlite3\"\n\nActiveRecord::Base.establish_connection(adapter: \"sqlite3\", database: \":memory:\")\nActiveRecord::Base.connection.tables.each do |table|\n  ActiveRecord::Base.connection.drop_table(table, force: :cascade)\nend\nActiveRecord::Schema.verbose = false\nActiveRecord::Base.logger = Logger.new($stdout)\n\nActiveRecord::Schema.define(version: 1) do\n  create_table(:payments) do |t|\n    t.belongs_to :user\n    t.integer :amount\n    t.timestamps\n  end\n  create_table(:users)\nend\n\ndef fill_database\n  10.times do\n    user = User.create!\n    10.times do\n      Payment.create!(user: user, amount: rand(1000))\n    end\n  end\nend"
  },
  {
    "path": "examples/core.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"n1_loader\"\n\nrequire_relative 'context/service'\n\n# Class that wants to request 3rd party service without N+1\nclass User\n  include N1Loader::Loadable\n\n  def unoptimized_call\n    Service.receive(self)[0]\n  end\n\n  n1_optimized :optimized_call do |users|\n    data = Service.receive(users)\n\n    users.each_with_index do |user, index|\n      fulfill(user, data[index])\n    end\n  end\nend\n\n# works fine for single case\nuser = User.new\np \"Works correctly: #{user.unoptimized_call == user.optimized_call}\"\n\nusers = [User.new, User.new]\n\n# Has N+1\ncount_before = Service.count\np users.map(&:unoptimized_call)\np \"Has N+1 #{Service.count == count_before + users.count}\"\n\n# Has no N+1 via explicit preloading\ncount_before = Service.count\nN1Loader::Preloader.new(users).preload(:optimized_call)\np users.map(&:optimized_call)\np \"Has no N+1: #{Service.count == count_before + 1}\"\n\nusers = [User.new, User.new]\n\n# Has no N+1 via n1_bind_to context sharing (see examples/n1_bind_to.rb)\nusers.each { |user| user.n1_bind_to(users) }\ncount_before = Service.count\np users.map(&:optimized_call)\np \"Has no N+1: #{Service.count == count_before + 1}\"\n"
  },
  {
    "path": "examples/goldiloader_integration.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"n1_loader/goldiloader\"\n\nrequire_relative \"context/setup_database\"\n\nclass User < ActiveRecord::Base\n  has_many :payments\n\n  n1_optimized :payments_total do |users|\n    total_per_user = Payment.group(:user_id).where(user: users).sum(:amount).tap { |h| h.default = 0 }\n\n    users.each do |user|\n      total = total_per_user[user.id]\n      fulfill(user, total)\n    end\n  end\nend\n\nclass Payment < ActiveRecord::Base\n  belongs_to :user\n\n  validates :amount, presence: true\nend\n\nfill_database\n\n# Has N+1\np(User.all.map { |user| user.payments.sum(&:amount) })\n\n# Has no N+1 and loads only required data (Goldiloader auto-batches by default)\np User.all.map(&:payments_total)\n"
  },
  {
    "path": "examples/goldiloader_integration_with_isolated_loader.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"n1_loader/goldiloader\"\n\nrequire_relative \"context/setup_database\"\n\nclass Loader < N1Loader::Loader\n  def perform(users)\n    total_per_user = Payment.group(:user_id).where(user: users).sum(:amount).tap { |h| h.default = 0 }\n\n    users.each do |user|\n      total = total_per_user[user.id]\n      fulfill(user, total)\n    end\n  end\nend\n\nclass User < ActiveRecord::Base\n  has_many :payments\nend\n\nclass Payment < ActiveRecord::Base\n  belongs_to :user\n\n  validates :amount, presence: true\nend\n\nfill_database\n\n# Has N+1 and loads redundant data\np(User.all.map { |user| user.payments.sum(&:amount) })\n\n# Has no N+1 and loads only required data (Goldiloader auto-batches by default)\np(User.all.map { |user| Loader.for(user) })\n"
  },
  {
    "path": "examples/graphql.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"n1_loader/ar_lazy_preload\"\nrequire 'graphql'\n\nrequire_relative 'context/setup_database'\nrequire_relative 'context/setup_ar_lazy'\n\nclass User < ActiveRecord::Base\n  has_many :payments\n\n  n1_optimized :payments_total do |users|\n    total_per_user = Payment.group(:user_id).where(user: users).sum(:amount).tap { |h| h.default = 0 }\n\n    users.each do |user|\n      total = total_per_user[user.id]\n      fulfill(user, total)\n    end\n  end\nend\n\nclass Payment < ActiveRecord::Base\n  belongs_to :user\n\n  validates :amount, presence: true\nend\n\n10.times do\n  user = User.create!\n  10.times do\n    Payment.create!(user: user, amount: rand(1000))\n  end\nend\n\nArLazyPreload.config.auto_preload = true\n# Or use +preload_associations_lazily+ when loading objects from database\n\nclass UserType < GraphQL::Schema::Object\n  field :payments_total, Integer\nend\n\nclass QueryType < GraphQL::Schema::Object\n  field  :users, [UserType]\n\n  def users\n    User.all\n  end\nend\n\nclass Schema < GraphQL::Schema\n  query QueryType\nend\n\nquery_string = <<~GQL\n  {\n    users {\n      paymentsTotal\n    }\n  }\nGQL\n\n# No N+1. And never will be!\np Schema.execute(query_string)['data']\n"
  },
  {
    "path": "examples/isolated_loader.rb",
    "content": "require 'n1_loader'\n\nclass IsolatedLoader < N1Loader::Loader\n  def perform(elements)\n    elements.each { |element| fulfill(element, [element]) }\n  end\nend\n\nobjects = [1, 2, 3, 4]\nloader = IsolatedLoader.new(objects)\nobjects.each do |object|\n  loader.for(object) # => it has no N+1 and it doesn't require to be injected in the class\nend"
  },
  {
    "path": "examples/lazy_loading.rb",
    "content": "require 'n1_loader'\n\nrequire_relative 'context/service'\n\n# Class that wants to request 3rd party service without N+1\nclass User\n  include N1Loader::Loadable\n\n  n1_optimized :optimized_call do |users|\n    data = Service.receive(users)\n\n    users.each_with_index do |user, index|\n      fulfill(user, data[index])\n    end\n  end\nend\n\nusers = [User.new, User.new, User.new]\n\n# Initialized loader but didn't perform it yet\nN1Loader::Preloader.new(users).preload(:optimized_call)\np \"No calls yet: #{Service.count == 0}\"\n\n# First time loading\nusers.map(&:optimized_call)\np \"First time loaded: #{Service.count == 1}\""
  },
  {
    "path": "examples/n1_bind_to.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"n1_loader\"\n\nrequire_relative 'context/service'\n\n# Class that wants to request 3rd party service without N+1\nclass User\n  include N1Loader::Loadable\n\n  n1_optimized :optimized_call do |users|\n    data = Service.receive(users)\n\n    users.each_with_index do |user, index|\n      fulfill(user, data[index])\n    end\n  end\nend\n\nusers = [User.new, User.new, User.new]\n\n# Without n1_bind_to: each user lazily loads independently causing N+1\ncount_before = Service.count\np users.map(&:optimized_call)\np \"Has N+1: #{Service.count == count_before + users.count}\"\n\nusers = [User.new, User.new, User.new]\n\n# With n1_bind_to: bind users to the collection so lazy loading is automatically batched\nusers.each { |user| user.n1_bind_to(users) }\n\ncount_before = Service.count\np users.map(&:optimized_call)\np \"Has no N+1: #{Service.count == count_before + 1}\"\n"
  },
  {
    "path": "examples/reloading.rb",
    "content": "require 'n1_loader'\n\nrequire_relative 'context/service'\n\nclass User\n  include N1Loader::Loadable\n\n  n1_optimized :optimized_call do |users|\n    data = Service.receive(users)\n\n    users.each_with_index do |user, index|\n      fulfill(user, data[index])\n    end\n  end\nend\n\nusers = [User.new, User.new, User.new]\n\n# Initialized loader but didn't perform it yet\nN1Loader::Preloader.new(users).preload(:optimized_call)\np \"No calls yet: #{Service.count == 0}\"\n\n# First time loading\nusers.map(&:optimized_call)\np \"First time loaded: #{Service.count == 1}\"\n\nusers.first.optimized_call(reload: true)\np \"Reloaded for this object only: #{Service.count == 2}\"\n\nusers.first.n1_clear_cache\nusers.first.optimized_call\np \"Reloaded for this object only: #{Service.count == 3}\""
  },
  {
    "path": "examples/shared_loader.rb",
    "content": "require 'n1_loader'\n\nrequire_relative 'context/service'\n\n# Loader that will be shared between multiple classes\nclass SharedLoader < N1Loader::Loader\n  def perform(objects)\n    data = Service.receive(objects)\n\n    objects.each_with_index do |user, index|\n      fulfill(user, data[index])\n    end\n  end\nend\n\nclass User\n  include N1Loader::Loadable\n\n  n1_optimized :optimized_call, SharedLoader\nend\n\nclass Payment\n  include N1Loader::Loadable\n\n  n1_optimized :optimized_call, SharedLoader\nend\n\nobjects = [User.new, Payment.new, User.new, Payment.new]\n\nN1Loader::Preloader.new(objects).preload(:optimized_call)\n\n# First time loading for all objects\nobjects.map(&:optimized_call)\np \"Loaded for all once: #{Service.count == 1}\""
  },
  {
    "path": "examples/single_case.rb",
    "content": "require 'n1_loader'\n\nrequire_relative 'context/service'\n\n# Loader that will be shared between multiple classes\nclass OptimizedLoader < N1Loader::Loader\n  def perform(objects)\n    data = Service.receive(objects)\n\n    objects.each_with_index do |user, index|\n      fulfill(user, data[index])\n    end\n  end\n\n  def single(object)\n    Service.single(object)\n  end\nend\n\nclass User\n  include N1Loader::Loadable\n\n  n1_optimized :optimized_call, OptimizedLoader\nend\n\nobjects = [User.new, User.new]\n\nN1Loader::Preloader.new(objects).preload(:optimized_call)\n\nobjects.map(&:optimized_call)\np \"Used multi-case perform: #{Service.count == 1}\"\n\nUser.new.optimized_call\np \"Used single-case perform: #{Service.count == 1}\""
  },
  {
    "path": "goldiloader-gemfiles/goldiloader.gemfile",
    "content": "# frozen_string_literal: true\n\ngem \"goldiloader\"\n"
  },
  {
    "path": "guides/enhanced-activerecord.md",
    "content": "# Enhanced ActiveRecord\n\n- Do you like `ActiveRecord` preloading?\n- How many times have you resolved your N+1 issues with `includes` or `preload`?\n- Do you know that preloading has limitations?\n\nIn this guide, I'd like to share with you tips and tricks about ActiveRecord\npreloading and how you can enhance it to the next level.\n\nLet's start by describing the models.\n\n```ruby\n# The model represents users in our application.\nclass User < ActiveRecord::Base\n  # Every user may have from 0 to many payments.\n  has_many :payments\nend\n\n# The model represents payments in our application.\nclass Payment < ActiveRecord::Base \n  # Every payment belongs to a user.\n  belongs_to :user\nend\n```\n\nAssuming we want to iterate over a group of users and check how many payments they have, we may do:\n\n```ruby\n# The query we want to use to fetch users from the database.\nusers = User.all\n# Iteration over selected users.\nusers.each do |user|\n  # Print amount of user's payments. \n  # This query will be called for every user, bringing an N+1 issue.\n  p user.payments.count\nend\n```\n\nWe can fix the N+1 issue above in a second.\nWe need to add ActiveRecord's `includes` to the query that fetches users.\n\n```ruby\n# The query to fetch users with preload payments for every selected user.\nusers = User.includes(:payments).all\n```\n\nThen, we can iterate over the group again without the N+1 issue.\n\n```ruby\nusers.each do |user|\n  p user.payments.count\nend\n```\n\nExperienced with ActiveRecord person may notice that the iteration above still will have an N+1 issue.\nThe reason is the `.count` method and its behavior.\nThis issue brings us to the first tip.\n\n### Tip 1. `count` vs `size` vs `length`\n\n- `count` - always queries the database with `COUNT` query;\n- `size` - queries the database with `COUNT` only when there is no preloaded data, returns array length otherwise;\n- `length` - always returns array length, in case there is no data, load it first.\n\n_Note:_ be careful with `size` as ordering is critical.\n\nMeaning, for `user = User.first`\n\n```ruby\n# Does `COUNT` query\nuser.payments.size\n# Does `SELECT` query\nuser.payments.each { |payment| }\n```\n\nis different from\n\n```ruby\n# Does `SELECT` query\nuser.payments.each { |payment| }\n# No query\nuser.payments.size\n```\n\nYou may notice that the above solution loads all payment information when the amount is only needed.\nThere is a well-known solution for this case called [counter_cache](https://guides.rubyonrails.org/association_basics.html#options-for-belongs-to-counter-cache).\n\nTo use that, you need to add `payments_count` field to `users` table and adjust `Payment` model.\n\n```ruby\n# Migration to add `payments_count` to `users` table.\nclass AddPaymentsCountToUsers < ActiveRecord::Migration\n  def change\n    add_column :users, :payments_count, :integer, default: 0, null: false\n  end\nend\n\n# Change `belongs_to` to have `counter_cache` option.\nclass Payment < ActiveRecord::Base\n  belongs_to :user, counter_cache: true\nend\n```\n\n_Note:_ avoid adding or removing payments from the database directly or through `insert_all`/`delete`/`delete_all` as\n`counter_cache` is using ActiveRecord callbacks to update the field's value.\n\nIt's worth mentioning [counter_culture](https://github.com/magnusvk/counter_culture) alternative that has many features compared with the built-in `counter_cache`\n\n## Associations with arguments\n\nNow, let's assume we want to fetch the number of payments in a time frame for every user in a group.\n\n```ruby\nfrom = 1.months.ago\nto = Time.current\n\n# Query to fetch users.\nusers = User.all\n\nusers.each do |user|\n  # Print the number of payments in a time frame for every user.\n  # Database query will be triggered for every user, meaning it has an N+1 issue.\n  p user.payments.where(created_at: from...to).count\nend\n```\n\nActiveRecord supports defining associations with arguments.\n\n```ruby\nclass User < ActiveRecord::Base\n  has_many :payments, -> (from, to) { where(created_at: from...to) }\nend\n```\n\nUnfortunately, such associations are not possible to preload with `includes`.\nGladly, there is a solution with [N1Loader](https://github.com/djezzzl/n1_loader/).\n\n```ruby\n# Install gem dependencies.\nrequire 'n1_loader/active_record'\n\nclass User < ActiveRecord::Base\n  n1_optimized :payments_count do\n    argument :from \n    argument :to \n    \n    def perform(users)\n      # Fetch the payment number once for all users.\n      payments = Payment.where(user: users).where(created_at: from...to).group(:user_id).count\n      \n      users.each do |user|\n        # Assign preloaded data to every user. \n        # Note: it doesn't use any promises.\n        fulfill(user, payments[user.id])\n      end\n    end\n  end\nend\n\nfrom = 1.month.ago \nto = Time.current\n\n# Preload `payments` N1Loader \"association\". Doesn't query the database yet.\nusers = User.includes(:payments_count).all\n\nusers.each do |user|\n  # Queries the database once, meaning has no N+1 issues.\n  p user.payments_count(from, to)\nend\n```\n\nLet's look at another example. Assuming we want to fetch the last payment for every user.\nWe can try to define scoped `has_one` association and use that.\n\n```ruby\nclass User < ActiveRecord::Base\n  has_one :last_payment, -> { order(id: :desc) }, class_name: 'Payment'\nend\n```\n\nWe can see that preloading is working.\n\n```ruby\nusers = User.includes(:last_payment)\n\nusers.each do |user|\n  # No N+1. Last payment was returned.\n  p user.last_payment\nend\n```\n\nAt first glance, we may think everything is alright. Unfortunately, it is not.\n\n### Tip 2. Enforce `has_one` associations on the database level\n\nActiveRecord, fetches all available payments for every user with provided order and then assigns only first payment to the association.\nFirst, such querying is inefficient as we load many redundant information.\nBut most importantly, this association may lead to big issues. Other engineers may use it, for example,\nfor `joins(:last_payment)`. Assuming that association has strict agreement on the database level that\na user may have none or a single payment in the database. Apparently, it may not be the case, and some queries\nwill return unexpected data.\n\nDescribed issues may be found with [DatabaseConsistency](https://github.com/djezzzl/database_consistency).\n\nBack to the task, we can solve it with [N1Loader](https://github.com/djezzzl/n1_loader) in the following way\n\n```ruby\nrequire 'n1_loader/active_record'\n\nclass User < ActiveRecord::Base\n  n1_optimized :last_payment do |users|\n    subquery = Payment.select('MAX(id)').where(user: users)\n    payments = Payment.where(id: subquery).index_by(&:user_id)\n    \n    users.each do |user|\n      fulfill(user, payments[user.id])\n    end\n  end\nend\n\nusers = User.includes(:last_payment).all\n\nusers.each do |user|\n  # Queries the database once, meaning no N+1.\n  p user.last_payment\nend\n```\n\nAttentive reader could notice that in every described case, it was a requirement to explicitly list data that we want to preload for a group of users.\nGladly, there is a simple solution! [ArLazyPreload](https://github.com/DmitryTsepelev/ar_lazy_preload) will make N+1 disappear just by enabling it.\nAs soon as you need to load association for any record, it will load it once for all records that were fetched along this one.\nAnd it works with ActiveRecord and N1Loader perfectly!\n\nLet's look at the example.\n\n```ruby\n# Require N1Loader with ArLazyPreload integration\nrequire 'n1_loader/ar_lazy_preload'\n\n# Enable ArLazyPreload globally, so you don't need to care about `includes` anymore\nArLazyPreload.config.auto_preload = true\n\nclass User < ActiveRecord::Base\n  has_many :payments\n\n  n1_optimized :last_payment do |users|\n    subquery = Payment.select('MAX(id)').where(user: users)\n    payments = Payment.where(id: subquery).index_by(&:user_id)\n\n    users.each do |user|\n      fulfill(user, payments[user.id])\n    end\n  end\nend\n\n# no need to specify `includes`\nusers = User.all\n\nusers.each do |user|\n  p user.payments # no N+1\n  p user.last_payment # no N+1\nend\n```\n\nAs you can see, there is no need to even remember about resolving N+1 when you have both [ArLazyPreload](https://github.com/DmitryTsepelev/ar_lazy_preload) and [N1Loader](https://github.com/djezzzl/n1_loader) in your pocket.\nIt works great with GraphQL API too. Give it and try and share your feedback!"
  },
  {
    "path": "lib/n1_loader/active_record/associations_preloader.rb",
    "content": "# frozen_string_literal: true\n\nmodule N1Loader\n  module ActiveRecord\n    module Associations\n      module Preloader # :nodoc:\n        N1LoaderReflection = Struct.new(:name, :loader) do\n          def options\n            {}\n          end\n\n          def deprecated?\n            false\n          end\n\n          def through_reflection?\n            false\n          end\n        end\n\n        def preloaders_for_reflection(reflection, records)\n          return super unless reflection.is_a?(N1LoaderReflection)\n\n          N1Loader::Preloader.new(records).preload(reflection.name)\n        end\n\n        def grouped_records # rubocop:disable Metrics/PerceivedComplexity, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/AbcSize\n          n1_load_records, records = source_records.partition do |record|\n            record.class.respond_to?(:n1_loaders) && record.class.n1_loaders[association]\n          end\n\n          h = n1_load_records.group_by do |record|\n            N1LoaderReflection.new(association, record.class.n1_loaders[association])\n          end\n\n          polymorphic_parent = !root? && parent.polymorphic?\n          records.each do |record|\n            reflection = record.class._reflect_on_association(association)\n            next if polymorphic_parent && !reflection || !record.association(association).klass\n\n            (h[reflection] ||= []) << record\n          end\n          h\n        end\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/n1_loader/active_record/associations_preloader_v5.rb",
    "content": "# frozen_string_literal: true\n\nmodule N1Loader\n  module ActiveRecord\n    module Associations\n      module Preloader # :nodoc:\n        N1LoaderReflection = Struct.new(:name, :loader) do\n          def options\n            {}\n          end\n        end\n\n        def preloaders_for_one(association, records, scope)\n          grouped_records(association, records).flat_map do |reflection, klasses|\n            next N1Loader::Preloader.new(records).preload(reflection.name) if reflection.is_a?(N1LoaderReflection)\n\n            klasses.map do |rhs_klass, rs|\n              loader = preloader_for(reflection, rs).new(rhs_klass, rs, reflection, scope)\n              loader.run self\n              loader\n            end\n          end\n        end\n\n        def grouped_records(association, records)\n          n1_load_records, records = records.partition do |record|\n            record.class.respond_to?(:n1_loaders) && record.class.n1_loaders.key?(association)\n          end\n\n          hash = n1_load_records.group_by do |record|\n            N1LoaderReflection.new(association, record.class.n1_loaders[association])\n          end\n\n          hash.merge(super)\n        end\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/n1_loader/active_record/associations_preloader_v6.rb",
    "content": "# frozen_string_literal: true\n\nmodule N1Loader\n  module ActiveRecord\n    module Associations\n      module Preloader # :nodoc:\n        N1LoaderReflection = Struct.new(:name, :loader) do\n          def options\n            {}\n          end\n        end\n\n        def preloaders_for_reflection(reflection, records, scope)\n          return super unless reflection.is_a?(N1LoaderReflection)\n\n          N1Loader::Preloader.new(records).preload(reflection.name)\n        end\n\n        def grouped_records(association, records, polymorphic_parent)\n          n1_load_records, records = records.partition do |record|\n            record.class.respond_to?(:n1_loaders) && record.class.n1_loaders[association]\n          end\n\n          hash = n1_load_records.group_by do |record|\n            N1LoaderReflection.new(association, record.class.n1_loaders[association])\n          end\n\n          hash.merge(super)\n        end\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/n1_loader/active_record/base.rb",
    "content": "# frozen_string_literal: true\n\nmodule N1Loader\n  module ActiveRecord\n    # Extension module for ActiveRecord::Base\n    module Base\n      extend ActiveSupport::Concern\n\n      include N1Loader::Loadable\n\n      # Clear N1Loader cache on reloading the object\n      def reload(*)\n        n1_clear_cache\n        super\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/n1_loader/active_record/loader.rb",
    "content": "# frozen_string_literal: true\n\nN1Loader::Loader.define_method :preloaded_records do\n  @preloaded_records ||= loaded? && loaded_by_value.values.flatten\nend\n"
  },
  {
    "path": "lib/n1_loader/active_record/loader_collection.rb",
    "content": "# frozen_string_literal: true\n\nN1Loader::LoaderCollection.define_method :preloaded_records do\n  raise N1Loader::ActiveRecord::InvalidPreloading, \"Cannot preload loader with arguments\" if loader_class.arguments\n\n  with.preloaded_records\nend\n\nN1Loader::LoaderCollection.define_method :runnable_loaders do\n  [self]\nend\n\nN1Loader::LoaderCollection.define_method :run? do\n  true\nend\n\nN1Loader::LoaderCollection.define_method :future_classes do\n  []\nend\n"
  },
  {
    "path": "lib/n1_loader/active_record.rb",
    "content": "# frozen_string_literal: true\n\n# Load core library\nrequire_relative \"../n1_loader\"\n\n# Load integration dependency\nrequire \"active_record\"\n\nmodule N1Loader\n  module ActiveRecord\n    class InvalidPreloading < N1Loader::Error; end\n  end\nend\n\n# Library integration\nActiveSupport.on_load(:active_record) do\n  require_relative \"active_record/loader\"\n  require_relative \"active_record/loader_collection\"\n  require_relative \"active_record/base\"\n\n  case ActiveRecord::VERSION::MAJOR\n  when 6\n    require_relative \"active_record/associations_preloader_v6\"\n    ActiveRecord::Associations::Preloader.prepend(N1Loader::ActiveRecord::Associations::Preloader)\n  when 5\n    require_relative \"active_record/associations_preloader_v5\"\n    ActiveRecord::Associations::Preloader.prepend(N1Loader::ActiveRecord::Associations::Preloader)\n  else\n    require_relative \"active_record/associations_preloader\"\n    ActiveRecord::Associations::Preloader::Branch.prepend(N1Loader::ActiveRecord::Associations::Preloader)\n  end\n\n  ActiveRecord::Base.include(N1Loader::ActiveRecord::Base)\nend\n"
  },
  {
    "path": "lib/n1_loader/ar_lazy_preload/associated_context_builder.rb",
    "content": "# frozen_string_literal: true\n\nmodule N1Loader\n  module ArLazyPreload\n    # Context builder for N1Loader\n    class AssociatedContextBuilder < ::ArLazyPreload::AssociatedContextBuilder\n      attr_reader :records\n\n      def initialize(parent_context:, association_name:, records:)\n        super(parent_context: parent_context, association_name: association_name)\n        @records = records\n      end\n\n      def perform\n        ::ArLazyPreload::Context.register(\n          records: records.flatten(1).select { |record| record.respond_to?(:lazy_preload_context=) },\n          association_tree: child_association_tree,\n          auto_preload: parent_context.auto_preload?\n        )\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/n1_loader/ar_lazy_preload/context.rb",
    "content": "# frozen_string_literal: true\n\n# Returns cached N1Loader::LoaderCollection from context for a loader.\n# In case there is none yet, saves passed block to a cache.\nArLazyPreload::Contexts::BaseContext.define_method :fetch_n1_loader_collection do |loader, &block|\n  (@n1_loader_collections ||= {})[loader] ||= block.call\nend\n"
  },
  {
    "path": "lib/n1_loader/ar_lazy_preload/context_adapter.rb",
    "content": "# frozen_string_literal: true\n\nmodule N1Loader\n  module ArLazyPreload\n    # Context adapter for injected N1Loader loaders.\n    class ContextAdapter\n      attr_reader :context\n\n      delegate_missing_to :context\n\n      def initialize(context)\n        @context = context\n      end\n\n      # Assign initialized preloader to +association_name+ in case it wasn't yet preloaded within the given context.\n      def try_preload_lazily(association_name)\n        return unless context&.send(:association_needs_preload?, association_name)\n\n        perform_preloading(association_name)\n      end\n\n      # Initialize preloader for +association_name+ with context builder callback.\n      # The callback will be executed when on records load.\n      def perform_preloading(association_name)\n        context_setup = lambda { |records|\n          AssociatedContextBuilder.prepare(\n            parent_context: self,\n            association_name: association_name,\n            records: records\n          )\n        }\n\n        N1Loader::Preloader.new(records, context_setup).preload(association_name)\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/n1_loader/ar_lazy_preload/loadable.rb",
    "content": "# frozen_string_literal: true\n\nmodule N1Loader\n  module ArLazyPreload\n    module Loadable # :nodoc:\n      def n1_loader(name)\n        return n1_loaders[name] if n1_loaders[name]\n\n        ContextAdapter.new(lazy_preload_context).try_preload_lazily(name) if respond_to?(:lazy_preload_context)\n\n        super\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/n1_loader/ar_lazy_preload/loader.rb",
    "content": "# frozen_string_literal: true\n\n# Raised when a single object without ArLazyPreload context support was passed to an isolated loader.\nN1Loader::Loader::UnsupportedArLazyPreload = Class.new(StandardError)\n\n# Defines a singleton method method that allows isolated loaders\n# to use ArLazyPreload context without passing sibling records.\nN1Loader::Loader.define_singleton_method(:for) do |element, **args|\n  # It is required to have an ArLazyPreload context supported\n  raise N1Loader::Loader::UnsupportedArLazyPreload unless element.respond_to?(:lazy_preload_context)\n\n  if element.lazy_preload_context.nil?\n    ArLazyPreload::Context.register(\n      records: [element],\n      association_tree: [],\n      auto_preload: true\n    )\n  end\n\n  # Fetch or initialize loader from ArLazyPreload context\n  loader_collection = element.lazy_preload_context.fetch_n1_loader_collection(self) do\n    context_setup = lambda { |records|\n      N1Loader::ArLazyPreload::AssociatedContextBuilder.prepare(\n        parent_context: element.lazy_preload_context,\n        association_name: \"cached_n1_loader_collection_#{self}\".downcase.to_sym,\n        records: records\n      )\n    }\n\n    N1Loader::LoaderCollection.new(self, element.lazy_preload_context.records).tap do |collection|\n      collection.context_setup = context_setup\n    end\n  end\n\n  # Fetch value from loader\n  loader_collection.with(**args).for(element)\nend\n"
  },
  {
    "path": "lib/n1_loader/ar_lazy_preload/loader_collection_patch.rb",
    "content": "# frozen_string_literal: true\n\nmodule N1Loader\n  module ArLazyPreload\n    # A patch to {N1Loader::LoaderCollection} to setup lazy context lazily.\n    module LoaderCollectionPatch\n      attr_accessor :context_setup\n\n      def with(**args)\n        result = super\n\n        result.context_setup = context_setup if context_setup && result.context_setup.nil?\n\n        result\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/n1_loader/ar_lazy_preload/loader_patch.rb",
    "content": "# frozen_string_literal: true\n\nmodule N1Loader\n  module ArLazyPreload\n    # A patch to {N1Loader::Loader} to setup lazy context lazily.\n    module LoaderPatch\n      attr_accessor :context_setup\n\n      def loaded?\n        return true if @already_loaded && @already_context\n\n        super\n\n        synchronize { non_thread_safe_context_setting unless @already_context }\n\n        true\n      end\n\n      def non_thread_safe_context_setting\n        return if @already_context\n\n        context_setup&.call(loaded_by_identity.values.flatten)\n\n        @already_context = true\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/n1_loader/ar_lazy_preload/preloader_patch.rb",
    "content": "# frozen_string_literal: true\n\nmodule N1Loader\n  module ArLazyPreload\n    # A patch to {N1Loader::Preloader} setup lazy context lazily.\n    module PreloaderPatch\n      def initialize(elements, context_setup = nil)\n        super(elements)\n        @context_setup = context_setup\n      end\n\n      def preload(*keys)\n        super.each do |loader_collection|\n          loader_collection.context_setup = context_setup\n        end\n      end\n\n      private\n\n      attr_reader :context_setup\n    end\n  end\nend\n"
  },
  {
    "path": "lib/n1_loader/ar_lazy_preload.rb",
    "content": "# frozen_string_literal: true\n\n# Load core library\nrequire_relative \"active_record\"\n\n# Load integration dependency\nrequire \"rails\"\nrequire \"ar_lazy_preload\"\n\n# Library integration\nrequire_relative \"ar_lazy_preload/loadable\"\nrequire_relative \"ar_lazy_preload/context_adapter\"\nrequire_relative \"ar_lazy_preload/associated_context_builder\"\nrequire_relative \"ar_lazy_preload/loader_collection_patch\"\nrequire_relative \"ar_lazy_preload/preloader_patch\"\nrequire_relative \"ar_lazy_preload/loader_patch\"\nrequire_relative \"ar_lazy_preload/loader\"\nrequire_relative \"ar_lazy_preload/context\"\n\nN1Loader::Loadable.prepend(N1Loader::ArLazyPreload::Loadable)\nN1Loader::Preloader.prepend(N1Loader::ArLazyPreload::PreloaderPatch)\nN1Loader::Loader.prepend(N1Loader::ArLazyPreload::LoaderPatch)\nN1Loader::LoaderCollection.prepend(N1Loader::ArLazyPreload::LoaderCollectionPatch)\n"
  },
  {
    "path": "lib/n1_loader/core/loadable.rb",
    "content": "# frozen_string_literal: true\n\nmodule N1Loader\n  # The module to be included to the class to define associated loaders.\n  #\n  #   class Example\n  #     include N1Loader::Loadable\n  #\n  #     # with inline loader\n  #     n1_optimized :something do\n  #       def perform(elements)\n  #         elements.each { |element| fulfill(element, element.calculate_something) }\n  #       end\n  #     end\n  #\n  #     # with custom loader\n  #     n1_optimized :something, MyLoader\n  #   end\n  #\n  #   # custom loader\n  #   class MyLoader < N1Loader::Loader\n  #     def perform(elements)\n  #       elements.each { |element| fulfill(element, element.calculate_something) }\n  #     end\n  #   end\n  module Loadable\n    def n1_loaders\n      @n1_loaders ||= {}\n    end\n\n    def n1_loader(name)\n      n1_loaders[name]\n    end\n\n    def n1_bind_to(collection)\n      unless collection.is_a?(Array) && collection.any? do |obj|\n        obj == self || obj.equal?(self)\n      end\n\n        raise InvalidBinding,\n              \"assigned collection should be array and include object\"\n      end\n\n      @n1_binding = collection\n    end\n\n    def n1_bind_to?\n      !@n1_binding.nil?\n    end\n\n    def n1_loader_reload(name)\n      elements = @n1_binding || [self]\n      collection = LoaderCollection.new(self.class.n1_loaders[name], elements)\n\n      @n1_binding&.each { |el| el.n1_loaders[name] = collection if el.respond_to?(:n1_loaders) }\n      n1_loaders[name] = collection\n    end\n\n    def n1_clear_cache\n      @n1_binding = nil\n      self.class.n1_loaders.each_key do |name|\n        n1_loaders[name] = nil\n      end\n    end\n\n    def self.included(base)\n      base.extend(ClassMethods)\n    end\n\n    module ClassMethods # :nodoc:\n      def n1_loaders\n        @n1_loaders ||= superclass.respond_to?(:n1_loaders) ? superclass.n1_loaders.dup : {}\n      end\n\n      def n1_optimized(name, loader = nil, &block)\n        loader ||= LoaderBuilder.build(&block)\n\n        n1_loaders[name] = loader\n\n        define_method(name) do |reload: false, **args|\n          n1_loader_reload(name) if reload || n1_loader(name).nil?\n\n          n1_loader(name).with(**args).for(self)\n        end\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/n1_loader/core/loader.rb",
    "content": "# frozen_string_literal: true\n\nmodule N1Loader\n  # Loader that performs the loading.\n  #\n  # Subclasses must define +perform+ method that accepts single argument\n  # and returns hash where key is the element and value is what we want to load.\n  class Loader\n    prepend MonitorMixin\n\n    class << self\n      attr_reader :arguments\n\n      # Defines an argument that can be accessed within the loader.\n      #\n      # First defined argument will have the value of first passed argument,\n      # meaning the order is important.\n      #\n      # @param name [Symbol]\n      # @param opts [Hash]\n      # @option opts [Boolean] optional false by default\n      # @option opts [Proc] default\n      def argument(name, **opts)\n        opts[:optional] = true if opts[:default]\n\n        @arguments ||= []\n\n        define_method(name) do\n          args.fetch(name) { args[name] = opts[:default]&.call }\n        end\n\n        @arguments << opts.merge(name: name)\n      end\n\n      # Defines a custom cache key that is calculated for passed arguments.\n      def cache_key(&block)\n        define_method(:cache_key) do\n          check_arguments!\n          instance_exec(&block)\n        end\n      end\n    end\n\n    def initialize(elements, **args)\n      @elements = elements\n      @args = args\n    end\n\n    def for(element)\n      return unless loaded?\n\n      if loaded_by_identity.empty? && elements.any?\n        raise NotFilled, \"Nothing was preloaded, perhaps you forgot to use fulfill method\"\n      end\n\n      return loaded_by_identity[element] if loaded_by_identity.key?(element)\n      return loaded_by_value[element] if loaded_by_value.key?(element)\n\n      raise NotLoaded, \"The data was not preloaded for the given element\"\n    end\n\n    def cache_key\n      check_arguments!\n      args.values.map(&:object_id)\n    end\n\n    private\n\n    attr_reader :elements, :args, :loaded_by_value, :loaded_by_identity\n\n    def check_missing_arguments!\n      return unless (arguments = self.class.arguments)\n\n      required_arguments = required_arguments(arguments)\n\n      return if required_arguments.all? { |argument| args.key?(argument) }\n\n      missing_arguments = required_arguments.reject { |argument| args.key?(argument) }\n\n      list = missing_arguments.map { |argument| \":#{argument}\" }.join(\", \")\n\n      raise MissingArgument, \"Loader requires [#{list}] arguments but they are missing\"\n    end\n\n    def required_arguments(args)\n      args.reject { |argument| argument[:optional] }\n          .map { |argument| argument[:name] }\n    end\n\n    def check_arguments!\n      check_missing_arguments!\n      check_invalid_arguments!\n    end\n\n    def check_invalid_arguments!\n      return unless (arguments = self.class.arguments)\n\n      args.each_key do |arg|\n        next if arguments.find { |argument| argument[:name] == arg }\n\n        raise InvalidArgument, \"Loader doesn't define #{arg} argument\"\n      end\n    end\n\n    def perform(_elements)\n      raise NotImplemented, \"Subclasses have to implement the method\"\n    end\n\n    def fulfill(element, value)\n      loaded_by_identity[element] = value\n      loaded_by_value[element] = value\n    end\n\n    def loaded?\n      return true if @already_loaded\n\n      synchronize { non_thread_safe_loading unless @already_loaded }\n\n      true\n    end\n\n    def non_thread_safe_loading # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity\n      return if @already_loaded\n\n      check_arguments!\n\n      @loaded_by_identity = {}.compare_by_identity\n      @loaded_by_value = {}\n\n      if respond_to?(:single) && elements.size == 1\n        fulfill(elements.first, single(elements.first))\n      elsif elements.any?\n        perform(elements)\n\n        # propagate context to loaded objects only when it was set\n        if elements.first.respond_to?(:n1_bind_to?) && elements.first.n1_bind_to?\n          loaded_objects = loaded_by_identity.values.flatten\n          loaded_objects.each { |el| el.n1_bind_to(loaded_objects) if el.respond_to?(:n1_bind_to) }\n        end\n      end\n\n      @already_loaded = true\n    end\n  end\nend\n"
  },
  {
    "path": "lib/n1_loader/core/loader_builder.rb",
    "content": "# frozen_string_literal: true\n\nmodule N1Loader\n  # The class builds {N1Loader::Loader}\n  class LoaderBuilder\n    def self.build(&block)\n      Class.new(N1Loader::Loader) do\n        if block.arity == 1\n          define_method(:perform, &block)\n        else\n          class_eval(&block)\n        end\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/n1_loader/core/loader_collection.rb",
    "content": "# frozen_string_literal: true\n\nmodule N1Loader\n  # The class is used for storing collections of loaders for elements per set of arguments.\n  class LoaderCollection\n    attr_reader :loader_class, :elements\n\n    def initialize(loader_class, elements)\n      @loader_class = loader_class\n      @elements = elements\n    end\n\n    def with(**args)\n      loader = loader_class.new(elements, **args)\n\n      loaders[loader.cache_key] ||= loader\n    end\n\n    private\n\n    def loaders\n      @loaders ||= {}\n    end\n  end\nend\n"
  },
  {
    "path": "lib/n1_loader/core/preloader.rb",
    "content": "# frozen_string_literal: true\n\nmodule N1Loader\n  # Preloader that lazily preloads data to every element.\n  #\n  # It supports multiple keys.\n  #\n  # It supports elements that have different loaders under the same key.\n  # It will properly preload data to each of the element of the similar group.\n  class Preloader\n    attr_reader :elements\n\n    def initialize(elements)\n      @elements = elements\n    end\n\n    def preload(*keys)\n      keys.flatten(1).flat_map do |key|\n        elements\n          .group_by { |element| loader_class(element, key) }\n          .select { |loader_class, _| loader_class }\n          .map do |(loader_class, grouped_elements)|\n            loader_collection = N1Loader::LoaderCollection.new(loader_class, grouped_elements)\n            grouped_elements.each { |grouped_element| grouped_element.n1_loaders[key] = loader_collection }\n            loader_collection\n          end\n      end\n    end\n\n    private\n\n    def loader_class(element, key)\n      element.class.respond_to?(:n1_loaders) && element.class.n1_loaders[key]\n    end\n  end\nend\n"
  },
  {
    "path": "lib/n1_loader/goldiloader/context.rb",
    "content": "# frozen_string_literal: true\n\n# Returns cached N1Loader::LoaderCollection from context for a loader.\n# In case there is none yet, saves passed block to a cache.\nGoldiloader::AutoIncludeContext.define_method :fetch_n1_loader_collection do |loader, &block|\n  (@n1_loader_collections ||= {})[loader] ||= block.call\nend\n"
  },
  {
    "path": "lib/n1_loader/goldiloader/context_adapter.rb",
    "content": "# frozen_string_literal: true\n\nmodule N1Loader\n  module Goldiloader\n    # Context adapter for injected N1Loader loaders.\n    class ContextAdapter\n      attr_reader :context\n\n      def initialize(context)\n        @context = context\n      end\n\n      # Trigger preloading for +association_name+ across all models in the context.\n      def try_preload_lazily(association_name)\n        perform_preloading(association_name) if context\n      end\n\n      # Initialize preloader for +association_name+ with context builder callback.\n      # The callback will be executed when records are loaded.\n      def perform_preloading(association_name)\n        context_setup = lambda { |records|\n          ar_records = records.flatten(1).select { |record| record.respond_to?(:auto_include_context=) }\n          ::Goldiloader::AutoIncludeContext.register_models(ar_records) unless ar_records.empty?\n        }\n\n        N1Loader::Preloader.new(context.models, context_setup).preload(association_name)\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/n1_loader/goldiloader/loadable.rb",
    "content": "# frozen_string_literal: true\n\nmodule N1Loader\n  module Goldiloader\n    module Loadable # :nodoc:\n      def n1_loader(name)\n        return n1_loaders[name] if n1_loaders[name]\n\n        ContextAdapter.new(auto_include_context).try_preload_lazily(name) if respond_to?(:auto_include_context)\n\n        super\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/n1_loader/goldiloader/loader.rb",
    "content": "# frozen_string_literal: true\n\n# Raised when a single object without Goldiloader context support was passed to an isolated loader.\nN1Loader::Loader::UnsupportedGoldiloader = Class.new(StandardError)\n\n# Defines a singleton method that allows isolated loaders\n# to use Goldiloader context without passing sibling records.\nN1Loader::Loader.define_singleton_method(:for) do |element, **args|\n  # It is required to have a Goldiloader context supported\n  raise N1Loader::Loader::UnsupportedGoldiloader unless element.respond_to?(:auto_include_context)\n\n  context = element.auto_include_context\n\n  # Fetch or initialize loader from Goldiloader context\n  loader_collection = context.fetch_n1_loader_collection(self) do\n    context_setup = lambda { |records|\n      ar_records = records.flatten(1).select { |record| record.respond_to?(:auto_include_context=) }\n      ::Goldiloader::AutoIncludeContext.register_models(ar_records) unless ar_records.empty?\n    }\n\n    N1Loader::LoaderCollection.new(self, context.models).tap do |collection|\n      collection.context_setup = context_setup\n    end\n  end\n\n  # Fetch value from loader\n  loader_collection.with(**args).for(element)\nend\n"
  },
  {
    "path": "lib/n1_loader/goldiloader/loader_collection_patch.rb",
    "content": "# frozen_string_literal: true\n\nmodule N1Loader\n  module Goldiloader\n    # A patch to {N1Loader::LoaderCollection} to setup lazy context lazily.\n    module LoaderCollectionPatch\n      attr_accessor :context_setup\n\n      def with(**args)\n        result = super\n\n        result.context_setup = context_setup if context_setup && result.context_setup.nil?\n\n        result\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/n1_loader/goldiloader/loader_patch.rb",
    "content": "# frozen_string_literal: true\n\nmodule N1Loader\n  module Goldiloader\n    # A patch to {N1Loader::Loader} to setup lazy context lazily.\n    module LoaderPatch\n      attr_accessor :context_setup\n\n      def loaded?\n        return true if @already_loaded && @already_context\n\n        super\n\n        synchronize { non_thread_safe_context_setting unless @already_context }\n\n        true\n      end\n\n      def non_thread_safe_context_setting\n        return if @already_context\n\n        context_setup&.call(loaded_by_identity.values.flatten)\n\n        @already_context = true\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/n1_loader/goldiloader/preloader_patch.rb",
    "content": "# frozen_string_literal: true\n\nmodule N1Loader\n  module Goldiloader\n    # A patch to {N1Loader::Preloader} to setup lazy context lazily.\n    module PreloaderPatch\n      def initialize(elements, context_setup = nil)\n        super(elements)\n        @context_setup = context_setup\n      end\n\n      def preload(*keys)\n        super.each do |loader_collection|\n          loader_collection.context_setup = context_setup\n        end\n      end\n\n      private\n\n      attr_reader :context_setup\n    end\n  end\nend\n"
  },
  {
    "path": "lib/n1_loader/goldiloader.rb",
    "content": "# frozen_string_literal: true\n\n# Load core library\nrequire_relative \"active_record\"\n\n# Load integration dependency\nrequire \"goldiloader\"\n\n# Library integration\nrequire_relative \"goldiloader/loadable\"\nrequire_relative \"goldiloader/context_adapter\"\nrequire_relative \"goldiloader/loader_collection_patch\"\nrequire_relative \"goldiloader/preloader_patch\"\nrequire_relative \"goldiloader/loader_patch\"\nrequire_relative \"goldiloader/loader\"\nrequire_relative \"goldiloader/context\"\n\nN1Loader::Loadable.prepend(N1Loader::Goldiloader::Loadable)\nN1Loader::Preloader.prepend(N1Loader::Goldiloader::PreloaderPatch)\nN1Loader::Loader.prepend(N1Loader::Goldiloader::LoaderPatch)\nN1Loader::LoaderCollection.prepend(N1Loader::Goldiloader::LoaderCollectionPatch)\n"
  },
  {
    "path": "lib/n1_loader/version.rb",
    "content": "# frozen_string_literal: true\n\nmodule N1Loader\n  VERSION = \"3.0.0\"\nend\n"
  },
  {
    "path": "lib/n1_loader.rb",
    "content": "# frozen_string_literal: true\n\nrequire_relative \"n1_loader/version\"\n\nrequire_relative \"n1_loader/core/loader_builder\"\nrequire_relative \"n1_loader/core/loader\"\nrequire_relative \"n1_loader/core/loader_collection\"\nrequire_relative \"n1_loader/core/loadable\"\nrequire_relative \"n1_loader/core/preloader\"\n\nmodule N1Loader # :nodoc:\n  class Error < StandardError; end\n  class NotImplemented < Error; end\n  class NotLoaded < Error; end\n  class NotFilled < Error; end\n  class MissingArgument < Error; end\n  class InvalidArgument < Error; end\n  class InvalidBinding < Error; end\nend\n"
  },
  {
    "path": "n1_loader.gemspec",
    "content": "# frozen_string_literal: true\n\nrequire_relative \"lib/n1_loader/version\"\n\nGem::Specification.new do |spec|\n  spec.name          = \"n1_loader\"\n  spec.version       = N1Loader::VERSION\n  spec.authors       = [\"Evgeniy Demin\"]\n  spec.email         = [\"lawliet.djez@gmail.com\"]\n\n  spec.summary       = \"Loader to solve N+1 issue for good.\"\n  spec.homepage      = \"https://github.com/djezzzl/n1_loader\"\n  spec.license       = \"MIT\"\n  spec.required_ruby_version = \">= 2.5.0\"\n\n  spec.metadata[\"homepage_uri\"] = spec.homepage\n  spec.metadata[\"source_code_uri\"] = \"https://github.com/djezzzl/n1_loader\"\n  spec.metadata[\"changelog_uri\"] = \"https://github.com/djezzzl/n1_loader/master/CHANGELOG.md\"\n  spec.metadata[\"funding_uri\"] = \"https://opencollective.com/n1_loader#support\"\n\n  spec.files = Dir[\"lib/**/*\"]\n  spec.require_paths = [\"lib\"]\n\n  spec.add_runtime_dependency \"mutex_m\"\n\n  spec.add_development_dependency \"activerecord\", \">= 5\"\n  spec.add_development_dependency \"ar_lazy_preload\", \">= 0.6\"\n  spec.add_development_dependency \"db-query-matchers\", \"~> 0.11\"\n  spec.add_development_dependency \"goldiloader\", \">= 3\"\n  spec.add_development_dependency \"graphql\", \"~> 2.0\"\n  spec.add_development_dependency \"rails\", \">= 5\"\n  spec.add_development_dependency \"rspec\", \"~> 3.0\"\n  spec.add_development_dependency \"rspec_junit_formatter\", \"~> 0.4\"\n  spec.add_development_dependency \"rubocop\", \"~> 1.7\"\n  spec.add_development_dependency \"sqlite3\", \">= 1.3\"\nend\n"
  },
  {
    "path": "spec/activerecord_spec.rb",
    "content": "# frozen_string_literal: true\n\nRSpec.describe \"N1Loader ActiveRecord integration\" do\n  require_relative \"../lib/n1_loader/active_record\"\n\n  before do\n    ActiveRecord::Base.establish_connection(adapter: \"sqlite3\", database: \":memory:\")\n    ActiveRecord::Base.connection.tables.each do |table|\n      ActiveRecord::Base.connection.drop_table(table, force: :cascade)\n    end\n    ActiveRecord::Schema.verbose = false\n\n    ActiveRecord::Schema.define(version: 1) do\n      create_table(:entities)\n      create_table(:companies) do |t|\n        t.belongs_to :entity\n      end\n      create_table(:employees) do |t|\n        t.belongs_to :company\n      end\n      create_table(:assignments) do |t|\n        t.belongs_to :employee\n      end\n    end\n\n    stub_const(\"Entity\", Class.new(ActiveRecord::Base) do\n      self.table_name = :entities\n\n      has_one :company, class_name: \"Company\"\n\n      class << self\n        def name\n          \"Entity\"\n        end\n\n        def perform!\n          @count = count + 1\n        end\n\n        def count\n          @count || 0\n        end\n      end\n\n      n1_optimized :data do\n        def perform(elements)\n          Entity.perform!\n\n          elements.each { |element| fulfill(element, [element]) }\n        end\n      end\n\n      n1_optimized :with_arguments do\n        argument :something\n\n        def perform(elements)\n          Entity.perform!\n\n          elements.each { |element| fulfill(element, [element, something]) }\n        end\n      end\n    end)\n\n    stub_const(\"Company\", Class.new(ActiveRecord::Base) do\n      self.table_name = :companies\n\n      belongs_to :entity, class_name: \"Entity\"\n      has_many :employees, class_name: \"Employee\"\n\n      class << self\n        def name\n          \"Company\"\n        end\n\n        def perform!\n          @count = count + 1\n        end\n\n        def count\n          @count || 0\n        end\n      end\n\n      n1_optimized :data do\n        def perform(elements)\n          Company.perform!\n\n          hash = Entity.where(id: elements.map(&:entity_id)).index_by(&:id)\n          elements.each { |element| fulfill(element, hash[element.entity_id]) }\n        end\n      end\n\n      n1_optimized :with_arguments do\n        argument :something\n\n        def perform(elements)\n          Company.perform!\n\n          hash = Entity.where(id: elements.map(&:entity_id)).index_by(&:id)\n          elements.each { |element| fulfill(element, [hash[element.entity_id], something]) }\n        end\n      end\n\n      n1_optimized :with_question_mark? do\n        argument :something\n\n        def perform(elements)\n          Entity.perform!\n\n          elements.each { |element| fulfill(element, [element, something]) }\n        end\n      end\n\n      n1_optimized :employee_data do\n        def perform(elements)\n          Company.perform!\n          employees_hash = Employee.where(company_id: elements.map(&:id)).group_by(&:company_id)\n          elements.each { |element| fulfill(element, employees_hash[element.id] || []) }\n        end\n      end\n    end)\n\n    stub_const(\"Employee\", Class.new(ActiveRecord::Base) do\n      self.table_name = :employees\n\n      belongs_to :company, class_name: \"Company\"\n      has_many :assignments, class_name: \"Assignment\"\n\n      class << self\n        def name\n          \"Employee\"\n        end\n\n        def perform!\n          @count = count + 1\n        end\n\n        def count\n          @count || 0\n        end\n      end\n    end)\n\n    stub_const(\"Assignment\", Class.new(ActiveRecord::Base) do\n      self.table_name = :assignments\n\n      belongs_to :employee, class_name: \"Employee\"\n\n      class << self\n        def name\n          \"Assignment\"\n        end\n\n        def perform!\n          @count = count + 1\n        end\n\n        def count\n          @count || 0\n        end\n      end\n    end)\n  end\n\n  let(:loader) do\n    Class.new(N1Loader::Loader) do\n      def perform(elements)\n        elements.each { |element| fulfill(element, [element]) }\n      end\n    end\n  end\n\n  let(:object) { Entity.create! }\n\n  it \"works\" do\n    expect { object.data }.to change(Entity, :count).by(1)\n    expect { object.data }.not_to change(Entity, :count)\n\n    expect(object.data).to eq([object])\n  end\n\n  describe \"loaded comparison\" do\n    it \"compares by value\" do\n      instance = loader.new([object])\n\n      expect(instance.for(object)).to eq([object])\n      expect(instance.for(Entity.find(object.id))).to eq([object])\n\n      expect { instance.for(Entity.create!) }.to raise_error(N1Loader::NotLoaded)\n    end\n  end\n\n  describe \"question mark support\" do\n    it \"works\" do\n      expect do\n        Company.includes(:with_question_mark?).each do |company|\n          expect(company.with_question_mark?(something: \"something\")).to eq([company, \"something\"])\n        end\n      end.not_to raise_error\n    end\n  end\n\n  context \"with preloader\" do\n    let(:objects) { [Entity.create!, Entity.create!] }\n\n    it \"works\" do\n      expect { N1Loader::Preloader.new(objects).preload(:data) }.not_to change(Entity, :count)\n\n      expect do\n        objects.each do |object|\n          expect(object.data).to eq([object])\n        end\n      end.to change(Entity, :count).by(1)\n    end\n  end\n\n  context \"with includes\" do\n    let(:objects) { Entity.includes(:data) }\n\n    before do\n      Entity.create!\n      Entity.create!\n    end\n\n    it \"works\" do\n      expect do\n        objects.each do |object|\n          expect(object.data).to eq([object])\n        end\n      end\n        .to make_database_queries(matching: /entities/, count: 1)\n        .and make_database_queries(count: 1)\n        .and change(Entity, :count).by(1)\n    end\n\n    context \"when preloading AR\" do\n      let(:objects) { Entity.includes(:company) }\n\n      it \"doesn't set context further for N1Loader\" do\n        expect do\n          objects.each do |object|\n            expect(object.company.data).to eq(object)\n          end\n            .to make_database_queries(matching: /companies/, count: 1)\n            .and make_database_queries(matching: /entities/, count: 2)\n            .and make_database_queries(count: 3)\n        end\n      end\n    end\n\n    context \"with arguments\" do\n      before { skip \"unsupported by ArLazyPreload\" if ar_lazy_preload_defined? }\n\n      let(:objects) { Entity.includes(:with_arguments) }\n\n      it \"works\" do\n        expect do\n          expect do\n            objects.each do |object|\n              object.with_arguments(something: \"something\")\n            end\n          end.to change(Entity, :count).by(1)\n          expect do\n            objects.each do |object|\n              object.with_arguments(something: \"something\")\n            end\n          end.not_to change(Entity, :count)\n          expect do\n            objects.each do |object|\n              object.with_arguments(something: \"anything\")\n            end\n          end.to change(Entity, :count).by(1)\n\n          objects.each do |object|\n            expect(object.with_arguments(something: \"something\")).to eq([object, \"something\"])\n          end\n        end\n          .to make_database_queries(matching: /entities/, count: 1)\n          .and make_database_queries(count: 1)\n      end\n    end\n  end\n\n  context \"with nested includes\" do\n    let(:objects) { Entity.includes(company: %i[entity data] + [{ employee_data: :assignments }]) }\n\n    before do\n      Company.create!(entity: Entity.create!, employees: [Employee.create!(assignments: [Assignment.create!])])\n      Company.create!(entity: Entity.create!, employees: [Employee.create!(assignments: [Assignment.create!])])\n    end\n\n    it \"works\" do\n      expect do\n        objects.each do |object|\n          expect(object.company.data).to eq(object)\n        end\n      end\n        .to make_database_queries(matching: /entities/, count: 2)\n        .and make_database_queries(matching: /companies/, count: 1)\n        .and make_database_queries(matching: /employees/, count: 1)\n        .and make_database_queries(matching: /assignments/, count: 1)\n        .and make_database_queries(count: 5)\n        .and change(Company, :count).by(2)\n      objects.each do |object|\n        expect(object.company.employee_data).to eq(object.company.employees)\n        expect(object.company.employee_data.map(&:assignments)).to eq(object.company.employees.map(&:assignments))\n      end\n    end\n\n    context \"with arguments\" do\n      let(:objects) { Entity.includes(company: :with_arguments) }\n\n      before { skip \"unsupported by ActiveRecord 6\" if ar_version == 6 }\n\n      it \"works\" do\n        expect do\n          objects.each do |object|\n            expect(object.company.with_arguments(something: \"something\")).to eq([object, \"something\"])\n          end\n        end\n          .to make_database_queries(matching: /entities/, count: 2)\n          .and make_database_queries(matching: /companies/, count: 1)\n          .and make_database_queries(count: 3)\n          .and change(Company, :count).by(1)\n      end\n    end\n  end\n\n  context \"with deep includes\" do\n    let(:objects) { Company.includes(data: :company) }\n\n    before do\n      Company.create!(entity: Entity.create!)\n      Company.create!(entity: Entity.create!)\n    end\n\n    it \"works\" do\n      expect do\n        objects.each do |object|\n          expect(object.data.company.id).to eq(object.id)\n        end\n      end\n        .to make_database_queries(matching: /entities/, count: 1)\n        .and make_database_queries(matching: /companies/, count: 2)\n        .and make_database_queries(count: 3)\n        .and change(Company, :count).by(1)\n    end\n\n    context \"with arguments\" do\n      let(:objects) { Company.includes(with_arguments: :company) }\n\n      it \"doesn't work\" do\n        expect do\n          objects.each do |object|\n            expect(object.with_arguments(something: \"something\").first.company.id).to eq(object.id)\n          end\n        end.to raise_error(N1Loader::ActiveRecord::InvalidPreloading)\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "spec/ar_lazy_preload_spec.rb",
    "content": "# frozen_string_literal: true\n\nRSpec.describe \"N1Loader AR Lazy Preload integration\" do\n  require_relative \"../lib/n1_loader/ar_lazy_preload\"\n\n  ActiveSupport.on_load(:active_record) do\n    ActiveRecord::Base.include(ArLazyPreload::Base)\n\n    ActiveRecord::Relation.prepend(ArLazyPreload::Relation)\n    ActiveRecord::AssociationRelation.prepend(ArLazyPreload::AssociationRelation)\n    ActiveRecord::Relation::Merger.prepend(ArLazyPreload::Merger)\n\n    [\n      ActiveRecord::Associations::CollectionAssociation,\n      ActiveRecord::Associations::Association\n    ].each { |klass| klass.prepend(ArLazyPreload::Association) }\n\n    ActiveRecord::Associations::CollectionAssociation.prepend(ArLazyPreload::CollectionAssociation)\n    ActiveRecord::Associations::CollectionProxy.prepend(ArLazyPreload::CollectionProxy)\n\n    ArLazyPreload::Preloader.patch_for_rails_7! if ActiveRecord::VERSION::MAJOR >= 7\n  end\n\n  before do\n    ActiveRecord::Base.establish_connection(adapter: \"sqlite3\", database: \":memory:\")\n    ActiveRecord::Base.connection.tables.each do |table|\n      ActiveRecord::Base.connection.drop_table(table, force: :cascade)\n    end\n    ActiveRecord::Schema.verbose = false\n\n    ActiveRecord::Schema.define(version: 1) do\n      create_table(:entities)\n      create_table(:companies) do |t|\n        t.belongs_to :entity\n      end\n    end\n\n    stub_const(\"Entity\", Class.new(ActiveRecord::Base) do\n      self.table_name = :entities\n\n      has_one :company, class_name: \"Company\"\n\n      class << self\n        def name\n          \"Entity\"\n        end\n\n        def perform!\n          @count = count + 1\n        end\n\n        def count\n          @count || 0\n        end\n      end\n\n      n1_optimized :data do\n        def perform(elements)\n          elements.first.class.perform!\n\n          elements.each { |element| fulfill(element, [element]) }\n        end\n      end\n\n      n1_optimized :with_arguments do\n        argument :something\n\n        def perform(elements)\n          Entity.perform!\n\n          elements.each { |element| fulfill(element, [element, something]) }\n        end\n      end\n    end)\n\n    stub_const(\"Company\", Class.new(ActiveRecord::Base) do\n      self.table_name = :companies\n\n      belongs_to :entity, class_name: \"Entity\"\n\n      class << self\n        def name\n          \"Company\"\n        end\n\n        def perform!\n          @count = count + 1\n        end\n\n        def count\n          @count || 0\n        end\n      end\n\n      n1_optimized :data do\n        def perform(elements)\n          elements.first.class.perform!\n\n          hash = Entity.where(id: elements.map(&:entity_id)).index_by(&:id)\n          elements.each { |element| fulfill(element, hash[element.entity_id]) }\n        end\n      end\n\n      n1_optimized :with_arguments do\n        argument :something\n\n        def perform(elements)\n          Company.perform!\n\n          hash = Entity.where(id: elements.map(&:entity_id)).index_by(&:id)\n          elements.each { |element| fulfill(element, [hash[element.entity_id], something]) }\n        end\n      end\n\n      n1_optimized :with_question_mark? do\n        argument :something\n\n        def perform(elements)\n          Entity.perform!\n\n          elements.each { |element| fulfill(element, [element, something]) }\n        end\n      end\n    end)\n  end\n\n  before do\n    Company.create!(entity: Entity.create!)\n    Company.create!(entity: Entity.create!)\n  end\n\n  describe \"question mark support\" do\n    it \"works\" do\n      expect do\n        Company.preload_associations_lazily.each do |company|\n          expect(company.with_question_mark?(something: \"something\")).to eq([company, \"something\"])\n        end\n      end.not_to raise_error\n    end\n  end\n\n  it \"works\" do\n    expect do\n      Company.preload_associations_lazily.all.map(&:data)\n    end\n      .to make_database_queries(matching: /entities/, count: 1)\n      .and make_database_queries(matching: /companies/, count: 1)\n      .and make_database_queries(count: 2)\n      .and change(Company, :count).by(1)\n\n    expect do\n      Entity.preload_associations_lazily.all.map(&:company).map(&:data)\n    end\n      .to make_database_queries(matching: /entities/, count: 2)\n      .and make_database_queries(matching: /companies/, count: 1)\n      .and make_database_queries(count: 3)\n      .and change(Company, :count).by(1)\n\n    expect do\n      Company.preload_associations_lazily.all.map(&:data).map(&:company)\n    end\n      .to make_database_queries(matching: /entities/, count: 1)\n      .and make_database_queries(matching: /companies/, count: 2)\n      .and make_database_queries(count: 3)\n      .and change(Company, :count).by(1)\n\n    expect do\n      Company.lazy_preload(data: :company).map(&:data).map(&:company)\n    end\n      .to make_database_queries(matching: /entities/, count: 1)\n      .and make_database_queries(matching: /companies/, count: 2)\n      .and make_database_queries(count: 3)\n      .and change(Company, :count).by(1)\n  end\n\n  context \"with arguments\" do\n    it \"works\" do\n      expect do\n        Company.preload_associations_lazily.all.each { |company| company.with_arguments(something: \"something\") }\n      end\n        .to make_database_queries(matching: /entities/, count: 1)\n        .and make_database_queries(matching: /companies/, count: 1)\n        .and make_database_queries(count: 2)\n        .and change(Company, :count).by(1)\n\n      expect do\n        Entity.preload_associations_lazily.all.each { |entity| entity.company.with_arguments(something: \"something\") }\n      end\n        .to make_database_queries(matching: /entities/, count: 2)\n        .and make_database_queries(matching: /companies/, count: 1)\n        .and make_database_queries(count: 3)\n\n      expect do\n        Company.preload_associations_lazily.each do |company|\n          company.with_arguments(something: \"something\").first.company\n        end\n      end\n        .to make_database_queries(matching: /entities/, count: 1)\n        .and make_database_queries(matching: /companies/, count: 2)\n        .and make_database_queries(count: 3)\n\n      expect do\n        Company.lazy_preload(with_arguments: :company).each do |company|\n          company.with_arguments(something: \"something\").first.company\n        end\n      end\n        .to make_database_queries(matching: /entities/, count: 1)\n        .and make_database_queries(matching: /companies/, count: 2)\n        .and make_database_queries(count: 3)\n    end\n  end\n\n  describe \"isolated loaders\" do\n    let(:loader) do\n      Class.new(N1Loader::Loader) do\n        argument :something\n\n        def perform(_companies)\n          Company.perform!\n\n          hash = Entity.where(id: elements.map(&:entity_id)).index_by(&:id)\n          elements.each { |element| fulfill(element, [hash[element.entity_id], something]) }\n        end\n      end\n    end\n\n    it \"works with ArLazyPreload context\" do\n      companies = Company.preload_associations_lazily.order(:id).to_a\n      entity1 = Entity.first\n      entity2 = Entity.second\n\n      loaded1 = loader.for(companies.first, something: \"tmp\")\n      loaded2 = loader.for(companies.second, something: \"tmp\")\n\n      expect(loaded1).to eq([entity1, \"tmp\"])\n      expect(loaded2).to eq([entity2, \"tmp\"])\n      expect(loaded1[0].lazy_preload_context)\n        .to be_present\n        .and eq(loaded2[0].lazy_preload_context)\n\n      expect do\n        companies.map do |company|\n          loader.for(company, something: \"something\")\n        end\n      end\n        .to make_database_queries(matching: /entities/, count: 1)\n        .and make_database_queries(count: 1)\n        .and change(Company, :count).by(1)\n\n      expect do\n        companies.each do |company|\n          loader.for(company, something: \"anything\")\n        end\n      end\n        .to make_database_queries(matching: /entities/, count: 1)\n        .and make_database_queries(count: 1)\n        .and change(Company, :count).by(1)\n    end\n\n    context \"when object does not have ArLazyPreload context\" do\n      it \"raises an error\" do\n        expect do\n          loader.for(\"string\", something: \"something\")\n        end.to raise_error(N1Loader::Loader::UnsupportedArLazyPreload)\n        expect do\n          loaded = loader.for(Company.first, something: \"something\")\n          expect(loaded[0].lazy_preload_context).to be_present\n        end.not_to raise_error\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "spec/goldiloader_spec.rb",
    "content": "# frozen_string_literal: true\n\nRSpec.describe \"N1Loader Goldiloader integration\" do\n  require_relative \"../lib/n1_loader/goldiloader\"\n\n  before do\n    ActiveRecord::Base.establish_connection(adapter: \"sqlite3\", database: \":memory:\")\n    ActiveRecord::Base.connection.tables.each do |table|\n      ActiveRecord::Base.connection.drop_table(table, force: :cascade)\n    end\n    ActiveRecord::Schema.verbose = false\n\n    ActiveRecord::Schema.define(version: 1) do\n      create_table(:entities)\n      create_table(:companies) do |t|\n        t.belongs_to :entity\n      end\n    end\n\n    stub_const(\"Entity\", Class.new(ActiveRecord::Base) do\n      self.table_name = :entities\n\n      has_one :company, class_name: \"Company\"\n\n      class << self\n        def name\n          \"Entity\"\n        end\n\n        def perform!\n          @count = count + 1\n        end\n\n        def count\n          @count || 0\n        end\n      end\n\n      n1_optimized :data do\n        def perform(elements)\n          elements.first.class.perform!\n\n          elements.each { |element| fulfill(element, [element]) }\n        end\n      end\n\n      n1_optimized :with_arguments do\n        argument :something\n\n        def perform(elements)\n          Entity.perform!\n\n          elements.each { |element| fulfill(element, [element, something]) }\n        end\n      end\n    end)\n\n    stub_const(\"Company\", Class.new(ActiveRecord::Base) do\n      self.table_name = :companies\n\n      belongs_to :entity, class_name: \"Entity\"\n\n      class << self\n        def name\n          \"Company\"\n        end\n\n        def perform!\n          @count = count + 1\n        end\n\n        def count\n          @count || 0\n        end\n      end\n\n      n1_optimized :data do\n        def perform(elements)\n          elements.first.class.perform!\n\n          hash = Entity.where(id: elements.map(&:entity_id)).index_by(&:id)\n          elements.each { |element| fulfill(element, hash[element.entity_id]) }\n        end\n      end\n\n      n1_optimized :with_arguments do\n        argument :something\n\n        def perform(elements)\n          Company.perform!\n\n          hash = Entity.where(id: elements.map(&:entity_id)).index_by(&:id)\n          elements.each { |element| fulfill(element, [hash[element.entity_id], something]) }\n        end\n      end\n\n      n1_optimized :with_question_mark? do\n        argument :something\n\n        def perform(elements)\n          Entity.perform!\n\n          elements.each { |element| fulfill(element, [element, something]) }\n        end\n      end\n    end)\n  end\n\n  before do\n    Company.create!(entity: Entity.create!)\n    Company.create!(entity: Entity.create!)\n  end\n\n  describe \"question mark support\" do\n    it \"works\" do\n      expect do\n        Company.all.each do |company|\n          expect(company.with_question_mark?(something: \"something\")).to eq([company, \"something\"])\n        end\n      end.not_to raise_error\n    end\n  end\n\n  it \"works\" do\n    expect do\n      Company.all.map(&:data)\n    end\n      .to make_database_queries(matching: /entities/, count: 1)\n      .and make_database_queries(matching: /companies/, count: 1)\n      .and make_database_queries(count: 2)\n      .and change(Company, :count).by(1)\n\n    expect do\n      Entity.all.map(&:company).map(&:data)\n    end\n      .to make_database_queries(matching: /entities/, count: 2)\n      .and make_database_queries(matching: /companies/, count: 1)\n      .and make_database_queries(count: 3)\n      .and change(Company, :count).by(1)\n\n    expect do\n      Company.all.map(&:data).map(&:company)\n    end\n      .to make_database_queries(matching: /entities/, count: 1)\n      .and make_database_queries(matching: /companies/, count: 2)\n      .and make_database_queries(count: 3)\n      .and change(Company, :count).by(1)\n  end\n\n  context \"with arguments\" do\n    it \"works\" do\n      expect do\n        Company.all.each { |company| company.with_arguments(something: \"something\") }\n      end\n        .to make_database_queries(matching: /entities/, count: 1)\n        .and make_database_queries(matching: /companies/, count: 1)\n        .and make_database_queries(count: 2)\n        .and change(Company, :count).by(1)\n\n      expect do\n        Entity.all.each { |entity| entity.company.with_arguments(something: \"something\") }\n      end\n        .to make_database_queries(matching: /entities/, count: 2)\n        .and make_database_queries(matching: /companies/, count: 1)\n        .and make_database_queries(count: 3)\n\n      expect do\n        Company.all.each do |company|\n          company.with_arguments(something: \"something\").first.company\n        end\n      end\n        .to make_database_queries(matching: /entities/, count: 1)\n        .and make_database_queries(matching: /companies/, count: 2)\n        .and make_database_queries(count: 3)\n    end\n  end\n\n  describe \"isolated loaders\" do\n    let(:loader) do\n      Class.new(N1Loader::Loader) do\n        argument :something\n\n        def perform(_companies)\n          Company.perform!\n\n          hash = Entity.where(id: elements.map(&:entity_id)).index_by(&:id)\n          elements.each { |element| fulfill(element, [hash[element.entity_id], something]) }\n        end\n      end\n    end\n\n    it \"works with Goldiloader context\" do\n      companies = Company.order(:id).to_a\n      entity1 = Entity.first\n      entity2 = Entity.second\n\n      loaded1 = loader.for(companies.first, something: \"tmp\")\n      loaded2 = loader.for(companies.second, something: \"tmp\")\n\n      expect(loaded1).to eq([entity1, \"tmp\"])\n      expect(loaded2).to eq([entity2, \"tmp\"])\n      expect(loaded1[0].auto_include_context)\n        .to be_present\n        .and eq(loaded2[0].auto_include_context)\n\n      expect do\n        companies.map do |company|\n          loader.for(company, something: \"something\")\n        end\n      end\n        .to make_database_queries(matching: /entities/, count: 1)\n        .and make_database_queries(count: 1)\n        .and change(Company, :count).by(1)\n\n      expect do\n        companies.each do |company|\n          loader.for(company, something: \"anything\")\n        end\n      end\n        .to make_database_queries(matching: /entities/, count: 1)\n        .and make_database_queries(count: 1)\n        .and change(Company, :count).by(1)\n    end\n\n    context \"when object does not have Goldiloader context\" do\n      it \"raises an error\" do\n        expect do\n          loader.for(\"string\", something: \"something\")\n        end.to raise_error(N1Loader::Loader::UnsupportedGoldiloader)\n        expect do\n          loaded = loader.for(Company.first, something: \"something\")\n          expect(loaded[0].auto_include_context).to be_present\n        end.not_to raise_error\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "spec/n1_loader_spec.rb",
    "content": "# frozen_string_literal: true\n\nRSpec.describe N1Loader do\n  let(:loader) do\n    Class.new(N1Loader::Loader) do\n      def perform(elements)\n        elements.each { |element| fulfill(element, [element]) }\n      end\n    end\n  end\n\n  let(:klass) do\n    custom_loader = loader\n\n    Class.new do\n      include N1Loader::Loadable\n\n      class << self\n        def perform!\n          @count = count + 1\n        end\n\n        def count\n          @count || 0\n        end\n      end\n\n      n1_optimized :inline do |elements|\n        elements.first.class.perform!\n\n        elements.each { |element| fulfill(element, [element]) }\n      end\n\n      n1_optimized :sleepy do |elements|\n        sleep(0.5)\n\n        elements.first.class.perform!\n        elements.each { |element| fulfill(element, [element]) }\n      end\n\n      n1_optimized :custom, custom_loader\n\n      n1_optimized :single_optimized do\n        def single(element)\n          [element]\n        end\n\n        def perform(_elements)\n          raise \"unknown\"\n        end\n      end\n\n      n1_optimized :missing_fulfill do\n        def perform(elements)\n          elements.group_by(&:itself)\n        end\n      end\n\n      n1_optimized :with_arguments do\n        argument :something\n        argument :anything\n\n        def perform(elements)\n          elements.first.class.perform!\n\n          elements.each do |element|\n            fulfill(element, [element, something, anything])\n          end\n        end\n      end\n\n      n1_optimized :with_optional_argument do\n        argument :something, optional: true\n        argument :anything\n\n        def perform(elements)\n          elements.first.class.perform!\n\n          elements.each do |element|\n            fulfill(element, [element, something, anything])\n          end\n        end\n      end\n\n      n1_optimized :with_default_argument do\n        argument :something, default: -> { [] }\n        argument :anything\n\n        def perform(elements)\n          elements.first.class.perform!\n\n          elements.each do |element|\n            fulfill(element, [element, something, anything])\n          end\n        end\n      end\n\n      n1_optimized :with_custom_arguments_key do\n        argument :something\n        argument :anything\n\n        cache_key { something + anything }\n\n        def perform(elements)\n          elements.first.class.perform!\n\n          elements.each do |element|\n            fulfill(element, [element, something, anything])\n          end\n        end\n      end\n\n      n1_optimized :with_question_mark? do\n        argument :something\n\n        def perform(elements)\n          elements.first.class.perform!\n\n          elements.each do |element|\n            fulfill(element, [element, something])\n          end\n        end\n      end\n\n      n1_optimized :new_objects do |elements|\n        elements.first.class.perform!\n\n        objects = elements.map { elements.first.class.new }\n\n        elements.each_with_index do |element, index|\n          fulfill(element, objects[index])\n        end\n      end\n    end\n  end\n\n  let(:child_klass) do\n    Class.new(klass) do\n      n1_optimized :child_something do |elements|\n        elements.first.class.perform!\n\n        elements.each do |element|\n          fulfill(element, [element])\n        end\n      end\n    end\n  end\n\n  let(:object) { klass.new }\n  let(:objects) { [klass.new, klass.new] }\n\n  it \"works with unsupported objects\" do\n    expect do\n      N1Loader::Preloader.new([object, 123]).preload(:with_arguments)\n    end.not_to raise_error(NoMethodError)\n  end\n\n  describe \"thread-safety\" do\n    it \"is thread-safe\" do\n      N1Loader::Preloader.new(objects).preload(:sleepy)\n\n      threads = []\n\n      10.times do\n        threads << Thread.new do\n          objects.each do |obj|\n            expect(obj.sleepy).to eq([obj])\n          end\n        end\n      end\n      threads.each(&:join)\n\n      expect(klass.count).to eq(1)\n    end\n  end\n\n  describe \"error handling\" do\n    it \"raises the same error on the subsequent calls\" do\n      faulty_klass = Class.new do\n        include N1Loader::Loadable\n\n        n1_optimized :faulty do |_|\n          raise StandardError, \"Something went wrong\"\n        end\n      end\n\n      faulty_object = faulty_klass.new\n\n      expect { faulty_object.faulty }.to raise_error(StandardError, \"Something went wrong\")\n      expect { faulty_object.faulty }.to raise_error(StandardError, \"Something went wrong\")\n    end\n  end\n\n  describe \"loaded comparison\" do\n    it \"compares by identity first\" do\n      instance = loader.new(objects)\n\n      expect(objects.first).to equal(objects.first)\n      expect(instance.for(objects.first)).to eq([objects.first])\n\n      expect { instance.for(object) }.to raise_error(N1Loader::NotLoaded)\n    end\n\n    it \"falls back to equality comparison when no identity match\" do\n      equal_klass = Struct.new(:id)\n\n      original = equal_klass.new(1)\n      equal_copy = equal_klass.new(1)\n\n      custom_loader = Class.new(N1Loader::Loader) do\n        def perform(elements)\n          elements.each { |element| fulfill(element, [element]) }\n        end\n      end\n\n      instance = custom_loader.new([original])\n\n      expect(original).not_to equal(equal_copy)\n      expect(instance.for(original)).to eq([original])\n      expect(instance.for(equal_copy)).to eq([original])\n    end\n  end\n\n  context \"when fulfill was not used\" do\n    it \"throws error\" do\n      expect { object.missing_fulfill }\n        .to raise_error(N1Loader::NotFilled, \"Nothing was preloaded, perhaps you forgot to use fulfill method\")\n    end\n  end\n\n  describe \"question mark support\" do\n    it \"works\" do\n      expect { object.with_question_mark?(something: \"something\") }.not_to raise_error\n      expect(object.with_question_mark?(something: \"something\")).to eq([object, \"something\"])\n    end\n  end\n\n  describe \"clear cache\" do\n    it \"works\" do\n      expect { object.inline }.to change(klass, :count).by(1)\n      expect { object.inline }.not_to change(klass, :count)\n      object.n1_clear_cache\n      expect { object.inline }.to change(klass, :count).by(1)\n      expect { object.inline }.not_to change(klass, :count)\n    end\n\n    context \"with parent loader\" do\n      let(:object) { child_klass.new }\n\n      it \"works\" do\n        expect { object.inline }.to change(child_klass, :count).by(1)\n        expect { object.child_something }.to change(child_klass, :count).by(1)\n        expect { object.inline }.not_to change(child_klass, :count)\n        expect { object.child_something }.not_to change(child_klass, :count)\n\n        object.n1_clear_cache\n\n        expect { object.inline }.to change(child_klass, :count).by(1)\n        expect { object.child_something }.to change(child_klass, :count).by(1)\n        expect { object.inline }.not_to change(child_klass, :count)\n        expect { object.child_something }.not_to change(child_klass, :count)\n      end\n    end\n\n    context \"with binding\" do\n      it \"clears binding so objects load independently after cache clear\" do\n        objects.each { |obj| obj.n1_bind_to(objects) }\n\n        # Binding ensures the whole collection loads in one batch\n        expect { objects.first.inline }.to change(klass, :count).by(1)\n        expect { objects.last.inline }.not_to change(klass, :count)\n\n        objects.each(&:n1_clear_cache)\n\n        # After clearing cache and binding, each object loads independently\n        expect { objects.first.inline }.to change(klass, :count).by(1)\n        expect { objects.last.inline }.to change(klass, :count).by(1)\n      end\n    end\n  end\n\n  describe \"arguments support\" do\n    it \"has to receive all arguments\" do\n      expect { object.with_arguments }.to raise_error(N1Loader::MissingArgument)\n      expect { object.with_arguments(something: \"something\") }.to raise_error(N1Loader::MissingArgument)\n      expect { object.with_arguments(\"something\") }.to raise_error(ArgumentError)\n\n      expect(object.with_arguments(something: \"something\",\n                                   anything: \"anything\")).to eq([object,\n                                                                 \"something\", \"anything\"])\n    end\n\n    it \"supports optional arguments\" do\n      expect { object.with_optional_argument }\n        .to raise_error(N1Loader::MissingArgument, \"Loader requires [:anything] arguments but they are missing\")\n      expect(object.with_optional_argument(anything: 2)).to eq([object, nil, 2])\n      expect(object.with_optional_argument(something: 1, anything: 2)).to eq([object, 1, 2])\n      expect { object.with_optional_argument(tmp: 1, anything: 2) }\n        .to raise_error(N1Loader::InvalidArgument, \"Loader doesn't define tmp argument\")\n    end\n\n    it \"supports default arguments\" do\n      expect { object.with_default_argument }\n        .to raise_error(N1Loader::MissingArgument, \"Loader requires [:anything] arguments but they are missing\")\n      expect(object.with_default_argument(anything: 2)).to eq([object, [], 2])\n      expect(object.with_default_argument(something: 1, anything: 2)).to eq([object, 1, 2])\n      expect { object.with_default_argument(tmp: 1, anything: 2) }\n        .to raise_error(N1Loader::InvalidArgument, \"Loader doesn't define tmp argument\")\n    end\n\n    it \"can have custom arguments key\" do\n      # The following two has the same result for custom key, so we only do one perform\n      expect { object.with_custom_arguments_key(something: 1, anything: 2) }.to change(klass, :count).by(1)\n      expect { object.with_custom_arguments_key(something: 2, anything: 1) }.not_to change(klass, :count)\n\n      expect { object.with_custom_arguments_key(something: 2, anything: 3) }.to change(klass, :count).by(1)\n    end\n\n    it \"supports named arguments\" do\n      expect do\n        object.with_custom_arguments_key\n      end.to raise_error(N1Loader::MissingArgument,\n                         \"Loader requires [:something, :anything] arguments but they are missing\")\n      expect do\n        object.with_custom_arguments_key(something: \"something\")\n      end.to raise_error(N1Loader::MissingArgument,\n                         \"Loader requires [:anything] arguments but they are missing\")\n\n      expect(object.with_custom_arguments_key(something: \"something\",\n                                              anything: \"anything\")).to eq([\n                                                                             object, \"something\", \"anything\"\n                                                                           ])\n    end\n\n    it \"supports falsey argument values\" do\n      expect(object.with_default_argument(anything: 2)).to eq([object, [], 2])                      # default value\n      expect(object.with_default_argument(something: false, anything: 2)).to eq([object, false, 2]) # false\n      expect(object.with_default_argument(something: nil, anything: 2)).to eq([object, nil, 2])     # nil\n    end\n\n    it \"works with preloading\" do\n      N1Loader::Preloader.new(objects).preload(:with_arguments)\n\n      expect do\n        objects.each do |object|\n          expect(object.with_arguments(something: \"something\",\n                                       anything: \"anything\")).to eq([object,\n                                                                     \"something\", \"anything\"])\n        end\n      end.to change(klass, :count).by(1)\n    end\n\n    it \"caches based on arguments\" do\n      N1Loader::Preloader.new(objects).preload(:with_arguments, :with_default_argument)\n\n      expect do\n        objects.each { |object| object.with_arguments(something: \"something\", anything: \"anything\") }\n      end.to change(klass, :count).by(1)\n\n      expect do\n        objects.each { |object| object.with_arguments(something: \"something2\", anything: \"anything\") }\n      end.to change(klass, :count).by(1)\n\n      expect do\n        objects.each { |object| object.with_arguments(something: \"something\", anything: \"anything2\") }\n      end.to change(klass, :count).by(1)\n\n      expect do\n        objects.each { |object| object.with_arguments(something: \"something\", anything: \"anything\") }\n      end.not_to change(klass, :count)\n\n      expect do\n        objects.each { |object| object.with_default_argument(something: false, anything: nil) }\n      end.to change(klass, :count).by(1)\n\n      expect do\n        objects.each { |object| object.with_default_argument(something: false, anything: nil) }\n      end.not_to change(klass, :count)\n    end\n\n    it \"supports reloading\" do\n      expect do\n        object.with_arguments(something: \"something\", anything: \"anything\")\n      end.to change(klass, :count).by(1)\n\n      expect do\n        object.with_arguments(something: \"something\", anything: \"anything\", reload: true)\n      end.to change(klass, :count).by(1)\n\n      expect do\n        object.with_arguments(something: \"something\", anything: \"anything\")\n      end.not_to change(klass, :count)\n    end\n  end\n\n  describe \"optimization for single object\" do\n    it \"uses optimization\" do\n      expect(object.single_optimized).to eq([object])\n\n      N1Loader::Preloader.new(objects).preload(:single_optimized)\n      expect { objects.map(&:single_optimized) }.to raise_error(StandardError, \"unknown\")\n    end\n  end\n\n  describe \"isolated loaders\" do\n    it \"does not need injection\" do\n      instance = loader.new(objects)\n\n      objects.each do |object|\n        expect(instance.for(object)).to eq([object])\n      end\n    end\n\n    it \"checks that element was provided\" do\n      instance = loader.new(objects)\n\n      objects.each do |object|\n        expect(instance.for(object)).to eq([object])\n      end\n      expect do\n        instance.for(object)\n      end.to raise_error(N1Loader::NotLoaded, \"The data was not preloaded for the given element\")\n    end\n  end\n\n  describe \"reloading\" do\n    context \"with preloading\" do\n      it \"reloads cached data\" do\n        N1Loader::Preloader.new(objects).preload(:inline)\n\n        expect { objects.map(&:inline) }.to change(klass, :count).by(1)\n\n        N1Loader::Preloader.new(objects).preload(:inline)\n        expect { objects.map(&:inline) }.to change(klass, :count).by(1)\n        expect { objects.map(&:inline) }.not_to change(klass, :count)\n      end\n    end\n\n    context \"without preloading\" do\n      it \"reloads cached data\" do\n        expect { object.inline }.to change(klass, :count).by(1)\n        expect { object.inline(reload: true) }.to change(klass, :count).by(1)\n        expect { object.inline(reload: false) }.not_to change(klass, :count)\n        expect { object.inline }.not_to change(klass, :count)\n      end\n    end\n  end\n\n  context \"with custom loader\" do\n    it \"works\" do\n      expect(object.custom).to eq([object])\n    end\n  end\n\n  context \"without preloading\" do\n    it \"returns right data\" do\n      expect(object.inline).to eq([object])\n    end\n\n    it \"caches data\" do\n      expect { object.inline }.to change(klass, :count).by(1)\n      expect { object.inline }.not_to change(klass, :count)\n    end\n  end\n\n  context \"with preloading\" do\n    it \"returns right data\" do\n      N1Loader::Preloader.new(objects).preload(:inline)\n\n      expect(objects.first.inline).to eq([objects.first])\n      expect(objects.last.inline).to eq([objects.last])\n    end\n\n    it \"lazy loads data\" do\n      expect { N1Loader::Preloader.new(objects).preload(:inline) }.not_to change(klass, :count)\n      expect { objects.map(&:inline) }.to change(klass, :count).by(1)\n    end\n\n    it \"uses preloaded data\" do\n      N1Loader::Preloader.new(objects).preload(:inline)\n\n      expect { objects.map(&:inline) }.to change(klass, :count).by(1)\n      expect { objects.map(&:inline) }.not_to change(klass, :count)\n    end\n  end\n\n  describe \"n1_bind_to\" do\n    it \"raises error if invalid collection was passed\" do\n      expect do\n        objects.each { |obj| obj.n1_bind_to([]) }\n      end.to raise_error N1Loader::InvalidBinding\n\n      expect do\n        objects.first.n1_bind_to([objects.last])\n      end.to raise_error N1Loader::InvalidBinding\n    end\n\n    it \"loads all bound objects in a single batch\" do\n      objects.each { |obj| obj.n1_bind_to(objects) }\n\n      expect { objects.map(&:inline) }.to change(klass, :count).by(1)\n    end\n\n    it \"caches the result after the first load\" do\n      objects.each { |obj| obj.n1_bind_to(objects) }\n\n      expect { objects.map(&:inline) }.to change(klass, :count).by(1)\n      expect { objects.map(&:inline) }.not_to change(klass, :count)\n    end\n\n    it \"lazily loads when the first object is accessed\" do\n      objects.each { |obj| obj.n1_bind_to(objects) }\n\n      expect { objects.first.inline }.to change(klass, :count).by(1)\n      expect { objects.last.inline }.not_to change(klass, :count)\n    end\n\n    it \"propagates context to loaded objects\" do\n      objects.each { |obj| obj.n1_bind_to(objects) }\n\n      expect do\n        objects.each(&:new_objects)\n      end.to change(klass, :count).by(1)\n\n      expect do\n        objects.map(&:new_objects).map(&:inline)\n      end.to change(klass, :count).by(1)\n    end\n  end\n\n  describe \"with preloader\" do\n    it \"doesn't propagate context to loaded objects\" do\n      N1Loader::Preloader.new(objects).preload(:new_objects)\n\n      expect do\n        objects.each(&:new_objects)\n      end.to change(klass, :count).by(1)\n\n      expect do\n        objects.map(&:new_objects).map(&:inline)\n      end.to change(klass, :count).by(2)\n    end\n  end\nend\n"
  },
  {
    "path": "spec/spec_helper.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"n1_loader\"\nrequire \"db-query-matchers\"\n\nDBQueryMatchers.configure do |config|\n  config.schemaless = true\nend\n\nRSpec.configure do |config|\n  # Enable flags like --only-failures and --next-failure\n  config.example_status_persistence_file_path = \".rspec_status\"\n\n  # Disable RSpec exposing methods globally on `Module` and `main`\n  config.disable_monkey_patching!\n\n  config.expect_with :rspec do |c|\n    c.syntax = :expect\n  end\n\n  def ar_lazy_preload_defined?\n    defined?(ArLazyPreload)\n  end\n\n  def ar_version\n    ActiveRecord::VERSION::MAJOR\n  end\nend\n"
  }
]