CommitFest 6564 Review: Reject ADD CONSTRAINT NOT NULL if name mismatches existing domain not-null constraint

code-review · generated from canonical Markdown

Type
code-review
Source
postgresql-master
Slug
commitfest-6564-review
Tags
code-reviewcommitfest-6564patch-reviewpostgresql
Updated
2026-08-30T08:09:26.701728+00:00
Verdict / Summaryneeds-review
Front matter
schema: review-report-v2
target: CommitFest patch 6564 — Reject ADD CONSTRAINT NOT NULL if name mismatches existing domain not-null constraint
head: 2fb8da5a245661287833b05a1b2e275ddf83bbd7
base: 2fb8da5a245661287833b05a1b2e275ddf83bbd7
verdict: needs-review
blocker_counts: verified=0 unverified=0
scope_identity: diff-29ef4979d31fdbe1953f7708d430efe869230f8e@2fb8da5a245661287833b05a1b2e275ddf83bbd7
model: glm-5.2:cloud
source: postgresql-master
slug: commitfest-6564-review

CommitFest Patch 6564 — Review Report

Reject ADD CONSTRAINT NOT NULL if name mismatches existing domain not-null constraint

Target: CommitFest patch 6564 Head: 2fb8da5a245661287833b05a1b2e275ddf83bbd7 Verdict: needs-review Blocker counts: verified=0, unverified=0 Model: glm-5.2:cloud Date: 2026-08-30

Patch summary

The patch extends the table-column name-mismatch check (commit 96e2af60504) to domain NOT NULL constraints in AlterDomainAddConstraint (src/backend/commands/typecmds.c). When ALTER DOMAIN ... ADD CONSTRAINT <name> NOT NULL is issued on a domain already marked typnotnull and the supplied name differs from the existing NOT NULL constraint's name, it now throws ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE instead of silently accepting the redundant add. Unnamed adds and same-name adds remain idempotent (no-op success), matching ALTER DOMAIN ... SET NOT NULL / ALTER DOMAIN ... ADD NOT NULL semantics and the table-column behavior.

Changed files: src/backend/commands/typecmds.c (+25 lines), src/test/regress/expected/domain.out (+11/-1), src/test/regress/sql/domain.sql (+6/-1).

Deterministic verdict

Verification Summary

IDSeverityClassStatusMethod
F1MAJORcorrectnessverifiedstatic-analysis-plus-toctou-reachability
F2MINORmaintainabilityverifiedstatic-code-inspection
F3MINORtestsverifiedstatic-parser-analysis
F4NITmaintainabilityverifiedstatic-style-inspection
F5NITdocsverifiedstatic-docs-inspection
F6MINORmaintainabilityverifiedstatic-memory-context-analysis
F7MINORcorrectnesscannot-verifystatic-toctou-analysis

Repair Validation Summary

Finding(s)Patchsha256Validation
F1, F2, F4, F6typecmds_repair.diff8556922760bfd4141aee2edbd1f1f81a46f6b6736b416a2d656acebec84f46d3passed (apply-check + diff-check + compile + targeted-test)
F3f3_domain_sql.diffd44d62184db92d5f725d3e773fb5e7fef3023a6b84bb9bef5588731c94677de4passed (apply-check + diff-check + targeted-test)
F5f5_docs.diffb4c3c20d93e01dcbb95ff78152edf2242935ebd5ccde50713c0eea98a9b92c13passed (apply-check + diff-check + doc-build)
F7waived (design-choice)n/awaived

All repairs validated in disposable scratch build. The read-only worktree was never modified. Patches are HEAD-bound to 2fb8da5a245661287833b05a1b2e275ddf83bbd7.

F1

repair detail (combined with F2, F4, F6)

  1. Demotes elog(ERROR) at the NULL-conTup branch to ereport(ERROR) with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE and errdetail.
  2. Groups declarations (F4).
  3. Adds cross-reference comment to domainAddNotNullConstraint's ERRCODE_DUPLICATE_OBJECT guard (F2).
  4. Adds heap_freetuple(conTup) before mismatch ereport and early return (F6).

Compiled clean with gcc -Wdeclaration-after-statement -Werror=vla -Wold-style-declaration. pg_regress domain passes.

Findings

F1

— MAJOR, correctness — src/backend/commands/typecmds.c:3060-3061

Mechanism: findDomainNotNullConstraint (src/backend/catalog/pg_constraint.c:661-698) returns only validated NOT NULL constraints. The elog(ERROR) at typecmds.c:3060-3061 is reachable via TOCTOU: typTup is a stale SearchSysCacheCopy1 snapshot, and a concurrent ALTER DOMAIN DROP NOT NULL can remove the NN constraint between the typnotnull check and the lookup, causing findDomainNotNullConstraint to return NULL and the internal elog(ERROR) (no errcode, shown as internal error to clients) to fire for a user-reachable concurrent DDL condition.

Failure: A user-reachable concurrent DDL scenario produces an internal-style elog(ERROR) without an SQLSTATE, degrading the user experience compared to a proper ereport. The elog is used for a condition that is not a true can't-happen invariant violation.

Verification status: verified Verification method: static-analysis-plus-toctou-reachability

Repair instruction: Demote elog(ERROR) at NULL-conTup branch to ereport(ERROR) with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE and errdetail; group Form_pg_constraint conform and HeapTuple conTup declarations; add cross-reference comment to domainAddNotNullConstraint duplicate-name guard; add heap_freetuple(conTup) before both the mismatch ereport and the early return; reword comment.

Intended postcondition: A NULL conTup raises a proper client-facing error with SQLSTATE 55000 instead of an internal elog; declarations are block-grouped; the duplicate-name guard relationship is documented; the palloc'd copy is freed on all paths leaving the block.

Repair validation: passed (methods: apply-check, diff-check, compile, targeted-test) Tested patch sha256: f090cd1981a76d129eeb669c3b32a55029605ed35ce5942971f51e81978cc8ea

F2

— MINOR, maintainability — src/backend/commands/typecmds.c:3054-3077

Mechanism: The new early-return block name check and domainAddNotNullConstraint duplicate-name guard (typecmds.c:3768-3777, ERRCODE_DUPLICATE_OBJECT) handle related name-in-use situations on different code paths with different errcodes. A cross-reference comment is missing.

Failure: Future readers cannot easily understand why the early-return branch needs its own name check rather than relying on domainAddNotNullConstraint's guard.

Verification status: verified Verification method: static-code-inspection

Repair instruction: Demote elog(ERROR) at NULL-conTup branch to ereport(ERROR) with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE and errdetail; group Form_pg_constraint conform and HeapTuple conTup declarations; add cross-reference comment to domainAddNotNullConstraint duplicate-name guard; add heap_freetuple(conTup) before both the mismatch ereport and the early return; reword comment.

Intended postcondition: A NULL conTup raises a proper client-facing error with SQLSTATE 55000 instead of an internal elog; declarations are block-grouped; the duplicate-name guard relationship is documented; the palloc'd copy is freed on all paths leaving the block.

Repair validation: passed (methods: apply-check, diff-check, compile, targeted-test) Tested patch sha256: f090cd1981a76d129eeb669c3b32a55029605ed35ce5942971f51e81978cc8ea

F3

— MINOR, tests — src/test/regress/sql/domain.sql:520

Mechanism: The added test 'alter domain connotnull add not null not valid' errors out in the parser (gram.y processCASbits at gram.y:20684-20695), not in AlterDomainAddConstraint. It does not exercise the new code path.

Failure: A reviewer scanning the diff could be misled into thinking the NOT VALID test exercises the new name-mismatch logic, when it tests pre-existing parser behavior.

Verification status: verified Verification method: static-parser-analysis

Repair instruction: Add a two-line SQL comment before 'alter domain connotnull add not null not valid' clarifying it is a parser-level rejection (gram.y DomainConstraintElem), not AlterDomainAddConstraint; mirror the comment lines in domain.out.

Intended postcondition: The test file documents that the NOT VALID rejection is parser-side, not AlterDomainAddConstraint; pg_regress domain still passes.

Repair validation: passed (methods: apply-check, diff-check, targeted-test) Tested patch sha256: 6e1ba21146f8214659f4433487f5c2e7b823013dcdc3e38bce1f208f87f125cb

F4

— NIT, maintainability — src/backend/commands/typecmds.c:3056-3058

Mechanism: HeapTuple conTup is declared mid-block after a blank line separating it from Form_pg_constraint conform. pgindent prefers declarations grouped. Comment punctuation is awkward.

Failure: Minor style deviation from PostgreSQL declaration-grouping convention; awkward comment wording.

Verification status: verified Verification method: static-style-inspection

Repair instruction: Demote elog(ERROR) at NULL-conTup branch to ereport(ERROR) with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE and errdetail; group Form_pg_constraint conform and HeapTuple conTup declarations; add cross-reference comment to domainAddNotNullConstraint duplicate-name guard; add heap_freetuple(conTup) before both the mismatch ereport and the early return; reword comment.

Intended postcondition: A NULL conTup raises a proper client-facing error with SQLSTATE 55000 instead of an internal elog; declarations are block-grouped; the duplicate-name guard relationship is documented; the palloc'd copy is freed on all paths leaving the block.

Repair validation: passed (methods: apply-check, diff-check, compile, targeted-test) Tested patch sha256: f090cd1981a76d129eeb669c3b32a55029605ed35ce5942971f51e81978cc8ea

F5

— NIT, docs — doc/src/sgml/ref/alter_domain.sgml:93

Mechanism: The patch adds a user-visible ERROR (cannot create not-null constraint for domain with mismatched name) but does not update doc/src/sgml/ref/alter_domain.sgml.

Failure: Users who hit the new error have no documentation explaining the behavior change.

Verification status: verified Verification method: static-docs-inspection

Repair instruction: In the ADD domain_constraint varlistentry paragraph (after 'NOT VALID is only accepted for CHECK constraints.'), append a sentence documenting that adding a named NOT NULL constraint to an already-NOT NULL domain with a differing name raises an error, and re-adding with the same name (or no name) is a no-op.

Intended postcondition: User-visible new error is documented; full docs HTML builds.

Repair validation: passed (methods: apply-check, diff-check, doc-build) Tested patch sha256: c86085555432b8d4a8e45655a4801db5dc71236837b41f2022de2f1cc03dd78d

F6

— MINOR, maintainability — src/backend/commands/typecmds.c:3058-3076

Mechanism: findDomainNotNullConstraint returns a heap_copytuple (palloc'd copy). On the ereport(ERROR) mismatch path and the early-return success path, conTup is not heap_freetuple'd. This is a transient allocation cleaned by transaction-abort context reset (memory-context false-positive filter applies).

Failure: Transient allocation not explicitly freed on error and success paths; cleaned by context reset but represents a code-hygiene gap. Thread status: already raised by Evan Li and self-resolved as benign.

Verification status: verified Verification method: static-memory-context-analysis

Repair instruction: Demote elog(ERROR) at NULL-conTup branch to ereport(ERROR) with ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE and errdetail; group Form_pg_constraint conform and HeapTuple conTup declarations; add cross-reference comment to domainAddNotNullConstraint duplicate-name guard; add heap_freetuple(conTup) before both the mismatch ereport and the early return; reword comment.

Intended postcondition: A NULL conTup raises a proper client-facing error with SQLSTATE 55000 instead of an internal elog; declarations are block-grouped; the duplicate-name guard relationship is documented; the palloc'd copy is freed on all paths leaving the block.

Repair validation: passed (methods: apply-check, diff-check, compile, targeted-test) Tested patch sha256: f090cd1981a76d129eeb669c3b32a55029605ed35ce5942971f51e81978cc8ea

F7

— MINOR, correctness — src/backend/commands/typecmds.c:3052-3058

Mechanism: typTup is a SearchSysCacheCopy1 snapshot (stale-on-arrival). Between the typnotnull check and the findDomainNotNullConstraint scan, a concurrent ALTER DOMAIN DROP NOT NULL can drop the NN constraint so the scan returns NULL. Worst case: a spurious error under concurrent DDL. Non-exploitable: owner-only operation, no catalog write on early-return path.

Failure: A concurrent DDL race can produce a spurious error to the domain owner. No privilege escalation, no data exposure, no catalog corruption.

Verification status: cannot-verify Verification method: static-toctou-analysis Verification reason: The TOCTOU race is real but non-exploitable and requires concurrent owner-level DDL to trigger. A deterministic reproduction would require test-only injection points and isolation infrastructure. The finding itself concludes no fix is required; the F1 repair mitigates the symptom by converting the internal elog to a proper ereport with SQLSTATE and errdetail explaining the likely concurrent drop.

Repair instruction: No production code fix required. The finding concludes no fix is needed. The F1 repair mitigates the symptom by converting the internal elog to a proper ereport with SQLSTATE and errdetail explaining the likely concurrent drop. A semantic fix (treating NULL conTup as concurrently dropped and falling through) would change behavior and is intentionally not applied.

Intended postcondition: The TOCTOU race remains as a spurious-error-only condition under concurrent owner-level DDL, now surfaced as a proper client-facing ereport (via F1 repair) instead of an internal elog. No privilege escalation, no catalog corruption, no data exposure.

Repair validation: waived

Changed-Code Coverage

Status: measured Head: 2fb8da5a245661287833b05a1b2e275ddf83bbd7 Counter epoch: fresh Execution lane: host-disposable (passed)

Test commands

Clean-counter evidence

Fresh coverage-counter epoch: rebuilt src/backend/commands/typecmds.c with gcc --coverage (-fprofile-arcs -ftest-coverage), relinked postgres backend with --coverage. Prior .gcda deleted; typecmds.gcda (mtime 2026-08-30T07:57:32Z) newer than typecmds.gcno (2026-08-30T07:56:24Z). integrity_errors: [].

Raw metrics

MetricValue
Changed executable lines7
Covered changed lines6
Compiler uncovered changed lines1
Uncovered changed lines1
Structural uncovered changed lines0
Changed lines without coverage coordinates0
Changed branches10
Covered changed branches8

Line coverage: 85.71% (6/7) of added executable lines. Branch coverage: 80.00% (8/10) of added branch outcomes.

Uncovered regions

src/backend/commands/typecmds.c:3061-3061 in AlterDomainAddConstraint

c
3061: 					elog(ERROR, "could not find not-null constraint on domain \"%s\"", NameStr(typTup->typname));

Coverability: infeasible-with-reason Reason: Defensive invariant guard: typnotnull==true guarantees a validated pg_constraint NN row exists (parser rejects NOT VALID for domain NN, all creation paths insert validated). findDomainNotNullConstraint returns NULL only under catalog corruption or TOCTOU concurrent DROP, neither reproducible via SQL on a non-broken cluster. No public injection point exists. Consistent with surrounding elog(ERROR, cache lookup failed) defensive style. Test example: waived

Uncovered branches

src/backend/commands/typecmds.c:3060 branch 0 in AlterDomainAddConstraint (count=0)

Coverability: infeasible-with-reason Reason: The true branch (conTup == NULL) is never taken because findDomainNotNullConstraint always finds a validated NN constraint when typnotnull is true under normal catalog invariants. The NULL return only occurs under catalog corruption or TOCTOU concurrent DROP, neither reproducible via SQL.

src/backend/commands/typecmds.c:3061 branch 0 in AlterDomainAddConstraint (count=0)

Coverability: infeasible-with-reason Reason: The elog(ERROR) call on this line never returns (longjumps via ereport), and the line itself is never executed (count=0) because the guard branch on line 3060 is never taken true. The fallthrough branch from this line is therefore never taken.

Touched functions

PathFunctionLinesEnteredWhole-function linesWhole-function branches
src/backend/commands/typecmds.cAlterDomainAddConstraint2984-3099True50/5022/28

Non-instrumented changes

Coverage conclusion

The domain regression test exercises all reachable added business logic: the name-mismatch error (3071-3076), the name-match redundant path (3070 false), and the no-name redundant path (3054 false). The only uncovered added lines (3060 true branch and 3061) form a defensive invariant guard infeasible to reach via SQL, waived with a typed reason. No COV-* finding emitted.

pgsql-hackers thread ledger

Status: reconciled Ledger report slug: commitfest-6564-review-handoff-thread-ledger

Reconstruction

Summary: Thread located and fully reconciled: 4 messages, 2026-03-01 to 2026-06-27. Jian He submitted the domain-extension patch; Evan Li reviewed (code good, raised conTup-not-freed, self-resolved as benign via context); Alvaro Herrera confirmed the redundant-vs-error ordering semantics are expected; Zizhuan Liu filed an unresolved backward-compat counter-proposal (NOTICE-and-skip instead of ERROR) extending to AlterDomainNotNull. Only F6 (conTup lifetime) was raised on-thread and resolved as benign. F1, F2, F3, F4, F5, F7 were not discussed. Zizhuan's NOTICE-vs-ERROR design question is open (no committer ruling).

Thread

Subject: Reject ADD CONSTRAINT NOT NULL if name mismatches existing domain not-null constraint Thread ID: 638358/638454 Participants: jian.universality@gmail.com (Jian He, author), li.evan.chao@gmail.com (Chao Li / Evan Li, reviewer), alvherre@kurilemu.de (Alvaro Herrera, committer of referenced prior commit 96e2af60504), 44973863@qq.com (Zizhuan Liu, counter-proposal)

Messages

  1. CACJufxEcTx2U8tQTcJ2g490wzd=jQgJNDpNb2L+f3gr9J=cpNA@mail.gmail.com (2026-03-01T01:53:04+00:00, jian.universality@gmail.com, patch): Submission: extend table-column name-mismatch guard (96e2af60504) to domain NOT NULL constraints in AlterDomainAddConstraint. Demo ERROR for mismatched name; repeated SET/ADD NOT NULL still allowed; CREATE DOMAIN already disallows multiple NN.
  1. F2DE0488-227A-424D-8659-26BED8F69E25@gmail.com (2026-03-02T06:38:54+00:00, li.evan.chao@gmail.com, review): Code good; errcode/message parity with 96e2af60504. Raises conTup not freed from findDomainNotNullConstraint(), concludes benign via memory context. Observes unnamed-then-named ordering fails (defaulted name != constr1), asks if expected.
  1. 202603021031.zk4whpelpxec@alvherre.pgsql (2026-03-02T10:34:11+00:00, alvherre@kurilemu.de, committer-reply): Confirms expected: a column/domain has only one NN constraint; default name won't match constr1. Named-first then unnamed = redundant match; unnamed-first then named = error. Corroborates patch semantics.
  1. tencent_84E11BD212851AB351001EA90D8D96782205@qq.com (2026-06-27T02:46:18+00:00, 44973863@qq.com, author-reply): Backward-compat counter-proposal: original AlterDomainAddConstraint returned silently when already NOT NULL; new ERROR breaks workflows. Proposes NOTICE-and-skip, extends to AlterDomainNotNull. Defers test updates pending agreement. No committer response in thread.

Finding links

FindingDispositionRationale
F1newNot raised. findDomainNotNullConstraint filters on convalidated, so for an existing NOT VALID NN constraint with typnotnull set, the lookup returns NU
F2newNot raised. New block hand-duplicates the name-mismatch logic without a cross-ref comment to the sibling domainAddNotNullConstraint path. Not discusse
F3newNot raised. The added add not null not valid test errors at processCASbits before reaching the patched branch, so it does not exercise the new guard.
F4newNot raised. Declaration ordering deviates from pgindent conventions. Not discussed.
F5newNot raised. New user-visible ERROR condition added with no alter_domain.sgml docs update. Not discussed.
F6already-raisedEvan Li review explicitly raised conTup-from-findDomainNotNullConstraint() not freed and self-resolved it as benign via memory-context reset. Covers t
F7newNot raised. TOCTOU between typTup->typnotnull read and the subsequent findDomainNotNullConstraint lookup; non-exploitable given RowExclusiveLock on pg

Aggregated Test Plan

Existing tests (patched)

Proposal-only tests (not applied)

Two-run validation requirement

Verified clean / non-findings

Provenance / model routes

SeatAgentModelHandoff slug
patch-reviewerpg-patch-reviewerglm-5.2:cloudcommitfest-6564-review-handoff-patch-reviewer
security-reviewerpg-security-reviewerglm-5.2:cloudcommitfest-6564-review-handoff-security-reviewer
thread-reconcilercross-reference-review-with-threadglm-5.2:cloudcommitfest-6564-review-handoff-thread-ledger
test-authorpg-test-authorglm-5.2:cloudcommitfest-6564-review-handoff-test-author
finding-verifierpg-finding-verifierglm-5.2:cloudcommitfest-6564-review-handoff-verifier
coverage-reviewerpg-change-coverage-reviewerglm-5.2:cloudcommitfest-6564-review-handoff-coverage

Residual risk

Validator

Artifact validated with validate_review_artifact.py (SHA-256 edbcb304ffbd062627196c1ee0ce3d9b60cae80dd7b201e420335fea0a31979e): ok: pg-review-artifact-v2 is structurally complete (exit 0).