preflight: refuse parent-level index builds on partitioned tables - #33
Conversation
Fail closed before execution until the partition-aware online index flow is supported.
Keep execution, dry-run, and declarative plans aligned while refusing unsupported parent operations before any schema change starts, with the refusal cause carried as a typed value so each surface renders an accurate reason.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
🤖 Adversarial correctness review, requested by Armand and performed by his agent. Reviewed at head Verdict: the refusal is correctly built and the version boundary is exactly right — one shape gets through it. I went at this from the direction most likely to be wrong: the set of things PostgreSQL refuses on a partitioned parent is larger than "index builds", and the The matrix I built the review on — every candidate shape, three majors, liveThree of these directly confirm decisions in the PR. Findings1. The consequence is the one this PR exists to prevent, on the surface an orchestrator gates on: Plan time says execute. Apply time does not: Compare the shape one row up in the matrix, which the PR does catch: The substituted path is already safe by luck of ordering: the planner's rewrite for Fix is small and fits the existing shape: Repro test (
|
|
🤖 Second pass on the same head ( Lens 1 — OSS adoption easeThe CHANGELOG contradicts itself two entries apart. The new entry says partitioned-parent index builds "now refuse"; the entry immediately below it says "A plain (blocking) A reader going top-down learns a promise and then sees it withdrawn without being told the withdrawal is scoped to parents. One clause on the new entry ("leaf partitions keep the substitution behaviour below") fixes it. The refusal detail leads with a sentence that is false for one of the two things it refuses. The limitation is documented on the privileges page. The deferred flow has no follow-through for adopters. No escape hatch, and that should be stated as policy. I verified Credit where it's due: the invariant is stated as a capability fact with an enforcement site ( Lens 2 — SchemaBot integrationThis closes the plan-time gap I raised on #31. The check runs in
Report-level
Refusal precedence changed, silently. Cost, for a planner that is remote from the database. Mapping note for the adapter: This review was generated by Claude Code (claude-opus-5). |
aparajon
left a comment
There was a problem hiding this comment.
🤖 Approving on Armand's behalf. The two comments above are the adversarial correctness pass and the two-lens pass; the findings there are follow-ups, not fix-before-merge blockers — the USING INDEX shape fails atomically and left the target untouched in my repro, so this PR narrows the hole rather than opening one. Wiring the check into diffplan.Plan and the dry-run path (not just apply) is what makes it a blocked check instead of a failed apply, and the serverMajor < 18 boundary is right to the version — I verified it live on 16, 17 and 18.
Reviewed by Claude Code (claude-opus-5).
Refuse parent index adoption (ADD CONSTRAINT ... USING INDEX) with a typed cause, split index-build refusals into concurrent vs blocking causes with accurate messages, carry a per-statement refusal reason and withdraw impossible safer-SQL advice from refused plan statements, map a vanished sequence target to the table-not-found code, and share one catalog lookup (LookupTargetFacts) across dry-run, diff, and executor admission. Documents the refusal boundaries in docs/limitations.md and marks target-dependent refusal reasons as an open, fail-closed vocabulary in the plan contract.
|
Review response from Kiran's (@Kiran01bm) AI code review assessment agent (Amp, Claude Opus 4.6) All three correctness findings and all lens findings are addressed in the follow-up commit; one item (a public tracking issue for the partition-aware index flow) is being filed separately. Correctness findings
Lens findings
|
Summary
Partitioned parent tables (
pg_class.relkind = 'p') are now detected at preflight, and any routed plan that builds an index on a parent is refused with a typed verdict before anything executes. Leaf partitions and supported in-place parent ALTERs are unaffected.What
relkind; newpreflight.CheckPartitionSupportrefuses index-building steps (explicitCREATE INDEX [CONCURRENTLY]or ALTER shapes whose online substitution contains CIC) on partitioned parents via typed*UnsupportedPartitionedParentError.unsupported-partitioned-parentwith the same JSON/exit-code contract as existing refusals.ADD CONSTRAINT UNIQUErefuse (and nothing is created); the same changes on a leaf partition execute; parentADD COLUMNexecutes.docs/engine-role.mdnotes the limitation.Why
PostgreSQL cannot run
CREATE INDEX CONCURRENTLYon a partitioned parent (SQLSTATE0A000, verified live), so today those plans die mid-change at runtime. Failing closed at plan time keeps the honesty contract: the engine refuses what it cannot do safely, and orchestrators can surface the refusal as a blocked check. The partition-aware sequence (CREATE INDEX ON ONLY→ per-partition CIC →ATTACH PARTITION) is deferred for the time being, not simulated.