CommitFest 6564 Review: Reject ADD CONSTRAINT NOT NULL if name mismatches existing domain not-null constraint
code-review · generated from canonical Markdown
code-review
postgresql-master
commitfest-6564-review2026-08-30T08:09:26.701728+00:00
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
- Verdict:
needs-review - Reason: One verified MAJOR correctness finding (F1: internal
elog(ERROR)for a user-reachable concurrent DDL condition). No BLOCKER findings. - Blocker split: verified=0, unverified=0
- Security: No security BLOCKER or MAJOR. Owner-only DDL semantics tightening; no privilege escalation, SQL injection, or authz bypass.
Verification Summary
| ID | Severity | Class | Status | Method |
|---|---|---|---|---|
| F1 | MAJOR | correctness | verified | static-analysis-plus-toctou-reachability |
| F2 | MINOR | maintainability | verified | static-code-inspection |
| F3 | MINOR | tests | verified | static-parser-analysis |
| F4 | NIT | maintainability | verified | static-style-inspection |
| F5 | NIT | docs | verified | static-docs-inspection |
| F6 | MINOR | maintainability | verified | static-memory-context-analysis |
| F7 | MINOR | correctness | cannot-verify | static-toctou-analysis |
- F1 (MAJOR, correctness): Verified.
findDomainNotNullConstraintreturns only validated NN constraints; theelog(ERROR)at the NULL branch is reachable via TOCTOU (staleSearchSysCacheCopy1snapshot + concurrentALTER DOMAIN DROP NOT NULL), producing an internal-style error without SQLSTATE for a user-reachable condition. - F2 (MINOR, maintainability): Verified. Missing cross-reference comment to
domainAddNotNullConstraint's duplicate-name guard. - F3 (MINOR, tests): Verified. The
NOT VALIDtest case errors in the parser, not inAlterDomainAddConstraint. - F4 (NIT, style): Verified. Declaration placement and comment wording deviate from pgindent conventions.
- F5 (NIT, docs): Verified. No
alter_domain.sgmlupdate for the new user-visible error condition. - F6 (MINOR, maintainability): Verified (benign). Transient
heap_copytuplenot freed; cleaned by transaction-abort context reset. Already raised on-thread by Evan Li. - F7 (MINOR, correctness/TOCTOU): Cannot-verify. Real but non-exploitable TOCTOU race; no fix required.
Repair Validation Summary
| Finding(s) | Patch | sha256 | Validation |
|---|---|---|---|
| F1, F2, F4, F6 | typecmds_repair.diff | 8556922760bfd4141aee2edbd1f1f81a46f6b6736b416a2d656acebec84f46d3 | passed (apply-check + diff-check + compile + targeted-test) |
| F3 | f3_domain_sql.diff | d44d62184db92d5f725d3e773fb5e7fef3023a6b84bb9bef5588731c94677de4 | passed (apply-check + diff-check + targeted-test) |
| F5 | f5_docs.diff | b4c3c20d93e01dcbb95ff78152edf2242935ebd5ccde50713c0eea98a9b92c13 | passed (apply-check + diff-check + doc-build) |
| F7 | waived (design-choice) | n/a | waived |
All repairs validated in disposable scratch build. The read-only worktree was never modified. Patches are HEAD-bound to 2fb8da5a245661287833b05a1b2e275ddf83bbd7.
repair detail (combined with F2, F4, F6)
- Demotes
elog(ERROR)at the NULL-conTup branch toereport(ERROR)withERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATEanderrdetail. - Groups declarations (F4).
- Adds cross-reference comment to
domainAddNotNullConstraint'sERRCODE_DUPLICATE_OBJECTguard (F2). - Adds
heap_freetuple(conTup)before mismatchereportand earlyreturn(F6).
Compiled clean with gcc -Wdeclaration-after-statement -Werror=vla -Wold-style-declaration. pg_regress domain passes.
Findings
— 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
— 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
— 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
— 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
— 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
— 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
— 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
make -C src/test/regress check-tests TESTS=domain
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
| Metric | Value |
|---|---|
| Changed executable lines | 7 |
| Covered changed lines | 6 |
| Compiler uncovered changed lines | 1 |
| Uncovered changed lines | 1 |
| Structural uncovered changed lines | 0 |
| Changed lines without coverage coordinates | 0 |
| Changed branches | 10 |
| Covered changed branches | 8 |
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
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
- Waiver kind: not-instrumentable
- Why: The guard protects against an invariant violation (typnotnull true but no validated NN pg_constraint row) that is unreachable through any SQL command. A deterministic test would require directly corrupting pg_constraint/pg_type, which is not permitted in pg_regress.
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
| Path | Function | Lines | Entered | Whole-function lines | Whole-function branches |
|---|---|---|---|---|---|
src/backend/commands/typecmds.c | AlterDomainAddConstraint | 2984-3099 | True | 50/50 | 22/28 |
Non-instrumented changes
src/test/regress/expected/domain.out: Test expected-output file; not instrumentable implementation code.src/test/regress/sql/domain.sql: Test SQL file; not instrumentable implementation code.
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
- 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.
- 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.
- 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.
- 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
| Finding | Disposition | Rationale |
|---|---|---|
| F1 | new | Not raised. findDomainNotNullConstraint filters on convalidated, so for an existing NOT VALID NN constraint with typnotnull set, the lookup returns NU |
| F2 | new | Not raised. New block hand-duplicates the name-mismatch logic without a cross-ref comment to the sibling domainAddNotNullConstraint path. Not discusse |
| F3 | new | Not 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. |
| F4 | new | Not raised. Declaration ordering deviates from pgindent conventions. Not discussed. |
| F5 | new | Not raised. New user-visible ERROR condition added with no alter_domain.sgml docs update. Not discussed. |
| F6 | already-raised | Evan Li review explicitly raised conTup-from-findDomainNotNullConstraint() not freed and self-resolved it as benign via memory-context reset. Covers t |
| F7 | new | Not raised. TOCTOU between typTup->typnotnull read and the subsequent findDomainNotNullConstraint lookup; non-exploitable given RowExclusiveLock on pg |
Aggregated Test Plan
Existing tests (patched)
src/test/regress/sql/domain.sqllines 513-520: name-mismatch error, same-name idempotent, unnamed idempotent, SET NOT NULL idempotent, NOT VALID parser rejection, count invariance.
Proposal-only tests (not applied)
src/test/regress/sql/domain_notnull_redundant.sql+expected/domain_notnull_redundant.out: self-contained block on dedicated domainnnredundant.
Two-run validation requirement
- Pre-patch: mismatched name add must FAIL (old code silently accepts). Post-patch: entire block must PASS.
Verified clean / non-findings
- RowExclusiveLock leak: NOT a finding. Error path releases via transaction abort; success path calls
table_close. - conTup syscache leak: NOT a finding.
heap_copytuple(palloc'd copy); cleaned by context reset. Consistent with existing caller atAlterDomainNotNull. - Error message/errcode: Consistent with table-column analog (commit
96e2af60504). - Backward compatibility: ERROR behavior consistent with shipped table-column behavior. On-list objection noted but ERROR is correct.
- CREATE DOMAIN: Not affected.
DefineDomaincallsdomainAddNotNullConstraintdirectly. - catversion/pg_dump: No catalog format change. No catversion bump needed.
Provenance / model routes
| Seat | Agent | Model | Handoff slug |
|---|---|---|---|
| patch-reviewer | pg-patch-reviewer | glm-5.2:cloud | commitfest-6564-review-handoff-patch-reviewer |
| security-reviewer | pg-security-reviewer | glm-5.2:cloud | commitfest-6564-review-handoff-security-reviewer |
| thread-reconciler | cross-reference-review-with-thread | glm-5.2:cloud | commitfest-6564-review-handoff-thread-ledger |
| test-author | pg-test-author | glm-5.2:cloud | commitfest-6564-review-handoff-test-author |
| finding-verifier | pg-finding-verifier | glm-5.2:cloud | commitfest-6564-review-handoff-verifier |
| coverage-reviewer | pg-change-coverage-reviewer | glm-5.2:cloud | commitfest-6564-review-handoff-coverage |
Residual risk
- F1 (MAJOR): The
elog(ERROR)at NULL-lookup branch is reachable via concurrent DDL (TOCTOU). Combined repair demotes to properereportwith SQLSTATE. Until applied, concurrentALTER DOMAIN DROP NOT NULLcan trigger internal-style error. - F7 (cannot-verify): TOCTOU race remains as spurious-error-only under concurrent owner-level DDL. Non-exploitable.
- Thread: Zizhuan Liu's backward-compat counter-proposal (NOTICE-and-skip) unresolved; no committer ruling. ERROR choice is consistent with shipped table-column behavior.
Validator
Artifact validated with validate_review_artifact.py (SHA-256 edbcb304ffbd062627196c1ee0ce3d9b60cae80dd7b201e420335fea0a31979e): ok: pg-review-artifact-v2 is structurally complete (exit 0).