Skip to main content

Squawk parity

SQLens treats Squawk as an amplifier, not a competitor. It runs the tool over the same SQL it captured, maps what comes back into its own rules and levels, and reports one finding where both saw one problem. This page says exactly where that leaves each of the 39 rules Squawk 2.61.0 knows.

It is generated from the rule map that ships with the package, so it cannot drift from what the code actually does. The reasoning in every row is written here; none of the upstream wording is carried over.

What the statuses mean

  • covered — SQLens has a rule that reaches the same verdict on the same statement. The tool's finding is recorded as a confirmation rather than reported twice.
  • covered_differently — SQLens covers the same ground from a different angle. Both findings can appear, because they are two pieces of advice about one statement.
  • mapped_only — SQLens has no rule of its own here. The tool's finding is reported under its own identifier — this is the coverage the amplifier adds.
  • intentionally_not_covered — SQLens deliberately does not report this, and the reasoning is in the row. A decision on the record, not a gap nobody noticed.

The rules

Squawk ruleStatusSQLens ruleWhy
adding-field-with-defaultintentionally_not_coveredAdding a column with a default rewrote the whole table before PostgreSQL 11 and has been metadata-only since. SQLens supports PostgreSQL 18 and up, so the risk this describes cannot occur on any server it runs against — a rule for it would report a hazard that the supported floor already rules out.
adding-foreign-key-constraintcovered_differentlyPG.L2.CONSTRAINT_NOT_VALIDATEDBoth are about a constraint that validates existing rows while holding a lock. SQLens reasons about the NOT VALID / VALIDATE split for constraints generally rather than foreign keys specifically, so the ground is the same and the shape is not.
adding-not-nullable-fieldcovered_differentlyPG.L2.SET_NOT_NULL_SCANThe same table scan under a lock, reached from the other direction: this looks at the column being added, ours at the SET NOT NULL that scans.
adding-required-fieldcovered_differentlyPG.L2.SET_NOT_NULL_SCANThe neighboring case of the rule above, and it maps to the same SQLens rule for the same reason — which is also why the two share a de-duplication group.
adding-serial-primary-key-fieldcovered_differentlyPG.L6.SERIAL_NOT_IDENTITYBoth prefer identity columns over serial. Ours is about the choice wherever it appears; this one is about the primary key specifically.
ban-alter-domain-with-add-constraintmapped_onlyAdding a constraint to a domain validates every column of every table using it, under a lock on each. SQLens has no domain rules yet, so the tool's finding is surfaced under its own identifier rather than translated into one of ours.
ban-char-fieldmapped_onlyBlank-padded CHAR is a portability and comparison trap rather than a lock hazard. SQLens has no opinion on it today; the tool's does no harm surfaced as its own.
ban-concurrent-index-creation-in-transactioncoveredPG.L3.CONCURRENTLY_IN_TRANSACTIONThe same statement, the same failure: CONCURRENTLY cannot run inside a transaction block, and Laravel wraps migrations by default. Ours reaches the same verdict from the canonical transaction context, so the two findings are the same problem twice.
ban-create-domain-with-constraintmapped_onlyA style position about where constraints belong. No SQLens rule; surfaced as the tool's.
ban-drop-columncoveredPG.L1.DROP_COLUMNDropping a column breaks readers that still select it, and both rules say so at the same point in a migration.
ban-drop-databasemapped_onlySQLens has no rule for it because a migration that drops a database is outside the schema-change vocabulary its rules are written in. The downtime class is left null rather than guessed: it is not a table operation, and none of the three classes describes it.
ban-drop-not-nullmapped_onlyRelaxing NOT NULL is metadata-only, so it costs no lock — but every reader that assumed the guarantee is now wrong. A real finding with no SQLens equivalent yet.
ban-drop-tablecoveredPG.L1.DROP_TABLEIdentical ground: a table that goes away while something still reads it.
ban-truncate-cascadecovered_differentlyPG.L1.TRUNCATEOurs is about TRUNCATE at all; this one is about the CASCADE that silently reaches further than the statement names. The same statement triggers both, so they group.
ban-uncommitted-transactionmapped_onlyA file that opens a transaction and never closes it. SQLens reads the transaction context rather than judging the file's shape, so it has no equivalent finding.
changing-column-typecoveredPG.L2.TYPE_CHANGE_REWRITEThe same rewrite under an exclusive lock. Ours additionally classifies which type changes avoid it, which is a refinement of the same finding rather than a different one.
constraint-missing-not-validcoveredPG.L2.CONSTRAINT_NOT_VALIDATEDThe direct counterpart of our rule: add the constraint NOT VALID, validate it afterwards.
disallowed-unique-constraintmapped_onlyAdding a unique constraint builds its index without CONCURRENTLY and holds an exclusive lock while it does. Close to our index rule but not the same statement, so it is surfaced rather than folded in — folding it in would report the wrong fix.
identifier-too-longmapped_onlyPostgreSQL truncates identifiers past 63 bytes silently, which turns two distinct names into one. No lock, no downtime, and a genuinely surprising failure.
prefer-bigint-over-intcovered_differentlyPG.L6.PK_NOT_BIGINTOurs is about primary keys specifically, where running out of range is unrecoverable without a rewrite. This one applies the same reasoning to any integer column.
prefer-bigint-over-smallintcovered_differentlyPG.L6.PK_NOT_BIGINTThe same position one width down, and the same relationship to our primary-key rule.
prefer-identitycoveredPG.L6.SERIAL_NOT_IDENTITYThe same recommendation with the same reasoning: identity columns are standard SQL and carry their sequence ownership explicitly.
prefer-repackmapped_onlyA suggestion to use pg_repack instead of VACUUM FULL, which locks the table exclusively. SQLens has no maintenance-command rules; the tool's advice stands on its own.
prefer-robust-stmtsintentionally_not_coveredThe position that every statement should be individually re-runnable through IF NOT EXISTS. SQLens deliberately does not hold it: Laravel records migrations as applied, so a re-run is the migrator's concern, and IF NOT EXISTS hides a real disagreement between the schema and the migration that a failed run would have surfaced.
prefer-text-fieldmapped_onlyVARCHAR(n) buys no storage benefit in PostgreSQL and makes a length change a schema change. A reasonable position SQLens has not taken a rule on.
prefer-timestamptzcoveredPG.L6.TIMESTAMP_NO_TZThe same finding: a timestamp without a time zone silently means whatever the session was set to when it was written.
renaming-columnmapped_onlyA rename breaks every reader that has not been deployed yet, which is the ordinary state of affairs during a rolling deploy. SQLens has no rename rule yet.
renaming-tablemapped_onlyThe same hazard one level up, and equally uncovered on our side.
require-concurrent-index-creationcoveredPG.L2.INDEX_NOT_CONCURRENTThe same statement and the same fix. Ours knows about the transaction context that makes CONCURRENTLY impossible, which is a neighboring rule rather than a difference here.
require-concurrent-index-deletioncoveredPG.L2.DROP_INDEX_NOT_CONCURRENTThe mirror of the rule above, on our side too.
require-concurrent-partition-detachmapped_onlyDetaching a partition without CONCURRENTLY holds an exclusive lock on the parent. SQLens has no partition rules yet, so this one is surfaced as the tool's.
require-concurrent-reindexmapped_onlyREINDEX without CONCURRENTLY blocks writes for the duration. Same gap, same treatment.
require-enum-value-orderingcovered_differentlyPG.L4.ENUM_ADD_VALUEBoth concern adding an enum value. Ours is about the transaction restriction that used to apply and the readers that do not know the new value; this one is about specifying where in the ordering it goes.
require-lock-timeoutcoveredPG.L3.MISSING_LOCK_TIMEOUTThe same requirement, and the reason SQLens holds it too: a migration that waits for a lock without a bound blocks everything queued behind it.
require-statement-timeoutcoveredPG.L3.MISSING_STATEMENT_TIMEOUTThe companion bound, and the companion rule.
require-table-schemamapped_onlyRequires an explicit schema on every table so the result does not depend on search_path. Off by default upstream. SQLens has no rule for it.
require-timeout-settingsmapped_onlyThe combined form of the two timeout rules. It shares their de-duplication group rather than mapping to one of ours, because it can stand for either — and reporting it beside the two SQLens timeout findings would be the same advice three times.
syntax-errorintentionally_not_coveredNot a rule at all: it is how the tool reports SQL it could not parse, and it is the one name --exclude rejects while the reporter emits it anyway (both measured). SQLens turns it into a named undetermined for the whole run rather than a finding, because an analysis that stopped at a parse error did not check what came after it.
transaction-nestingmapped_onlyBEGIN inside a migration the migrator already wrapped. SQLens reads the resulting transaction context rather than reporting the nesting itself.

What is not in the table above

This page compares SQLens against a tool that reads .sql files, so it compares on that ground. Everything below has no counterpart there — not because Squawk covers it badly, but because a file of SQL is not where the question lives.

  • The migration, not the statement. Whether down() exists, whether it really inverts up(), whether it is more destructive than the migration it reverses. 4 driver-neutral rules judge the migration as a unit, and a .sql file has no down() to judge.
  • The deploy, not the file. 24 checks run against the real target database at deploy time and afterwards: the locks currently held, replication lag, disk headroom, and — once the migration has run — the indexes that came out invalid and the constraints that were never validated.
  • The database that already exists. The audit suite reads the live catalog rather than pending changes: a foreign key nobody indexed, a column type that was a mistake three years ago, drift between what the migrations describe and what the server holds. None of that is in any file.
  • MySQL at all. Squawk is PostgreSQL-only. 40 MySQL rules ship here, reasoning about MySQL 8.4 semantics — the online-DDL algorithms, the privilege model, the charset traps — against a shipped matrix rather than a guess.

The point of the table above is that the gaps are visible. The point of this section is that the comparison is not the whole measurement.