Why it complains
The one sentence the whole feature follows from, and the four rules that fall out of it.
Every decision in the gripes engine follows from a single sentence:
A gripe that is merely rude is noise and users disable it within a week. A gripe that is correct and rude is a feature they screenshot into the team channel.
So correctness outranks coverage, and silence outranks a guess. The rest of this page is what that costs.
A rule that cannot tell says nothing
There is a hedged sentence the brand specification carries as a counter-example, and it is worth reading because it is exactly what most tools of this kind produce:
Hmm, this query might be a little slow, maybe?
It is rejected for being hedged into uselessness. That is not a style note, it is a hard property of the engine: a rule that needs the schema and cannot see it returns nothing. It does not downgrade to a guess and it does not append "possibly".
This has a visible consequence. The client's view of your schema is the completion catalogue, which fetches a table's columns on demand — so a schema rule's first look usually knows nothing, and correctly says nothing. Asking for the columns is the side effect. When they arrive the catalogue notifies, the analysis re-runs, and the finding appears a beat late. Late is a cost worth paying. Wrong is not.
The same rule is why a null from the schema may never be read as
false. A rule treating "not fetched yet" as "NOT NULL" would go silent
on a real problem; one treating it as "nullable" would invent one.
No rule is about formatting
sqlfluff exists, and it is better at that than this will ever be. A rule earns its place in DataGripe by knowing something about this database — the row count, the missing index, the column that is nullable in practice — or by being about damage rather than tidiness.
A finding derivable from the query text alone, with no reference to the schema or the cost, is usually a lint rule and belongs to a formatter. Shipping those is the fastest way to get the whole feature switched off, and a feature that is switched off catches nothing.
There is a concrete casualty. select.unqualified-star was specified and
is not going to ship: in a SQL client, select * from t limit 100 is the
single most common legitimate query there is. The case with real teeth —
a star frozen into a view definition at creation — shipped as
view.select-star. The ad-hoc case did not.
The wording is fixed, and written by a person
Four strings per rule. No LLM, no template shuffling, no synonym rotation. The same rule firing twice uses the same wording; variants to seem clever are explicitly out.
What this buys is that a gripe is auditable. Its footer carries
severity · rule id · line, and the rule id is the same string at every
attitude level and for every reader. You can grep it, dismiss it by it,
and file a bug against it.
It also means the strings are copy, reviewed like copy. Mechanical
constraints are enforced by a test — length caps, a barred-term list, no
profanity at notice, and every placeholder resolving — but the judgment
of whether a sentence is good is a person's, every time.
It criticises the query, never you
The complaint is always about the statement, the schema or the grant. It is never about the competence of whoever wrote it. That line is the difference between a tool with a personality and a tool that is unpleasant to use, and it is a hard rule rather than a preference.
Three more constraints hold it in place:
- A gripe never blocks anything. It does not prevent a run, gate a save, or fail a build. It is an opinion held loudly.
- There is no score. No grade, no "query health 62%". A number invites gaming and averages away the one blocker that mattered.
- Everything is dismissable, at every attitude level, and the app always tells you it is hiding something. See dismissing gripes.
Where correctness actually bit
The discipline above is cheap to write down. These are the places it cost something, each of which is a rule that was wrong before its test caught it:
delete.no-wherereads the statement's main verb, looking through a leadingWITH. Without that,create trigger t after delete on xreads as an unqualified delete — and it would have fired on every trigger in the database.view.select-stardistinguishes a projection star from multiplication by what surrounds it. Depth alone is not enough: the*inselect qty * pricesits at the same depth as a real one.index.not-concurrentis gated on the dialect, not the adapter id.CONCURRENTLYis not merely unhelpful on MySQL and SQLite, it is a syntax error, so the advice would have been actively wrong.routine.volatile-but-readonlyreads the routine body, not its definition. Reading the whole definition findsCREATEin every routine, so the rule never fired at all — which is how it was first written.- Two bugs underneath the rules had to be fixed before any of them could
be trusted, and both were silent. The tokenizer dropped
!, soa != 1tokenized identically toa = 1and a rule about equality would have fired on its exact opposite. AndSqlDialectwas structurallystring, so every caller type-checked while a hardcoded"postgres"was passed for every connection.
That list is the actual argument for the discipline. Every one of those would have produced a confident, specific, wrong gripe — and a wrong gripe costs more trust than a missing one.
If you find one, report it. That is a bug, and it is treated as one.
Related
- The rules — all of them, rendered from the catalogue.
- Gripe levels — the four-level dial.
- Writing a rule — what adding one costs.
This page as Markdown, for an agent or a diff.