CommitFest 6270 review: UPDATE run check constraints for affected columns only (v7)

code-review · generated from canonical Markdown

Type
code-review
Source
postgresql-master
Slug
commitfest-6270-review
Tags
code-reviewpatch-reviewpostgresql
Updated
2026-08-19T07:46:40.026152+00:00
Verdict / Summarywaiting-on-author
Front matter
schema: review-report-v2
report_slug: commitfest-6270-review
source: postgresql-master
target: "UPDATE run check constraints for affected columns only (CommitFest 6270, v7)"
head: be97b984f43079fd87c561e8ca6cd614c18e652e
base: 1bb858c83103025742b7d2ca4d6f926564ba60e6
verdict: waiting-on-author
blocker_counts:
  verified: 1
  unverified: 0

CommitFest 6270 — Review Report (review-report-v2)

Patch: "UPDATE run check constraints for affected columns only" (v7, jian he <jian.universality@gmail.com>) Thread: pgsql-hackers, Thread-ID 629617 Verdict: waiting-on-author — 1 verified BLOCKER (constraint-enforcement bypass via BEFORE ROW UPDATE trigger) Blocker split: verified=1, unverified=0

Summary

The patch adds a CmdType operation parameter to ExecConstraints/ExecRelCheck and, for CMD_UPDATE, skips evaluating enforced CHECK constraints whose referenced columns (computed via pull_varattnos) do not overlap the UPDATE target columns (ExecGetUpdatedCols/ExecGetExtraUpdatedCols). A new bool *ri_UpdateSkipConstrCheck field is added to ResultRelInfo. Call sites are updated; ExecSimpleRelationUpdate (logical replication) passes CMD_INSERT.

The core optimization is sound and mirrors ExecInitGenerated, but the staged v7 omits the BEFORE ROW UPDATE trigger guard that ExecInitGenerated applies (!(rel->trigdesc && rel->trigdesc->trig_update_before_row)). The author claimed on 2025-12-07 to have applied this guard, but the staged diff does not contain it. A BEFORE UPDATE FOR EACH ROW trigger that mutates a non-SET column referenced by a CHECK silently bypasses that CHECK on UPDATE.

Verification Summary

FindingSeverityClassStatusMethod
F1-SEC-01BLOCKERcorrectness (security-adjacent)verifieddisposable-build two-run repro gate
F2-SEC-03MAJORcorrectnessverifiedstatic reading + apply-check + compile
F3-SEC-02MAJORcorrectnessverifiedstatic reasoning + apply-check + compile
F4MINORmaintainabilityverifiedcompile
F5MINORmaintainabilityverifiedcompile
F6MINORtestsverifiedTAP two-run gate
F7NITdocsverifiedcompile

F1 (BLOCKER) verification — two-run repro gate (executed):

Findings

F1

SEC-01 — BLOCKER — correctness — Missing BEFORE ROW UPDATE trigger guard

  • Location: src/backend/executor/execMain.c:1893 (skip computation) and :1941-1943 (skip application)
  • Mechanism: ExecRelCheck computes the per-constraint CHECK skip set for CMD_UPDATE from ExecGetUpdatedCols/ExecGetExtraUpdatedCols without guarding for a BEFORE ROW UPDATE trigger, unlike ExecInitGenerated (nodeModifyTable.c:476-480) which guards with !(rel->trigdesc && rel->trigdesc->trig_update_before_row). A BEFORE UPDATE FOR EACH ROW trigger may mutate a column not in the SET clause; a CHECK referencing only such a trigger-modified column is skipped.
  • Failure: A CHECK constraint used as a row-state invariant is silently bypassed on UPDATE when a BEFORE ROW trigger rewrites a non-SET column the CHECK depends on, storing a violating tuple. Reachable with table UPDATE privilege + trigger-function ownership (table owner qualifies); no pg_* role needed. Same hazard Tom Lane flagged on 2025-12-01 and the author claimed to fix on 2025-12-07 but the staged v7 omits the guard.
  • Repair: Add the 2-line guard: if (operation == CMD_UPDATE && !(rel->trigdesc && rel->trigdesc->trig_update_before_row)). HEAD-bound patch repair-f1-clean.patch (sha256 d1925783...), validated passed (apply-check + compile + two-run gate + staged TAP).
F2

SEC-03 — MAJOR — correctness — ExecSimpleRelationUpdate passes CMD_INSERT

  • Location: src/backend/executor/execReplication.c:947
  • Mechanism: ExecSimpleRelationUpdate passes CMD_INSERT to ExecConstraints, so the skip branch never runs for logical-replication UPDATEs. Not a bypass (constraints still all checked — conservative) but a parameter-wiring bug contradicting the patch scope. carol.li2025 flagged this on 2025-12-01.
  • Repair: Change to CMD_UPDATE after F1 is fixed (so the trigger guard protects the replication path). Patch repair-f2.patch validated partial (apply-check + compile; full runtime needs a subscription cluster).
F3

SEC-02 — MAJOR — correctness — Caching/ordering unsoundness for MERGE/ON CONFLICT

  • Location: src/backend/executor/execMain.c:1868
  • Mechanism: ri_UpdateSkipConstrCheck is built once per ResultRelInfo (gated on ri_CheckConstraintExprs == NULL) and computed only when the first ExecRelCheck call passes CMD_UPDATE. In MERGE / INSERT ... ON CONFLICT DO UPDATE, ExecInsert (CMD_INSERT) runs first on the same ResultRelInfo, pinning the flags to all-false; the later CMD_UPDATE lreplace never skips. The skip set is a function of (relation, updatedCols) but is cached as a function of (relation) only. No present-day bypass (the operation == CMD_UPDATE runtime gate at :1941 protects INSERT paths), but fragile. Florin Irion flagged the MERGE sharing on 2026-03-06.
  • Repair: Add ri_UpdateSkipConstrCheck_valid and recompute on the first CMD_UPDATE call (with the F1 guard). Patch repair-f3.patch validated partial (apply-check + compile).
F4

— MINOR — maintainability — palloc0 vs palloc0_array

  • Location: src/backend/executor/execMain.c:1876
  • Mechanism: palloc0(sizeof(bool) * ncheck) instead of palloc0_array(bool, ncheck) (the overflow-checked variant used at line 1874). Style inconsistency, not a memory bug. Repair validated passed (compile).
F5

— MINOR — maintainability — == true on bool

  • Location: src/backend/executor/execMain.c:1942
  • Mechanism: ri_UpdateSkipConstrCheck[i] == true uses an explicit == true; PG style prefers a bare boolean test. Repair validated passed (compile).
F6

— MINOR — tests — Inadequate test coverage

  • Location: src/test/modules/test_misc/t/001_constraint_validation.pl:43-92
  • Mechanism: The added TAP tests cover positive skip cases only. Missing: BEFORE ROW UPDATE trigger (the F1 hazard — committer prerequisite), MERGE, ON CONFLICT DO UPDATE, cross-partition UPDATE, negative violation case. The DEBUG grep is not anchored to the relation name on a partitioned table. Florin Irion noted the MERGE/cross-partition test gaps on 2026-03-06.
  • Repair: repair-f6-cov.patch adds the BEFORE-trigger test (two-run gate: FAILS on v7, PASSES on F1-repaired), plus a no-Var CHECK (COV-1) and a COPY test (COV-3). Validated passed.
F7

— NIT — docs — Misleading comment

  • Location: src/include/nodes/execnodes.h:587-591
  • Mechanism: Comment typo "applys" and a claim that the optimization applies to ON CONFLICT DO UPDATE / MERGE UPDATE, which F3 shows is not reliably true. Repair validated passed (compile).

Repair Validation Summary

FindingPatchsha256Validation
F1-SEC-01repair-f1-clean.patchd1925783e4bca270035ee449df6b1c6e3cc1e17b8459b29b268767bf95632071passed (apply-check, compile, targeted-test, two-run)
F2-SEC-03repair-f2.patch0fa854acdeee7ab7fb2a28eafa2994f5d1202f0b3733a637809c62c04936f671partial (apply-check, compile; runtime needs subscription cluster)
F3-SEC-02repair-f3.patchebb913d701f03ddbce1678c687e3a4dfedc89d716a103d0671a7f45c96a07c85partial (apply-check, compile; runtime needs MERGE/ON-CONFLICT matrix)
F4repair-f4.patch9528518f6dd7b9d28747841df6f5784fca8e2b5761b3c9fd1e9879a9ba22f4cbpassed (apply-check, compile)
F5repair-f5.patch107fa6e6398042dbcba11b925176c77255e9a51c890828444f82fd357df241e2passed (apply-check, compile)
F6repair-f6-cov.patchb1de70ed864f8d83f98ae823494c9b5d63799726895e6e5b8d9495f6426cbf18passed (apply-check, targeted-test, two-run)
F7repair-f7.patch5e421bf67f42df36d5648d8bbce7498dd047685fd94c26af8e5d3a645dd9cf6cpassed (apply-check, compile)

All repair patches are HEAD-bound, applied in disposable scratch only; the reviewed worktree was not modified. A successful repair does not prove a finding; a failed repair does not refute it.

Changed-Code Coverage

pgsql-hackers Thread Ledger

Deterministic Verdict

Provenance

Maintainer CC