warn-not-enforced-constraint
NOT ENFORCED constraint will not enforce data integrity. Invalid data can be inserted.
- operation
- Constraints
- lock taken
- no table lock
- remediation
- Manual rewrite
- category
- Constraints & keys
Known limitation
The bundled parser (libpg-query) is built on the PostgreSQL 17 grammar, so the PG18 syntax this rule is about — NOT ENFORCED — does not parse. A migration written that way is reported as a parse error rather than analysed, and this rule cannot fire on it. Upgrading the parser is a tracked fast-follow; the PG18 rules shipped in release 1.5.0.
What triggers it
A regex match for NOT\s+ENFORCED against the statement's raw originalSql text, checked only when ctx.pgVersion is 18 or higher.
What does not
Any migration targeting PostgreSQL below 18 returns immediately, and any statement whose SQL text doesn't contain the literal phrase NOT ENFORCED.
Where it applies
Applies to PostgreSQL 18 and later. It works on the SQL text alone — no database connection needed.
The lock, and what it blocks
None — NOT ENFORCED constraints exist purely as catalog metadata, so there's no lock behavior for this rule to describe.
Why it matters
PostgreSQL 18 NOT ENFORCED constraints exist only as metadata hints for the query planner. The database will NOT reject invalid data. Useful for documentation or gradual migration, but dangerous if you expect enforcement.
Unsafe, and safe
Flagged
ALTER TABLE orders ADD CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id) NOT ENFORCED; -- Invalid user_id values will NOT be rejected!
Safe alternative
-- If you need enforcement: ALTER TABLE orders ADD CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id) NOT VALID; ALTER TABLE orders VALIDATE CONSTRAINT fk_user;
What it assumes
Assumes NOT ENFORCED is worth flagging every time, but it's a legitimate tool for documenting an invariant the application already guarantees, or for staging a constraint mid-migration — the rule can't tell that deliberate use apart from an accidental one. The match is also on raw text rather than the constraint's own AST node, so it doesn't distinguish the clause on a real constraint from the same phrase inside a comment.
What backs this rule
Every rule is a claim about PostgreSQL, so it carries what the claim rests on: a handbook chapter that cites the manual, the incidents that put it there, and the version it was last checked against.
PostgreSQL 18 removed the need for the CHECK-constraint dance in entry 04.
- verified against
- PostgreSQL 18.4
- last checked
- 2026-08-11
- confidence
- Medium
PostgreSQL manual
What the CLI prints
No sample output: the flagged example uses PostgreSQL 18 syntax the bundled parser cannot read yet, so the CLI reports a parse error rather than this finding. See the limitation above.
Turning it off
For one statement, put a comment on the line before it:
-- migrationpilot-disable MP082 ALTER TABLE orders ADD CONSTRAINT fk_user
For the whole project, in .migrationpilotrc.yml — by name or by id:
rules:
MP082: false
# or keep it, and downgrade it
rules:
MP082:
severity: warningTry it
Open this rule's flagged example in the playground. It runs in your browser — edit it and watch the finding appear and disappear.
Run MP082 in the playground