Skip to main content

Capture modes: pretend and shadow

Before SQLens can lint a migration, it has to get the SQL the migration will run. There are two ways to obtain it, and they answer different questions. Knowing which one you are in — and why a run sometimes says it cannot answer at all — is most of understanding the lint suite.

Pretend mode

Pretend mode is the default. It runs each pending migration through Laravel's own pretend path: the migration's schema and query builder calls are intercepted and the SQL they would produce is recorded, but nothing is executed. A table "created" under pretend does not exist afterwards. This is fast, needs no throwaway database, and is what the single-file fast path uses.

What pretend mode cannot do is the important part, because it is structural, not a bug:

  • It runs the PHP around the SQL for real. Pretend intercepts database statements, not the rest of the method. A migration that sends a notification, makes an HTTP request, or dispatches a job would do those things for real during a pretend run — from a command whose whole purpose is to look at a migration without running it. SQLens refuses to let that happen: a migration that would reach outside the database is flagged by the static pre-scan and never pretend-executed.
  • Every query it intercepts answers empty. A SELECT under pretend returns no rows. So a migration whose SQL depends on a query result — a chunk() loop, an if ($query->exists()) branch, a backfill — captures only a fragment of what it really runs, and the missing part is invisible. These migrations are flagged too, rather than captured as if they were complete.

Both of those are why a pretend run sometimes reports undetermined instead of a result: pretend is honest about what it structurally cannot see.

Shadow mode

Shadow mode is the answer when pretend cannot give one. It runs the migration for real against a disposable shadow database, then reads the resulting schema back from the catalog. Because the migration actually executes, the empty-result and guarded-block problems disappear: a chunk() loop runs over real rows, an introspection guard gets a real answer, and the full SQL is observed.

Shadow mode costs more — it provisions and tears down a database — and it runs only behind the production guard, because running a migration for real is exactly the thing a safety tool must never do to the wrong database. It is the truth mode you reach for when a pretend run tells you it could not decide.

It is also the one mode that needs more than read-only access. Before you turn it on, see Shadow mode: setup, privileges, topologies for the dedicated least-privilege role it uses, the pooler and read/write-split rules, and why it never clones your live data.

Which mode to use

Start in pretend mode; it covers the overwhelming majority of migrations. When a run reports an undetermined result whose reason points here, re-run that migration in shadow mode to get the real answer — or change the migration so pretend can capture it (move a backfill into a job, drop an unnecessary introspection guard). See Understanding undetermined for the full set of ways to resolve one.

The two modes share every downstream stage — the same canonicalization, the same rules — so a rule never behaves differently depending on how the SQL was captured. There is deliberately no separate "fast but approximate" path.