Full Code of djezzzl/n1_loader for AI

master f54f236b22e8 cached
79 files
126.5 KB
34.8k tokens
233 symbols
1 requests
Download .txt
Repository: djezzzl/n1_loader
Branch: master
Commit: f54f236b22e8
Files: 79
Total size: 126.5 KB

Directory structure:
gitextract_b233zkyp/

├── .github/
│   ├── FUNDING.yml
│   ├── copilot-instructions.md
│   └── workflows/
│       ├── copilot-setup-steps.yml
│       ├── release.yml
│       ├── rubocop.yml
│       └── tests.yml
├── .gitignore
├── .rspec
├── .rubocop.yml
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── activerecord-gemfiles/
│   ├── ar_5_latest.gemfile
│   ├── ar_6_latest.gemfile
│   ├── ar_7_latest.gemfile
│   └── ar_8_latest.gemfile
├── ar_lazy_preload-gemfiles/
│   ├── ar_lazy_preload_0.6.1.gemfile
│   └── ar_lazy_preload_master.gemfile
├── bin/
│   ├── console
│   └── setup
├── examples/
│   ├── active_record_integration.rb
│   ├── ar_lazy_integration.rb
│   ├── ar_lazy_integration_with_isolated_loader.rb
│   ├── arguments_support.rb
│   ├── context/
│   │   ├── service.rb
│   │   ├── setup_ar_lazy.rb
│   │   └── setup_database.rb
│   ├── core.rb
│   ├── goldiloader_integration.rb
│   ├── goldiloader_integration_with_isolated_loader.rb
│   ├── graphql.rb
│   ├── isolated_loader.rb
│   ├── lazy_loading.rb
│   ├── n1_bind_to.rb
│   ├── reloading.rb
│   ├── shared_loader.rb
│   └── single_case.rb
├── goldiloader-gemfiles/
│   └── goldiloader.gemfile
├── guides/
│   └── enhanced-activerecord.md
├── lib/
│   ├── n1_loader/
│   │   ├── active_record/
│   │   │   ├── associations_preloader.rb
│   │   │   ├── associations_preloader_v5.rb
│   │   │   ├── associations_preloader_v6.rb
│   │   │   ├── base.rb
│   │   │   ├── loader.rb
│   │   │   └── loader_collection.rb
│   │   ├── active_record.rb
│   │   ├── ar_lazy_preload/
│   │   │   ├── associated_context_builder.rb
│   │   │   ├── context.rb
│   │   │   ├── context_adapter.rb
│   │   │   ├── loadable.rb
│   │   │   ├── loader.rb
│   │   │   ├── loader_collection_patch.rb
│   │   │   ├── loader_patch.rb
│   │   │   └── preloader_patch.rb
│   │   ├── ar_lazy_preload.rb
│   │   ├── core/
│   │   │   ├── loadable.rb
│   │   │   ├── loader.rb
│   │   │   ├── loader_builder.rb
│   │   │   ├── loader_collection.rb
│   │   │   └── preloader.rb
│   │   ├── goldiloader/
│   │   │   ├── context.rb
│   │   │   ├── context_adapter.rb
│   │   │   ├── loadable.rb
│   │   │   ├── loader.rb
│   │   │   ├── loader_collection_patch.rb
│   │   │   ├── loader_patch.rb
│   │   │   └── preloader_patch.rb
│   │   ├── goldiloader.rb
│   │   └── version.rb
│   └── n1_loader.rb
├── n1_loader.gemspec
└── spec/
    ├── activerecord_spec.rb
    ├── ar_lazy_preload_spec.rb
    ├── goldiloader_spec.rb
    ├── n1_loader_spec.rb
    └── spec_helper.rb

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/FUNDING.yml
================================================
# Supported platforms for funding
open_collective: n1_loader
github: djezzzl


================================================
FILE: .github/copilot-instructions.md
================================================
# Copilot Instructions

Every PR must have no errors from:

```
bundle exec rubocop
bundle exec rspec spec/n1_loader_spec.rb
bundle exec rspec spec/n1_loader_spec.rb spec/activerecord_spec.rb
bundle exec rspec spec/n1_loader_spec.rb spec/activerecord_spec.rb spec/ar_lazy_preload_spec.rb
```


================================================
FILE: .github/workflows/copilot-setup-steps.yml
================================================
name: Copilot Setup Steps

# Automatically run the setup steps when they are changed to allow for easy validation, and
# allow manual testing through the repository's "Actions" tab
on:
  workflow_dispatch:
  push:
    paths:
      - .github/workflows/copilot-setup-steps.yml
  pull_request:
    paths:
      - .github/workflows/copilot-setup-steps.yml
      
jobs:
  copilot-setup-steps:
    runs-on: ubuntu-latest
    permissions:
      contents: read

    env:
      ACTIVERECORD_GEMFILE: 'ar_8_latest'
      AR_LAZY_PRELOAD_GEMFILE: 'ar_lazy_preload_master'

    steps:
      - uses: actions/checkout@v3

      - name: Set up Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: head

      - name: Install dependencies
        run: bundle install


================================================
FILE: .github/workflows/release.yml
================================================
name: Release

on:
  workflow_dispatch:
    inputs:
      version:
        description: 'Version to release (e.g. 2.1.4)'
        required: true

jobs:
  release:
    runs-on: ubuntu-latest
    if: github.actor == 'djezzzl'
    permissions:
      contents: write

    steps:
      - uses: actions/checkout@v3

      - name: Set up Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: '3.2'
          bundler-cache: true

      - name: Install dependencies
        run: bundle install

      - name: Validate version format
        run: |
          if ! echo "${{ github.event.inputs.version }}" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
            echo "Invalid version format: ${{ github.event.inputs.version }}. Expected semver (e.g. 1.2.3)"
            exit 1
          fi

      - name: Bump version
        run: |
          sed -i 's/VERSION = ".*"/VERSION = "${{ github.event.inputs.version }}"/' lib/n1_loader/version.rb

      - name: Update changelog
        env:
          VERSION: ${{ github.event.inputs.version }}
        run: |
          DATE=$(date +%Y/%m/%d)
          sed -i.bak -e "s|^## \[Unreleased\]|## [$VERSION] - $DATE|" CHANGELOG.md
          rm -f CHANGELOG.md.bak

      - name: Git status
        run: git status

      - name: Commit version bump
        run: |
          git config user.name "Evgenii Demin"
          git config user.email "lawliet.djez@gmail.com"
          git add lib/n1_loader/version.rb CHANGELOG.md
          git commit -m "Release v${{ github.event.inputs.version }}"
          git push origin HEAD

      - name: Release gem
        env:
          GEM_HOST_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }}
        run: bundle exec rake release


================================================
FILE: .github/workflows/rubocop.yml
================================================
name: Rubocop

on:
  pull_request:
  schedule:
    - cron: '0 0 * * 0'

jobs:
  rubocop:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Set up Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: 2.7

      - name: Install dependencies
        run: bundle install

      - name: Run Rubocop
        run: bundle exec rubocop


================================================
FILE: .github/workflows/tests.yml
================================================
name: RSpec tests

on:
  pull_request:
  schedule:
    - cron: '0 0 * * 0'

jobs:
  tests:
    runs-on: ubuntu-latest
    continue-on-error: true
    strategy:
      matrix:
        include:
          - ruby-version: '2.7'
            activerecord-gemfile: 'ar_5_latest'
            ar_lazy_preload-gemfile: 'ar_lazy_preload_0.6.1'

          - ruby-version: '2.7'
            activerecord-gemfile: 'ar_6_latest'
            ar_lazy_preload-gemfile: 'ar_lazy_preload_0.6.1'

          - ruby-version: '3.0'
            activerecord-gemfile: 'ar_6_latest'
            ar_lazy_preload-gemfile: 'ar_lazy_preload_0.6.1'

          - ruby-version: '3.0'
            activerecord-gemfile: 'ar_7_latest'
            ar_lazy_preload-gemfile: 'ar_lazy_preload_master'

          - ruby-version: 'head'
            activerecord-gemfile: 'ar_7_latest'
            ar_lazy_preload-gemfile: 'ar_lazy_preload_master'

          - ruby-version: 'head'
            activerecord-gemfile: 'ar_8_latest'
            ar_lazy_preload-gemfile: 'ar_lazy_preload_master'
    env:
      ACTIVERECORD_GEMFILE: ${{ matrix.activerecord-gemfile }}
      AR_LAZY_PRELOAD_GEMFILE: ${{ matrix.ar_lazy_preload-gemfile }}

    steps:
      - uses: actions/checkout@v3

      - name: Set up Ruby ${{ matrix.ruby-version }}
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: ${{ matrix.ruby-version }}

      - name: Install dependencies
        run: bundle install

      - name: Run Core tests
        run: bundle exec rspec spec/n1_loader_spec.rb

      - name: Run ActiveRecord tests
        run: bundle exec rspec spec/n1_loader_spec.rb spec/activerecord_spec.rb

      - name: Run ArLazyPreload tests
        run: bundle exec rspec spec/n1_loader_spec.rb spec/activerecord_spec.rb spec/ar_lazy_preload_spec.rb

      - name: Run Goldiloader tests
        run: bundle exec rspec spec/n1_loader_spec.rb spec/activerecord_spec.rb spec/ar_lazy_preload_spec.rb


================================================
FILE: .gitignore
================================================
/.bundle/
/.yardoc
/_yardoc/
/coverage/
/doc/
/pkg/
/spec/reports/
/tmp/
/.idea
.rspec_status
Gemfile.lock
.ruby-version
/vendor


================================================
FILE: .rspec
================================================
--format documentation
--color
--require spec_helper


================================================
FILE: .rubocop.yml
================================================
AllCops:
  TargetRubyVersion: 2.5
  Exclude:
    - examples/**/*
    - vendor/bundle/**/*

Style/StringLiterals:
  Enabled: true
  EnforcedStyle: double_quotes

Style/StringLiteralsInInterpolation:
  Enabled: true
  EnforcedStyle: double_quotes

Layout/LineLength:
  Max: 120

Metrics/BlockLength:
  Exclude:
    - spec/**/*


================================================
FILE: CHANGELOG.md
================================================
## [3.0.0] - 2026/03/18

- Add support of [Goldiloader](https://github.com/KentaaNL/goldiloader)

## [2.2.1] - 2026/03/17

- Fix a rare thread-safety issue for setting context 
- Raise errors if `n1_bind_to` received unexpected arguments
- Fix context propagation to loaded objects when using `n1_bind_to`

## [2.2.0] - 2026/03/16

- 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!
- Clear binding when `n1_clear_cache` is called, so objects load independently after a cache reset.

## [2.1.0] - 2026/03/07

- 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!

## [2.0.1] - 2025/12/28

- Add support of Rails 8.

## [2.0.0] - 2025/12/28

- Make loader thread-safe. 
- Make loader idempotent.

## [1.7.4] - 2023/12/18

- Fix nested association does not preload properly. Thanks [dannyongtey](https://github.com/dannyongtey) for reporting and fixing the issue!

## [1.7.3] - 2023/08/04

- Decrease the package size by 60%. 

## [1.7.2] - 2023/08/04

- Refactor core that ended up with speed boost.

## [1.7.1] - 2023/07/30

- Fix interface discrepancy for `N1LoaderReflection`. Thanks [Denis Talakevich](https://github.com/senid231) for suggesting it!

## [1.7.0] - 2023/07/30

Extend the flexibility of loading data comparison. Thanks [Nazar Matus](https://github.com/FunkyloverOne) for suggesting it! 

**BREAKING CHANGES:**

Loose comparison of loaded data. Before loaded data was initialized with identity comparator in mind:

```ruby
@loaded = {}.compare_by_identity
```

Now it will be:

```ruby
@loaded = {}
```

This might bring unwanted results for cases when strict comparison was wanted. 

On the other hand, it gives more flexibility for many other cases, especially with isolated loader.
For example, this will work now, when it wasn't working before.

```ruby
# ActiveRecord object
object = Entity.first

# Initialize isolated loader
instance = loader.new([object])

# This was working before because the loaded object is identical to passed object by `#object_id`
instance.for(object)

# This wasn't working before because the loaded object is not identical to passed one by `#object_id`
# 
# But it will be working now, because object == Entity.find(object.id)
instance.for(Entity.find(object.id))
```

If you need strict comparison support, please feel free to open the issue or the PR.

## [1.6.6] - 2023/07/30

- Fix naive check of required arguments. Thanks [Nazar Matus](https://github.com/FunkyloverOne) for the issue!

## [1.6.5] - 2023/07/30

- Fix nested preloading for ActiveRecord 7. Thanks [Igor Gonchar](https://github.com/gigorok) for the issue!

## [1.6.4] - 2023/07/30

- Add support of `n1_optimized` ending with `?` (question mark). Thanks [Ilya Kamenko](https://github.com/Galathius) for the suggestion!

## [1.6.3] - 2022/12/30

- Performance optimization: avoid unnecessary calls. Thanks [Nazar Matus](https://github.com/FunkyloverOne) for the [contribution](https://github.com/djezzzl/n1_loader/pull/33).

## [1.6.2] - 2022/11/23

- Add fund metadata

## [1.6.1] - 2022/10/29

- Fix ArLazyPreload context setup when using isolated loaders for objects without the context.

## [1.6.0] - 2022/10/24

- Add support of ArLazyPreload context for isolated loaders.

## [1.5.1] - 2022/09/20

- 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)!

## [1.5.0] - 2022/05/01

- Add support of Rails 7

## [1.4.4] - 2022/04/29

- Inject `N1Loader::Loadable` to `ActiveRecord::Base` automatically
- Make `reload` to call `n1_clear_cache`

## [1.4.3] - 2022-04-13

- Add `default` support to arguments

## [1.4.2] - 2022-03-01

- Add n1_clear_cache method which is useful for cases like reload in ActiveRecord

## [1.4.1] - 2022-02-24

- Fix preloading of invalid objects

## [1.4.0] - 2022-02-22

- add support of optional arguments

BREAKING CHANGES:
- rework arguments to use single definition through `argument <name>` only
- use keyword arguments

## [1.3.0] - 2022-02-22

- add support of named arguments with `argument <name>`

BREAKING CHANGES:
- rename `n1_load` to `n1_optimized`
- rework `def self.arguments_key` to `cache_key`

## [1.2.0] - 2022-01-14

- Introduce arguments support.

## [1.1.0] - 2021-12-27

- Introduce `fulfill` method to abstract the storage.

## [1.0.0] - 2021-12-26

- Various of great features.

## [0.1.0] - 2021-12-16

- Initial release.


================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct

## Our Pledge

We 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.

We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.

## Our Standards

Examples of behavior that contributes to a positive environment for our community include:

* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall community

Examples of unacceptable behavior include:

* The use of sexualized language or imagery, and sexual attention or
  advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
  address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
  professional setting

## Enforcement Responsibilities

Community 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.

Community 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.

## Scope

This 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.

## Enforcement

Instances 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.

All community leaders are obligated to respect the privacy and security of the reporter of any incident.

## Enforcement Guidelines

Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:

### 1. Correction

**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.

**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.

### 2. Warning

**Community Impact**: A violation through a single incident or series of actions.

**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.

### 3. Temporary Ban

**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.

**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.

### 4. Permanent Ban

**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.

**Consequence**: A permanent ban from any sort of public interaction within the community.

## Attribution

This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.

Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).

[homepage]: https://www.contributor-covenant.org

For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.


================================================
FILE: Gemfile
================================================
# frozen_string_literal: true

source "https://rubygems.org"

# Specify your gem's dependencies in n1_loader.gemspec
gemspec

# Hack to make Github work with Circle CI job names with slashes
gemfiles = []
gemfiles << "activerecord-gemfiles/#{ENV["ACTIVERECORD_GEMFILE"]}.gemfile" if ENV["ACTIVERECORD_GEMFILE"]
gemfiles << "ar_lazy_preload-gemfiles/#{ENV["AR_LAZY_PRELOAD_GEMFILE"]}.gemfile" if ENV["AR_LAZY_PRELOAD_GEMFILE"]
gemfiles << "goldiloader-gemfiles/#{ENV["GOLDILOADER_GEMFILE"]}.gemfile" if ENV["GOLDILOADER_GEMFILE"]

gemfiles.each do |path|
  eval(File.read(path)) # rubocop:disable Security/Eval
end


================================================
FILE: LICENSE.txt
================================================
The MIT License (MIT)

Copyright (c) 2021 TODO: Write your name

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


================================================
FILE: README.md
================================================
# N1Loader

[![Gem Version][3]][4]
[![][11]][12]
[![][13]][14]
[![][9]][10]

N1Loader is designed to provide a simple way for avoiding [N+1 issues][7] of any kind. 
For example, it can help with resolving N+1 for:
- database querying (most common case)
- 3rd party service calls
- complex calculations
- and many more

> If the project helps you or your organization, I would be very grateful if you [contribute][15] or [donate][10].  
> Your support is an incredible motivation and the biggest reward for my hard work.

___Support:___ ActiveRecord 5, 6, 7, and 8.

Follow me and stay tuned for the updates:
- [LinkedIn](https://www.linkedin.com/in/evgeniydemin/)
- [Medium](https://evgeniydemin.medium.com/)
- [Twitter](https://twitter.com/EvgeniyDemin/)
- [GitHub](https://github.com/djezzzl)

## Killer feature for GraphQL API

N1Loader in combination with [ArLazyPreload][6] or [Goldiloader][16] is a killer feature for your GraphQL API. 
Give it a try now and see incredible results instantly! Check out the [example](examples/graphql.rb) and start benefiting from it in your projects!

```ruby
gem 'n1_loader', require: 'n1_loader/ar_lazy_preload'
# or
gem 'n1_loader', require: 'n1_loader/goldiloader'
```

## Enhance [ActiveRecord][5]

Are 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!
Check out the detailed [guide](guides/enhanced-activerecord.md) with examples or its [short version](examples/active_record_integration.rb).

```ruby
gem 'n1_loader', require: 'n1_loader/active_record'
```

Are you ready to forget about N+1 once and for all? Install [ArLazyPreload][6] or [Goldiloader][16] and see dreams come true!

```ruby
gem 'n1_loader', require: 'n1_loader/ar_lazy_preload'
# or
gem 'n1_loader', require: 'n1_loader/goldiloader'
```

## Standalone mode

Are you not working with [ActiveRecord][5]? N1Loader is ready to be used as standalone solution! ([full snippet](examples/core.rb))

```ruby
gem 'n1_loader'
```

Want 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))

```ruby
users = [User.new, User.new, User.new]

# Bind users to the collection — lazy access is now automatically batched
users.each { |user| user.n1_bind_to(users) }

users.map(&:optimized_call) # loads all in a single batch, no N+1
```

## How to use it?

N1Loader provides DSL that allows you to define N+1 ready loaders that can 
be injected into your objects in a way that you can avoid N+1 issues.

> _Disclaimer_: examples below are working but designed to show N1Loader potentials only.
In real live applications, N1Loader can be applied anywhere and in more [elegant way](examples/isolated_loader.rb).  

Let's look at simple example below ([full snippet](examples/active_record_integration.rb)):
```ruby
class User < ActiveRecord::Base
  has_many :payments

  n1_optimized :payments_total do |users|
    total_per_user = 
      Payment.group(:user_id)
        .where(user: users)
        .sum(:amount)
        .tap { |h| h.default = 0 }

    users.each do |user|
      total = total_per_user[user.id]
      fulfill(user, total)
    end
  end
end

class Payment < ActiveRecord::Base
  belongs_to :user

  validates :amount, presence: true
end

# A user has many payments. 
# Assuming, we want to know for group of users, what is a total of their payments, we can do the following:

# Has N+1 issue
p User.all.map { |user| user.payments.sum(&:amount) }

# Has no N+1 but we load too many data that we don't actually need
p User.all.includes(:payments).map { |user| user.payments.sum(&:amount) }

# Has no N+1 and we load only what we need
p User.all.includes(:payments_total).map { |user| user.payments_total }
```

Let's assume now, that we want to calculate the total of payments for the given period for a group of users. 
N1Loader can do that as well! ([full snippet](examples/arguments_support.rb)) 

```ruby
class User < ActiveRecord::Base
  has_many :payments

  n1_optimized :payments_total do
    argument :from
    argument :to

    def perform(users)
      total_per_user =
        Payment
          .group(:user_id)
          .where(created_at: from..to)
          .where(user: users)
          .sum(:amount)
          .tap { |h| h.default = 0 }

      users.each do |user|
        total = total_per_user[user.id]
        fulfill(user, total)
      end
    end
  end
end

class Payment < ActiveRecord::Base
  belongs_to :user

  validates :amount, presence: true
end

# Has N+1
p User.all.map { |user| user.payments.select { |payment| payment.created_at >= from && payment.created_at <= to }.sum(&:amount) }

# Has no N+1 but we load too many data that we don't need
p User.all.includes(:payments).map { |user| user.payments.select { |payment| payment.created_at >= from && payment.created_at <= to }.sum(&:amount) }

# Has no N+1 and calculation is the most efficient
p User.all.includes(:payments_total).map { |user| user.payments_total(from: from, to: to) }
```

## Features and benefits

- N1Loader doesn't use Promises which means it's easy to debug
- Doesn't require injection to objects, can be used in [isolation](examples/isolated_loader.rb)
- Loads data [lazily](examples/lazy_loading.rb)
- Loaders can be [shared](examples/shared_loader.rb) between multiple classes
- Loaded data can be [re-fetched](examples/reloading.rb)
- Loader can be optimized for [single cases](examples/single_case.rb)
- Loader support [arguments](examples/arguments_support.rb)
- Has [integration](examples/active_record_integration.rb) with [ActiveRecord][5] which makes it brilliant
- Has [integration](examples/ar_lazy_integration.rb) with [ArLazyPreload][6] which makes it excellent
- Has [integration](examples/goldiloader_integration.rb) with [Goldiloader][16] which makes it excellent
- Supports [context sharing](examples/n1_bind_to.rb) for plain Ruby objects without ActiveRecord

### Feature killer for [ArLazyPreload][6] and [Goldiloader][16] integration with isolated loaders

In [version 1.6.0](CHANGELOG.md#160---20221019) isolated loaders were integrated with [ArLazyPreload][6] context.
This means, it isn't required to inject `N1Loader` into your [ActiveRecord][5] models to avoid N+1 issues out of the box.
It is especially great as many engineers are trying to avoid extra coupling between their models/services when it's possible.
And this feature was designed exactly for this without losing an out of a box solution for N+1.

Without 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.

_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.

### Context sharing for plain Ruby objects with `n1_bind_to`

In [version 2.1.0](CHANGELOG.md#210---20260307) context sharing was added for plain Ruby objects.
This allows you to get lazy, N+1-free loading without [ActiveRecord][5] or explicit preloading.

By calling `n1_bind_to(collection)` on each element, you bind them to their shared collection.
When 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.

Have a look at the [example](examples/n1_bind_to.rb) to see how simple it is.

## Funding

### Open Collective Backers

You'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)]

<a href="https://opencollective.com/n1_loader/backer/0/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/0/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/1/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/1/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/2/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/2/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/3/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/3/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/4/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/4/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/5/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/5/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/6/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/6/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/7/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/7/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/8/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/8/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/9/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/9/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/10/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/10/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/11/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/11/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/12/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/12/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/13/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/13/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/14/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/14/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/15/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/15/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/16/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/16/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/17/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/17/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/18/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/18/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/19/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/19/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/20/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/20/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/21/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/21/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/22/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/22/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/23/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/23/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/24/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/24/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/25/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/25/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/26/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/26/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/27/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/27/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/28/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/28/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/29/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/29/avatar.svg"></a>

### Open Collective Sponsors

You'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)]

<a href="https://opencollective.com/n1_loader/sponsor/0/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/0/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/1/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/1/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/2/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/2/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/3/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/3/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/4/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/4/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/5/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/5/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/6/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/6/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/7/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/7/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/8/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/8/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/9/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/9/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/10/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/10/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/11/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/11/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/12/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/12/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/13/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/13/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/14/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/14/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/15/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/15/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/16/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/16/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/17/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/17/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/18/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/18/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/19/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/19/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/20/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/20/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/21/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/21/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/22/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/22/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/23/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/23/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/24/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/24/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/25/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/25/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/26/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/26/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/27/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/27/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/28/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/28/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/29/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/29/avatar.svg"></a>

## Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/djezzzl/n1_loader. 
This 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).

## License

The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).

## Code of Conduct

Everyone 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).

## Changelog

*N1Loader*'s changelog is available [here](CHANGELOG.md).

## Copyright

Copyright (c) Evgeniy Demin. See [LICENSE.txt](LICENSE.txt) for further details.

[3]: https://badge.fury.io/rb/n1_loader.svg
[4]: https://badge.fury.io/rb/n1_loader
[5]: https://github.com/rails/rails/tree/main/activerecord
[6]: https://github.com/DmitryTsepelev/ar_lazy_preload
[7]: https://stackoverflow.com/questions/97197/what-is-the-n1-selects-problem-in-orm-object-relational-mapping
[8]: https://github.com/djezzzl/n1_loader
[9]: https://opencollective.com/n1_loader/tiers/badge.svg
[10]: https://opencollective.com/n1_loader#support
[11]: https://github.com/djezzzl/n1_loader/actions/workflows/tests.yml/badge.svg?branch=master
[12]: https://github.com/djezzzl/n1_loader/actions/workflows/tests.yml?query=event%3Aschedule
[13]: https://github.com/djezzzl/n1_loader/actions/workflows/rubocop.yml/badge.svg?branch=master
[14]: https://github.com/djezzzl/n1_loader/actions/workflows/rubocop.yml?query=event%3Aschedule
[15]: https://github.com/djezzzl/n1_loader#contributing
[16]: https://github.com/KentaaNL/goldiloader


================================================
FILE: Rakefile
================================================
# frozen_string_literal: true

require "bundler/gem_tasks"
require "rspec/core/rake_task"

RSpec::Core::RakeTask.new(:spec)

require "rubocop/rake_task"

RuboCop::RakeTask.new

task default: %i[spec rubocop]


================================================
FILE: activerecord-gemfiles/ar_5_latest.gemfile
================================================
# frozen_string_literal: true

gem "activerecord", "~> 5"
gem "concurrent-ruby", "= 1.3.4"


================================================
FILE: activerecord-gemfiles/ar_6_latest.gemfile
================================================
# frozen_string_literal: true

gem "activerecord", "~> 6"
gem "benchmark"
gem "bigdecimal"
gem "concurrent-ruby", "= 1.3.4"
gem "sqlite3", "~> 1.4"


================================================
FILE: activerecord-gemfiles/ar_7_latest.gemfile
================================================
# frozen_string_literal: true

gem "activerecord", "~> 7"
gem "benchmark"
gem "bigdecimal"
gem "concurrent-ruby", "= 1.3.4"
gem "sqlite3", "< 2.4.0"


================================================
FILE: activerecord-gemfiles/ar_8_latest.gemfile
================================================
# frozen_string_literal: true

gem "activerecord", "~> 8"
gem "benchmark"
gem "bigdecimal"
gem "sqlite3", "< 2.4.0"


================================================
FILE: ar_lazy_preload-gemfiles/ar_lazy_preload_0.6.1.gemfile
================================================
# frozen_string_literal: true

gem "ar_lazy_preload", "= 0.6.1"


================================================
FILE: ar_lazy_preload-gemfiles/ar_lazy_preload_master.gemfile
================================================
# frozen_string_literal: true

gem "ar_lazy_preload", git: "https://github.com/DmitryTsepelev/ar_lazy_preload", branch: "master"


================================================
FILE: bin/console
================================================
#!/usr/bin/env ruby
# frozen_string_literal: true

require "bundler/setup"
require "n1_loader"

# You can add fixtures and/or initialization code here to make experimenting
# with your gem easier. You can also use a different console, if you like.

# (If you use this, don't forget to add pry to your Gemfile!)
# require "pry"
# Pry.start

require "irb"
IRB.start(__FILE__)


================================================
FILE: bin/setup
================================================
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
set -vx

bundle install

# Do any other automated setup that you need to do here


================================================
FILE: examples/active_record_integration.rb
================================================
# frozen_string_literal: true

require "n1_loader/active_record"

require_relative 'context/setup_database'

class User < ActiveRecord::Base
  has_many :payments

  n1_optimized :payments_total do |users|
    total_per_user = Payment.group(:user_id).where(user: users).sum(:amount).tap { |h| h.default = 0 }

    users.each do |user|
      total = total_per_user[user.id]
      fulfill(user, total)
    end
  end
end

class Payment < ActiveRecord::Base
  belongs_to :user

  validates :amount, presence: true
end

fill_database

# Has N+1
p User.all.map { |user| user.payments.sum(&:amount) }
# Has no N+1 but we load too many data that we don't need
p User.all.includes(:payments).map { |user| user.payments.sum(&:amount) }
# Has no N+1 and calculation is the most efficient
p User.all.includes(:payments_total).map(&:payments_total)


================================================
FILE: examples/ar_lazy_integration.rb
================================================
# frozen_string_literal: true

require "n1_loader/ar_lazy_preload"

require_relative 'context/setup_ar_lazy'
require_relative 'context/setup_database'

class User < ActiveRecord::Base
  has_many :payments

  n1_optimized :payments_total do |users|
    total_per_user = Payment.group(:user_id).where(user: users).sum(:amount).tap { |h| h.default = 0 }

    users.each do |user|
      total = total_per_user[user.id]
      fulfill(user, total)
    end
  end
end

class Payment < ActiveRecord::Base
  belongs_to :user

  validates :amount, presence: true
end

fill_database

# Has N+1
p User.all.map { |user| user.payments.sum(&:amount) }

# Has no N+1 and loads only required data
p User.preload_associations_lazily.map(&:payments_total)
# or
ArLazyPreload.config.auto_preload = true
User.all.map(&:payments_total)


================================================
FILE: examples/ar_lazy_integration_with_isolated_loader.rb
================================================
# frozen_string_literal: true

require "n1_loader/ar_lazy_preload"

require_relative 'context/setup_ar_lazy'
require_relative 'context/setup_database'

class Loader < N1Loader::Loader
  def perform(users)
    total_per_user = Payment.group(:user_id).where(user: users).sum(:amount).tap { |h| h.default = 0 }

    users.each do |user|
      total = total_per_user[user.id]
      fulfill(user, total)
    end
  end
end

class User < ActiveRecord::Base
  has_many :payments
end

class Payment < ActiveRecord::Base
  belongs_to :user

  validates :amount, presence: true
end

fill_database

# Has N+1 and loads redundant data
p User.all.map { |user| user.payments.sum(&:amount) }

# Has no N+1 and loads only required data
p User.preload_associations_lazily.all.map { |user| Loader.for(user) }

# or
ArLazyPreload.config.auto_preload = true
p User.all.map { |user| Loader.for(user) }


================================================
FILE: examples/arguments_support.rb
================================================
# frozen_string_literal: true

require "n1_loader/active_record"

require_relative 'context/setup_database'

class User < ActiveRecord::Base
  has_many :payments

  n1_optimized :payments_total do
    # Arguments can be:
    # argument :something, optional: true
    # argument :something, default: -> { 100 }
    #
    # Note: do not use mutable (mostly timing related) defaults like:
    # argument :from, default -> { 2.minutes.from_now }
    # because such values will be unique for every loader call which will make N+1 issue stay
    argument :from
    argument :to

    # This is used to define logic how loaders are compared to each other
    # default is:
    # cache_key { *arguments.map(&:object_id) }
    cache_key { [from, to] }

    def perform(users)
      total_per_user =
        Payment
        .group(:user_id)
        .where(created_at: from..to)
        .where(user: users)
        .sum(:amount)
        .tap { |h| h.default = 0 }

      users.each do |user|
        total = total_per_user[user.id]
        fulfill(user, total)
      end
    end
  end
end

class Payment < ActiveRecord::Base
  belongs_to :user

  validates :amount, presence: true
end

fill_database

from = 2.days.ago
to = 1.day.ago

# Has N+1
p User.all.map { |user|
  user.payments.select do |payment|
    payment.created_at >= from && payment.created_at <= to
  end.sum(&:amount)
}
# Has no N+1 but we load too many data that we don't need
p User.all.includes(:payments).map { |user|
  user.payments.select do |payment|
    payment.created_at >= from && payment.created_at <= to
  end.sum(&:amount)
}
# Has no N+1 and calculation is the most efficient
p User.all.includes(:payments_total).map { |user| user.payments_total(from: from, to: to) }


================================================
FILE: examples/context/service.rb
================================================
# 3rd party service, or database, or anything else that can perform in batches
class Service
  def self.count
    @count ||= 0
  end

  def self.increase!
    @count = (@count || 0) + 1
  end

  def self.receive(*users)
    increase!

    users.flatten.map(&:object_id)
  end

  def self.single(user)
    user.object_id
  end
end

================================================
FILE: examples/context/setup_ar_lazy.rb
================================================
ActiveSupport.on_load(:active_record) do
  ActiveRecord::Base.include(ArLazyPreload::Base)

  ActiveRecord::Relation.prepend(ArLazyPreload::Relation)
  ActiveRecord::AssociationRelation.prepend(ArLazyPreload::AssociationRelation)
  ActiveRecord::Relation::Merger.prepend(ArLazyPreload::Merger)

  [
    ActiveRecord::Associations::CollectionAssociation,
    ActiveRecord::Associations::Association
  ].each { |klass| klass.prepend(ArLazyPreload::Association) }

  ActiveRecord::Associations::CollectionAssociation.prepend(ArLazyPreload::CollectionAssociation)
  ActiveRecord::Associations::CollectionProxy.prepend(ArLazyPreload::CollectionProxy)
end

================================================
FILE: examples/context/setup_database.rb
================================================
require "sqlite3"

ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.connection.tables.each do |table|
  ActiveRecord::Base.connection.drop_table(table, force: :cascade)
end
ActiveRecord::Schema.verbose = false
ActiveRecord::Base.logger = Logger.new($stdout)

ActiveRecord::Schema.define(version: 1) do
  create_table(:payments) do |t|
    t.belongs_to :user
    t.integer :amount
    t.timestamps
  end
  create_table(:users)
end

def fill_database
  10.times do
    user = User.create!
    10.times do
      Payment.create!(user: user, amount: rand(1000))
    end
  end
end

================================================
FILE: examples/core.rb
================================================
# frozen_string_literal: true

require "n1_loader"

require_relative 'context/service'

# Class that wants to request 3rd party service without N+1
class User
  include N1Loader::Loadable

  def unoptimized_call
    Service.receive(self)[0]
  end

  n1_optimized :optimized_call do |users|
    data = Service.receive(users)

    users.each_with_index do |user, index|
      fulfill(user, data[index])
    end
  end
end

# works fine for single case
user = User.new
p "Works correctly: #{user.unoptimized_call == user.optimized_call}"

users = [User.new, User.new]

# Has N+1
count_before = Service.count
p users.map(&:unoptimized_call)
p "Has N+1 #{Service.count == count_before + users.count}"

# Has no N+1 via explicit preloading
count_before = Service.count
N1Loader::Preloader.new(users).preload(:optimized_call)
p users.map(&:optimized_call)
p "Has no N+1: #{Service.count == count_before + 1}"

users = [User.new, User.new]

# Has no N+1 via n1_bind_to context sharing (see examples/n1_bind_to.rb)
users.each { |user| user.n1_bind_to(users) }
count_before = Service.count
p users.map(&:optimized_call)
p "Has no N+1: #{Service.count == count_before + 1}"


================================================
FILE: examples/goldiloader_integration.rb
================================================
# frozen_string_literal: true

require "n1_loader/goldiloader"

require_relative "context/setup_database"

class User < ActiveRecord::Base
  has_many :payments

  n1_optimized :payments_total do |users|
    total_per_user = Payment.group(:user_id).where(user: users).sum(:amount).tap { |h| h.default = 0 }

    users.each do |user|
      total = total_per_user[user.id]
      fulfill(user, total)
    end
  end
end

class Payment < ActiveRecord::Base
  belongs_to :user

  validates :amount, presence: true
end

fill_database

# Has N+1
p(User.all.map { |user| user.payments.sum(&:amount) })

# Has no N+1 and loads only required data (Goldiloader auto-batches by default)
p User.all.map(&:payments_total)


================================================
FILE: examples/goldiloader_integration_with_isolated_loader.rb
================================================
# frozen_string_literal: true

require "n1_loader/goldiloader"

require_relative "context/setup_database"

class Loader < N1Loader::Loader
  def perform(users)
    total_per_user = Payment.group(:user_id).where(user: users).sum(:amount).tap { |h| h.default = 0 }

    users.each do |user|
      total = total_per_user[user.id]
      fulfill(user, total)
    end
  end
end

class User < ActiveRecord::Base
  has_many :payments
end

class Payment < ActiveRecord::Base
  belongs_to :user

  validates :amount, presence: true
end

fill_database

# Has N+1 and loads redundant data
p(User.all.map { |user| user.payments.sum(&:amount) })

# Has no N+1 and loads only required data (Goldiloader auto-batches by default)
p(User.all.map { |user| Loader.for(user) })


================================================
FILE: examples/graphql.rb
================================================
# frozen_string_literal: true

require "n1_loader/ar_lazy_preload"
require 'graphql'

require_relative 'context/setup_database'
require_relative 'context/setup_ar_lazy'

class User < ActiveRecord::Base
  has_many :payments

  n1_optimized :payments_total do |users|
    total_per_user = Payment.group(:user_id).where(user: users).sum(:amount).tap { |h| h.default = 0 }

    users.each do |user|
      total = total_per_user[user.id]
      fulfill(user, total)
    end
  end
end

class Payment < ActiveRecord::Base
  belongs_to :user

  validates :amount, presence: true
end

10.times do
  user = User.create!
  10.times do
    Payment.create!(user: user, amount: rand(1000))
  end
end

ArLazyPreload.config.auto_preload = true
# Or use +preload_associations_lazily+ when loading objects from database

class UserType < GraphQL::Schema::Object
  field :payments_total, Integer
end

class QueryType < GraphQL::Schema::Object
  field  :users, [UserType]

  def users
    User.all
  end
end

class Schema < GraphQL::Schema
  query QueryType
end

query_string = <<~GQL
  {
    users {
      paymentsTotal
    }
  }
GQL

# No N+1. And never will be!
p Schema.execute(query_string)['data']


================================================
FILE: examples/isolated_loader.rb
================================================
require 'n1_loader'

class IsolatedLoader < N1Loader::Loader
  def perform(elements)
    elements.each { |element| fulfill(element, [element]) }
  end
end

objects = [1, 2, 3, 4]
loader = IsolatedLoader.new(objects)
objects.each do |object|
  loader.for(object) # => it has no N+1 and it doesn't require to be injected in the class
end

================================================
FILE: examples/lazy_loading.rb
================================================
require 'n1_loader'

require_relative 'context/service'

# Class that wants to request 3rd party service without N+1
class User
  include N1Loader::Loadable

  n1_optimized :optimized_call do |users|
    data = Service.receive(users)

    users.each_with_index do |user, index|
      fulfill(user, data[index])
    end
  end
end

users = [User.new, User.new, User.new]

# Initialized loader but didn't perform it yet
N1Loader::Preloader.new(users).preload(:optimized_call)
p "No calls yet: #{Service.count == 0}"

# First time loading
users.map(&:optimized_call)
p "First time loaded: #{Service.count == 1}"

================================================
FILE: examples/n1_bind_to.rb
================================================
# frozen_string_literal: true

require "n1_loader"

require_relative 'context/service'

# Class that wants to request 3rd party service without N+1
class User
  include N1Loader::Loadable

  n1_optimized :optimized_call do |users|
    data = Service.receive(users)

    users.each_with_index do |user, index|
      fulfill(user, data[index])
    end
  end
end

users = [User.new, User.new, User.new]

# Without n1_bind_to: each user lazily loads independently causing N+1
count_before = Service.count
p users.map(&:optimized_call)
p "Has N+1: #{Service.count == count_before + users.count}"

users = [User.new, User.new, User.new]

# With n1_bind_to: bind users to the collection so lazy loading is automatically batched
users.each { |user| user.n1_bind_to(users) }

count_before = Service.count
p users.map(&:optimized_call)
p "Has no N+1: #{Service.count == count_before + 1}"


================================================
FILE: examples/reloading.rb
================================================
require 'n1_loader'

require_relative 'context/service'

class User
  include N1Loader::Loadable

  n1_optimized :optimized_call do |users|
    data = Service.receive(users)

    users.each_with_index do |user, index|
      fulfill(user, data[index])
    end
  end
end

users = [User.new, User.new, User.new]

# Initialized loader but didn't perform it yet
N1Loader::Preloader.new(users).preload(:optimized_call)
p "No calls yet: #{Service.count == 0}"

# First time loading
users.map(&:optimized_call)
p "First time loaded: #{Service.count == 1}"

users.first.optimized_call(reload: true)
p "Reloaded for this object only: #{Service.count == 2}"

users.first.n1_clear_cache
users.first.optimized_call
p "Reloaded for this object only: #{Service.count == 3}"

================================================
FILE: examples/shared_loader.rb
================================================
require 'n1_loader'

require_relative 'context/service'

# Loader that will be shared between multiple classes
class SharedLoader < N1Loader::Loader
  def perform(objects)
    data = Service.receive(objects)

    objects.each_with_index do |user, index|
      fulfill(user, data[index])
    end
  end
end

class User
  include N1Loader::Loadable

  n1_optimized :optimized_call, SharedLoader
end

class Payment
  include N1Loader::Loadable

  n1_optimized :optimized_call, SharedLoader
end

objects = [User.new, Payment.new, User.new, Payment.new]

N1Loader::Preloader.new(objects).preload(:optimized_call)

# First time loading for all objects
objects.map(&:optimized_call)
p "Loaded for all once: #{Service.count == 1}"

================================================
FILE: examples/single_case.rb
================================================
require 'n1_loader'

require_relative 'context/service'

# Loader that will be shared between multiple classes
class OptimizedLoader < N1Loader::Loader
  def perform(objects)
    data = Service.receive(objects)

    objects.each_with_index do |user, index|
      fulfill(user, data[index])
    end
  end

  def single(object)
    Service.single(object)
  end
end

class User
  include N1Loader::Loadable

  n1_optimized :optimized_call, OptimizedLoader
end

objects = [User.new, User.new]

N1Loader::Preloader.new(objects).preload(:optimized_call)

objects.map(&:optimized_call)
p "Used multi-case perform: #{Service.count == 1}"

User.new.optimized_call
p "Used single-case perform: #{Service.count == 1}"

================================================
FILE: goldiloader-gemfiles/goldiloader.gemfile
================================================
# frozen_string_literal: true

gem "goldiloader"


================================================
FILE: guides/enhanced-activerecord.md
================================================
# Enhanced ActiveRecord

- Do you like `ActiveRecord` preloading?
- How many times have you resolved your N+1 issues with `includes` or `preload`?
- Do you know that preloading has limitations?

In this guide, I'd like to share with you tips and tricks about ActiveRecord
preloading and how you can enhance it to the next level.

Let's start by describing the models.

```ruby
# The model represents users in our application.
class User < ActiveRecord::Base
  # Every user may have from 0 to many payments.
  has_many :payments
end

# The model represents payments in our application.
class Payment < ActiveRecord::Base 
  # Every payment belongs to a user.
  belongs_to :user
end
```

Assuming we want to iterate over a group of users and check how many payments they have, we may do:

```ruby
# The query we want to use to fetch users from the database.
users = User.all
# Iteration over selected users.
users.each do |user|
  # Print amount of user's payments. 
  # This query will be called for every user, bringing an N+1 issue.
  p user.payments.count
end
```

We can fix the N+1 issue above in a second.
We need to add ActiveRecord's `includes` to the query that fetches users.

```ruby
# The query to fetch users with preload payments for every selected user.
users = User.includes(:payments).all
```

Then, we can iterate over the group again without the N+1 issue.

```ruby
users.each do |user|
  p user.payments.count
end
```

Experienced with ActiveRecord person may notice that the iteration above still will have an N+1 issue.
The reason is the `.count` method and its behavior.
This issue brings us to the first tip.

### Tip 1. `count` vs `size` vs `length`

- `count` - always queries the database with `COUNT` query;
- `size` - queries the database with `COUNT` only when there is no preloaded data, returns array length otherwise;
- `length` - always returns array length, in case there is no data, load it first.

_Note:_ be careful with `size` as ordering is critical.

Meaning, for `user = User.first`

```ruby
# Does `COUNT` query
user.payments.size
# Does `SELECT` query
user.payments.each { |payment| }
```

is different from

```ruby
# Does `SELECT` query
user.payments.each { |payment| }
# No query
user.payments.size
```

You may notice that the above solution loads all payment information when the amount is only needed.
There is a well-known solution for this case called [counter_cache](https://guides.rubyonrails.org/association_basics.html#options-for-belongs-to-counter-cache).

To use that, you need to add `payments_count` field to `users` table and adjust `Payment` model.

```ruby
# Migration to add `payments_count` to `users` table.
class AddPaymentsCountToUsers < ActiveRecord::Migration
  def change
    add_column :users, :payments_count, :integer, default: 0, null: false
  end
end

# Change `belongs_to` to have `counter_cache` option.
class Payment < ActiveRecord::Base
  belongs_to :user, counter_cache: true
end
```

_Note:_ avoid adding or removing payments from the database directly or through `insert_all`/`delete`/`delete_all` as
`counter_cache` is using ActiveRecord callbacks to update the field's value.

It's worth mentioning [counter_culture](https://github.com/magnusvk/counter_culture) alternative that has many features compared with the built-in `counter_cache`

## Associations with arguments

Now, let's assume we want to fetch the number of payments in a time frame for every user in a group.

```ruby
from = 1.months.ago
to = Time.current

# Query to fetch users.
users = User.all

users.each do |user|
  # Print the number of payments in a time frame for every user.
  # Database query will be triggered for every user, meaning it has an N+1 issue.
  p user.payments.where(created_at: from...to).count
end
```

ActiveRecord supports defining associations with arguments.

```ruby
class User < ActiveRecord::Base
  has_many :payments, -> (from, to) { where(created_at: from...to) }
end
```

Unfortunately, such associations are not possible to preload with `includes`.
Gladly, there is a solution with [N1Loader](https://github.com/djezzzl/n1_loader/).

```ruby
# Install gem dependencies.
require 'n1_loader/active_record'

class User < ActiveRecord::Base
  n1_optimized :payments_count do
    argument :from 
    argument :to 
    
    def perform(users)
      # Fetch the payment number once for all users.
      payments = Payment.where(user: users).where(created_at: from...to).group(:user_id).count
      
      users.each do |user|
        # Assign preloaded data to every user. 
        # Note: it doesn't use any promises.
        fulfill(user, payments[user.id])
      end
    end
  end
end

from = 1.month.ago 
to = Time.current

# Preload `payments` N1Loader "association". Doesn't query the database yet.
users = User.includes(:payments_count).all

users.each do |user|
  # Queries the database once, meaning has no N+1 issues.
  p user.payments_count(from, to)
end
```

Let's look at another example. Assuming we want to fetch the last payment for every user.
We can try to define scoped `has_one` association and use that.

```ruby
class User < ActiveRecord::Base
  has_one :last_payment, -> { order(id: :desc) }, class_name: 'Payment'
end
```

We can see that preloading is working.

```ruby
users = User.includes(:last_payment)

users.each do |user|
  # No N+1. Last payment was returned.
  p user.last_payment
end
```

At first glance, we may think everything is alright. Unfortunately, it is not.

### Tip 2. Enforce `has_one` associations on the database level

ActiveRecord, fetches all available payments for every user with provided order and then assigns only first payment to the association.
First, such querying is inefficient as we load many redundant information.
But most importantly, this association may lead to big issues. Other engineers may use it, for example,
for `joins(:last_payment)`. Assuming that association has strict agreement on the database level that
a user may have none or a single payment in the database. Apparently, it may not be the case, and some queries
will return unexpected data.

Described issues may be found with [DatabaseConsistency](https://github.com/djezzzl/database_consistency).

Back to the task, we can solve it with [N1Loader](https://github.com/djezzzl/n1_loader) in the following way

```ruby
require 'n1_loader/active_record'

class User < ActiveRecord::Base
  n1_optimized :last_payment do |users|
    subquery = Payment.select('MAX(id)').where(user: users)
    payments = Payment.where(id: subquery).index_by(&:user_id)
    
    users.each do |user|
      fulfill(user, payments[user.id])
    end
  end
end

users = User.includes(:last_payment).all

users.each do |user|
  # Queries the database once, meaning no N+1.
  p user.last_payment
end
```

Attentive 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.
Gladly, there is a simple solution! [ArLazyPreload](https://github.com/DmitryTsepelev/ar_lazy_preload) will make N+1 disappear just by enabling it.
As soon as you need to load association for any record, it will load it once for all records that were fetched along this one.
And it works with ActiveRecord and N1Loader perfectly!

Let's look at the example.

```ruby
# Require N1Loader with ArLazyPreload integration
require 'n1_loader/ar_lazy_preload'

# Enable ArLazyPreload globally, so you don't need to care about `includes` anymore
ArLazyPreload.config.auto_preload = true

class User < ActiveRecord::Base
  has_many :payments

  n1_optimized :last_payment do |users|
    subquery = Payment.select('MAX(id)').where(user: users)
    payments = Payment.where(id: subquery).index_by(&:user_id)

    users.each do |user|
      fulfill(user, payments[user.id])
    end
  end
end

# no need to specify `includes`
users = User.all

users.each do |user|
  p user.payments # no N+1
  p user.last_payment # no N+1
end
```

As 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.
It works great with GraphQL API too. Give it and try and share your feedback!

================================================
FILE: lib/n1_loader/active_record/associations_preloader.rb
================================================
# frozen_string_literal: true

module N1Loader
  module ActiveRecord
    module Associations
      module Preloader # :nodoc:
        N1LoaderReflection = Struct.new(:name, :loader) do
          def options
            {}
          end

          def deprecated?
            false
          end

          def through_reflection?
            false
          end
        end

        def preloaders_for_reflection(reflection, records)
          return super unless reflection.is_a?(N1LoaderReflection)

          N1Loader::Preloader.new(records).preload(reflection.name)
        end

        def grouped_records # rubocop:disable Metrics/PerceivedComplexity, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/AbcSize
          n1_load_records, records = source_records.partition do |record|
            record.class.respond_to?(:n1_loaders) && record.class.n1_loaders[association]
          end

          h = n1_load_records.group_by do |record|
            N1LoaderReflection.new(association, record.class.n1_loaders[association])
          end

          polymorphic_parent = !root? && parent.polymorphic?
          records.each do |record|
            reflection = record.class._reflect_on_association(association)
            next if polymorphic_parent && !reflection || !record.association(association).klass

            (h[reflection] ||= []) << record
          end
          h
        end
      end
    end
  end
end


================================================
FILE: lib/n1_loader/active_record/associations_preloader_v5.rb
================================================
# frozen_string_literal: true

module N1Loader
  module ActiveRecord
    module Associations
      module Preloader # :nodoc:
        N1LoaderReflection = Struct.new(:name, :loader) do
          def options
            {}
          end
        end

        def preloaders_for_one(association, records, scope)
          grouped_records(association, records).flat_map do |reflection, klasses|
            next N1Loader::Preloader.new(records).preload(reflection.name) if reflection.is_a?(N1LoaderReflection)

            klasses.map do |rhs_klass, rs|
              loader = preloader_for(reflection, rs).new(rhs_klass, rs, reflection, scope)
              loader.run self
              loader
            end
          end
        end

        def grouped_records(association, records)
          n1_load_records, records = records.partition do |record|
            record.class.respond_to?(:n1_loaders) && record.class.n1_loaders.key?(association)
          end

          hash = n1_load_records.group_by do |record|
            N1LoaderReflection.new(association, record.class.n1_loaders[association])
          end

          hash.merge(super)
        end
      end
    end
  end
end


================================================
FILE: lib/n1_loader/active_record/associations_preloader_v6.rb
================================================
# frozen_string_literal: true

module N1Loader
  module ActiveRecord
    module Associations
      module Preloader # :nodoc:
        N1LoaderReflection = Struct.new(:name, :loader) do
          def options
            {}
          end
        end

        def preloaders_for_reflection(reflection, records, scope)
          return super unless reflection.is_a?(N1LoaderReflection)

          N1Loader::Preloader.new(records).preload(reflection.name)
        end

        def grouped_records(association, records, polymorphic_parent)
          n1_load_records, records = records.partition do |record|
            record.class.respond_to?(:n1_loaders) && record.class.n1_loaders[association]
          end

          hash = n1_load_records.group_by do |record|
            N1LoaderReflection.new(association, record.class.n1_loaders[association])
          end

          hash.merge(super)
        end
      end
    end
  end
end


================================================
FILE: lib/n1_loader/active_record/base.rb
================================================
# frozen_string_literal: true

module N1Loader
  module ActiveRecord
    # Extension module for ActiveRecord::Base
    module Base
      extend ActiveSupport::Concern

      include N1Loader::Loadable

      # Clear N1Loader cache on reloading the object
      def reload(*)
        n1_clear_cache
        super
      end
    end
  end
end


================================================
FILE: lib/n1_loader/active_record/loader.rb
================================================
# frozen_string_literal: true

N1Loader::Loader.define_method :preloaded_records do
  @preloaded_records ||= loaded? && loaded_by_value.values.flatten
end


================================================
FILE: lib/n1_loader/active_record/loader_collection.rb
================================================
# frozen_string_literal: true

N1Loader::LoaderCollection.define_method :preloaded_records do
  raise N1Loader::ActiveRecord::InvalidPreloading, "Cannot preload loader with arguments" if loader_class.arguments

  with.preloaded_records
end

N1Loader::LoaderCollection.define_method :runnable_loaders do
  [self]
end

N1Loader::LoaderCollection.define_method :run? do
  true
end

N1Loader::LoaderCollection.define_method :future_classes do
  []
end


================================================
FILE: lib/n1_loader/active_record.rb
================================================
# frozen_string_literal: true

# Load core library
require_relative "../n1_loader"

# Load integration dependency
require "active_record"

module N1Loader
  module ActiveRecord
    class InvalidPreloading < N1Loader::Error; end
  end
end

# Library integration
ActiveSupport.on_load(:active_record) do
  require_relative "active_record/loader"
  require_relative "active_record/loader_collection"
  require_relative "active_record/base"

  case ActiveRecord::VERSION::MAJOR
  when 6
    require_relative "active_record/associations_preloader_v6"
    ActiveRecord::Associations::Preloader.prepend(N1Loader::ActiveRecord::Associations::Preloader)
  when 5
    require_relative "active_record/associations_preloader_v5"
    ActiveRecord::Associations::Preloader.prepend(N1Loader::ActiveRecord::Associations::Preloader)
  else
    require_relative "active_record/associations_preloader"
    ActiveRecord::Associations::Preloader::Branch.prepend(N1Loader::ActiveRecord::Associations::Preloader)
  end

  ActiveRecord::Base.include(N1Loader::ActiveRecord::Base)
end


================================================
FILE: lib/n1_loader/ar_lazy_preload/associated_context_builder.rb
================================================
# frozen_string_literal: true

module N1Loader
  module ArLazyPreload
    # Context builder for N1Loader
    class AssociatedContextBuilder < ::ArLazyPreload::AssociatedContextBuilder
      attr_reader :records

      def initialize(parent_context:, association_name:, records:)
        super(parent_context: parent_context, association_name: association_name)
        @records = records
      end

      def perform
        ::ArLazyPreload::Context.register(
          records: records.flatten(1).select { |record| record.respond_to?(:lazy_preload_context=) },
          association_tree: child_association_tree,
          auto_preload: parent_context.auto_preload?
        )
      end
    end
  end
end


================================================
FILE: lib/n1_loader/ar_lazy_preload/context.rb
================================================
# frozen_string_literal: true

# Returns cached N1Loader::LoaderCollection from context for a loader.
# In case there is none yet, saves passed block to a cache.
ArLazyPreload::Contexts::BaseContext.define_method :fetch_n1_loader_collection do |loader, &block|
  (@n1_loader_collections ||= {})[loader] ||= block.call
end


================================================
FILE: lib/n1_loader/ar_lazy_preload/context_adapter.rb
================================================
# frozen_string_literal: true

module N1Loader
  module ArLazyPreload
    # Context adapter for injected N1Loader loaders.
    class ContextAdapter
      attr_reader :context

      delegate_missing_to :context

      def initialize(context)
        @context = context
      end

      # Assign initialized preloader to +association_name+ in case it wasn't yet preloaded within the given context.
      def try_preload_lazily(association_name)
        return unless context&.send(:association_needs_preload?, association_name)

        perform_preloading(association_name)
      end

      # Initialize preloader for +association_name+ with context builder callback.
      # The callback will be executed when on records load.
      def perform_preloading(association_name)
        context_setup = lambda { |records|
          AssociatedContextBuilder.prepare(
            parent_context: self,
            association_name: association_name,
            records: records
          )
        }

        N1Loader::Preloader.new(records, context_setup).preload(association_name)
      end
    end
  end
end


================================================
FILE: lib/n1_loader/ar_lazy_preload/loadable.rb
================================================
# frozen_string_literal: true

module N1Loader
  module ArLazyPreload
    module Loadable # :nodoc:
      def n1_loader(name)
        return n1_loaders[name] if n1_loaders[name]

        ContextAdapter.new(lazy_preload_context).try_preload_lazily(name) if respond_to?(:lazy_preload_context)

        super
      end
    end
  end
end


================================================
FILE: lib/n1_loader/ar_lazy_preload/loader.rb
================================================
# frozen_string_literal: true

# Raised when a single object without ArLazyPreload context support was passed to an isolated loader.
N1Loader::Loader::UnsupportedArLazyPreload = Class.new(StandardError)

# Defines a singleton method method that allows isolated loaders
# to use ArLazyPreload context without passing sibling records.
N1Loader::Loader.define_singleton_method(:for) do |element, **args|
  # It is required to have an ArLazyPreload context supported
  raise N1Loader::Loader::UnsupportedArLazyPreload unless element.respond_to?(:lazy_preload_context)

  if element.lazy_preload_context.nil?
    ArLazyPreload::Context.register(
      records: [element],
      association_tree: [],
      auto_preload: true
    )
  end

  # Fetch or initialize loader from ArLazyPreload context
  loader_collection = element.lazy_preload_context.fetch_n1_loader_collection(self) do
    context_setup = lambda { |records|
      N1Loader::ArLazyPreload::AssociatedContextBuilder.prepare(
        parent_context: element.lazy_preload_context,
        association_name: "cached_n1_loader_collection_#{self}".downcase.to_sym,
        records: records
      )
    }

    N1Loader::LoaderCollection.new(self, element.lazy_preload_context.records).tap do |collection|
      collection.context_setup = context_setup
    end
  end

  # Fetch value from loader
  loader_collection.with(**args).for(element)
end


================================================
FILE: lib/n1_loader/ar_lazy_preload/loader_collection_patch.rb
================================================
# frozen_string_literal: true

module N1Loader
  module ArLazyPreload
    # A patch to {N1Loader::LoaderCollection} to setup lazy context lazily.
    module LoaderCollectionPatch
      attr_accessor :context_setup

      def with(**args)
        result = super

        result.context_setup = context_setup if context_setup && result.context_setup.nil?

        result
      end
    end
  end
end


================================================
FILE: lib/n1_loader/ar_lazy_preload/loader_patch.rb
================================================
# frozen_string_literal: true

module N1Loader
  module ArLazyPreload
    # A patch to {N1Loader::Loader} to setup lazy context lazily.
    module LoaderPatch
      attr_accessor :context_setup

      def loaded?
        return true if @already_loaded && @already_context

        super

        synchronize { non_thread_safe_context_setting unless @already_context }

        true
      end

      def non_thread_safe_context_setting
        return if @already_context

        context_setup&.call(loaded_by_identity.values.flatten)

        @already_context = true
      end
    end
  end
end


================================================
FILE: lib/n1_loader/ar_lazy_preload/preloader_patch.rb
================================================
# frozen_string_literal: true

module N1Loader
  module ArLazyPreload
    # A patch to {N1Loader::Preloader} setup lazy context lazily.
    module PreloaderPatch
      def initialize(elements, context_setup = nil)
        super(elements)
        @context_setup = context_setup
      end

      def preload(*keys)
        super.each do |loader_collection|
          loader_collection.context_setup = context_setup
        end
      end

      private

      attr_reader :context_setup
    end
  end
end


================================================
FILE: lib/n1_loader/ar_lazy_preload.rb
================================================
# frozen_string_literal: true

# Load core library
require_relative "active_record"

# Load integration dependency
require "rails"
require "ar_lazy_preload"

# Library integration
require_relative "ar_lazy_preload/loadable"
require_relative "ar_lazy_preload/context_adapter"
require_relative "ar_lazy_preload/associated_context_builder"
require_relative "ar_lazy_preload/loader_collection_patch"
require_relative "ar_lazy_preload/preloader_patch"
require_relative "ar_lazy_preload/loader_patch"
require_relative "ar_lazy_preload/loader"
require_relative "ar_lazy_preload/context"

N1Loader::Loadable.prepend(N1Loader::ArLazyPreload::Loadable)
N1Loader::Preloader.prepend(N1Loader::ArLazyPreload::PreloaderPatch)
N1Loader::Loader.prepend(N1Loader::ArLazyPreload::LoaderPatch)
N1Loader::LoaderCollection.prepend(N1Loader::ArLazyPreload::LoaderCollectionPatch)


================================================
FILE: lib/n1_loader/core/loadable.rb
================================================
# frozen_string_literal: true

module N1Loader
  # The module to be included to the class to define associated loaders.
  #
  #   class Example
  #     include N1Loader::Loadable
  #
  #     # with inline loader
  #     n1_optimized :something do
  #       def perform(elements)
  #         elements.each { |element| fulfill(element, element.calculate_something) }
  #       end
  #     end
  #
  #     # with custom loader
  #     n1_optimized :something, MyLoader
  #   end
  #
  #   # custom loader
  #   class MyLoader < N1Loader::Loader
  #     def perform(elements)
  #       elements.each { |element| fulfill(element, element.calculate_something) }
  #     end
  #   end
  module Loadable
    def n1_loaders
      @n1_loaders ||= {}
    end

    def n1_loader(name)
      n1_loaders[name]
    end

    def n1_bind_to(collection)
      unless collection.is_a?(Array) && collection.any? do |obj|
        obj == self || obj.equal?(self)
      end

        raise InvalidBinding,
              "assigned collection should be array and include object"
      end

      @n1_binding = collection
    end

    def n1_bind_to?
      !@n1_binding.nil?
    end

    def n1_loader_reload(name)
      elements = @n1_binding || [self]
      collection = LoaderCollection.new(self.class.n1_loaders[name], elements)

      @n1_binding&.each { |el| el.n1_loaders[name] = collection if el.respond_to?(:n1_loaders) }
      n1_loaders[name] = collection
    end

    def n1_clear_cache
      @n1_binding = nil
      self.class.n1_loaders.each_key do |name|
        n1_loaders[name] = nil
      end
    end

    def self.included(base)
      base.extend(ClassMethods)
    end

    module ClassMethods # :nodoc:
      def n1_loaders
        @n1_loaders ||= superclass.respond_to?(:n1_loaders) ? superclass.n1_loaders.dup : {}
      end

      def n1_optimized(name, loader = nil, &block)
        loader ||= LoaderBuilder.build(&block)

        n1_loaders[name] = loader

        define_method(name) do |reload: false, **args|
          n1_loader_reload(name) if reload || n1_loader(name).nil?

          n1_loader(name).with(**args).for(self)
        end
      end
    end
  end
end


================================================
FILE: lib/n1_loader/core/loader.rb
================================================
# frozen_string_literal: true

module N1Loader
  # Loader that performs the loading.
  #
  # Subclasses must define +perform+ method that accepts single argument
  # and returns hash where key is the element and value is what we want to load.
  class Loader
    prepend MonitorMixin

    class << self
      attr_reader :arguments

      # Defines an argument that can be accessed within the loader.
      #
      # First defined argument will have the value of first passed argument,
      # meaning the order is important.
      #
      # @param name [Symbol]
      # @param opts [Hash]
      # @option opts [Boolean] optional false by default
      # @option opts [Proc] default
      def argument(name, **opts)
        opts[:optional] = true if opts[:default]

        @arguments ||= []

        define_method(name) do
          args.fetch(name) { args[name] = opts[:default]&.call }
        end

        @arguments << opts.merge(name: name)
      end

      # Defines a custom cache key that is calculated for passed arguments.
      def cache_key(&block)
        define_method(:cache_key) do
          check_arguments!
          instance_exec(&block)
        end
      end
    end

    def initialize(elements, **args)
      @elements = elements
      @args = args
    end

    def for(element)
      return unless loaded?

      if loaded_by_identity.empty? && elements.any?
        raise NotFilled, "Nothing was preloaded, perhaps you forgot to use fulfill method"
      end

      return loaded_by_identity[element] if loaded_by_identity.key?(element)
      return loaded_by_value[element] if loaded_by_value.key?(element)

      raise NotLoaded, "The data was not preloaded for the given element"
    end

    def cache_key
      check_arguments!
      args.values.map(&:object_id)
    end

    private

    attr_reader :elements, :args, :loaded_by_value, :loaded_by_identity

    def check_missing_arguments!
      return unless (arguments = self.class.arguments)

      required_arguments = required_arguments(arguments)

      return if required_arguments.all? { |argument| args.key?(argument) }

      missing_arguments = required_arguments.reject { |argument| args.key?(argument) }

      list = missing_arguments.map { |argument| ":#{argument}" }.join(", ")

      raise MissingArgument, "Loader requires [#{list}] arguments but they are missing"
    end

    def required_arguments(args)
      args.reject { |argument| argument[:optional] }
          .map { |argument| argument[:name] }
    end

    def check_arguments!
      check_missing_arguments!
      check_invalid_arguments!
    end

    def check_invalid_arguments!
      return unless (arguments = self.class.arguments)

      args.each_key do |arg|
        next if arguments.find { |argument| argument[:name] == arg }

        raise InvalidArgument, "Loader doesn't define #{arg} argument"
      end
    end

    def perform(_elements)
      raise NotImplemented, "Subclasses have to implement the method"
    end

    def fulfill(element, value)
      loaded_by_identity[element] = value
      loaded_by_value[element] = value
    end

    def loaded?
      return true if @already_loaded

      synchronize { non_thread_safe_loading unless @already_loaded }

      true
    end

    def non_thread_safe_loading # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
      return if @already_loaded

      check_arguments!

      @loaded_by_identity = {}.compare_by_identity
      @loaded_by_value = {}

      if respond_to?(:single) && elements.size == 1
        fulfill(elements.first, single(elements.first))
      elsif elements.any?
        perform(elements)

        # propagate context to loaded objects only when it was set
        if elements.first.respond_to?(:n1_bind_to?) && elements.first.n1_bind_to?
          loaded_objects = loaded_by_identity.values.flatten
          loaded_objects.each { |el| el.n1_bind_to(loaded_objects) if el.respond_to?(:n1_bind_to) }
        end
      end

      @already_loaded = true
    end
  end
end


================================================
FILE: lib/n1_loader/core/loader_builder.rb
================================================
# frozen_string_literal: true

module N1Loader
  # The class builds {N1Loader::Loader}
  class LoaderBuilder
    def self.build(&block)
      Class.new(N1Loader::Loader) do
        if block.arity == 1
          define_method(:perform, &block)
        else
          class_eval(&block)
        end
      end
    end
  end
end


================================================
FILE: lib/n1_loader/core/loader_collection.rb
================================================
# frozen_string_literal: true

module N1Loader
  # The class is used for storing collections of loaders for elements per set of arguments.
  class LoaderCollection
    attr_reader :loader_class, :elements

    def initialize(loader_class, elements)
      @loader_class = loader_class
      @elements = elements
    end

    def with(**args)
      loader = loader_class.new(elements, **args)

      loaders[loader.cache_key] ||= loader
    end

    private

    def loaders
      @loaders ||= {}
    end
  end
end


================================================
FILE: lib/n1_loader/core/preloader.rb
================================================
# frozen_string_literal: true

module N1Loader
  # Preloader that lazily preloads data to every element.
  #
  # It supports multiple keys.
  #
  # It supports elements that have different loaders under the same key.
  # It will properly preload data to each of the element of the similar group.
  class Preloader
    attr_reader :elements

    def initialize(elements)
      @elements = elements
    end

    def preload(*keys)
      keys.flatten(1).flat_map do |key|
        elements
          .group_by { |element| loader_class(element, key) }
          .select { |loader_class, _| loader_class }
          .map do |(loader_class, grouped_elements)|
            loader_collection = N1Loader::LoaderCollection.new(loader_class, grouped_elements)
            grouped_elements.each { |grouped_element| grouped_element.n1_loaders[key] = loader_collection }
            loader_collection
          end
      end
    end

    private

    def loader_class(element, key)
      element.class.respond_to?(:n1_loaders) && element.class.n1_loaders[key]
    end
  end
end


================================================
FILE: lib/n1_loader/goldiloader/context.rb
================================================
# frozen_string_literal: true

# Returns cached N1Loader::LoaderCollection from context for a loader.
# In case there is none yet, saves passed block to a cache.
Goldiloader::AutoIncludeContext.define_method :fetch_n1_loader_collection do |loader, &block|
  (@n1_loader_collections ||= {})[loader] ||= block.call
end


================================================
FILE: lib/n1_loader/goldiloader/context_adapter.rb
================================================
# frozen_string_literal: true

module N1Loader
  module Goldiloader
    # Context adapter for injected N1Loader loaders.
    class ContextAdapter
      attr_reader :context

      def initialize(context)
        @context = context
      end

      # Trigger preloading for +association_name+ across all models in the context.
      def try_preload_lazily(association_name)
        perform_preloading(association_name) if context
      end

      # Initialize preloader for +association_name+ with context builder callback.
      # The callback will be executed when records are loaded.
      def perform_preloading(association_name)
        context_setup = lambda { |records|
          ar_records = records.flatten(1).select { |record| record.respond_to?(:auto_include_context=) }
          ::Goldiloader::AutoIncludeContext.register_models(ar_records) unless ar_records.empty?
        }

        N1Loader::Preloader.new(context.models, context_setup).preload(association_name)
      end
    end
  end
end


================================================
FILE: lib/n1_loader/goldiloader/loadable.rb
================================================
# frozen_string_literal: true

module N1Loader
  module Goldiloader
    module Loadable # :nodoc:
      def n1_loader(name)
        return n1_loaders[name] if n1_loaders[name]

        ContextAdapter.new(auto_include_context).try_preload_lazily(name) if respond_to?(:auto_include_context)

        super
      end
    end
  end
end


================================================
FILE: lib/n1_loader/goldiloader/loader.rb
================================================
# frozen_string_literal: true

# Raised when a single object without Goldiloader context support was passed to an isolated loader.
N1Loader::Loader::UnsupportedGoldiloader = Class.new(StandardError)

# Defines a singleton method that allows isolated loaders
# to use Goldiloader context without passing sibling records.
N1Loader::Loader.define_singleton_method(:for) do |element, **args|
  # It is required to have a Goldiloader context supported
  raise N1Loader::Loader::UnsupportedGoldiloader unless element.respond_to?(:auto_include_context)

  context = element.auto_include_context

  # Fetch or initialize loader from Goldiloader context
  loader_collection = context.fetch_n1_loader_collection(self) do
    context_setup = lambda { |records|
      ar_records = records.flatten(1).select { |record| record.respond_to?(:auto_include_context=) }
      ::Goldiloader::AutoIncludeContext.register_models(ar_records) unless ar_records.empty?
    }

    N1Loader::LoaderCollection.new(self, context.models).tap do |collection|
      collection.context_setup = context_setup
    end
  end

  # Fetch value from loader
  loader_collection.with(**args).for(element)
end


================================================
FILE: lib/n1_loader/goldiloader/loader_collection_patch.rb
================================================
# frozen_string_literal: true

module N1Loader
  module Goldiloader
    # A patch to {N1Loader::LoaderCollection} to setup lazy context lazily.
    module LoaderCollectionPatch
      attr_accessor :context_setup

      def with(**args)
        result = super

        result.context_setup = context_setup if context_setup && result.context_setup.nil?

        result
      end
    end
  end
end


================================================
FILE: lib/n1_loader/goldiloader/loader_patch.rb
================================================
# frozen_string_literal: true

module N1Loader
  module Goldiloader
    # A patch to {N1Loader::Loader} to setup lazy context lazily.
    module LoaderPatch
      attr_accessor :context_setup

      def loaded?
        return true if @already_loaded && @already_context

        super

        synchronize { non_thread_safe_context_setting unless @already_context }

        true
      end

      def non_thread_safe_context_setting
        return if @already_context

        context_setup&.call(loaded_by_identity.values.flatten)

        @already_context = true
      end
    end
  end
end


================================================
FILE: lib/n1_loader/goldiloader/preloader_patch.rb
================================================
# frozen_string_literal: true

module N1Loader
  module Goldiloader
    # A patch to {N1Loader::Preloader} to setup lazy context lazily.
    module PreloaderPatch
      def initialize(elements, context_setup = nil)
        super(elements)
        @context_setup = context_setup
      end

      def preload(*keys)
        super.each do |loader_collection|
          loader_collection.context_setup = context_setup
        end
      end

      private

      attr_reader :context_setup
    end
  end
end


================================================
FILE: lib/n1_loader/goldiloader.rb
================================================
# frozen_string_literal: true

# Load core library
require_relative "active_record"

# Load integration dependency
require "goldiloader"

# Library integration
require_relative "goldiloader/loadable"
require_relative "goldiloader/context_adapter"
require_relative "goldiloader/loader_collection_patch"
require_relative "goldiloader/preloader_patch"
require_relative "goldiloader/loader_patch"
require_relative "goldiloader/loader"
require_relative "goldiloader/context"

N1Loader::Loadable.prepend(N1Loader::Goldiloader::Loadable)
N1Loader::Preloader.prepend(N1Loader::Goldiloader::PreloaderPatch)
N1Loader::Loader.prepend(N1Loader::Goldiloader::LoaderPatch)
N1Loader::LoaderCollection.prepend(N1Loader::Goldiloader::LoaderCollectionPatch)


================================================
FILE: lib/n1_loader/version.rb
================================================
# frozen_string_literal: true

module N1Loader
  VERSION = "3.0.0"
end


================================================
FILE: lib/n1_loader.rb
================================================
# frozen_string_literal: true

require_relative "n1_loader/version"

require_relative "n1_loader/core/loader_builder"
require_relative "n1_loader/core/loader"
require_relative "n1_loader/core/loader_collection"
require_relative "n1_loader/core/loadable"
require_relative "n1_loader/core/preloader"

module N1Loader # :nodoc:
  class Error < StandardError; end
  class NotImplemented < Error; end
  class NotLoaded < Error; end
  class NotFilled < Error; end
  class MissingArgument < Error; end
  class InvalidArgument < Error; end
  class InvalidBinding < Error; end
end


================================================
FILE: n1_loader.gemspec
================================================
# frozen_string_literal: true

require_relative "lib/n1_loader/version"

Gem::Specification.new do |spec|
  spec.name          = "n1_loader"
  spec.version       = N1Loader::VERSION
  spec.authors       = ["Evgeniy Demin"]
  spec.email         = ["lawliet.djez@gmail.com"]

  spec.summary       = "Loader to solve N+1 issue for good."
  spec.homepage      = "https://github.com/djezzzl/n1_loader"
  spec.license       = "MIT"
  spec.required_ruby_version = ">= 2.5.0"

  spec.metadata["homepage_uri"] = spec.homepage
  spec.metadata["source_code_uri"] = "https://github.com/djezzzl/n1_loader"
  spec.metadata["changelog_uri"] = "https://github.com/djezzzl/n1_loader/master/CHANGELOG.md"
  spec.metadata["funding_uri"] = "https://opencollective.com/n1_loader#support"

  spec.files = Dir["lib/**/*"]
  spec.require_paths = ["lib"]

  spec.add_runtime_dependency "mutex_m"

  spec.add_development_dependency "activerecord", ">= 5"
  spec.add_development_dependency "ar_lazy_preload", ">= 0.6"
  spec.add_development_dependency "db-query-matchers", "~> 0.11"
  spec.add_development_dependency "goldiloader", ">= 3"
  spec.add_development_dependency "graphql", "~> 2.0"
  spec.add_development_dependency "rails", ">= 5"
  spec.add_development_dependency "rspec", "~> 3.0"
  spec.add_development_dependency "rspec_junit_formatter", "~> 0.4"
  spec.add_development_dependency "rubocop", "~> 1.7"
  spec.add_development_dependency "sqlite3", ">= 1.3"
end


================================================
FILE: spec/activerecord_spec.rb
================================================
# frozen_string_literal: true

RSpec.describe "N1Loader ActiveRecord integration" do
  require_relative "../lib/n1_loader/active_record"

  before do
    ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
    ActiveRecord::Base.connection.tables.each do |table|
      ActiveRecord::Base.connection.drop_table(table, force: :cascade)
    end
    ActiveRecord::Schema.verbose = false

    ActiveRecord::Schema.define(version: 1) do
      create_table(:entities)
      create_table(:companies) do |t|
        t.belongs_to :entity
      end
      create_table(:employees) do |t|
        t.belongs_to :company
      end
      create_table(:assignments) do |t|
        t.belongs_to :employee
      end
    end

    stub_const("Entity", Class.new(ActiveRecord::Base) do
      self.table_name = :entities

      has_one :company, class_name: "Company"

      class << self
        def name
          "Entity"
        end

        def perform!
          @count = count + 1
        end

        def count
          @count || 0
        end
      end

      n1_optimized :data do
        def perform(elements)
          Entity.perform!

          elements.each { |element| fulfill(element, [element]) }
        end
      end

      n1_optimized :with_arguments do
        argument :something

        def perform(elements)
          Entity.perform!

          elements.each { |element| fulfill(element, [element, something]) }
        end
      end
    end)

    stub_const("Company", Class.new(ActiveRecord::Base) do
      self.table_name = :companies

      belongs_to :entity, class_name: "Entity"
      has_many :employees, class_name: "Employee"

      class << self
        def name
          "Company"
        end

        def perform!
          @count = count + 1
        end

        def count
          @count || 0
        end
      end

      n1_optimized :data do
        def perform(elements)
          Company.perform!

          hash = Entity.where(id: elements.map(&:entity_id)).index_by(&:id)
          elements.each { |element| fulfill(element, hash[element.entity_id]) }
        end
      end

      n1_optimized :with_arguments do
        argument :something

        def perform(elements)
          Company.perform!

          hash = Entity.where(id: elements.map(&:entity_id)).index_by(&:id)
          elements.each { |element| fulfill(element, [hash[element.entity_id], something]) }
        end
      end

      n1_optimized :with_question_mark? do
        argument :something

        def perform(elements)
          Entity.perform!

          elements.each { |element| fulfill(element, [element, something]) }
        end
      end

      n1_optimized :employee_data do
        def perform(elements)
          Company.perform!
          employees_hash = Employee.where(company_id: elements.map(&:id)).group_by(&:company_id)
          elements.each { |element| fulfill(element, employees_hash[element.id] || []) }
        end
      end
    end)

    stub_const("Employee", Class.new(ActiveRecord::Base) do
      self.table_name = :employees

      belongs_to :company, class_name: "Company"
      has_many :assignments, class_name: "Assignment"

      class << self
        def name
          "Employee"
        end

        def perform!
          @count = count + 1
        end

        def count
          @count || 0
        end
      end
    end)

    stub_const("Assignment", Class.new(ActiveRecord::Base) do
      self.table_name = :assignments

      belongs_to :employee, class_name: "Employee"

      class << self
        def name
          "Assignment"
        end

        def perform!
          @count = count + 1
        end

        def count
          @count || 0
        end
      end
    end)
  end

  let(:loader) do
    Class.new(N1Loader::Loader) do
      def perform(elements)
        elements.each { |element| fulfill(element, [element]) }
      end
    end
  end

  let(:object) { Entity.create! }

  it "works" do
    expect { object.data }.to change(Entity, :count).by(1)
    expect { object.data }.not_to change(Entity, :count)

    expect(object.data).to eq([object])
  end

  describe "loaded comparison" do
    it "compares by value" do
      instance = loader.new([object])

      expect(instance.for(object)).to eq([object])
      expect(instance.for(Entity.find(object.id))).to eq([object])

      expect { instance.for(Entity.create!) }.to raise_error(N1Loader::NotLoaded)
    end
  end

  describe "question mark support" do
    it "works" do
      expect do
        Company.includes(:with_question_mark?).each do |company|
          expect(company.with_question_mark?(something: "something")).to eq([company, "something"])
        end
      end.not_to raise_error
    end
  end

  context "with preloader" do
    let(:objects) { [Entity.create!, Entity.create!] }

    it "works" do
      expect { N1Loader::Preloader.new(objects).preload(:data) }.not_to change(Entity, :count)

      expect do
        objects.each do |object|
          expect(object.data).to eq([object])
        end
      end.to change(Entity, :count).by(1)
    end
  end

  context "with includes" do
    let(:objects) { Entity.includes(:data) }

    before do
      Entity.create!
      Entity.create!
    end

    it "works" do
      expect do
        objects.each do |object|
          expect(object.data).to eq([object])
        end
      end
        .to make_database_queries(matching: /entities/, count: 1)
        .and make_database_queries(count: 1)
        .and change(Entity, :count).by(1)
    end

    context "when preloading AR" do
      let(:objects) { Entity.includes(:company) }

      it "doesn't set context further for N1Loader" do
        expect do
          objects.each do |object|
            expect(object.company.data).to eq(object)
          end
            .to make_database_queries(matching: /companies/, count: 1)
            .and make_database_queries(matching: /entities/, count: 2)
            .and make_database_queries(count: 3)
        end
      end
    end

    context "with arguments" do
      before { skip "unsupported by ArLazyPreload" if ar_lazy_preload_defined? }

      let(:objects) { Entity.includes(:with_arguments) }

      it "works" do
        expect do
          expect do
            objects.each do |object|
              object.with_arguments(something: "something")
            end
          end.to change(Entity, :count).by(1)
          expect do
            objects.each do |object|
              object.with_arguments(something: "something")
            end
          end.not_to change(Entity, :count)
          expect do
            objects.each do |object|
              object.with_arguments(something: "anything")
            end
          end.to change(Entity, :count).by(1)

          objects.each do |object|
            expect(object.with_arguments(something: "something")).to eq([object, "something"])
          end
        end
          .to make_database_queries(matching: /entities/, count: 1)
          .and make_database_queries(count: 1)
      end
    end
  end

  context "with nested includes" do
    let(:objects) { Entity.includes(company: %i[entity data] + [{ employee_data: :assignments }]) }

    before do
      Company.create!(entity: Entity.create!, employees: [Employee.create!(assignments: [Assignment.create!])])
      Company.create!(entity: Entity.create!, employees: [Employee.create!(assignments: [Assignment.create!])])
    end

    it "works" do
      expect do
        objects.each do |object|
          expect(object.company.data).to eq(object)
        end
      end
        .to make_database_queries(matching: /entities/, count: 2)
        .and make_database_queries(matching: /companies/, count: 1)
        .and make_database_queries(matching: /employees/, count: 1)
        .and make_database_queries(matching: /assignments/, count: 1)
        .and make_database_queries(count: 5)
        .and change(Company, :count).by(2)
      objects.each do |object|
        expect(object.company.employee_data).to eq(object.company.employees)
        expect(object.company.employee_data.map(&:assignments)).to eq(object.company.employees.map(&:assignments))
      end
    end

    context "with arguments" do
      let(:objects) { Entity.includes(company: :with_arguments) }

      before { skip "unsupported by ActiveRecord 6" if ar_version == 6 }

      it "works" do
        expect do
          objects.each do |object|
            expect(object.company.with_arguments(something: "something")).to eq([object, "something"])
          end
        end
          .to make_database_queries(matching: /entities/, count: 2)
          .and make_database_queries(matching: /companies/, count: 1)
          .and make_database_queries(count: 3)
          .and change(Company, :count).by(1)
      end
    end
  end

  context "with deep includes" do
    let(:objects) { Company.includes(data: :company) }

    before do
      Company.create!(entity: Entity.create!)
      Company.create!(entity: Entity.create!)
    end

    it "works" do
      expect do
        objects.each do |object|
          expect(object.data.company.id).to eq(object.id)
        end
      end
        .to make_database_queries(matching: /entities/, count: 1)
        .and make_database_queries(matching: /companies/, count: 2)
        .and make_database_queries(count: 3)
        .and change(Company, :count).by(1)
    end

    context "with arguments" do
      let(:objects) { Company.includes(with_arguments: :company) }

      it "doesn't work" do
        expect do
          objects.each do |object|
            expect(object.with_arguments(something: "something").first.company.id).to eq(object.id)
          end
        end.to raise_error(N1Loader::ActiveRecord::InvalidPreloading)
      end
    end
  end
end


================================================
FILE: spec/ar_lazy_preload_spec.rb
================================================
# frozen_string_literal: true

RSpec.describe "N1Loader AR Lazy Preload integration" do
  require_relative "../lib/n1_loader/ar_lazy_preload"

  ActiveSupport.on_load(:active_record) do
    ActiveRecord::Base.include(ArLazyPreload::Base)

    ActiveRecord::Relation.prepend(ArLazyPreload::Relation)
    ActiveRecord::AssociationRelation.prepend(ArLazyPreload::AssociationRelation)
    ActiveRecord::Relation::Merger.prepend(ArLazyPreload::Merger)

    [
      ActiveRecord::Associations::CollectionAssociation,
      ActiveRecord::Associations::Association
    ].each { |klass| klass.prepend(ArLazyPreload::Association) }

    ActiveRecord::Associations::CollectionAssociation.prepend(ArLazyPreload::CollectionAssociation)
    ActiveRecord::Associations::CollectionProxy.prepend(ArLazyPreload::CollectionProxy)

    ArLazyPreload::Preloader.patch_for_rails_7! if ActiveRecord::VERSION::MAJOR >= 7
  end

  before do
    ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
    ActiveRecord::Base.connection.tables.each do |table|
      ActiveRecord::Base.connection.drop_table(table, force: :cascade)
    end
    ActiveRecord::Schema.verbose = false

    ActiveRecord::Schema.define(version: 1) do
      create_table(:entities)
      create_table(:companies) do |t|
        t.belongs_to :entity
      end
    end

    stub_const("Entity", Class.new(ActiveRecord::Base) do
      self.table_name = :entities

      has_one :company, class_name: "Company"

      class << self
        def name
          "Entity"
        end

        def perform!
          @count = count + 1
        end

        def count
          @count || 0
        end
      end

      n1_optimized :data do
        def perform(elements)
          elements.first.class.perform!

          elements.each { |element| fulfill(element, [element]) }
        end
      end

      n1_optimized :with_arguments do
        argument :something

        def perform(elements)
          Entity.perform!

          elements.each { |element| fulfill(element, [element, something]) }
        end
      end
    end)

    stub_const("Company", Class.new(ActiveRecord::Base) do
      self.table_name = :companies

      belongs_to :entity, class_name: "Entity"

      class << self
        def name
          "Company"
        end

        def perform!
          @count = count + 1
        end

        def count
          @count || 0
        end
      end

      n1_optimized :data do
        def perform(elements)
          elements.first.class.perform!

          hash = Entity.where(id: elements.map(&:entity_id)).index_by(&:id)
          elements.each { |element| fulfill(element, hash[element.entity_id]) }
        end
      end

      n1_optimized :with_arguments do
        argument :something

        def perform(elements)
          Company.perform!

          hash = Entity.where(id: elements.map(&:entity_id)).index_by(&:id)
          elements.each { |element| fulfill(element, [hash[element.entity_id], something]) }
        end
      end

      n1_optimized :with_question_mark? do
        argument :something

        def perform(elements)
          Entity.perform!

          elements.each { |element| fulfill(element, [element, something]) }
        end
      end
    end)
  end

  before do
    Company.create!(entity: Entity.create!)
    Company.create!(entity: Entity.create!)
  end

  describe "question mark support" do
    it "works" do
      expect do
        Company.preload_associations_lazily.each do |company|
          expect(company.with_question_mark?(something: "something")).to eq([company, "something"])
        end
      end.not_to raise_error
    end
  end

  it "works" do
    expect do
      Company.preload_associations_lazily.all.map(&:data)
    end
      .to make_database_queries(matching: /entities/, count: 1)
      .and make_database_queries(matching: /companies/, count: 1)
      .and make_database_queries(count: 2)
      .and change(Company, :count).by(1)

    expect do
      Entity.preload_associations_lazily.all.map(&:company).map(&:data)
    end
      .to make_database_queries(matching: /entities/, count: 2)
      .and make_database_queries(matching: /companies/, count: 1)
      .and make_database_queries(count: 3)
      .and change(Company, :count).by(1)

    expect do
      Company.preload_associations_lazily.all.map(&:data).map(&:company)
    end
      .to make_database_queries(matching: /entities/, count: 1)
      .and make_database_queries(matching: /companies/, count: 2)
      .and make_database_queries(count: 3)
      .and change(Company, :count).by(1)

    expect do
      Company.lazy_preload(data: :company).map(&:data).map(&:company)
    end
      .to make_database_queries(matching: /entities/, count: 1)
      .and make_database_queries(matching: /companies/, count: 2)
      .and make_database_queries(count: 3)
      .and change(Company, :count).by(1)
  end

  context "with arguments" do
    it "works" do
      expect do
        Company.preload_associations_lazily.all.each { |company| company.with_arguments(something: "something") }
      end
        .to make_database_queries(matching: /entities/, count: 1)
        .and make_database_queries(matching: /companies/, count: 1)
        .and make_database_queries(count: 2)
        .and change(Company, :count).by(1)

      expect do
        Entity.preload_associations_lazily.all.each { |entity| entity.company.with_arguments(something: "something") }
      end
        .to make_database_queries(matching: /entities/, count: 2)
        .and make_database_queries(matching: /companies/, count: 1)
        .and make_database_queries(count: 3)

      expect do
        Company.preload_associations_lazily.each do |company|
          company.with_arguments(something: "something").first.company
        end
      end
        .to make_database_queries(matching: /entities/, count: 1)
        .and make_database_queries(matching: /companies/, count: 2)
        .and make_database_queries(count: 3)

      expect do
        Company.lazy_preload(with_arguments: :company).each do |company|
          company.with_arguments(something: "something").first.company
        end
      end
        .to make_database_queries(matching: /entities/, count: 1)
        .and make_database_queries(matching: /companies/, count: 2)
        .and make_database_queries(count: 3)
    end
  end

  describe "isolated loaders" do
    let(:loader) do
      Class.new(N1Loader::Loader) do
        argument :something

        def perform(_companies)
          Company.perform!

          hash = Entity.where(id: elements.map(&:entity_id)).index_by(&:id)
          elements.each { |element| fulfill(element, [hash[element.entity_id], something]) }
        end
      end
    end

    it "works with ArLazyPreload context" do
      companies = Company.preload_associations_lazily.order(:id).to_a
      entity1 = Entity.first
      entity2 = Entity.second

      loaded1 = loader.for(companies.first, something: "tmp")
      loaded2 = loader.for(companies.second, something: "tmp")

      expect(loaded1).to eq([entity1, "tmp"])
      expect(loaded2).to eq([entity2, "tmp"])
      expect(loaded1[0].lazy_preload_context)
        .to be_present
        .and eq(loaded2[0].lazy_preload_context)

      expect do
        companies.map do |company|
          loader.for(company, something: "something")
        end
      end
        .to make_database_queries(matching: /entities/, count: 1)
        .and make_database_queries(count: 1)
        .and change(Company, :count).by(1)

      expect do
        companies.each do |company|
          loader.for(company, something: "anything")
        end
      end
        .to make_database_queries(matching: /entities/, count: 1)
        .and make_database_queries(count: 1)
        .and change(Company, :count).by(1)
    end

    context "when object does not have ArLazyPreload context" do
      it "raises an error" do
        expect do
          loader.for("string", something: "something")
        end.to raise_error(N1Loader::Loader::UnsupportedArLazyPreload)
        expect do
          loaded = loader.for(Company.first, something: "something")
          expect(loaded[0].lazy_preload_context).to be_present
        end.not_to raise_error
      end
    end
  end
end


================================================
FILE: spec/goldiloader_spec.rb
================================================
# frozen_string_literal: true

RSpec.describe "N1Loader Goldiloader integration" do
  require_relative "../lib/n1_loader/goldiloader"

  before do
    ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
    ActiveRecord::Base.connection.tables.each do |table|
      ActiveRecord::Base.connection.drop_table(table, force: :cascade)
    end
    ActiveRecord::Schema.verbose = false

    ActiveRecord::Schema.define(version: 1) do
      create_table(:entities)
      create_table(:companies) do |t|
        t.belongs_to :entity
      end
    end

    stub_const("Entity", Class.new(ActiveRecord::Base) do
      self.table_name = :entities

      has_one :company, class_name: "Company"

      class << self
        def name
          "Entity"
        end

        def perform!
          @count = count + 1
        end

        def count
          @count || 0
        end
      end

      n1_optimized :data do
        def perform(elements)
          elements.first.class.perform!

          elements.each { |element| fulfill(element, [element]) }
        end
      end

      n1_optimized :with_arguments do
        argument :something

        def perform(elements)
          Entity.perform!

          elements.each { |element| fulfill(element, [element, something]) }
        end
      end
    end)

    stub_const("Company", Class.new(ActiveRecord::Base) do
      self.table_name = :companies

      belongs_to :entity, class_name: "Entity"

      class << self
        def name
          "Company"
        end

        def perform!
          @count = count + 1
        end

        def count
          @count || 0
        end
      end

      n1_optimized :data do
        def perform(elements)
          elements.first.class.perform!

          hash = Entity.where(id: elements.map(&:entity_id)).index_by(&:id)
          elements.each { |element| fulfill(element, hash[element.entity_id]) }
        end
      end

      n1_optimized :with_arguments do
        argument :something

        def perform(elements)
          Company.perform!

          hash = Entity.where(id: elements.map(&:entity_id)).index_by(&:id)
          elements.each { |element| fulfill(element, [hash[element.entity_id], something]) }
        end
      end

      n1_optimized :with_question_mark? do
        argument :something

        def perform(elements)
          Entity.perform!

          elements.each { |element| fulfill(element, [element, something]) }
        end
      end
    end)
  end

  before do
    Company.create!(entity: Entity.create!)
    Company.create!(entity: Entity.create!)
  end

  describe "question mark support" do
    it "works" do
      expect do
        Company.all.each do |company|
          expect(company.with_question_mark?(something: "something")).to eq([company, "something"])
        end
      end.not_to raise_error
    end
  end

  it "works" do
    expect do
      Company.all.map(&:data)
    end
      .to make_database_queries(matching: /entities/, count: 1)
      .and make_database_queries(matching: /companies/, count: 1)
      .and make_database_queries(count: 2)
      .and change(Company, :count).by(1)

    expect do
      Entity.all.map(&:company).map(&:data)
    end
      .to make_database_queries(matching: /entities/, count: 2)
      .and make_database_queries(matching: /companies/, count: 1)
      .and make_database_queries(count: 3)
      .and change(Company, :count).by(1)

    expect do
      Company.all.map(&:data).map(&:company)
    end
      .to make_database_queries(matching: /entities/, count: 1)
      .and make_database_queries(matching: /companies/, count: 2)
      .and make_database_queries(count: 3)
      .and change(Company, :count).by(1)
  end

  context "with arguments" do
    it "works" do
      expect do
        Company.all.each { |company| company.with_arguments(something: "something") }
      end
        .to make_database_queries(matching: /entities/, count: 1)
        .and make_database_queries(matching: /companies/, count: 1)
        .and make_database_queries(count: 2)
        .and change(Company, :count).by(1)

      expect do
        Entity.all.each { |entity| entity.company.with_arguments(something: "something") }
      end
        .to make_database_queries(matching: /entities/, count: 2)
        .and make_database_queries(matching: /companies/, count: 1)
        .and make_database_queries(count: 3)

      expect do
        Company.all.each do |company|
          company.with_arguments(something: "something").first.company
        end
      end
        .to make_database_queries(matching: /entities/, count: 1)
        .and make_database_queries(matching: /companies/, count: 2)
        .and make_database_queries(count: 3)
    end
  end

  describe "isolated loaders" do
    let(:loader) do
      Class.new(N1Loader::Loader) do
        argument :something

        def perform(_companies)
          Company.perform!

          hash = Entity.where(id: elements.map(&:entity_id)).index_by(&:id)
          elements.each { |element| fulfill(element, [hash[element.entity_id], something]) }
        end
      end
    end

    it "works with Goldiloader context" do
      companies = Company.order(:id).to_a
      entity1 = Entity.first
      entity2 = Entity.second

      loaded1 = loader.for(companies.first, something: "tmp")
      loaded2 = loader.for(companies.second, something: "tmp")

      expect(loaded1).to eq([entity1, "tmp"])
      expect(loaded2).to eq([entity2, "tmp"])
      expect(loaded1[0].auto_include_context)
        .to be_present
        .and eq(loaded2[0].auto_include_context)

      expect do
        companies.map do |company|
          loader.for(company, something: "something")
        end
      end
        .to make_database_queries(matching: /entities/, count: 1)
        .and make_database_queries(count: 1)
        .and change(Company, :count).by(1)

      expect do
        companies.each do |company|
          loader.for(company, something: "anything")
        end
      end
        .to make_database_queries(matching: /entities/, count: 1)
        .and make_database_queries(count: 1)
        .and change(Company, :count).by(1)
    end

    context "when object does not have Goldiloader context" do
      it "raises an error" do
        expect do
          loader.for("string", something: "something")
        end.to raise_error(N1Loader::Loader::UnsupportedGoldiloader)
        expect do
          loaded = loader.for(Company.first, something: "something")
          expect(loaded[0].auto_include_context).to be_present
        end.not_to raise_error
      end
    end
  end
end


================================================
FILE: spec/n1_loader_spec.rb
================================================
# frozen_string_literal: true

RSpec.describe N1Loader do
  let(:loader) do
    Class.new(N1Loader::Loader) do
      def perform(elements)
        elements.each { |element| fulfill(element, [element]) }
      end
    end
  end

  let(:klass) do
    custom_loader = loader

    Class.new do
      include N1Loader::Loadable

      class << self
        def perform!
          @count = count + 1
        end

        def count
          @count || 0
        end
      end

      n1_optimized :inline do |elements|
        elements.first.class.perform!

        elements.each { |element| fulfill(element, [element]) }
      end

      n1_optimized :sleepy do |elements|
        sleep(0.5)

        elements.first.class.perform!
        elements.each { |element| fulfill(element, [element]) }
      end

      n1_optimized :custom, custom_loader

      n1_optimized :single_optimized do
        def single(element)
          [element]
        end

        def perform(_elements)
          raise "unknown"
        end
      end

      n1_optimized :missing_fulfill do
        def perform(elements)
          elements.group_by(&:itself)
        end
      end

      n1_optimized :with_arguments do
        argument :something
        argument :anything

        def perform(elements)
          elements.first.class.perform!

          elements.each do |element|
            fulfill(element, [element, something, anything])
          end
        end
      end

      n1_optimized :with_optional_argument do
        argument :something, optional: true
        argument :anything

        def perform(elements)
          elements.first.class.perform!

          elements.each do |element|
            fulfill(element, [element, something, anything])
          end
        end
      end

      n1_optimized :with_default_argument do
        argument :something, default: -> { [] }
        argument :anything

        def perform(elements)
          elements.first.class.perform!

          elements.each do |element|
            fulfill(element, [element, something, anything])
          end
        end
      end

      n1_optimized :with_custom_arguments_key do
        argument :something
        argument :anything

        cache_key { something + anything }

        def perform(elements)
          elements.first.class.perform!

          elements.each do |element|
            fulfill(element, [element, something, anything])
          end
        end
      end

      n1_optimized :with_question_mark? do
        argument :something

        def perform(elements)
          elements.first.class.perform!

          elements.each do |element|
            fulfill(element, [element, something])
          end
        end
      end

      n1_optimized :new_objects do |elements|
        elements.first.class.perform!

        objects = elements.map { elements.first.class.new }

        elements.each_with_index do |element, index|
          fulfill(element, objects[index])
        end
      end
    end
  end

  let(:child_klass) do
    Class.new(klass) do
      n1_optimized :child_something do |elements|
        elements.first.class.perform!

        elements.each do |element|
          fulfill(element, [element])
        end
      end
    end
  end

  let(:object) { klass.new }
  let(:objects) { [klass.new, klass.new] }

  it "works with unsupported objects" do
    expect do
      N1Loader::Preloader.new([object, 123]).preload(:with_arguments)
    end.not_to raise_error(NoMethodError)
  end

  describe "thread-safety" do
    it "is thread-safe" do
      N1Loader::Preloader.new(objects).preload(:sleepy)

      threads = []

      10.times do
        threads << Thread.new do
          objects.each do |obj|
            expect(obj.sleepy).to eq([obj])
          end
        end
      end
      threads.each(&:join)

      expect(klass.count).to eq(1)
    end
  end

  describe "error handling" do
    it "raises the same error on the subsequent calls" do
      faulty_klass = Class.new do
        include N1Loader::Loadable

        n1_optimized :faulty do |_|
          raise StandardError, "Something went wrong"
        end
      end

      faulty_object = faulty_klass.new

      expect { faulty_object.faulty }.to raise_error(StandardError, "Something went wrong")
      expect { faulty_object.faulty }.to raise_error(StandardError, "Something went wrong")
    end
  end

  describe "loaded comparison" do
    it "compares by identity first" do
      instance = loader.new(objects)

      expect(objects.first).to equal(objects.first)
      expect(instance.for(objects.first)).to eq([objects.first])

      expect { instance.for(object) }.to raise_error(N1Loader::NotLoaded)
    end

    it "falls back to equality comparison when no identity match" do
      equal_klass = Struct.new(:id)

      original = equal_klass.new(1)
      equal_copy = equal_klass.new(1)

      custom_loader = Class.new(N1Loader::Loader) do
        def perform(elements)
          elements.each { |element| fulfill(element, [element]) }
        end
      end

      instance = custom_loader.new([original])

      expect(original).not_to equal(equal_copy)
      expect(instance.for(original)).to eq([original])
      expect(instance.for(equal_copy)).to eq([original])
    end
  end

  context "when fulfill was not used" do
    it "throws error" do
      expect { object.missing_fulfill }
        .to raise_error(N1Loader::NotFilled, "Nothing was preloaded, perhaps you forgot to use fulfill method")
    end
  end

  describe "question mark support" do
    it "works" do
      expect { object.with_question_mark?(something: "something") }.not_to raise_error
      expect(object.with_question_mark?(something: "something")).to eq([object, "something"])
    end
  end

  describe "clear cache" do
    it "works" do
      expect { object.inline }.to change(klass, :count).by(1)
      expect { object.inline }.not_to change(klass, :count)
      object.n1_clear_cache
      expect { object.inline }.to change(klass, :count).by(1)
      expect { object.inline }.not_to change(klass, :count)
    end

    context "with parent loader" do
      let(:object) { child_klass.new }

      it "works" do
        expect { object.inline }.to change(child_klass, :count).by(1)
        expect { object.child_something }.to change(child_klass, :count).by(1)
        expect { object.inline }.not_to change(child_klass, :count)
        expect { object.child_something }.not_to change(child_klass, :count)

        object.n1_clear_cache

        expect { object.inline }.to change(child_klass, :count).by(1)
        expect { object.child_something }.to change(child_klass, :count).by(1)
        expect { object.inline }.not_to change(child_klass, :count)
        expect { object.child_something }.not_to change(child_klass, :count)
      end
    end

    context "with binding" do
      it "clears binding so objects load independently after cache clear" do
        objects.each { |obj| obj.n1_bind_to(objects) }

        # Binding ensures the whole collection loads in one batch
        expect { objects.first.inline }.to change(klass, :count).by(1)
        expect { objects.last.inline }.not_to change(klass, :count)

        objects.each(&:n1_clear_cache)

        # After clearing cache and binding, each object loads independently
        expect { objects.first.inline }.to change(klass, :count).by(1)
        expect { objects.last.inline }.to change(klass, :count).by(1)
      end
    end
  end

  describe "arguments support" do
    it "has to receive all arguments" do
      expect { object.with_arguments }.to raise_error(N1Loader::MissingArgument)
      expect { object.with_arguments(something: "something") }.to raise_error(N1Loader::MissingArgument)
      expect { object.with_arguments("something") }.to raise_error(ArgumentError)

      expect(object.with_arguments(something: "something",
                                   anything: "anything")).to eq([object,
                                                                 "something", "anything"])
    end

    it "supports optional arguments" do
      expect { object.with_optional_argument }
        .to raise_error(N1Loader::MissingArgument, "Loader requires [:anything] arguments but they are missing")
      expect(object.with_optional_argument(anything: 2)).to eq([object, nil, 2])
      expect(object.with_optional_argument(something: 1, anything: 2)).to eq([object, 1, 2])
      expect { object.with_optional_argument(tmp: 1, anything: 2) }
        .to raise_error(N1Loader::InvalidArgument, "Loader doesn't define tmp argument")
    end

    it "supports default arguments" do
      expect { object.with_default_argument }
        .to raise_error(N1Loader::MissingArgument, "Loader requires [:anything] arguments but they are missing")
      expect(object.with_default_argument(anything: 2)).to eq([object, [], 2])
      expect(object.with_default_argument(something: 1, anything: 2)).to eq([object, 1, 2])
      expect { object.with_default_argument(tmp: 1, anything: 2) }
        .to raise_error(N1Loader::InvalidArgument, "Loader doesn't define tmp argument")
    end

    it "can have custom arguments key" do
      # The following two has the same result for custom key, so we only do one perform
      expect { object.with_custom_arguments_key(something: 1, anything: 2) }.to change(klass, :count).by(1)
      expect { object.with_custom_arguments_key(something: 2, anything: 1) }.not_to change(klass, :count)

      expect { object.with_custom_arguments_key(something: 2, anything: 3) }.to change(klass, :count).by(1)
    end

    it "supports named arguments" do
      expect do
        object.with_custom_arguments_key
      end.to raise_error(N1Loader::MissingArgument,
                         "Loader requires [:something, :anything] arguments but they are missing")
      expect do
        object.with_custom_arguments_key(something: "something")
      end.to raise_error(N1Loader::MissingArgument,
                         "Loader requires [:anything] arguments but they are missing")

      expect(object.with_custom_arguments_key(something: "something",
                                              anything: "anything")).to eq([
                                                                             object, "something", "anything"
                                                                           ])
    end

    it "supports falsey argument values" do
      expect(object.with_default_argument(anything: 2)).to eq([object, [], 2])                      # default value
      expect(object.with_default_argument(something: false, anything: 2)).to eq([object, false, 2]) # false
      expect(object.with_default_argument(something: nil, anything: 2)).to eq([object, nil, 2])     # nil
    end

    it "works with preloading" do
      N1Loader::Preloader.new(objects).preload(:with_arguments)

      expect do
        objects.each do |object|
          expect(object.with_arguments(something: "something",
                                       anything: "anything")).to eq([object,
                                                                     "something", "anything"])
        end
      end.to change(klass, :count).by(1)
    end

    it "caches based on arguments" do
      N1Loader::Preloader.new(objects).preload(:with_arguments, :with_default_argument)

      expect do
        objects.each { |object| object.with_arguments(something: "something", anything: "anything") }
      end.to change(klass, :count).by(1)

      expect do
        objects.each { |object| object.with_arguments(something: "something2", anything: "anything") }
      end.to change(klass, :count).by(1)

      expect do
        objects.each { |object| object.with_arguments(something: "something", anything: "anything2") }
      end.to change(klass, :count).by(1)

      expect do
        objects.each { |object| object.with_arguments(something: "something", anything: "anything") }
      end.not_to change(klass, :count)

      expect do
        objects.each { |object| object.with_default_argument(something: false, anything: nil) }
      end.to change(klass, :count).by(1)

      expect do
        objects.each { |object| object.with_default_argument(something: false, anything: nil) }
      end.not_to change(klass, :count)
    end

    it "supports reloading" do
      expect do
        object.with_arguments(something: "something", anything: "anything")
      end.to change(klass, :count).by(1)

      expect do
        object.with_arguments(something: "something", anything: "anything", reload: true)
      end.to change(klass, :count).by(1)

      expect do
        object.with_arguments(something: "something", anything: "anything")
      end.not_to change(klass, :count)
    end
  end

  describe "optimization for single object" do
    it "uses optimization" do
      expect(object.single_optimized).to eq([object])

      N1Loader::Preloader.new(objects).preload(:single_optimized)
      expect { objects.map(&:single_optimized) }.to raise_error(StandardError, "unknown")
    end
  end

  describe "isolated loaders" do
    it "does not need injection" do
      instance = loader.new(objects)

      objects.each do |object|
        expect(instance.for(object)).to eq([object])
      end
    end

    it "checks that element was provided" do
      instance = loader.new(objects)

      objects.each do |object|
        expect(instance.for(object)).to eq([object])
      end
      expect do
        instance.for(object)
      end.to raise_error(N1Loader::NotLoaded, "The data was not preloaded for the given element")
    end
  end

  describe "reloading" do
    context "with preloading" do
      it "reloads cached data" do
        N1Loader::Preloader.new(objects).preload(:inline)

        expect { objects.map(&:inline) }.to change(klass, :count).by(1)

        N1Loader::Preloader.new(objects).preload(:inline)
        expect { objects.map(&:inline) }.to change(klass, :count).by(1)
        expect { objects.map(&:inline) }.not_to change(klass, :count)
      end
    end

    context "without preloading" do
      it "reloads cached data" do
        expect { object.inline }.to change(klass, :count).by(1)
        expect { object.inline(reload: true) }.to change(klass, :count).by(1)
        expect { object.inline(reload: false) }.not_to change(klass, :count)
        expect { object.inline }.not_to change(klass, :count)
      end
    end
  end

  context "with custom loader" do
    it "works" do
      expect(object.custom).to eq([object])
    end
  end

  context "without preloading" do
    it "returns right data" do
      expect(object.inline).to eq([object])
    end

    it "caches data" do
      expect { object.inline }.to change(klass, :count).by(1)
      expect { object.inline }.not_to change(klass, :count)
    end
  end

  context "with preloading" do
    it "returns right data" do
      N1Loader::Preloader.new(objects).preload(:inline)

      expect(objects.first.inline).to eq([objects.first])
      expect(objects.last.inline).to eq([objects.last])
    end

    it "lazy loads data" do
      expect { N1Loader::Preloader.new(objects).preload(:inline) }.not_to change(klass, :count)
      expect { objects.map(&:inline) }.to change(klass, :count).by(1)
    end

    it "uses preloaded data" do
      N1Loader::Preloader.new(objects).preload(:inline)

      expect { objects.map(&:inline) }.to change(klass, :count).by(1)
      expect { objects.map(&:inline) }.not_to change(klass, :count)
    end
  end

  describe "n1_bind_to" do
    it "raises error if invalid collection was passed" do
      expect do
        objects.each { |obj| obj.n1_bind_to([]) }
      end.to raise_error N1Loader::InvalidBinding

      expect do
        objects.first.n1_bind_to([objects.last])
      end.to raise_error N1Loader::InvalidBinding
    end

    it "loads all bound objects in a single batch" do
      objects.each { |obj| obj.n1_bind_to(objects) }

      expect { objects.map(&:inline) }.to change(klass, :count).by(1)
    end

    it "caches the result after the first load" do
      objects.each { |obj| obj.n1_bind_to(objects) }

      expect { objects.map(&:inline) }.to change(klass, :count).by(1)
      expect { objects.map(&:inline) }.not_to change(klass, :count)
    end

    it "lazily loads when the first object is accessed" do
      objects.each { |obj| obj.n1_bind_to(objects) }

      expect { objects.first.inline }.to change(klass, :count).by(1)
      expect { objects.last.inline }.not_to change(klass, :count)
    end

    it "propagates context to loaded objects" do
      objects.each { |obj| obj.n1_bind_to(objects) }

      expect do
        objects.each(&:new_objects)
      end.to change(klass, :count).by(1)

      expect do
        objects.map(&:new_objects).map(&:inline)
      end.to change(klass, :count).by(1)
    end
  end

  describe "with preloader" do
    it "doesn't propagate context to loaded objects" do
      N1Loader::Preloader.new(objects).preload(:new_objects)

      expect do
        objects.each(&:new_objects)
      end.to change(klass, :count).by(1)

      expect do
        objects.map(&:new_objects).map(&:inline)
      end.to change(klass, :count).by(2)
    end
  end
end


================================================
FILE: spec/spec_helper.rb
================================================
# frozen_string_literal: true

require "n1_loader"
require "db-query-matchers"

DBQueryMatchers.configure do |config|
  config.schemaless = true
end

RSpec.configure do |config|
  # Enable flags like --only-failures and --next-failure
  config.example_status_persistence_file_path = ".rspec_status"

  # Disable RSpec exposing methods globally on `Module` and `main`
  config.disable_monkey_patching!

  config.expect_with :rspec do |c|
    c.syntax = :expect
  end

  def ar_lazy_preload_defined?
    defined?(ArLazyPreload)
  end

  def ar_version
    ActiveRecord::VERSION::MAJOR
  end
end
Download .txt
gitextract_b233zkyp/

├── .github/
│   ├── FUNDING.yml
│   ├── copilot-instructions.md
│   └── workflows/
│       ├── copilot-setup-steps.yml
│       ├── release.yml
│       ├── rubocop.yml
│       └── tests.yml
├── .gitignore
├── .rspec
├── .rubocop.yml
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── activerecord-gemfiles/
│   ├── ar_5_latest.gemfile
│   ├── ar_6_latest.gemfile
│   ├── ar_7_latest.gemfile
│   └── ar_8_latest.gemfile
├── ar_lazy_preload-gemfiles/
│   ├── ar_lazy_preload_0.6.1.gemfile
│   └── ar_lazy_preload_master.gemfile
├── bin/
│   ├── console
│   └── setup
├── examples/
│   ├── active_record_integration.rb
│   ├── ar_lazy_integration.rb
│   ├── ar_lazy_integration_with_isolated_loader.rb
│   ├── arguments_support.rb
│   ├── context/
│   │   ├── service.rb
│   │   ├── setup_ar_lazy.rb
│   │   └── setup_database.rb
│   ├── core.rb
│   ├── goldiloader_integration.rb
│   ├── goldiloader_integration_with_isolated_loader.rb
│   ├── graphql.rb
│   ├── isolated_loader.rb
│   ├── lazy_loading.rb
│   ├── n1_bind_to.rb
│   ├── reloading.rb
│   ├── shared_loader.rb
│   └── single_case.rb
├── goldiloader-gemfiles/
│   └── goldiloader.gemfile
├── guides/
│   └── enhanced-activerecord.md
├── lib/
│   ├── n1_loader/
│   │   ├── active_record/
│   │   │   ├── associations_preloader.rb
│   │   │   ├── associations_preloader_v5.rb
│   │   │   ├── associations_preloader_v6.rb
│   │   │   ├── base.rb
│   │   │   ├── loader.rb
│   │   │   └── loader_collection.rb
│   │   ├── active_record.rb
│   │   ├── ar_lazy_preload/
│   │   │   ├── associated_context_builder.rb
│   │   │   ├── context.rb
│   │   │   ├── context_adapter.rb
│   │   │   ├── loadable.rb
│   │   │   ├── loader.rb
│   │   │   ├── loader_collection_patch.rb
│   │   │   ├── loader_patch.rb
│   │   │   └── preloader_patch.rb
│   │   ├── ar_lazy_preload.rb
│   │   ├── core/
│   │   │   ├── loadable.rb
│   │   │   ├── loader.rb
│   │   │   ├── loader_builder.rb
│   │   │   ├── loader_collection.rb
│   │   │   └── preloader.rb
│   │   ├── goldiloader/
│   │   │   ├── context.rb
│   │   │   ├── context_adapter.rb
│   │   │   ├── loadable.rb
│   │   │   ├── loader.rb
│   │   │   ├── loader_collection_patch.rb
│   │   │   ├── loader_patch.rb
│   │   │   └── preloader_patch.rb
│   │   ├── goldiloader.rb
│   │   └── version.rb
│   └── n1_loader.rb
├── n1_loader.gemspec
└── spec/
    ├── activerecord_spec.rb
    ├── ar_lazy_preload_spec.rb
    ├── goldiloader_spec.rb
    ├── n1_loader_spec.rb
    └── spec_helper.rb
Download .txt
SYMBOL INDEX (233 symbols across 44 files)

FILE: examples/active_record_integration.rb
  class User (line 7) | class User < ActiveRecord::Base
  class Payment (line 20) | class Payment < ActiveRecord::Base

FILE: examples/ar_lazy_integration.rb
  class User (line 8) | class User < ActiveRecord::Base
  class Payment (line 21) | class Payment < ActiveRecord::Base

FILE: examples/ar_lazy_integration_with_isolated_loader.rb
  class Loader (line 8) | class Loader < N1Loader::Loader
    method perform (line 9) | def perform(users)
  class User (line 19) | class User < ActiveRecord::Base
  class Payment (line 23) | class Payment < ActiveRecord::Base

FILE: examples/arguments_support.rb
  class User (line 7) | class User < ActiveRecord::Base
    method perform (line 26) | def perform(users)
  class Payment (line 43) | class Payment < ActiveRecord::Base

FILE: examples/context/service.rb
  class Service (line 2) | class Service
    method count (line 3) | def self.count
    method increase! (line 7) | def self.increase!
    method receive (line 11) | def self.receive(*users)
    method single (line 17) | def self.single(user)

FILE: examples/context/setup_database.rb
  function fill_database (line 19) | def fill_database

FILE: examples/core.rb
  class User (line 8) | class User
    method unoptimized_call (line 11) | def unoptimized_call

FILE: examples/goldiloader_integration.rb
  class User (line 7) | class User < ActiveRecord::Base
  class Payment (line 20) | class Payment < ActiveRecord::Base

FILE: examples/goldiloader_integration_with_isolated_loader.rb
  class Loader (line 7) | class Loader < N1Loader::Loader
    method perform (line 8) | def perform(users)
  class User (line 18) | class User < ActiveRecord::Base
  class Payment (line 22) | class Payment < ActiveRecord::Base

FILE: examples/graphql.rb
  class User (line 9) | class User < ActiveRecord::Base
  class Payment (line 22) | class Payment < ActiveRecord::Base
  class UserType (line 38) | class UserType < GraphQL::Schema::Object
  class QueryType (line 42) | class QueryType < GraphQL::Schema::Object
    method users (line 45) | def users
  class Schema (line 50) | class Schema < GraphQL::Schema

FILE: examples/isolated_loader.rb
  class IsolatedLoader (line 3) | class IsolatedLoader < N1Loader::Loader
    method perform (line 4) | def perform(elements)

FILE: examples/lazy_loading.rb
  class User (line 6) | class User

FILE: examples/n1_bind_to.rb
  class User (line 8) | class User

FILE: examples/reloading.rb
  class User (line 5) | class User

FILE: examples/shared_loader.rb
  class SharedLoader (line 6) | class SharedLoader < N1Loader::Loader
    method perform (line 7) | def perform(objects)
  class User (line 16) | class User
  class Payment (line 22) | class Payment

FILE: examples/single_case.rb
  class OptimizedLoader (line 6) | class OptimizedLoader < N1Loader::Loader
    method perform (line 7) | def perform(objects)
    method single (line 15) | def single(object)
  class User (line 20) | class User

FILE: lib/n1_loader.rb
  type N1Loader (line 11) | module N1Loader # :nodoc:
    class Error (line 12) | class Error < StandardError; end
    class NotImplemented (line 13) | class NotImplemented < Error; end
    class NotLoaded (line 14) | class NotLoaded < Error; end
    class NotFilled (line 15) | class NotFilled < Error; end
    class MissingArgument (line 16) | class MissingArgument < Error; end
    class InvalidArgument (line 17) | class InvalidArgument < Error; end
    class InvalidBinding (line 18) | class InvalidBinding < Error; end

FILE: lib/n1_loader/active_record.rb
  type N1Loader (line 9) | module N1Loader
    type ActiveRecord (line 10) | module ActiveRecord
      class InvalidPreloading (line 11) | class InvalidPreloading < N1Loader::Error; end

FILE: lib/n1_loader/active_record/associations_preloader.rb
  type N1Loader (line 3) | module N1Loader
    type ActiveRecord (line 4) | module ActiveRecord
      type Associations (line 5) | module Associations
        type Preloader (line 6) | module Preloader # :nodoc:
          function options (line 8) | def options
          function deprecated? (line 12) | def deprecated?
          function through_reflection? (line 16) | def through_reflection?
          function preloaders_for_reflection (line 21) | def preloaders_for_reflection(reflection, records)
          function grouped_records (line 27) | def grouped_records # rubocop:disable Metrics/PerceivedComplexit...

FILE: lib/n1_loader/active_record/associations_preloader_v5.rb
  type N1Loader (line 3) | module N1Loader
    type ActiveRecord (line 4) | module ActiveRecord
      type Associations (line 5) | module Associations
        type Preloader (line 6) | module Preloader # :nodoc:
          function options (line 8) | def options
          function preloaders_for_one (line 13) | def preloaders_for_one(association, records, scope)
          function grouped_records (line 25) | def grouped_records(association, records)

FILE: lib/n1_loader/active_record/associations_preloader_v6.rb
  type N1Loader (line 3) | module N1Loader
    type ActiveRecord (line 4) | module ActiveRecord
      type Associations (line 5) | module Associations
        type Preloader (line 6) | module Preloader # :nodoc:
          function options (line 8) | def options
          function preloaders_for_reflection (line 13) | def preloaders_for_reflection(reflection, records, scope)
          function grouped_records (line 19) | def grouped_records(association, records, polymorphic_parent)

FILE: lib/n1_loader/active_record/base.rb
  type N1Loader (line 3) | module N1Loader
    type ActiveRecord (line 4) | module ActiveRecord
      type Base (line 6) | module Base
        function reload (line 12) | def reload(*)

FILE: lib/n1_loader/ar_lazy_preload/associated_context_builder.rb
  type N1Loader (line 3) | module N1Loader
    type ArLazyPreload (line 4) | module ArLazyPreload
      class AssociatedContextBuilder (line 6) | class AssociatedContextBuilder < ::ArLazyPreload::AssociatedContextB...
        method initialize (line 9) | def initialize(parent_context:, association_name:, records:)
        method perform (line 14) | def perform

FILE: lib/n1_loader/ar_lazy_preload/context_adapter.rb
  type N1Loader (line 3) | module N1Loader
    type ArLazyPreload (line 4) | module ArLazyPreload
      class ContextAdapter (line 6) | class ContextAdapter
        method initialize (line 11) | def initialize(context)
        method try_preload_lazily (line 16) | def try_preload_lazily(association_name)
        method perform_preloading (line 24) | def perform_preloading(association_name)

FILE: lib/n1_loader/ar_lazy_preload/loadable.rb
  type N1Loader (line 3) | module N1Loader
    type ArLazyPreload (line 4) | module ArLazyPreload
      type Loadable (line 5) | module Loadable # :nodoc:
        function n1_loader (line 6) | def n1_loader(name)

FILE: lib/n1_loader/ar_lazy_preload/loader_collection_patch.rb
  type N1Loader (line 3) | module N1Loader
    type ArLazyPreload (line 4) | module ArLazyPreload
      type LoaderCollectionPatch (line 6) | module LoaderCollectionPatch
        function with (line 9) | def with(**args)

FILE: lib/n1_loader/ar_lazy_preload/loader_patch.rb
  type N1Loader (line 3) | module N1Loader
    type ArLazyPreload (line 4) | module ArLazyPreload
      type LoaderPatch (line 6) | module LoaderPatch
        function loaded? (line 9) | def loaded?
        function non_thread_safe_context_setting (line 19) | def non_thread_safe_context_setting

FILE: lib/n1_loader/ar_lazy_preload/preloader_patch.rb
  type N1Loader (line 3) | module N1Loader
    type ArLazyPreload (line 4) | module ArLazyPreload
      type PreloaderPatch (line 6) | module PreloaderPatch
        function initialize (line 7) | def initialize(elements, context_setup = nil)
        function preload (line 12) | def preload(*keys)

FILE: lib/n1_loader/core/loadable.rb
  type N1Loader (line 3) | module N1Loader
    type Loadable (line 26) | module Loadable
      function n1_loaders (line 27) | def n1_loaders
      function n1_loader (line 31) | def n1_loader(name)
      function n1_bind_to (line 35) | def n1_bind_to(collection)
      function n1_bind_to? (line 47) | def n1_bind_to?
      function n1_loader_reload (line 51) | def n1_loader_reload(name)
      function n1_clear_cache (line 59) | def n1_clear_cache
      function included (line 66) | def self.included(base)
      type ClassMethods (line 70) | module ClassMethods # :nodoc:
        function n1_loaders (line 71) | def n1_loaders
        function n1_optimized (line 75) | def n1_optimized(name, loader = nil, &block)

FILE: lib/n1_loader/core/loader.rb
  type N1Loader (line 3) | module N1Loader
    class Loader (line 8) | class Loader
      method argument (line 23) | def argument(name, **opts)
      method cache_key (line 36) | def cache_key(&block)
      method initialize (line 44) | def initialize(elements, **args)
      method for (line 49) | def for(element)
      method cache_key (line 62) | def cache_key
      method check_missing_arguments! (line 71) | def check_missing_arguments!
      method required_arguments (line 85) | def required_arguments(args)
      method check_arguments! (line 90) | def check_arguments!
      method check_invalid_arguments! (line 95) | def check_invalid_arguments!
      method perform (line 105) | def perform(_elements)
      method fulfill (line 109) | def fulfill(element, value)
      method loaded? (line 114) | def loaded?
      method non_thread_safe_loading (line 122) | def non_thread_safe_loading # rubocop:disable Metrics/AbcSize, Metri...

FILE: lib/n1_loader/core/loader_builder.rb
  type N1Loader (line 3) | module N1Loader
    class LoaderBuilder (line 5) | class LoaderBuilder
      method build (line 6) | def self.build(&block)

FILE: lib/n1_loader/core/loader_collection.rb
  type N1Loader (line 3) | module N1Loader
    class LoaderCollection (line 5) | class LoaderCollection
      method initialize (line 8) | def initialize(loader_class, elements)
      method with (line 13) | def with(**args)
      method loaders (line 21) | def loaders

FILE: lib/n1_loader/core/preloader.rb
  type N1Loader (line 3) | module N1Loader
    class Preloader (line 10) | class Preloader
      method initialize (line 13) | def initialize(elements)
      method preload (line 17) | def preload(*keys)
      method loader_class (line 32) | def loader_class(element, key)

FILE: lib/n1_loader/goldiloader/context_adapter.rb
  type N1Loader (line 3) | module N1Loader
    type Goldiloader (line 4) | module Goldiloader
      class ContextAdapter (line 6) | class ContextAdapter
        method initialize (line 9) | def initialize(context)
        method try_preload_lazily (line 14) | def try_preload_lazily(association_name)
        method perform_preloading (line 20) | def perform_preloading(association_name)

FILE: lib/n1_loader/goldiloader/loadable.rb
  type N1Loader (line 3) | module N1Loader
    type Goldiloader (line 4) | module Goldiloader
      type Loadable (line 5) | module Loadable # :nodoc:
        function n1_loader (line 6) | def n1_loader(name)

FILE: lib/n1_loader/goldiloader/loader_collection_patch.rb
  type N1Loader (line 3) | module N1Loader
    type Goldiloader (line 4) | module Goldiloader
      type LoaderCollectionPatch (line 6) | module LoaderCollectionPatch
        function with (line 9) | def with(**args)

FILE: lib/n1_loader/goldiloader/loader_patch.rb
  type N1Loader (line 3) | module N1Loader
    type Goldiloader (line 4) | module Goldiloader
      type LoaderPatch (line 6) | module LoaderPatch
        function loaded? (line 9) | def loaded?
        function non_thread_safe_context_setting (line 19) | def non_thread_safe_context_setting

FILE: lib/n1_loader/goldiloader/preloader_patch.rb
  type N1Loader (line 3) | module N1Loader
    type Goldiloader (line 4) | module Goldiloader
      type PreloaderPatch (line 6) | module PreloaderPatch
        function initialize (line 7) | def initialize(elements, context_setup = nil)
        function preload (line 12) | def preload(*keys)

FILE: lib/n1_loader/version.rb
  type N1Loader (line 3) | module N1Loader

FILE: spec/activerecord_spec.rb
  function name (line 32) | def name
  function perform! (line 36) | def perform!
  function count (line 40) | def count
  function perform (line 46) | def perform(elements)
  function perform (line 56) | def perform(elements)
  function name (line 71) | def name
  function perform! (line 75) | def perform!
  function count (line 79) | def count
  function perform (line 85) | def perform(elements)
  function perform (line 96) | def perform(elements)
  function perform (line 107) | def perform(elements)
  function perform (line 115) | def perform(elements)
  function name (line 130) | def name
  function perform! (line 134) | def perform!
  function count (line 138) | def count
  function name (line 150) | def name
  function perform! (line 154) | def perform!
  function count (line 158) | def count
  function perform (line 167) | def perform(elements)

FILE: spec/ar_lazy_preload_spec.rb
  function name (line 44) | def name
  function perform! (line 48) | def perform!
  function count (line 52) | def count
  function perform (line 58) | def perform(elements)
  function perform (line 68) | def perform(elements)
  function name (line 82) | def name
  function perform! (line 86) | def perform!
  function count (line 90) | def count
  function perform (line 96) | def perform(elements)
  function perform (line 107) | def perform(elements)
  function perform (line 118) | def perform(elements)
  function perform (line 218) | def perform(_companies)

FILE: spec/goldiloader_spec.rb
  function name (line 26) | def name
  function perform! (line 30) | def perform!
  function count (line 34) | def count
  function perform (line 40) | def perform(elements)
  function perform (line 50) | def perform(elements)
  function name (line 64) | def name
  function perform! (line 68) | def perform!
  function count (line 72) | def count
  function perform (line 78) | def perform(elements)
  function perform (line 89) | def perform(elements)
  function perform (line 100) | def perform(elements)
  function perform (line 183) | def perform(_companies)

FILE: spec/n1_loader_spec.rb
  function perform (line 6) | def perform(elements)
  function perform! (line 19) | def perform!
  function count (line 23) | def count
  function single (line 44) | def single(element)
  function perform (line 48) | def perform(_elements)
  function perform (line 54) | def perform(elements)
  function perform (line 63) | def perform(elements)
  function perform (line 76) | def perform(elements)
  function perform (line 89) | def perform(elements)
  function perform (line 104) | def perform(elements)
  function perform (line 116) | def perform(elements)
  function perform (line 211) | def perform(elements)

FILE: spec/spec_helper.rb
  function ar_lazy_preload_defined? (line 21) | def ar_lazy_preload_defined?
  function ar_version (line 25) | def ar_version
Condensed preview — 79 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (140K chars).
[
  {
    "path": ".github/FUNDING.yml",
    "chars": 77,
    "preview": "# Supported platforms for funding\nopen_collective: n1_loader\ngithub: djezzzl\n"
  },
  {
    "path": ".github/copilot-instructions.md",
    "chars": 292,
    "preview": "# Copilot Instructions\n\nEvery PR must have no errors from:\n\n```\nbundle exec rubocop\nbundle exec rspec spec/n1_loader_spe"
  },
  {
    "path": ".github/workflows/copilot-setup-steps.yml",
    "chars": 774,
    "preview": "name: Copilot Setup Steps\n\n# Automatically run the setup steps when they are changed to allow for easy validation, and\n#"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 1716,
    "preview": "name: Release\n\non:\n  workflow_dispatch:\n    inputs:\n      version:\n        description: 'Version to release (e.g. 2.1.4)"
  },
  {
    "path": ".github/workflows/rubocop.yml",
    "chars": 388,
    "preview": "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   "
  },
  {
    "path": ".github/workflows/tests.yml",
    "chars": 1946,
    "preview": "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  "
  },
  {
    "path": ".gitignore",
    "chars": 129,
    "preview": "/.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"
  },
  {
    "path": ".rspec",
    "chars": 53,
    "preview": "--format documentation\n--color\n--require spec_helper\n"
  },
  {
    "path": ".rubocop.yml",
    "chars": 325,
    "preview": "AllCops:\n  TargetRubyVersion: 2.5\n  Exclude:\n    - examples/**/*\n    - vendor/bundle/**/*\n\nStyle/StringLiterals:\n  Enabl"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 4819,
    "preview": "## [3.0.0] - 2026/03/18\n\n- Add support of [Goldiloader](https://github.com/KentaaNL/goldiloader)\n\n## [2.2.1] - 2026/03/1"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "chars": 5228,
    "preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participa"
  },
  {
    "path": "Gemfile",
    "chars": 614,
    "preview": "# frozen_string_literal: true\n\nsource \"https://rubygems.org\"\n\n# Specify your gem's dependencies in n1_loader.gemspec\ngem"
  },
  {
    "path": "LICENSE.txt",
    "chars": 1088,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2021 TODO: Write your name\n\nPermission is hereby granted, free of charge, to any pe"
  },
  {
    "path": "README.md",
    "chars": 19022,
    "preview": "# N1Loader\n\n[![Gem Version][3]][4]\n[![][11]][12]\n[![][13]][14]\n[![][9]][10]\n\nN1Loader is designed to provide a simple wa"
  },
  {
    "path": "Rakefile",
    "chars": 208,
    "preview": "# frozen_string_literal: true\n\nrequire \"bundler/gem_tasks\"\nrequire \"rspec/core/rake_task\"\n\nRSpec::Core::RakeTask.new(:sp"
  },
  {
    "path": "activerecord-gemfiles/ar_5_latest.gemfile",
    "chars": 91,
    "preview": "# frozen_string_literal: true\n\ngem \"activerecord\", \"~> 5\"\ngem \"concurrent-ruby\", \"= 1.3.4\"\n"
  },
  {
    "path": "activerecord-gemfiles/ar_6_latest.gemfile",
    "chars": 148,
    "preview": "# frozen_string_literal: true\n\ngem \"activerecord\", \"~> 6\"\ngem \"benchmark\"\ngem \"bigdecimal\"\ngem \"concurrent-ruby\", \"= 1.3"
  },
  {
    "path": "activerecord-gemfiles/ar_7_latest.gemfile",
    "chars": 149,
    "preview": "# frozen_string_literal: true\n\ngem \"activerecord\", \"~> 7\"\ngem \"benchmark\"\ngem \"bigdecimal\"\ngem \"concurrent-ruby\", \"= 1.3"
  },
  {
    "path": "activerecord-gemfiles/ar_8_latest.gemfile",
    "chars": 116,
    "preview": "# 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",
    "chars": 64,
    "preview": "# frozen_string_literal: true\n\ngem \"ar_lazy_preload\", \"= 0.6.1\"\n"
  },
  {
    "path": "ar_lazy_preload-gemfiles/ar_lazy_preload_master.gemfile",
    "chars": 129,
    "preview": "# frozen_string_literal: true\n\ngem \"ar_lazy_preload\", git: \"https://github.com/DmitryTsepelev/ar_lazy_preload\", branch: "
  },
  {
    "path": "bin/console",
    "chars": 374,
    "preview": "#!/usr/bin/env ruby\n# frozen_string_literal: true\n\nrequire \"bundler/setup\"\nrequire \"n1_loader\"\n\n# You can add fixtures a"
  },
  {
    "path": "bin/setup",
    "chars": 131,
    "preview": "#!/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 "
  },
  {
    "path": "examples/active_record_integration.rb",
    "chars": 835,
    "preview": "# frozen_string_literal: true\n\nrequire \"n1_loader/active_record\"\n\nrequire_relative 'context/setup_database'\n\nclass User "
  },
  {
    "path": "examples/ar_lazy_integration.rb",
    "chars": 813,
    "preview": "# frozen_string_literal: true\n\nrequire \"n1_loader/ar_lazy_preload\"\n\nrequire_relative 'context/setup_ar_lazy'\nrequire_rel"
  },
  {
    "path": "examples/ar_lazy_integration_with_isolated_loader.rb",
    "chars": 880,
    "preview": "# frozen_string_literal: true\n\nrequire \"n1_loader/ar_lazy_preload\"\n\nrequire_relative 'context/setup_ar_lazy'\nrequire_rel"
  },
  {
    "path": "examples/arguments_support.rb",
    "chars": 1736,
    "preview": "# frozen_string_literal: true\n\nrequire \"n1_loader/active_record\"\n\nrequire_relative 'context/setup_database'\n\nclass User "
  },
  {
    "path": "examples/context/service.rb",
    "chars": 329,
    "preview": "# 3rd party service, or database, or anything else that can perform in batches\nclass Service\n  def self.count\n    @count"
  },
  {
    "path": "examples/context/setup_ar_lazy.rb",
    "chars": 649,
    "preview": "ActiveSupport.on_load(:active_record) do\n  ActiveRecord::Base.include(ArLazyPreload::Base)\n\n  ActiveRecord::Relation.pre"
  },
  {
    "path": "examples/context/setup_database.rb",
    "chars": 626,
    "preview": "require \"sqlite3\"\n\nActiveRecord::Base.establish_connection(adapter: \"sqlite3\", database: \":memory:\")\nActiveRecord::Base."
  },
  {
    "path": "examples/core.rb",
    "chars": 1162,
    "preview": "# frozen_string_literal: true\n\nrequire \"n1_loader\"\n\nrequire_relative 'context/service'\n\n# Class that wants to request 3r"
  },
  {
    "path": "examples/goldiloader_integration.rb",
    "chars": 706,
    "preview": "# frozen_string_literal: true\n\nrequire \"n1_loader/goldiloader\"\n\nrequire_relative \"context/setup_database\"\n\nclass User < "
  },
  {
    "path": "examples/goldiloader_integration_with_isolated_loader.rb",
    "chars": 757,
    "preview": "# frozen_string_literal: true\n\nrequire \"n1_loader/goldiloader\"\n\nrequire_relative \"context/setup_database\"\n\nclass Loader "
  },
  {
    "path": "examples/graphql.rb",
    "chars": 1183,
    "preview": "# frozen_string_literal: true\n\nrequire \"n1_loader/ar_lazy_preload\"\nrequire 'graphql'\n\nrequire_relative 'context/setup_da"
  },
  {
    "path": "examples/isolated_loader.rb",
    "chars": 335,
    "preview": "require 'n1_loader'\n\nclass IsolatedLoader < N1Loader::Loader\n  def perform(elements)\n    elements.each { |element| fulfi"
  },
  {
    "path": "examples/lazy_loading.rb",
    "chars": 607,
    "preview": "require 'n1_loader'\n\nrequire_relative 'context/service'\n\n# Class that wants to request 3rd party service without N+1\ncla"
  },
  {
    "path": "examples/n1_bind_to.rb",
    "chars": 879,
    "preview": "# frozen_string_literal: true\n\nrequire \"n1_loader\"\n\nrequire_relative 'context/service'\n\n# Class that wants to request 3r"
  },
  {
    "path": "examples/reloading.rb",
    "chars": 758,
    "preview": "require 'n1_loader'\n\nrequire_relative 'context/service'\n\nclass User\n  include N1Loader::Loadable\n\n  n1_optimized :optimi"
  },
  {
    "path": "examples/shared_loader.rb",
    "chars": 721,
    "preview": "require 'n1_loader'\n\nrequire_relative 'context/service'\n\n# Loader that will be shared between multiple classes\nclass Sha"
  },
  {
    "path": "examples/single_case.rb",
    "chars": 706,
    "preview": "require 'n1_loader'\n\nrequire_relative 'context/service'\n\n# Loader that will be shared between multiple classes\nclass Opt"
  },
  {
    "path": "goldiloader-gemfiles/goldiloader.gemfile",
    "chars": 49,
    "preview": "# frozen_string_literal: true\n\ngem \"goldiloader\"\n"
  },
  {
    "path": "guides/enhanced-activerecord.md",
    "chars": 8264,
    "preview": "# Enhanced ActiveRecord\n\n- Do you like `ActiveRecord` preloading?\n- How many times have you resolved your N+1 issues wit"
  },
  {
    "path": "lib/n1_loader/active_record/associations_preloader.rb",
    "chars": 1436,
    "preview": "# frozen_string_literal: true\n\nmodule N1Loader\n  module ActiveRecord\n    module Associations\n      module Preloader # :n"
  },
  {
    "path": "lib/n1_loader/active_record/associations_preloader_v5.rb",
    "chars": 1185,
    "preview": "# frozen_string_literal: true\n\nmodule N1Loader\n  module ActiveRecord\n    module Associations\n      module Preloader # :n"
  },
  {
    "path": "lib/n1_loader/active_record/associations_preloader_v6.rb",
    "chars": 929,
    "preview": "# frozen_string_literal: true\n\nmodule N1Loader\n  module ActiveRecord\n    module Associations\n      module Preloader # :n"
  },
  {
    "path": "lib/n1_loader/active_record/base.rb",
    "chars": 340,
    "preview": "# frozen_string_literal: true\n\nmodule N1Loader\n  module ActiveRecord\n    # Extension module for ActiveRecord::Base\n    m"
  },
  {
    "path": "lib/n1_loader/active_record/loader.rb",
    "chars": 155,
    "preview": "# frozen_string_literal: true\n\nN1Loader::Loader.define_method :preloaded_records do\n  @preloaded_records ||= loaded? && "
  },
  {
    "path": "lib/n1_loader/active_record/loader_collection.rb",
    "chars": 448,
    "preview": "# frozen_string_literal: true\n\nN1Loader::LoaderCollection.define_method :preloaded_records do\n  raise N1Loader::ActiveRe"
  },
  {
    "path": "lib/n1_loader/active_record.rb",
    "chars": 1060,
    "preview": "# frozen_string_literal: true\n\n# Load core library\nrequire_relative \"../n1_loader\"\n\n# Load integration dependency\nrequir"
  },
  {
    "path": "lib/n1_loader/ar_lazy_preload/associated_context_builder.rb",
    "chars": 705,
    "preview": "# frozen_string_literal: true\n\nmodule N1Loader\n  module ArLazyPreload\n    # Context builder for N1Loader\n    class Assoc"
  },
  {
    "path": "lib/n1_loader/ar_lazy_preload/context.rb",
    "chars": 322,
    "preview": "# frozen_string_literal: true\n\n# Returns cached N1Loader::LoaderCollection from context for a loader.\n# In case there is"
  },
  {
    "path": "lib/n1_loader/ar_lazy_preload/context_adapter.rb",
    "chars": 1105,
    "preview": "# frozen_string_literal: true\n\nmodule N1Loader\n  module ArLazyPreload\n    # Context adapter for injected N1Loader loader"
  },
  {
    "path": "lib/n1_loader/ar_lazy_preload/loadable.rb",
    "chars": 334,
    "preview": "# frozen_string_literal: true\n\nmodule N1Loader\n  module ArLazyPreload\n    module Loadable # :nodoc:\n      def n1_loader("
  },
  {
    "path": "lib/n1_loader/ar_lazy_preload/loader.rb",
    "chars": 1396,
    "preview": "# frozen_string_literal: true\n\n# Raised when a single object without ArLazyPreload context support was passed to an isol"
  },
  {
    "path": "lib/n1_loader/ar_lazy_preload/loader_collection_patch.rb",
    "chars": 397,
    "preview": "# frozen_string_literal: true\n\nmodule N1Loader\n  module ArLazyPreload\n    # A patch to {N1Loader::LoaderCollection} to s"
  },
  {
    "path": "lib/n1_loader/ar_lazy_preload/loader_patch.rb",
    "chars": 595,
    "preview": "# frozen_string_literal: true\n\nmodule N1Loader\n  module ArLazyPreload\n    # A patch to {N1Loader::Loader} to setup lazy "
  },
  {
    "path": "lib/n1_loader/ar_lazy_preload/preloader_patch.rb",
    "chars": 502,
    "preview": "# frozen_string_literal: true\n\nmodule N1Loader\n  module ArLazyPreload\n    # A patch to {N1Loader::Preloader} setup lazy "
  },
  {
    "path": "lib/n1_loader/ar_lazy_preload.rb",
    "chars": 858,
    "preview": "# frozen_string_literal: true\n\n# Load core library\nrequire_relative \"active_record\"\n\n# Load integration dependency\nrequi"
  },
  {
    "path": "lib/n1_loader/core/loadable.rb",
    "chars": 2167,
    "preview": "# frozen_string_literal: true\n\nmodule N1Loader\n  # The module to be included to the class to define associated loaders.\n"
  },
  {
    "path": "lib/n1_loader/core/loader.rb",
    "chars": 4084,
    "preview": "# frozen_string_literal: true\n\nmodule N1Loader\n  # Loader that performs the loading.\n  #\n  # Subclasses must define +per"
  },
  {
    "path": "lib/n1_loader/core/loader_builder.rb",
    "chars": 325,
    "preview": "# frozen_string_literal: true\n\nmodule N1Loader\n  # The class builds {N1Loader::Loader}\n  class LoaderBuilder\n    def sel"
  },
  {
    "path": "lib/n1_loader/core/loader_collection.rb",
    "chars": 513,
    "preview": "# frozen_string_literal: true\n\nmodule N1Loader\n  # The class is used for storing collections of loaders for elements per"
  },
  {
    "path": "lib/n1_loader/core/preloader.rb",
    "chars": 1063,
    "preview": "# frozen_string_literal: true\n\nmodule N1Loader\n  # Preloader that lazily preloads data to every element.\n  #\n  # It supp"
  },
  {
    "path": "lib/n1_loader/goldiloader/context.rb",
    "chars": 317,
    "preview": "# frozen_string_literal: true\n\n# Returns cached N1Loader::LoaderCollection from context for a loader.\n# In case there is"
  },
  {
    "path": "lib/n1_loader/goldiloader/context_adapter.rb",
    "chars": 1006,
    "preview": "# frozen_string_literal: true\n\nmodule N1Loader\n  module Goldiloader\n    # Context adapter for injected N1Loader loaders."
  },
  {
    "path": "lib/n1_loader/goldiloader/loadable.rb",
    "chars": 332,
    "preview": "# frozen_string_literal: true\n\nmodule N1Loader\n  module Goldiloader\n    module Loadable # :nodoc:\n      def n1_loader(na"
  },
  {
    "path": "lib/n1_loader/goldiloader/loader.rb",
    "chars": 1168,
    "preview": "# frozen_string_literal: true\n\n# Raised when a single object without Goldiloader context support was passed to an isolat"
  },
  {
    "path": "lib/n1_loader/goldiloader/loader_collection_patch.rb",
    "chars": 395,
    "preview": "# frozen_string_literal: true\n\nmodule N1Loader\n  module Goldiloader\n    # A patch to {N1Loader::LoaderCollection} to set"
  },
  {
    "path": "lib/n1_loader/goldiloader/loader_patch.rb",
    "chars": 593,
    "preview": "# frozen_string_literal: true\n\nmodule N1Loader\n  module Goldiloader\n    # A patch to {N1Loader::Loader} to setup lazy co"
  },
  {
    "path": "lib/n1_loader/goldiloader/preloader_patch.rb",
    "chars": 503,
    "preview": "# frozen_string_literal: true\n\nmodule N1Loader\n  module Goldiloader\n    # A patch to {N1Loader::Preloader} to setup lazy"
  },
  {
    "path": "lib/n1_loader/goldiloader.rb",
    "chars": 740,
    "preview": "# frozen_string_literal: true\n\n# Load core library\nrequire_relative \"active_record\"\n\n# Load integration dependency\nrequi"
  },
  {
    "path": "lib/n1_loader/version.rb",
    "chars": 71,
    "preview": "# frozen_string_literal: true\n\nmodule N1Loader\n  VERSION = \"3.0.0\"\nend\n"
  },
  {
    "path": "lib/n1_loader.rb",
    "chars": 572,
    "preview": "# frozen_string_literal: true\n\nrequire_relative \"n1_loader/version\"\n\nrequire_relative \"n1_loader/core/loader_builder\"\nre"
  },
  {
    "path": "n1_loader.gemspec",
    "chars": 1448,
    "preview": "# frozen_string_literal: true\n\nrequire_relative \"lib/n1_loader/version\"\n\nGem::Specification.new do |spec|\n  spec.name   "
  },
  {
    "path": "spec/activerecord_spec.rb",
    "chars": 9818,
    "preview": "# frozen_string_literal: true\n\nRSpec.describe \"N1Loader ActiveRecord integration\" do\n  require_relative \"../lib/n1_loade"
  },
  {
    "path": "spec/ar_lazy_preload_spec.rb",
    "chars": 8291,
    "preview": "# frozen_string_literal: true\n\nRSpec.describe \"N1Loader AR Lazy Preload integration\" do\n  require_relative \"../lib/n1_lo"
  },
  {
    "path": "spec/goldiloader_spec.rb",
    "chars": 6631,
    "preview": "# frozen_string_literal: true\n\nRSpec.describe \"N1Loader Goldiloader integration\" do\n  require_relative \"../lib/n1_loader"
  },
  {
    "path": "spec/n1_loader_spec.rb",
    "chars": 17212,
    "preview": "# frozen_string_literal: true\n\nRSpec.describe N1Loader do\n  let(:loader) do\n    Class.new(N1Loader::Loader) do\n      def"
  },
  {
    "path": "spec/spec_helper.rb",
    "chars": 593,
    "preview": "# frozen_string_literal: true\n\nrequire \"n1_loader\"\nrequire \"db-query-matchers\"\n\nDBQueryMatchers.configure do |config|\n  "
  }
]

About this extraction

This page contains the full source code of the djezzzl/n1_loader GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 79 files (126.5 KB), approximately 34.8k tokens, and a symbol index with 233 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!