Scope and limits
A tool that reports nothing is indistinguishable from a tool that found nothing. This page is the difference: what SQLens covers, what it does not, and — for every gap — how the tool says so rather than passing over it.
Nothing here is a caveat buried for legal comfort. Each limit below either produces a
named undetermined in your report or refuses the run outright, because a boundary
you cannot see is a boundary that misleads you.
The engines it runs against
PostgreSQL 18 or newer, and MySQL 8.4 or newer. Both floors are enforced against the server that answers, not assumed from configuration.
SQLite is supported for your application's own test suite and is deliberately not auditable. There is no catalog to read that would answer the questions this tool asks, and mapping them onto SQLite would produce confident answers about a database engine your production system does not use. Pointing an audit at a SQLite connection is refused, by name.
One instance per run, on purpose
An audit run addresses exactly one instance and says which one in its header.
That is a decision, not a missing feature. An aggregate over several tenant databases is a different kind of claim — it would have to say which tenants disagreed and why — and a report that merged them while wearing the header of a single-instance run would be read as a statement about your system when it describes one customer.
So there is no --all-tenants. On a project that looks multi-tenant, the run stops and
asks which tenant the report is about, rather than auditing whichever connection
happened to be the default. The detection reads your configuration and never a
database: a routine that opened five production connections in order to decide whether
you have tenants would be worse than the question it was answering.
It is the gate, not the crane
SQLens never runs your deployment. It does not call migrate, it starts no deploy step,
and it ships no script you could point at a server. What it ships is a verdict and an
exit code; what does the deploying is Forge, Envoyer, Deployer, your CI job — whatever
you already use.
That line is held by a test rather than by this paragraph. The shipped source starts an
external process from exactly one place, the seam every analyzer goes through
(Squawk, the Postgres Language Server, the formatters), and that seam is named in a
register the test shrinks when it stops being needed. Nothing under src/Console/ may
start a process at all, no command may call migrate or any Artisan command, and the
package ships no .sh file and nothing with an executable bit.
Why the boundary is worth a guard instead of a convention: it does not break, it
erodes. An example in a docs page becomes a helper, a helper becomes a command, and
one day a command runs migrate because that was convenient for one person on one
project. Each step looks small. The result is a tool that both judges a deploy and
performs it, which is the one shape a safety gate must never take — there is nobody left
to stop it.
If you want the recipe, it is a code block you copy into your own pipeline. It stays a code block on purpose: a script this package shipped would be a script this package maintains, and then it is the crane after all.
What a replica cannot answer
Every rule declares what its verdict is about: the cluster, this instance and its write path, or one database.
A server setting read on a replica reads perfectly well — that is exactly the trap — and the value describes the replica. So on an instance that is not on the write path, instance-scoped rules are withheld and each says so with a named reason. Schema rules keep running, because a schema is the same object on every node that carries the database, and withholding those would make a replica-targeted audit report nothing at all without making it any safer.
An instance whose role could not be established withholds exactly as a replica does. A managed database or a narrow grant routinely refuses the indication, and reading that silence as "this is the primary" is how a tool returns a confident verdict about a machine nobody placed.
Where an external analyzer would sharpen a rule
Some judgments about whether a statement rewrites a table are more precise with a dedicated analyzer than with SQLens's own canonicalization. The integration point for one exists; no analyzer ships with this version.
The consequence is visible rather than silent: the affected rule reports undetermined
with the reason missing_external_tool, and under --strict the run fails rather than
passing. You are told the check could not be made. You are never told it passed.
The rule set is smaller than the tool
The suites, levels, and axes are complete; the rule set inside them is not, and it grows with each release. Two things follow, and both are deliberate:
- A clean report means the rules that exist found nothing — not that your schema is beyond reproach. The header states how many rules actually ran, so a narrow selection cannot look like a broad clean bill of health.
- A run whose filters admit no rule at all is reported as a misconfiguration, not as a pass. "Nothing was checked" and "everything checked out" produce the same silence otherwise, and only one of them is good news.
Suppression is not filtering
Findings you have chosen to ignore stay counted and are listed under whatever hid them, with the total in the header. A tool that dropped them while assembling the report would produce output identical to a clean run over a database with known problems — which is the exact leak an ignore list is supposed to be the alternative to.
An ignore entry that matches nothing is reported too. An orphaned pattern and a working one produce the same report, so the only way to learn your ignore list has rotted is to be told.