Repository navigation
planner: name an unnamed CHECK or FOREIGN KEY so its online sequence can be built - #219
Conversation
…can be built The NOT VALID -> VALIDATE sequence names the constraint in its VALIDATE step, so an ADD CHECK or ADD FOREIGN KEY written without a name was refused as rewrite-required and handed back to the author as guidance. AddNotValid now gives such a constraint the name PostgreSQL would choose first (<table>_<column>_check, <table>_check, <table>_<columns>_fkey, truncated the way makeObjectName truncates) and the rewrite states it. A table that already has a constraint of that name fails the NOT VALID step before anything is added. A server-oracle test checks the name against the one PostgreSQL chooses for single, repeated, multi- and zero-column checks, qualified and quoted references, truncation inside a multibyte character, and foreign keys on one, two, and a truncated list of columns. The refuse demo switches to an inline UNIQUE on ADD COLUMN, which still refuses with typed help. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
A quoted "*" column receives an incorrect constraint name, potentially causing an avoidable collision.
1 open issue
What changed in this PR
Enables pg-sprite to construct online CHECK and FOREIGN KEY sequences without requiring users to name constraints.
Changes:
- Generates default constraint names for the NOT VALID → VALIDATE sequence.
- Adds naming and execution tests.
- Updates capability documentation, guidance, and the refusal demo.
| File | Description |
|---|---|
| pkg/suggest/suggest.go | Updates guidance comments for unnamed constraints. |
| pkg/suggest/suggest_test.go | Expects runnable unnamed-constraint rewrites. |
| pkg/statement/rewrite.go | Generates names for unnamed constraints. |
| pkg/statement/ops_test.go | Removes the unnamed-constraint refusal case. |
| pkg/statement/constraint_name_integration_test.go | Compares generated names with PostgreSQL. |
| pkg/planner/planner_test.go | Expects two-step unnamed-constraint plans. |
| pkg/plan/plan_test.go | Removes obsolete naming-guidance expectations. |
| pkg/migrate/run_integration_test.go | Tests unnamed CHECK execution and validation. |
| pkg/capabilities/capabilities.yaml | Records naming and collision behavior. |
| docs/suggest-report.md | Updates constraint guidance documentation. |
| docs/demos/refuse.tape | Uses inline UNIQUE to demonstrate refusal. |
| docs/capabilities.md | Documents unnamed-constraint support. |
Review effort: Balanced
…rence
The whole-row marker among a CHECK's column references is now the empty
string, which no column can be named, so CHECK ("*" > 0) on a column
named "*" takes the server's t_*_check rather than t_check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
🤖 The one open issue in the Copilot review summary (quoted Addressed by Claude Code |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The compose role shares its name with the engine schema, so once a copy-swap run has created that schema an unqualified seed lands there and the visible commands no longer find the table. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…onstraints # Conflicts: # docs/capabilities.md
|
🤖 Review findings - created by Kiran's code review agent - for pg-sprite/pull/219, 6cd72fe. Verdict: 3 findings — 0 blocking, 1 non-blocking (an unnamed constraint can collide with an existing name), 2 suggestions (the predicted name differs from the server's in two cases). Non-blocking
General suggestions
The one thing that could have broken, verifiedThe riskiest mechanism is the predicted constraint name drifting from the one in the VALIDATE step, which would leave a NOT VALID constraint that never gets validated. That cannot happen. The rewrite sets the name explicitly before both statements are built. The planner quotes it in VALIDATE with Verified correct
This review was generated by Claude Code (claude-opus-5-5). |
…y holds The NOT VALID rewrite gave an unnamed CHECK or FOREIGN KEY the server's first choice even when the table already had a constraint by that name, so the rewrite failed where the original statement would have run. A dry-run and Run now read the constraint names in the table's schema and number past a taken one the way ChooseConstraintName does. A bare reference to the table's own name now reads as its whole row when the table has no such column, as the server resolves it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n scope Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
🤖 Thanks for the review. All three findings are addressed in 17061df and add994f:
On the PG16 failure: Addressed by Claude Code |
A field selected from the whole row, (t).qty, reads that column, and ROW(t.*) expands to every column, so both now name the constraint by the column as the server does. A dry-run reads an unqualified table where the session search_path resolves it, as Run does, so both read the same constraint names. Lint and suggest classify a script in order and number an unnamed constraint past the names its earlier statements claim. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>


Why
The online form of
ADD CHECKandADD FOREIGN KEYisADD CONSTRAINT ... NOT VALIDfollowed byVALIDATE CONSTRAINT <name>, and the second step has to name the constraint. When the statement named none, pg-sprite refused it asrewrite-requiredand told the author to name it themselves. That turned the most common way to write a check constraint into a manual rewrite, even though PostgreSQL's choice of name is deterministic.What
statement.AddNotValidnow gives an unnamed CHECK or FOREIGN KEY the name PostgreSQL would choose, and the rewrite states it:<table>_<columns>_fkeyfor a foreign key.<table>_<column>_checkfor a check that references exactly one distinct column.<table>_checkfor any other check.Names are truncated the way the server's
makeObjectNametruncates, reusing the helperImplicitRelationNamesalready mirrors. The server moves past its first choice when a constraint anywhere in the schema already holds that name, numbering the label (check1,fkey1, ...). A dry-run and a run read the schema's constraint names and the table's columns, and number the name the same way. A bare reference to the table's own name reads as its whole row unless the table has a column by that name. A dry-run reads an unqualified table where the sessionsearch_pathresolves it, as a run does. Suggest and lint read no live table, so they state the first choice, numbered past the names the script's own earlier statements claim. A field selected from the whole row,(t).qty, counts as that column, andROW(t.*)as every column, as the server's analysis resolves them.Example:
ALTER TABLE orders ADD CHECK (qty > 0)on a liveorderstable.A server-oracle test adds each unnamed constraint with plain DDL and asserts that
AddNotValidpicks the name PostgreSQL chose. The cases are:name-constraint-then-validatestays in the guidance vocabulary for a CHECK or FOREIGN KEY whose rewrite cannot be built, so the vocabulary andformat_versionare unchanged. Therefusedemo tape now uses an inlineUNIQUEonADD COLUMN, which still refuses with typed help.Before:
migrate --alter 'ALTER TABLE orders ADD CHECK (qty > 0)' --dry-runAfter:
migrate --alter 'ALTER TABLE orders ADD CHECK (qty > 0)' --dry-run🤖 Generated with Claude Code