preflight: LookupOwnedRelationNames reads a table's owned index and sequence names - #83
Conversation
…uence names For implicit constraint-index and sequence names PostgreSQL never raises a duplicate-name SQLSTATE; it suffixes. A caller that wants to prove the create step got the names it claimed needs the names the server actually assigned, read from the catalog after the step. This adds that read; the comparison in the create path stacks on it.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
…tables as ErrNotTable Addresses the agent review: joining pg_constraint on conindid also matched foreign keys, whose conindid is the referenced table's index, so another table's index names (and a self-referencing key's duplicate of the PK) leaked into ConstraintIndexes. Only p/u/x constraints build an index on the table itself. A view or foreign table at the name was reported as ErrTableNotFound, conflating two causes with the sibling lookups.
|
🤖 Adversarial correctness review (1/2 — correctness) — The set this reads has to match the set
Mutations — 13 run, 10 killed, 3 survivors. The three survivors are all on the sequence arm and they are all defense in depth rather than dead weight — details in Verified below, including why the
1 — the two
|
|
🤖 Adversarial correctness review (2/2 — invariants, docs, and the stack) — InvariantsExtends ST-7's discipline to the names an absence proof structurally cannot cover. ST-7 is about identity: the executor "refuses, before anything executes, any statement whose target table does not match the preflight proof it was handed," and OC-1 is the entry finding 1 in my first comment lands on. It forbids converting "engine-state uncertainty" into "a passing/ready/succeeded status", which is precisely what a silently-empty Nothing to add to RF, correctly. Every RF entry is a refusal, and this PR refuses nothing — it's a reader whose only errors are "the table isn't there" and "that isn't a table." The refusal that will eventually cite the RF preamble is the one the create path raises when the two sets disagree, and it isn't here yet. No new invariant — but this registry has a convention that would accommodate one, so the choice is worth stating. The rule worth pinning is "a create step's server-chosen names are proven against the names the plan claimed", and nothing enforces it in shipped code yet, by this PR's own admission. Deferring the entry to the PR that wires the comparison is defensible. But it isn't the only option here: DocsThe
and closes with "The create path does not yet run that comparison itself." That's the sentence that keeps the section honest — a reader who stops at "so a caller can compare them" would otherwise walk away thinking the window is closed. Keep it in any later edit; it's the load-bearing clause. The FK carve-out is also called out at the right level of detail for that document ("foreign keys do not contribute the indexes they borrow from the referenced table") — it's the one behaviour of this function an integrator could plausibly guess wrong. The stackThe claim side can carry duplicates and the readback side can't, so the comparison has to be set-based. This is the one thing I'd want settled before the next PR, because it's a difference the obvious implementation gets wrong. Verified both sides: the claim really does return the name twice, and the server really does collapse the two identical The reason this is worth flagging rather than obvious is that ST-8 deliberately establishes a positional contract for the create path — statements ordered at construction so "the position mapping between a greenfield plan's statements and the create path's step verdicts holds by construction." The surrounding code is positional by design, so reaching for the same shape here is the natural move, and it's the wrong one for these lists. Deduping the claim side before comparing (or comparing sets and reporting the difference both ways) is the fix, and it's cheap to build in from the start. One thing the comparison will need that this PR can't give it. The lookup is a point-in-time read, so a comparison built on it proves the names are right at readback, not that they stayed right. That's fine for the collision case — the names it's checking were fixed the moment the create committed — but it means the readback has to happen inside whatever ownership the create step already holds rather than as an independent follow-up query, or the next PR reintroduces a window of its own. LK-1 is the intended holder, though its own enforcement is still marked planned, so "reuse the create step's ownership" is the constraint to write down now rather than a lock to lean on yet. This review was generated by Claude Code (claude-opus-5). |
aparajon
left a comment
There was a problem hiding this comment.
🤖 Approving at 6f23d254. The symmetry with statement.ImplicitRelationNames holds, the suffix test pins the property this exists for by cross-checking the two sides directly, and 10 of 13 mutations died — the three survivors are defense-in-depth on the sequence arm, not gaps.
Two low findings in my first comment (the 'pg_class'::regclass casts resolving through search_path; the loose case not exercising the deptype pair its comment credits) and the claim/readback duplicate asymmetry in my second — none of them block.
This stamp was left by Claude Code (claude-opus-5).
|
🤖 Review response — created by Kiran's code review agent (Amp, Claude Opus 4.5) — pull/83, follow-up commit Seven of eight findings fixed in the follow-up commit; one (the sequence-arm predicates' negative cases) is answered rather than changed because no DDL can reach the mutant. Both surviving mutants that are reachable — FK leak and the empty-schema guard — were reintroduced and now fail.
Verification on the follow-up: Agent review at head |
preflight.LookupOwnedRelationNamesreads the constraint-index and column-owned-sequence names a table actually owns, so a caller can compare them against the names a create claimed.Why
The create path proves a table's implicit constraint-index and sequence names absent before the
CREATE TABLEstep, but PostgreSQL never raises42P07/42710for an implicit name taken inside the time-of-check window — it silently suffixes (t_pkey1,t_id_seq1) and the create succeeds with names the plan never claimed. Closing that window needs a read of what the table ended up owning, after the step, from the catalog. No such read exists today;CheckNamesAbsentonly asks whether a name is free.What
OwnedRelationNames{ConstraintIndexes, Sequences}andLookupOwnedRelationNames(ctx, pool, schema, table)inpkg/preflight/owned.go: one query overpg_constraint(contypep/u/x— the constraints that build an index on the table itself; a foreign key'sconindidis the referenced table's index and is excluded) for constraint indexes andpg_depend(deptypea/i, relkindS) for column-owned sequences, both sorted and duplicate-free. A name is present whether the server invented it or the desired file stated it — a named constraint's index and anALTER SEQUENCE … OWNED BYsequence are owned just the same. Schema is required — the caller must inspect the exact schema its absence proof covered, not asearch_pathresolution; a missing relation returnsErrTableNotFoundand a non-table at the name returnsErrNotTable, as the sibling lookups do. Standalone indexes are excluded because their ownCREATE INDEXsteps report duplicate-name SQLSTATEs, and the table itself because its name is already covered by the absence proof. Integration tests on PostgreSQL 16 cover primary-key, unique, exclusion and named-constraint indexes with serial and identity sequences, foreign keys to another table and to itself, an adopted versus a merely-defaulted-from sequence, the server-suffixed case (t_pkey1,t_id_seq1), a table with no owned relations, a missing table, a view at the name, a same-named table in another schema, a partitioned parent, and the empty-schema error.docs/schemabot-integration.mdstates what the function is for. This PR adds the read only; the create path's comparison after step 1 stacks on it.Before / after