All jobs

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 RushIn 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 with this body:

{
  "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.

The use case this job follows, with its automation and trigger: Fix a bug from an alert.

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.