CAP.L0.DOWN_FAILED — the down() leg raised an error
While --roundtrip replayed the migration in a throwaway database, its down() raised an error.
CAP.L0.DOWN_NOT_INVERTIBLE — down() did not restore what up() changed
The roundtrip replayed up, then down, then up again inside a throwaway database, and the second up failed.
CAP.L0.MIGRATE_ERROR — Error during the real shadow migrate
The migration failed while it was being run for real against the throwaway shadow database.
CAP.L0.NOT_CAPTURABLE — No capturable SQL
The capture ran to completion but produced no SQL to lint.
CAP.L0.PRETEND_ERROR — Error during the pretend run
The migration threw while it was being captured under pretend, so its SQL could not be linted.
CAP.L0.UNDETERMINED_CAPTURE — Capture could not conclude
Without this finding such a migration would contribute nothing to the run and vanish from it — the silent green the tool forbids.
CAP.PRESCAN.INDIRECT_CALL — Indirect call from a migration
This migration reaches its effect through your own code — (new Backfill)->run(), app(Importer::class)->handle() — the most common way around the side-effect catalog.
CAP.PRESCAN.INTROSPECTION_GUARD — Schema introspection guard
This migration's DDL sits behind a schema-introspection guard — if (!
CAP.PRESCAN.RESULT_DEPENDENT — Result-dependent migration
This migration's SQL depends on what a query answers.
CAP.PRESCAN.SIDE_EFFECT — Side effect in a migration
This migration would reach outside the database if it ran — a notification, an HTTP request, a queued job, a mail, a cache write.
GEN.L1.DOWN_MORE_DESTRUCTIVE — A rollback that destroys more than the migration built
Laravel rolls back by calling down(), and infers nothing from up().
GEN.L4.DOWN_MISSING — A migration that cannot be rolled back
Laravel rolls back by calling down(), and nothing else.
MY.L1.DROP_COLUMN — Dropping a column on MySQL commits itself
DROP COLUMN deletes the column and its data, and on MySQL no transaction takes it back.
MY.L1.DROP_TABLE — Dropping a table on MySQL commits itself
DROP TABLE is DDL on MySQL, so no transaction takes it back — it is final the instant it runs.
MY.L1.TRUNCATE — TRUNCATE in a migration, where no transaction takes it back
TRUNCATE in a migration's up() empties the table with no confirmation and no predicate to limit it.
MY.L2.COPY_ALTER_CHARSET — Character-set conversion that copies the table
ALTER TABLE … CONVERT TO CHARACTER SET … re-encodes every value in every string column.
MY.L4.DROP_WITHOUT_DEPLOY_WINDOW — A drop breaks whoever is still reading
Dropping a table or column breaks any old application version that still references it.
MY.L2.COPY_ALTER_TYPE — Column redefinition that rebuilds the table
$table->…->change() compiles to ALTER TABLE … MODIFY, and MySQL's MODIFY takes the column's whole definition rather than a delta.
MY.L2.FK_TARGET_NON_UNIQUE — Foreign key onto a target no unique key covers
So this is a deploy that breaks today, on the version this package supports, in the configuration a user gets without doing anything.
MY.L2.NO_PRIMARY_KEY — InnoDB table left without a primary key
On MySQL a table without a primary key is not a matter of schema taste.
MY.L3.ALGORITHM_LOCK_UNEXPRESSIBLE — An operation MySQL will not run online, written the one way that cannot say so
Laravel's MySQL grammar has no way to emit ALGORITHM= or LOCK=.
MY.L3.MIXED_DDL_DML_NOT_ATOMIC — A migration that changes the schema and then writes data
On MySQL, a migration that changes the schema and then writes data is not one operation, whatever you assumed while writing it.
MY.L3.MISSING_LOCK_WAIT_TIMEOUT — A migration that waits for a metadata lock without a bound
MySQL waits a year by default for a metadata lock, and grants those locks in order — so a stuck schema change also stops the reads queued behind it.
MY.L4.ENUM_CHANGE — Changing an ENUM's member list
MySQL stores an ENUM value as its ordinal position in the member list, not as the string.
MY.L6.EXPLICIT_DEFAULTS_FOR_TIMESTAMP_OFF — legacy TIMESTAMP behavior the migration never asked for
A server with explicit_defaults_for_timestamp off rewrites TIMESTAMP columns on update and turns a written NULL into the current time.
MY.L6.TIME_ZONE_NOT_UTC — a server whose clock is the host's, not a decision
A MySQL server on time_zone = SYSTEM converts timestamps using the host's zone, so the same schema behaves differently on a different machine.
PG.L5.FK_NO_INDEX — Foreign key whose referencing column no index covers
PostgreSQL indexes the referenced side of a foreign key and not the referencing one, so every delete on the parent scans the child table.
MY.L6.INNODB_ROW_FORMAT_NOT_DYNAMIC — the 767-byte ceiling that aborts a deploy halfway
A server creating InnoDB tables as COMPACT gives them a 767-byte index key limit, so the first unique string index fails mid-deploy.
PG.L5.NO_PRIMARY_KEY — Table with no primary key and nothing that can stand in for one
A table PostgreSQL cannot identify a row in cannot be replicated, audited, or recovered row by row.
MY.L6.CHARACTER_SET_SERVER_NOT_UTF8MB4 — a default charset that quietly cannot hold your data
Objects created without an explicit CHARACTER SET take the server's, and on latin1 or utf8mb3 they silently alter values MySQL cannot represent.
MY.L5.SQL_MODE_NOT_STRICT — a truncated value the server called a success
A MySQL server whose sql_mode omits STRICT_TRANS_TABLES, so a truncated value the server called a success.
MY.L5.SQL_MODE_DIVISION_BY_ZERO_SILENT — a division by zero that answers NULL instead of raising
A MySQL server whose sql_mode omits ERROR_FOR_DIVISION_BY_ZERO, so a division by zero that answers NULL instead of raising.
MY.L6.SQL_MODE_ENGINE_SUBSTITUTION — a table that quietly got a different storage engine
A MySQL server whose sql_mode omits NO_ENGINE_SUBSTITUTION, so a table that quietly got a different storage engine.
MY.L3.EXCHANGE_PARTITION_CLAUSE_IGNORED — An ALGORITHM or LOCK clause MySQL parses and discards
Every other ALTER TABLE checks the clause and refuses a contradiction. EXCHANGE PARTITION takes it and does nothing.
MY.L6.SQL_MODE_LOOSE_GROUP_BY — a GROUP BY that returns a value from a row nobody chose
A MySQL server whose sql_mode omits ONLY_FULL_GROUP_BY, so a GROUP BY that returns a value from a row nobody chose.
MY.L6.DEFAULT_STORAGE_ENGINE_NOT_INNODB — a table with no transactions and no foreign keys
A server whose default storage engine is not InnoDB creates tables without transactions, foreign keys or crash recovery when a migration does not name one.
MY.L6.TIME_ZONE_TABLES_EMPTY — a named zone the server cannot resolve
A MySQL server on a named time zone whose mysql.time_zone_name holds no rows cannot resolve it, and CONVERT_TZ answers NULL instead of raising.
MY.L6.LOWER_CASE_TABLE_NAMES_RISK — the development-against-production split you cannot switch off
lower_case_table_names is fixed when a server is initialized, so a mixed-case migration can pass locally and fail in production.
PG.L5.COLLATION_VERSION_MISMATCH — indexes sorted by rules the server no longer uses
A collation whose installed version has moved since the indexes were built can make an equality lookup miss a row that is present.
PG.L1.DROP_SCHEMA — Dropping a schema is every table in it at once
DROP SCHEMA is not scoped to one object, and CASCADE reaches past the schema entirely.
PG.L1.DROP_TABLE — Dropping a table is a deploy you cannot undo
DROP TABLE takes the rows with it, and CASCADE takes whatever was built on them.
PG.L1.DROP_COLUMN — The old app version is still selecting it
Dropping a column breaks every running release that still reads it, and the data is not coming back.
PG.L1.TRUNCATE — Emptying a table locks out every reader
TRUNCATE is not a fast DELETE — it takes a lock nobody can read through, and there is no WHERE clause to have forgotten.
PG.L2.INDEX_NOT_CONCURRENT — Building an index blocks every write until it finishes
CREATE INDEX holds a lock against writes for the whole build. CONCURRENTLY does not — and cannot run in a transaction.
PG.L2.DROP_INDEX_NOT_CONCURRENT — Dropping an index locks the table, not the index
DROP INDEX takes an ACCESS EXCLUSIVE lock on the table for the duration. CONCURRENTLY does not.
PG.L2.CONSTRAINT_NOT_VALIDATED — Adding a constraint scans every row under a lock
ADD CONSTRAINT verifies the whole table before it returns. NOT VALID plus VALIDATE splits that into two cheap halves.
PG.L2.SET_NOT_NULL_SCAN — Proving no row is null costs a full scan
SET NOT NULL scans the table under its lock. A validated CHECK constraint lets PostgreSQL skip that scan entirely.
PG.L2.TYPE_CHANGE_REWRITE — A type change can rewrite the entire table
Unless the new type is binary-coercible from the old one, ALTER COLUMN TYPE rewrites every row under a lock.
PG.L3.CONCURRENTLY_IN_TRANSACTION — CONCURRENTLY cannot run inside Laravel's transaction
PostgreSQL rejects a concurrent index build inside a transaction block, and Laravel opens one for every migration.
PG.L3.MISSING_LOCK_TIMEOUT — Without it, the migration waits and everything queues behind it
A lock_timeout aborts your migration instead of stalling the application. That trade is almost always the right one.
PG.L3.MISSING_STATEMENT_TIMEOUT — Bounding how long a step may hold what it took
lock_timeout bounds the wait. statement_timeout bounds the hold. A migration needs both.
PG.L3.RISKY_OPS_SINGLE_TX — Locks are held until the whole transaction commits
Two locking operations in one migration hold both locks until the last one finishes, not until each one does.
PG.L4.ENUM_ADD_VALUE — Adding an enum value is a one-way change
PostgreSQL has no ALTER TYPE … DROP VALUE, so down() cannot undo it. A CHECK-constrained column can.
PG.L4.ENUM_VALUE_REMOVED — Renaming an enum value rewrites the meaning of stored data
Rows already hold the old name. Renaming the value changes what they mean without touching them.
PG.L4.CHECK_ENUM_CHANGE — Changing a Laravel enum() column re-checks every row
A Laravel enum() is a varchar with a CHECK. Swapping the constraint validates the whole table under a lock and leaves no rolling-deploy window.
PG.L4.TYPE_NARROWING — Making a column smaller can lose what is in it
Narrowing asks the server to fit every stored value into a smaller type. It errors, or it truncates.
PG.L4.DROP_WITHOUT_DEPLOY_WINDOW — The previous release is still running
A drop takes effect on commit. Every instance still serving the old release starts failing at that moment.
PG.L4.CONSTRAINT_VALIDATION_PENDING — A constraint was added NOT VALID and never validated
NOT VALID is the right first step, and only the first. Until VALIDATE CONSTRAINT runs, the constraint does not hold for the rows that were already there.
PG.L6.TIMEZONE_NOT_UTC — a server whose day boundary is somewhere else
The instant is stored correctly either way. What moves is every date cast, every date_trunc and every "today" filter.
PG.L6.STANDARD_CONFORMING_STRINGS_OFF — the same migration builds two different databases
With it off, a backslash in an ordinary literal escapes the next character. Nothing raises; the row simply holds something else.
PG.L6.DEFAULT_TRANSACTION_ISOLATION_DRIFT — a new error class your code has no reason to handle
Raising the server default changes what every transaction does, including the ones the framework opens for itself.
PG.L6.DEFAULT_TRANSACTION_READ_ONLY — the write fails before anything can reason about it
A transaction that does not say READ WRITE refuses writes, and the error reads like a permissions problem without being one.
PG.L7.DATA_CHECKSUMS_DISABLED — corruption from below is returned as data
With checksums on, a damaged page is detected on read. Without them it is handed to your application as a value.
GEN.L1.DML_WITHOUT_WHERE — the predicate you meant to type
An UPDATE or DELETE with no WHERE touches every row. The statement is valid, which is the whole problem.
GEN.L3.DML_ON_SCHEMA_CHANGED_TABLE — the backfill runs while the schema lock is still held
Change the schema in one migration and fill the data in the next. Together, the lock lasts for both.
LINT.NO_ACTIVE_RULES — the scope admitted no rule, so nothing was checked
A run that checked nothing and a run that found nothing produce the same silence. This finding tells them apart.
LINT.SKIPPED — the run ended without linting anything
Nothing was pending, nothing could be captured, or the path matched no migration. Each reason is named.
LINT.SKIPPED.MISSING_TOOL — an optional external tool is not installed
The rules that needed it did not run. They are named, and the run says so rather than reporting silence.
LINT.VERSION_SKEW — the pinned version and the server disagree
Version-gated rules were applied for the version you pinned, not the one that answered.
LINT.VERSION_PIN_UNREADABLE — the pin could not be read, and nothing was assumed instead
A malformed assume_server_version is not silently ignored. Version-gated rules report undetermined rather than guessing a version.
AUDIT.CATALOG.UNREAD — the catalog could not be read, so nothing was judged
An audit that read nothing and a database with nothing wrong produce the same empty report. This is the difference.
LINT.NO_MIGRATIONS_READ — the run judged no migration at all
`--path` lints the pending migrations, and nothing is pending on a database that has been migrated. This finding is what stops that from reading as a clean run.
PG.L6.PK_UUID_V4 — Random UUID primary key where PostgreSQL 18 offers a time-ordered one
A v4 UUID primary key scatters inserts across the index; PostgreSQL 18's uuidv7() keeps the uniqueness and adds order.
PG.L6.TIMESTAMP_NO_TZ — Timestamp column with no time zone
A zone-less timestamp stores the digits it was handed and no clock to read them against, and the failure is silent.
PG.L6.JSON_NOT_JSONB — A json column where jsonb is almost always meant
json re-parses the document on every read and cannot be indexed; jsonb parses once and can. The exception is when the exact bytes matter.
PG.L6.PK_NOT_BIGINT — An integer primary key too narrow to grow into
The key runs out with no warning, and the fix falls due exactly when it is most expensive.
MY.L6.PK_NOT_BIGINT — An integer primary key too narrow to grow into
The key runs out with no warning, and the fix falls due exactly when it is most expensive.
PG.L6.SERIAL_NOT_IDENTITY — A key generated by serial rather than by an identity column
serial is a macro, not a type. Its three pieces can drift apart; an identity column keeps them together.
MY.L6.CHARSET_NOT_UTF8MB4 — Text stored under something narrower than utf8mb4
latin1 announces itself; utf8mb3 is the one that looks solved and silently drops every emoji.
MY.L6.COLLATION_LEGACY — A utf8mb4 object still sorting by a pre-8.0 collation
utf8mb4_general_ci does not just sort differently — for several languages it sorts wrongly.
MY.L5.COLLATION_MIXED — A join across a collation boundary, which costs an index
MySQL converts one side at runtime, and the converted column stops being answerable from its index.
PG.L5.FK_NULLABLE_IN_UNIQUE — A unique key that does not hold, because one of its foreign keys may be NULL
UNIQUE is one of the few words in SQL people believe without checking. Over a nullable column it constrains nothing.
MY.L5.FK_NULLABLE_IN_UNIQUE — A unique key that does not hold, because one of its foreign keys may be NULL
MySQL counts two NULLs as different values, and offers no mode that says otherwise.
PG.L5.FLOAT_MONEY — Money in a floating-point column
A float cannot represent 0.10 exactly. The cent goes missing months later, with no way back to the column.
MY.L5.FLOAT_MONEY — Money in a floating-point column
A float cannot represent 0.10 exactly. The cent goes missing months later, with no way back to the column.
PG.L5.MONEY_TYPE — The type called money is not the one you want
Its value depends on a server setting, so a restore onto another locale reads the same characters as a different number.
PG.L7.INDEX_REDUNDANT — An index whose work another index already does
It answers nothing the wider index cannot, and is still written on every insert.
MY.L7.INDEX_REDUNDANT — An index whose work another index already does
It answers nothing the wider index cannot, and is still written on every insert.
PG.L7.INDEX_UNUSED — An index nobody has read, as far as resettable counters can say
The counter is the easy half. The window it covers decides whether the number means anything.
MY.L7.INDEX_UNUSED — An index nobody has read, on an engine that cannot say for how long
performance_schema answers how often; nothing answers since when. That gap decides the rule.
PG.L7.UNBATCHED_MASS_DML — A backfill that nothing bounds
A data write in a migration that runs as one statement over however many rows the table happens to hold.
MY.L7.UNBATCHED_MASS_DML — A backfill that nothing bounds
A data write in a migration that runs as one statement over however many rows the table happens to hold.
CAP.L0.NO_ACTIVE_RULES — nothing was checked, and that is not a clean result
A level and category combination that admits no rule produces the same empty report as a healthy database. This is the difference.
CAP.L0.INSTANCE_AMBIGUOUS — more than one connection could be the one to audit
An audit describes one database. When the configuration leaves several candidates and nothing chose, the run stops rather than picking.
CAP.L0.AMBIGUOUS_READ_HOSTS — the connection offers several read hosts and nothing chose
Laravel picks a read host at random. An audit that accepted that would describe a different server on different runs.
CAP.L0.UNOFFERED_HOST — the named host is not one this connection configures
A host pin that the configuration does not offer is refused, never dialed. Auditing a server the project never configured is worse than not running.
CAP.L0.PINNED_HOST_DIVERGED — the server that answered is not the one that was pinned
A pin that did not take effect makes every finding a statement about a database nobody asked for, and it looks exactly like a real report.
CAP.L0.PINNED_HOST_UNVERIFIED — the pin could not be checked against the server
The pin may be right. Nobody can say so, and a report that quietly assumed it would be asserting something it never confirmed.
CAP.L0.INSTANCE_DIVERGENCE — the server is not the one the configuration describes
The configuration says one thing and the server says another. SQLens cannot know which is right; it can refuse to let one be read as the other.
CAP.L0.CONNECTION_POOLED — the session is multiplexed, so instance-wide facts cannot be trusted
A transaction pooler hands your session to a different backend between statements. Anything an audit concludes about "this instance" stops holding.
CAP.L0.SERVER_UNREACHABLE — the connection could not be opened, so nothing was audited
An empty report because the run never reached a database is not an empty report because the database is in good order.
CAP.L0.SETTINGS_UNREADABLE — the server would not report its own configuration
Every server-baseline rule then has nothing to judge, and a rule with no subject reports nothing — which is what a healthy server also produces.
CAP.L0.RULE_WITHHELD_BY_VERSION — a rule was not applied because of the server version
A rule set that shrinks against an older server produces a shorter report in which every finding is still true and nothing says a check was skipped.
CAP.L0.INSTANCE_SCOPE_UNANSWERABLE — this instance cannot answer what the rule asks
A rule scoped to the write path reads perfectly well on a replica. That is exactly the trap: the value it returns describes the replica.
CAP.L0.INVALID_IGNORE_LIST — the ignore list names something that is not a rule
A pattern matching nothing looks exactly like a pattern whose findings are gone. That is the same output for months.
CAP.L0.ORPHANED_IGNORE — an ignore entry that no longer silences anything
A suppression list nobody prunes stops describing what a project accepts and becomes a list of things it once did.
CAP.L0.NO_BASELINE_TO_IGNORE — `--ignore-baseline` was passed and there is no baseline
A flag that silently does nothing teaches a reader that it did something. This says it did not.
CAP.L0.TENANCY_NOT_DECLARED — this project looks multi-tenant and has not said so
An audit across N tenant databases is not one statement. A report about whichever tenant was default reads exactly like a report about the application.
CAP.L0.TENANCY_REFERENCE_MISSING — tenancy is `explicit` and no reference tenant is named
The project answered the question and stopped halfway. The run needs to know which tenant the report is about.
CAP.L0.ASSUMED_VERSION_SKEW — the pinned version and the real server disagree
A pin makes a lint run reproducible. When the server it is compared against is a different version, findings are true about a database nobody deploys to.
LINT.SERVER_BELOW_FLOOR — the version this run reasons from is below the supported floor
The findings were still produced, and they may be wrong in both directions.
CAP.L0.SERVER_BELOW_FLOOR — the instance that answered is below the supported floor
The audit ran and reported, but its verdicts may not describe this server.
CAP.L0.UNSUPPORTED_ENGINE — the engine that answered is not the one the driver names
The audit stopped without checking anything, and that is the honest answer.
SEC.PRIV.GRANT_PUBLIC — A privilege granted to PUBLIC
PUBLIC is every role that exists and every role that ever will, so a grant to it cannot be reviewed by looking at who has access.
CAP.L0.MISSING_TOOL — an amplifier was registered and did not answer
An optional external tool could not run, so the checks it brings did not happen. Reported rather than skipped, because a smaller run must never look like a clean one.
SEC.PRIV.GRANT_PUBLIC_IN_MIGRATION — A migration grants a privilege to PUBLIC
The catalog rule finds this after it has been applied. This one finds it in the migration, which is the last moment it is still a decision rather than a state.
SEC.PRIV.ROLE_SUPERUSER — The account is a superuser, or one SET ROLE away
Superuser is not a strong permission — it is the absence of permission checks.
SEC.PRIV.GRANT_EXCESSIVE_IN_MIGRATION — A migration grants everything, or the right to grant it on
Two shapes that are permanent and invisible once they reach the catalog: every privilege on an object, and the option to hand the privilege to somebody else.
SEC.PRIV.ROLE_CREATEROLE — The account may create roles
It can mint login accounts and administer them — narrower than it used to be, and still a way to keep access.
SEC.PRIV.ROUTINE_DEFINER_NO_PATH_IN_MIGRATION — A definer routine that was never told where to look
The routine runs as its owner while its names resolve against the caller. That is a privilege escalation with no injection and no bug in the function.
SEC.PRIV.GRANT_SCOPE_BROAD_IN_MIGRATION — A grant whose scope is a wildcard
`ON *.*` reaches every database on the server; `ON db.*` reaches every table in one — including the tables added after this migration ran.
SEC.PRIV.ROLE_BYPASSRLS — Row-level security does not apply to this account
The policies are correct and beside the point for an account holding BYPASSRLS.
SEC.PRIV.GRANT_ADMIN_IN_MIGRATION — A migration grants an administrative privilege
FILE, PROCESS, RELOAD, SHUTDOWN, CREATE USER and the dynamic SUPER successors govern the server, not the data. An application account needs none of them.
SEC.PRIV.RUNTIME_DDL — The runtime role may change the schema
An injection that reaches a role with CREATE can leave a table, a function or a trigger behind.
SEC.PRIV.CONNECTIONS_UNSEPARATED — One connection serves requests and deploys migrations
The runtime role holds whatever DDL the migrations need, so an injection inherits it.
SEC.PRIV.GRANT_WILDCARD_HOST_IN_MIGRATION — A grantee reachable from anywhere
In MySQL the host half of an account is an access control, not a label. `'app'@'%'` is the same credentials with that control removed.
SEC.RLS.DISABLED — A tenant table without row-level security
The table you listed as holding tenant data is readable in full by anything with SELECT.
SEC.AUTH.PASSWORD_LITERAL_IN_MIGRATION — A password written into a migration
The credential is now in version control, in every clone, and in the reflog after somebody removes it in a later commit.
SEC.RLS.POLICY_ALWAYS_TRUE — A policy that lets every row through
Row-level security is on, the table has a policy, and the separation still does not exist.
SEC.RLS.CHECK_ALWAYS_TRUE — Reads are separated, writes are not
A tenant sees only its own rows and can write a row belonging to anyone.
SEC.RLS.NO_POLICY — Row-level security is on and there is no policy
Nothing is exposed; the table is simply unreadable for everyone but its owner.
SEC.RLS.NOT_FORCED — Your policies do not apply to your own connection
The application connects as the role that owns its tables, and owners are exempt.
SEC.RLS.OWNER_UNRESTRICTED — The table's owner is exempt from its policies
Your application path is restricted; migrations and console sessions are not.
SEC.SKIPPED.* — Part of the security reading was refused
On a managed database this is the ordinary state, and it is stated rather than hidden.
SEC.AUTH.HBA_TRUST — A network line that asks for no password
A `host` line in pg_hba.conf authenticates with `trust`, so anyone who can reach the server is whoever they say they are.
SEC.AUTH.HBA_TRUST_LOCAL — Unix-socket connections without authentication
A `local` line authenticates with `trust`, so OS access to the host is database access as any role.
SEC.AUTH.HBA_CLEARTEXT — The password crosses the wire as typed
The `password` auth method sends the credential unhashed and unchallenged.
SEC.AUTH.HBA_MD5 — md5 authentication, whose verifier is the password
The md5 method stores `md5(password || rolname)`, which is crackable offline and usable as-is.
SEC.AUTH.HBA_OPEN_CIDR — The line accepts the whole address space
A host line matching 0.0.0.0/0 or ::/0 makes every other weakness reachable from anywhere.
SEC.AUTH.HBA_PARSE_ERROR — A line the server could not parse
PostgreSQL rejected a line of pg_hba.conf, so the restriction it was written to impose is not in force.
SEC.PRIV.ROUTINE_DEFINER_MUTABLE_PATH — EXECUTE means "run code as the owner"
A SECURITY DEFINER routine that does not pin its search_path lets its caller substitute the functions it calls.
SEC.PRIV.ROUTINE_DEFINER — A routine that runs as its owner, correctly
Nothing is wrong here. It is on the report because EXECUTE on this routine is a loan of the owner's rights.
SEC.PRIV.ROUTINE_DEFINER_UNSAFE_PATH — a pinned search_path is not automatically a safe one
A SECURITY DEFINER routine whose pinned search_path names a schema somebody else can write to, or places pg_temp anywhere but last.
SEC.AUTH.ROLE_DEPRECATED_PASSWORD_HASH — The account's password is stored under a retired verifier
PostgreSQL calls it md5, MySQL calls it mysql_native_password, and MySQL 9.0 removes it outright.
SEC.AUTH.ROLE_DEPRECATED_PASSWORD_HASH_LOCKED — A locked account still carrying the retired verifier
Locking an account is not fixing it — unlocking is one statement, and it comes back as it left.
LINT.INVALID_CONFIG_REFERENCE — Your baseline names a rule that does not exist
A baseline entry for an unknown rule suppresses nothing while looking like a decision that still holds.
LINT.DEBT.UNRECORDED — this run owes a debt the ledger has never heard of
An open end the account does not know about. Reported, never recorded: writing is a decision somebody makes.
SEC.AUTH.ROLE_NO_PASSWORD — The account can log in and has no password at all
A MySQL account on a password plugin with nothing stored under it. Reach the port, know the name, you are in.
LINT.DEBT.STALE_ENTRY — the ledger holds a debt this run no longer owes
The project settled a debt and the file has not caught up. Bookkeeping, not a defect.
SEC.AUTH.ROLE_NO_PASSWORD_LOCKED — A locked account with nothing behind the lock
Unlocking is one statement, and what comes back is an account anyone who knows its name can log into.
LINT.DEBT.ACKNOWLEDGED_GONE — a debt somebody chose to carry has stopped being detected
Deliberately not the same as a stale entry: an acknowledgment carries a written argument, and that is the thing that would be lost.
SEC.AUTH.ROLE_WILDCARD_HOST — The account may connect from any host
The host half of a MySQL account name is an access control, and `%` matches every address.
LINT.DEBT.LEDGER_UNREADABLE — the account exists and this build cannot act on it
Undetermined, never an empty account: a file this build cannot read must not be reported as a project with no debts.
SEC.AUTH.ROLE_WILDCARD_HOST_PRIVILEGED — Reachable from anywhere, and able to do anything
Either half is ordinary. Together they leave one guessed credential between the network and everything.
LINT.DEBT.NOT_RECORDABLE — a recording run was asked for from a view that cannot support one
--debt=record needs the full pending set. One migration cannot tell an open debt from a settled one.
SEC.AUTH.ROLE_ANONYMOUS — An account with no name is an account for anyone
An empty MySQL user name does not mean unused. It matches any name the server does not otherwise know.
LINT.DEBT.ACKNOWLEDGMENT_EXPIRED — the decision to carry this debt was due for review
review_at is what keeps an acknowledgment from becoming permanent. This is it doing its job.
SEC.AUTH.ROLE_ANONYMOUS_NO_PASSWORD — No name required, and no password either
The two worst account states at once, and the shortest way into a server that exists.
DEBT.STILL_OPEN — the account records this debt and the catalog still shows it
An open end nobody finished, with its age attached. This is what the debt account is for.
SEC.PRIV.GRANT_OPTION — The account can hand its access on
WITH GRANT OPTION makes every other limit on the server voluntary.
DEBT.RESOLVED — a recorded debt the catalog shows as settled
Somebody finished it. Reported here and removed in the repository, never from a deploy server.
SEC.PRIV.GRANT_OPTION_STRUCTURAL — It can hand on the power to change the schema
Not who may read what exists — who may decide what exists.
DEBT.OBJECT_NOT_FOUND — the recorded object is not in the catalog
Not 'settled'. A dropped table, a schema outside this run's scope and a missing privilege all look exactly like this.
DEBT.UNRECORDED — a debt the database carries and the account has never heard of
The oldest debt in a project is usually this one: a constraint that predates the tool, reported on every run with no age and no way to acknowledge it.
SEC.PRIV.GRANT_ALL — An account holds every privilege the engine has
There is no privilege escalation left to perform against an account that already has everything.
SEC.PRIV.GRANT_SERVER_ADMIN — The account administers the server, not a database
And it does not need SUPER to do it — MySQL 8 split those powers across a family of dynamic privileges.
DEBT.LEDGER_MISSING — the account was expected on this machine and is not there
The one place where a missing file must not be read as an empty account.
SEC.PRIV.GRANT_FILE — An account can read and write files as the server
The privilege that turns a SQL injection into a problem on the host, not in the database.
DEBT.LEDGER_UNREADABLE — the account is present and this build cannot act on it
Its own finding, separate from a missing one: an absent file and a corrupt file send you to two different places.
SEC.PRIV.GRANT_PROCESS — An account can read every other session's statements
The leak is the statement text, not the session list: anything interpolated rather than bound travels in it.
SEC.CFG.SECURE_FILE_PRIV — File I/O has no limit, and the value that says so looks like nothing
The empty value is the dangerous one. MySQL writes the switched-off state as the string NULL.
SEC.CFG.LOCAL_INFILE — The server may ask the client for a file
The direction is the opposite of what the statement name suggests: the server names the path.
SEC.CFG.GENERAL_LOG — Every statement is being written down, verbatim
The log does not redact. Passwords and tokens go in as they were sent.
SEC.CFG.GENERAL_LOG_PERSONAL_DATA — Row values are being written to the query log
Every WHERE email = … with its value, in a file that outlives the row an erasure request removed.
SEC.CFG.REQUIRE_SECURE_TRANSPORT — Nothing enforces the encryption the server already offers
Not "encryption is off". A connection that skips it is accepted like any other.
SEC.CFG.TLS_DISABLED — The server does not offer TLS at all
Not "encryption is optional" — no connection to this server can be encrypted, including the one you checked.
SEC.CFG.PASSWORD_ENCRYPTION — New passwords are stored with a deprecated hash
The one md5 finding that reports a server which is clean today — and will not stay that way.
SEC.CFG.STATEMENT_LOGGING_SECRETS — Passwords and tokens are going into the server log
CREATE ROLE … LOGIN PASSWORD is logged verbatim. PostgreSQL does not rewrite it.
SEC.CFG.LOCAL_INFILE_FILE_GRANT — The capability and an account that can use it
Two settings that are harmless apart and a route together — reported only when the grant tables could actually be read.
SEC.CFG.PATCH_EOL — The server runs a release series that is out of support
Nothing is broken today. The server has simply left the list that receives the fix for whatever is found next.
SEC.SKIPPED.NOTHING_CHECKED — The security run examined nothing
Not a clean result. A suite that checked nothing has to say so, or its silence reads like a healthy database.
SEC.CFG.STATEMENT_LOGGING — Statements and their values are written to the server log
The same setting is good practice on a laptop and a disclosure in production, so this rule asks where it is looking first.
SEC.INJ.RAW_SQL_WITHOUT_REASON — Raw SQL that nobody wrote a reason for
A policy rule, not a vulnerability check. It never reads your query; it asks whether reaching for raw SQL was a decision somebody stated.
SEC.INJ.RAW_INTERPOLATION — A runtime value was built into the statement instead of bound to it
The property that decides whether an injection is possible at all — reported without ever reading your query.
SEC.INJ.RAW_SQL_REASON_STALE — a written reason that no longer covers any raw SQL
The expiry direction. An exemption that outlives its reason reads for years as a decision somebody weighed, and the next reader trusts it.
SEC.INJ.DYNAMIC_IDENTIFIER — A column or sort direction is coming from the request
An identifier is part of the statement, not a value, so it cannot be bound. The only fix is an allowlist — and the rule recognizes the ones you already write.
SEC.PII.UNENCRYPTED_COLUMN — A column that looks like personal data is stored in the clear
The column name matches the privacy dictionary and no Eloquent model casts it to an encrypted type.
DEPLOY.CONTEXT.READ_ONLY_TARGET — The instance this deploy points at will not accept writes
What the check reads on PostgreSQL and on MySQL, why a standby and a deliberately read-only primary are named separately, and what to do with either.
DEPLOY.CONTEXT.SESSION_DEFENSE_NOT_APPLIED — The timeouts this run set on its own session are not in force
What the check reads back from its own session, why a `SET` that returned no error can still stop applying, and which connection to point the preflight at.
DEPLOY.CONTEXT.SETTING — The settings this migration is about to run under, read instead of assumed
The four settings each engine reads right before `migrate --force`, why an ordinary everyday value becomes a finding at that moment, and what each one asks you to decide.
DEPLOY.CONTEXT.VERSION_SKEW — the version CI checked against is not the server this deploy meets
What the check compares, what each of its answers means for the deploy, and which decision each one asks you to make.
DEPLOY.LEGACY.CONSTRAINT_NOT_VALIDATED — A constraint was added NOT VALID and the VALIDATE never followed
What an unvalidated constraint costs, the one moment it stops being free, and the single statement that closes it.
DEPLOY.LEGACY.INVALID_INDEX — An index a `CREATE INDEX CONCURRENTLY` never finished
The leftover helps no query, costs every write, and makes the re-run fail on a name conflict. What the deploy gate reads, and what it deliberately refuses to do about it.
DEPLOY.PREFLIGHT.DISK_HEADROOM — How much space the pending migration will need, against what the instance can see
What the check estimates before a deploy, why the honest answer on a managed database is undetermined with the number attached, and what to do with that number.
DEPLOY.PREFLIGHT.INACTIVE_REPLICATION_SLOT — A replication slot with no consumer is holding WAL
Which slot has no consumer connected, how much WAL it is holding, what the server's own wal_status says about it, and why the decision to reconnect or drop belongs to a human.
DEPLOY.PREFLIGHT.LOCK_BLOCKER — Something is already holding a table this migration is about to lock
What the deploy gate reads out of the live activity views in the minute before a migration, what the finding names about the blocking session, and the decision it deliberately leaves to you.
DEPLOY.PREFLIGHT.METADATA_LOCK_BLOCKER — a session is sitting on a table this migration is about to change
What is holding a metadata lock on the tables the pending migration will alter, and why an empty reading is only a pass when nothing went unread.
DEPLOY.PREFLIGHT.MISSING_PRIVILEGE — the role running the migrations may not do what they ask for
What each engine is asked about the migration role, why a privilege MySQL cannot find is undetermined rather than missing, and the grant that answers the finding.
DEPLOY.PREFLIGHT.REPLICATION_LAG — How far behind the replicas are, one moment before a migration adds to their work
What the check reads on each engine, the two states it reports, the line it judges against, and what to decide when a replica is behind.
PG.L4.IDENTIFIER_LENGTH — An identifier over PostgreSQL's limit
PostgreSQL does not refuse an over-long name, it truncates it and carries on — so the object exists under a name nobody wrote.
MY.L4.IDENTIFIER_LENGTH — An identifier over MySQL's limit
MySQL refuses the statement with ERROR 1059 and the deploy stops — the rule moves that failure from the deploy to the diff.
SEC.CFG.TLS_MIN_VERSION — The server still negotiates a withdrawn TLS version
Everything reports as encrypted. That is what makes this one harder to notice than TLS being off.
CAP.L0.RULE_WITHHELD_BY_DEPRECATION — a rule was not applied because it was retired
A deprecation makes the report shorter and leaves everything in it true, which is exactly what a clean run looks like.
PG.L9.VIEW_SELECT_STAR — `SELECT *` in a view definition
A view over a star freezes its column list at creation, so a column added later never appears — and no migration says so.
MY.L9.VIEW_SELECT_STAR — `SELECT *` in a view definition
A view over a star freezes its column list at creation, so a column added later never appears — and no migration says so.
DEPLOY.CONTEXT.GRANT.OWNERSHIP_MISSING — the migration role holds every grant and still cannot ALTER
PostgreSQL does not let anybody grant ALTER TABLE. It requires ownership, which is why a grant check that stops at privileges can be green and wrong.
DEPLOY.LEGACY.INVALID_INDEX_NAME_COLLISION — an invalid index is standing where this deploy wants to build one
The same wreckage as a plain invalid index, except the migration about to run will fail on the name.
PG.L9.TYPE_IMPLICIT_CAST — A foreign key whose two ends are different types
An int column referencing a bigint key can address only the first 2.1 billion of it — and the day the sequence passes that number is years away.
MY.L9.TYPE_IMPLICIT_CAST — A foreign key whose two ends are different types
MySQL refuses the integer case outright, so what survives and still differs is a pair it accepted and converts on every comparison.
DEPLOY.PREFLIGHT.CONCURRENT_INDEX_BLOCKER — a concurrent index build is about to wait on unrelated work
CREATE INDEX CONCURRENTLY waits for every older transaction, not only the ones touching its own table. The build does not fail — it sits there.
DEPLOY.PREFLIGHT.STATISTICS_UNREAD — the escalation could not read the object a finding names
An undetermined that keeps a lint verdict honest: the run could not measure the object, so nothing was raised.
PG.L9.DOC_MISSING_COMMENT — A table or column with no comment
The most opinionated check in the catalog, and off by default — level 9 is not its gate, a config switch is.
MY.L9.DOC_MISSING_COMMENT — A table or column with no comment
The most opinionated check in the catalog, and off by default — level 9 is not its gate, a config switch is.
DEPLOY.DRIFT.UNEXPECTED_IN_DATABASE — The database holds something no migration describes
The hotfix-straight-into-production case: every migration ran, the deploy was green, and the schema still carries an object no file accounts for. What the comparison sees, what it cannot see, and why the next rebuild is the deadline.
PG.L8.NAMING_SNAKE_CASE — An identifier PostgreSQL will not hand back the way you wrote it
An unquoted identifier is folded to lower case, so a mixed-case name means one thing quoted and another unquoted.
MY.L8.NAMING_SNAKE_CASE — An identifier whose meaning depends on the server it lands on
lower_case_table_names differs between a developer machine and a Linux server, so the same migration makes different tables.
PG.L8.FK_ID_SUFFIX — A foreign key column Laravel cannot derive a relation from
The convention is what makes a relation resolve with nothing written down; departing from it is legal and costs an explicit key at every call site.
MY.L8.FK_ID_SUFFIX — A foreign key column Laravel cannot derive a relation from
The convention is what makes a relation resolve with nothing written down; departing from it is legal and costs an explicit key at every call site.
DEPLOY.DRIFT.MISSING_IN_DATABASE — The migrations describe it and the database does not have it
A migration that failed, was skipped, or was rolled back and never re-applied. Why the loud version of this finding is the harmless one, and how to tell a failed migration from one that never ran.
DEPLOY.DRIFT.DIVERGENT — Both sides have it, and describe it differently
The class where the tool is most able to be wrong about itself. What the field-level diff names, why the canonical form is what makes the finding trustworthy, and what to do when the two sides genuinely disagree.
DEPLOY.LEGACY.ORPHAN_TRANSITION_OBJECT — An object whose name says it was meant to be temporary
users_old, tmp_backfill_state, orders_20260721 — what an unfinished expand/contract migration leaves behind, reported as a question rather than a verdict.
DEPLOY.DRIFT.UNCOMPARED — One object type could not be read, so it was never compared
The finding that keeps an empty drift report honest. Why "no drift found" and "nothing was looked at" must not share an exit code, which side could not be read, and how to accept it deliberately without muting real drift.
DEPLOY.LEGACY.OSC_ARTIFACT — What an online-schema-change tool left behind
gh-ost's ghost table, pt-osc's triggers, InnoDB's #sql- temporary — names a tool generates, and one of them is still costing you a write on every row.
DEPLOY.LEGACY.CONSTRAINT_NOT_ENFORCED — A CHECK constraint that is not one
ENFORCED = NO is in the catalog, in SHOW CREATE TABLE and in code review — and admits every row it claims to refuse.
DEPLOY.RUN.TIME_BUDGET_EXCEEDED — The post-deploy run took longer than you allow
The one finding that is about SQLens rather than about your database. Why a run that hangs off every deploy needs a budget it can visibly break, why it is reported rather than aborted, and why sqlens:drift deliberately has none.
PG.L4.EXPAND_WITHOUT_CONTRACT — A column was added and back-filled, and the one it replaces never dropped
Expand is the right first step, and only the first. Until the contract step runs, the table carries two columns for one fact and nothing says which is authoritative.
MY.L4.EXPAND_WITHOUT_CONTRACT — A column was added and back-filled, and the one it replaces never dropped
Expand is the right first step, and only the first. Until the contract step runs, the table carries two columns for one fact and nothing says which is authoritative.