# Fix N+1 Queries in a Laravel App

Yes. An agent turns on Laravel's preventLazyLoading, finds each lazy load on your main pages, fixes it with eager loading and adds a query-count test per page.

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

## What the agent does

The agent first gets the app and its test suite running in its sandbox. In a Laravel app like BookStackApp/BookStack it reads `composer.json` (`laravel/framework` ^v13.35.0, PHP ^8.3.0, PHPUnit ^12.5.38), sees that `composer test` runs PHPUnit and that `phpunit.xml` sets `DB_CONNECTION=mysql_testing`, and starts MySQL 8.4 as the repository's `docker-compose.yml` does. It seeds enough books, chapters, pages, shelves and tags that every list page shows several items.

Then it makes lazy loading loud. BookStack never calls `Model::preventLazyLoading()` or `Model::shouldBeStrict()`, so a relation read in a Blade loop silently runs one query per row. The agent enables `preventLazyLoading` for the local environment in a service provider, opens each main page in the sandbox's browser, and reads the `LazyLoadingViolationException` each one throws, which names the model and the relation. It also counts queries per page with `DB::listen`, since nothing in `app/` or `tests/` counts them today.

Here that finds the shelves list. `BookshelfController` lists shelves through `BookshelfQueries::visibleForListWithCover()`, which loads only `cover`, but `resources/views/shelves/parts/list-item.blade.php` runs `@foreach($shelf->visibleBooks as $book)`, one more query per shelf. Other pages are already right, such as recently updated pages in `PageController`, which loads `updatedBy`, `book` and `chapter` with visibility scopes, so the agent leaves them alone. It also checks `entities/list-item.blade.php`, where `$entity->tags->count()` runs per item whenever `showTags` is set.

Each fix goes where the query is built: the `with()` in the queries class, with the same visibility scope the relation already uses, so eager loading does not reveal books a user cannot see. For each page it adds a feature test that seeds several rows, requests the page, and asserts the number of queries with `DB::listen` or `expectsDatabaseQueryCount`, so a new lazy load fails the test. It runs `composer test` and records each page's 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 Laravel app's main pages. Start the app and its database with enough seeded data that each list shows several items. Enable Model::preventLazyLoading() in local and testing environments only, open each main page, and record every lazy-loading violation and each page's query count. Fix each at the query with eager loading (with(), load() or withCount()), keeping any visibility or permission scope the relation uses. Add one feature test per page that seeds several rows and fails if the page's query count grows. Run the test suite. Open a pull request with each page's query count before and after.",
  "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 that changes the query-building code, not the templates: added `with()` or `withCount()` calls in controllers or query classes like BookStack's `BookshelfQueries`, a service provider line that enables `preventLazyLoading` outside production, and one feature test per main page under `tests/`. The description has a table of pages with their query counts before and after on the same seeded data, and names the pages that were already eager-loaded.

The pull request's checks run the suite. The trajectory keeps each lazy-loading violation the agent hit, with the model and relation it named, and every test run, so the reason for each `with()` is on record.

Before merging, a reviewer checks that eager-loaded relations carry the same permission scope as before (in an app with per-item visibility like BookStack this is the important check), that `preventLazyLoading` is off in production so a missed case logs rather than throws for users, and that the tests count queries with enough rows to catch a loop.

## Run it on every lazy-loading violation in staging

With `preventLazyLoading` on in staging, Laravel lets you report violations instead of throwing, through `Model::handleLazyLoadingViolationUsing`. Send each new violation (model, relation, URL) to a handler that starts this request with those details in `input` and the repository in `resources`; the agent fixes that page and adds its test.

Use the model and relation as the `Idempotency-Key` header so the same violation reported 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?

Lazy loading in Laravel is invisible until a page lists enough rows to be slow, and finding every case means seeding data, running the app and walking each page, which no one does on a laptop between tickets. On Rush the agent does it in its own sandbox with its own database and browser, and the trajectory keeps each violation and query count it recorded, so the pull request comes with the evidence for every `with()`.

The fix lasts because of the tests: each page gets a query-count assertion, and `preventLazyLoading` in local and testing turns the next lazy load into an exception during development. Several apps can be audited at once, each run isolated, each pull request reviewed on its own.

## Frequently asked questions

### Will preventLazyLoading break production?

Not as the agent sets it up: it enables it only when the app is not in production, which is the pattern Laravel's documentation shows. In production a missed lazy load keeps working as before. The pull request says where the switch is so you can change the condition.

### Does eager loading bypass my permission scopes?

It can, if the eager load is written without the scope the lazy relation had. The agent passes the same constraint into `with()`, as BookStack's own `PageController` does with its visibility scope, and the reviewer checklist in the pull request calls this out.

### Should it use withCount instead of loading the relation?

When the page shows only a number, yes: `withCount('tags')` adds one subquery instead of loading every tag. Where the page shows the related rows themselves, as the shelves list does with its books, it loads them with `with()`. The agent picks per page and says which in the description.
