# Fix N+1 Queries in a Rails App

Yes. An agent runs your Rails pages with the Bullet gem, fixes each N+1 it reports with includes or preload, and adds a spec per page that counts queries.

[Run this on Rush](https://cloud.getrush.ai/run/fix-n-plus-one-queries/rails/), in a Project that has your repository.

## What the agent does

The agent first gets the app running the way its developers do. In a Rails app like lobsters/lobsters it reads `Gemfile.lock` (Rails 8.1.3.1), `.ruby-version` (Ruby 4.0.0) and `config/database.yml.sample`, which uses SQLite, copies it into place, and runs `bundle install` and the database setup. The specs run with `make test`, which is `bundle exec rspec` over directories from `spec/models` to `spec/requests` and `spec/features`.

Bullet is not in the Gemfile, and nothing in `spec/` counts queries today, so the agent adds `bullet` to the development and test groups and configures it in `config/environments/development.rb` and `test.rb` to raise on unused eager loading and on N+1s. It seeds enough stories, comments, users, tags and votes that every list shows many rows, then opens the main pages in the sandbox's browser: `/` (`home#index`, hottest stories), `/newest`, `/active`, `/recent`, `/comments`, a story at `/s/:id`, and a user at `/~:username`, with their threads and stories. For each it records Bullet's report and the number of SQL queries from an `ActiveSupport::Notifications` subscription on `sql.active_record`.

A mature app is often mostly right already, and the agent reports that rather than inventing changes. Lobsters eager-loads in model scopes: `Story.for_presentation` includes `domain`, `origin`, `hidings`, `user`, `tags` and `taggings: :tag`, and `StoriesPaginator` applies it to every story list; comments preload `user`, `hat`, `moderation: :moderator` and `story: :user`. So the agent looks where scopes are not used: actions that build their own relation, partials that read an association the scope does not include, and per-user data such as votes and hidings on a signed-in page, which only shows up when it signs in as a seeded user.

Each fix extends the scope or relation the page already uses (`includes`, `preload` or `eager_load`), rather than adding a query in the view. For each main page it adds a request spec that seeds several rows, counts queries around the request, and fails if the count grows with the rows. It runs the whole suite with Bullet raising, so any N+1 a spec exercises fails it, and records each page's query count before and after.

## Start it with one API call

`POST https://api.prix.dev/o/{org}/sessions`

```json
{
  "agent": "claude",
  "input": "Find the N+1 queries on this Rails app's main pages. Add the bullet gem to the development and test groups and configure it to raise. Seed enough data that each list shows many rows, sign in as a seeded user as well as browsing signed out, open each main page, and record Bullet's report and each page's SQL query count. Fix each N+1 by extending the scope or relation the page already uses with includes, preload or eager_load; remove any eager load Bullet reports as unused. Add one request spec per page that seeds several rows and fails if the query count grows. Run the full suite. Open a pull request with each page's query count before and after, including pages that were already correct.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app"
    }
  ]
}
```

1. **Get an API key.** In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
2. **Send the request.** POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
3. **Read what came back.** Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.

## What comes back

What comes back is a pull request with Bullet in the `Gemfile` and `Gemfile.lock` under development and test, its configuration in the environment files, changed scopes or controller relations where an N+1 was found, and request specs under `spec/requests/` for each main page with a small query-counting helper in `spec/support/`. The description has a table of pages, signed in and signed out, with query counts before and after, and says plainly which pages were already eager-loaded.

The pull request's own checks run the suite with Bullet enabled. The trajectory keeps Bullet's report for each page, the SQL counts and every spec run, so each `includes` in the diff has the report that justified it.

Before merging, a reviewer checks that a new `includes` does not load a large association the page never shows (Bullet's unused eager loading warning is there for that), that `eager_load` was not used where a `JOIN` changes which rows a paginated query returns, and that the query-count specs seed enough rows to catch a loop.

## Run it on every Bullet warning in staging

With Bullet enabled in staging and its notifier pointed at your error tracker or a log, each new N+1 warning names the model, the association and the call site. Have the alert handler send this request with those details in `input` and the repository in `resources`, and the agent fixes that page and adds its spec.

Use the model and association as the `Idempotency-Key` header so the same warning from many requests starts one run. Give the API key a webhook URL and Rush POSTs `run.completed` or `run.failed` when the turn ends, with the session URL to read the pull request link from.

## Why run it on Rush?

N+1s in Rails come from views and partials reading associations the controller did not load, and they show only with real data and a signed-in user whose votes, saves and hidings join in. Reproducing that means seeding, signing in and walking every page, which is slow on a laptop and easy to skip. On Rush the agent does it in its own sandbox with its own database and browser, and keeps Bullet's reports and query counts in the trajectory as evidence.

The result protects itself: Bullet raising in the test environment and a query-count spec per page turn the next N+1 into a failing build. And because each run is isolated, a team with several Rails apps can audit them at once, each in its own pull request.

## Frequently asked questions

### Will Bullet run in production?

No. The agent adds it to the development and test groups of the Gemfile only, and enables it in those environments' config. Enabling it in staging with a notifier rather than raising is a separate choice the pull request mentions but does not make.

### includes, preload or eager_load: which does it use?

`preload` when the page only reads the association, `eager_load` when the query filters or orders on it, and `includes` where Rails can decide. It avoids `eager_load` on paginated lists where a join with a has-many association would change which rows the page returns, and explains each choice in the description.

### What if my app is already eager-loaded?

Then the pull request says so, with the measured counts, and adds the specs and Bullet configuration that keep it that way. A clean report from a seeded, signed-in walk through every main page is worth having, and the specs are the part that protects future changes.

### Does it work with Postgres or MySQL instead of SQLite?

Yes. It uses whatever `config/database.yml` or its sample names and starts that database in the sandbox. Query counts do not depend on the adapter, though `eager_load` SQL can perform differently on each, which the agent notes when it chooses it.
