The strictness levels — how much a project wants to be told
SQLens has ten cumulative strictness levels. Setting level to n runs every rule at n and
below, so a project moves up the band as it decides it wants to hear more.
The single most useful thing to understand about them: a level is an appetite, not a measure of how wrong something is. A level-6 finding is not a lesser defect than a level-2 one. It is a different kind of statement, and reading the band as a severity scale is how a team ends up either ignoring the top of it or trying to clear it like a bug tracker.
The two axes, and why they are separate
| Axis | Question | Who sets it |
|---|---|---|
| Level | How much do you want to hear? | the project, in sqlens.level |
| Severity | How dangerous is this particular finding? | the rule, and only for security and privacy |
Every other category — safety, performance, idiom, convention — is gated by level alone
and carries no severity at all. That is deliberate: a rule that carried both would let a level-2 run
report a critical, and nobody could explain from the output why.
The security and privacy categories are the exception, and they are severity-gated instead of level-gated, so a security finding cannot be silenced by lowering the level band.
Levels 0–4: things that break
The lower band is about deploys. It is where a finding means this migration can hurt you: an
unguarded destructive statement, an index built without CONCURRENTLY, a lock held across a
transaction, a change that breaks the previous release while it is still running.
A project usually settles here and stays. These levels answer a question with a right answer.
Level 5 — mistakes that create load
The first level of the audit band, and the last one that is still about something being wrong.
A foreign key with no index means every parent delete scans the child table. A unique key over a nullable column does not constrain what it appears to. Money in a binary float loses cents that nobody reconciles later. A join across a collation boundary silently costs an index.
These are not matters of taste. What separates them from levels 0–4 is when they bite: not at deploy time, but afterwards, under load, in a way that is hard to trace back to the schema.
Level 6 — idiom, which is a conversation
This is the level people misread, so it is worth being blunt: level 6 is not a defect list.
A timestamp column with no time zone is not broken. A json column that could be jsonb works. An
int primary key is fine right up until it is not. utf8mb3 holds everything most applications ever
store.
Every level-6 rule reports something that a reasonable engineer might have chosen on purpose — and
several of them name that reason in the finding itself rather than leaving you to argue with the
tool. The rule for a random UUID key tells you that the time-ordered alternative leaks creation time.
The rule for json tells you that jsonb does not preserve key order, which matters if the document
is evidence rather than data.
Turning level 6 on is a project saying: tell me about these, I want to decide. It is not a promise that the list will ever be empty, and a project that treats it as one will spend its time arguing with a tool instead of using it.
Level 7 — performance heuristics
Level 7 reports things that are probably costing you something, from evidence that is real but partial.
A redundant index genuinely costs writes — but "redundant" is a judgment, and the rule refuses to make it about any index it cannot fully compare. An unused index genuinely costs writes — but the counter that says so can be reset, is per instance, and says nothing at all if it started counting yesterday.
So the level-7 rules are the most conservative in the package. They exclude more than they report, each exclusion is a named skip rather than a silence, and the ones resting on the shakiest ground are not on by default at all (see below).
Levels 8–9 — convention and pedantry
Naming, casing, and the choices that are entirely a house style. Nobody has to run these; they exist so that a project that wants one opinionated voice can have it without that voice leaking into everybody else's report.
What "no finding" means at each level
A clean report says the rules that exist found nothing — never that a schema is beyond reproach. Scope and limits says what the tool deliberately does not look at, and understanding undetermined covers the third result: a check that could not run says so, with a named reason, rather than passing quietly.
Stability: which rules run before you ask for them
A rule also carries a stability tier, and it is a separate question from the level:
stable— runs whenever its level is in range. This is where all but four of the shipped checks sit.preview— opt-in. From 1.0 on, a newly added rule lands here so that upgrading a minor version cannot start failing a build over code nobody touched.experimental— opt-in, and for a stronger reason than immaturity.
Four checks ship non-stable today, in two groups:
| Check | Tier | Why |
|---|---|---|
| PG.L7.INDEX_UNUSED | experimental | its verdict depends on when you ask |
| MY.L7.INDEX_UNUSED | experimental | the same, and MySQL cannot even say how long it has been counting |
| CAP.PRESCAN.RESULT_DEPENDENT | preview | it reads the migration's source, so it reasons about intent rather than about a statement |
| CAP.PRESCAN.INDIRECT_CALL | preview | the same, and a call it cannot resolve is a judgment call about your code |
The two experimental ones answer from counters rather than from the schema, which puts them in
tension with the package's promise that the same state produces the same result. Every other rule
answers from the schema, and a schema does not move while you look at it.
The two preview ones are pre-scan detectors: they read migration source before anything is
captured, so their subject is code rather than SQL. That is a younger kind of judgment, and the
tier says so.
The tier is what keeps both tensions out of everybody else's run:
// config/sqlens.php
'stability' => ['preview'], // the two pre-scan detectors
'stability' => ['preview', 'experimental'], // …and the two index-usage rules as well
The default is an empty list, which admits stable only — the opposite of how categories reads,
where empty means all. Without an opt-in these four report nothing at all, and that is the correct
default rather than caution: it is the versioning promise made operative, so a minor release cannot
start failing a build over code nobody touched.
Turning the band up
// config/sqlens.php
'level' => 7,
A reasonable path: start at 4, live with it, then move to 5 once the lower band is quiet. Level 6 is worth a deliberate afternoon rather than a config change on the way past — it will have opinions about a schema that has been fine for years, and every one of them is a decision somebody should make on purpose.