Skip to content

Fix constraint validation when merging with a NOT ENFORCED constraint#81

Draft
NikolayS wants to merge 1 commit into
masterfrom
claude/postgres-check-constraint-patch-yr01s8
Draft

Fix constraint validation when merging with a NOT ENFORCED constraint#81
NikolayS wants to merge 1 commit into
masterfrom
claude/postgres-check-constraint-patch-yr01s8

Conversation

@NikolayS

Copy link
Copy Markdown
Owner

Summary

Fixes a correctness bug where ALTER TABLE child ADD CONSTRAINT on a legacy-inheritance child could mark an inherited NOT ENFORCED CHECK constraint as both enforced and validated without scanning existing rows, leaving trusted-but-false catalog metadata. With constraint_exclusion = on, the planner then trusted the false constraint and silently returned wrong query results (rows filtered out that actually exist in the table).

Root cause

MergeWithExistingConstraint() in src/backend/catalog/heap.c, when merging a local ENFORCED constraint into an existing inherited NOT ENFORCED one, unconditionally set:

con->conenforced = true;
con->convalidated = true;

No Phase 3 verification scan was ever queued, so rows violating the constraint survived while pg_constraint claimed the constraint was validated.

Fix

Two coordinated changes:

  1. src/backend/catalog/heap.cMergeWithExistingConstraint() now sets convalidated from the new constraint's initially_valid flag instead of unconditionally to true. A merge with a NOT VALID constraint now also correctly leaves the constraint NOT VALID (previously the not-valid state was silently ignored), to be validated later with ALTER TABLE ... VALIDATE CONSTRAINT.

  2. src/backend/commands/tablecmds.cATAddCheckNNConstraint() records, before calling AddRelationNewConstraints(), whether an identically-named NOT ENFORCED check constraint exists that the merge would promote to enforced. If the merge happens and the new constraint is initially valid, it queues a Phase 3 verification scan of the existing rows — mirroring what ALTER TABLE ... ALTER CONSTRAINT ... ENFORCED does. If existing rows violate the constraint, the ALTER now fails with check constraint ... is violated by some row and the catalog change rolls back.

Behavior after the fix

  • ALTER TABLE child ADD CONSTRAINT iso CHECK (tenant = 1) over an inherited NOT ENFORCED constraint with violating rows → error, catalog unchanged, query results stay correct.
  • Same command with compliant rows → merge succeeds, constraint enforced and validated (rows were actually verified).
  • ... NOT VALID variant → merge succeeds without a scan; constraint becomes enforced but stays NOT VALID; VALIDATE CONSTRAINT then behaves as expected.
  • ALTER COLUMN ... TYPE re-add path re-verifies rows through the same mechanism.

Testing

  • New regression tests in src/test/regress/sql/inherit.sql covering the violating-row error, the NOT VALID merge state, and the successful verified merge; expected output updated (including the pre-existing inh_check_constraint10 case, whose NOT VALID state is now preserved instead of ignored).
  • Full core regression suite passes: All 245 tests passed (make check, assert-enabled build).
  • Original repro from the bug report verified fixed manually.

A mailing-list-ready format-patch of this commit is available for posting to pgsql-hackers.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WKfageiP8Th6TgzZuyCpfQ


Generated by Claude Code

When a local ENFORCED check constraint was added to a legacy-inheritance
child table having an identically named inherited NOT ENFORCED
constraint, MergeWithExistingConstraint() marked the merged constraint
as both enforced and validated without checking the table's existing
rows.  Rows violating the constraint could therefore remain in the
table while the catalogs claimed the constraint was valid, and the
planner would then trust the false metadata (e.g. for constraint
exclusion), silently returning wrong query results.

To fix, have MergeWithExistingConstraint() set convalidated according
to the new constraint's initially-valid status instead of
unconditionally setting it to true, and have ATAddCheckNNConstraint()
queue a Phase 3 verification scan of the existing rows when such a
merge promotes a NOT ENFORCED constraint to enforced, like ALTER TABLE
... ALTER CONSTRAINT ... ENFORCED does.  As a side effect, a
constraint added with NOT VALID is no longer silently marked validated
when merged this way; it keeps its not-valid state and can be
validated later with ALTER TABLE ... VALIDATE CONSTRAINT.

Oversight in the introduction of NOT ENFORCED constraints in
PostgreSQL 18.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WKfageiP8Th6TgzZuyCpfQ
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant