migrate: greenfield desired plans take the executor create path - #64
migrate: greenfield desired plans take the executor create path#64Kiran01bm wants to merge 4 commits into
Conversation
#62) Adds the create-path executor: `ExecuteCreate` runs a validated desired schema (one CREATE TABLE plus its indexes) against a name proven absent, with an off-ladder privilege proof for greenfield creation. ## Why The declarative front door can diff a desired table into existence, but nothing below it could execute that creation under the engine's proof discipline: the sequence executor consumes a `PreflightedTable`, which by definition cannot exist for a table that does not. The create path needs its own proof pair — the target name is free (`AbsentTarget`, already landed) and the role may create in the schema — and an executor that re-verifies both at the point of use. This lands that executor, dormant until the front door routes to it. ## What - `executor.ExecuteCreate` / `ExecuteCreateWithProgress`: qualifies every desired statement into the proof's schema, re-parses and admits by shape and target (ST-7), orders the CREATE TABLE first, and runs each step as a brief bounded transaction under the existing lock-retry machinery. Failure returns the committed-prefix `SequenceReport` contract; the duplicate-name SQLSTATEs (42P07 for a relation, 42710 for a standalone type holding the name) map to the typed `ErrCreateCollision` so the caller re-diffs instead of assuming. - Indexes build plainly, never CONCURRENTLY: the table is born this run with no traffic to protect, a plain build on an empty table is fast, and it cannot leave an INVALID index behind a failure. - Refusals, all at admission before anything executes: `IF NOT EXISTS` (table or index — a name-only no-op proves nothing); `CREATE TABLE PARTITION OF`, `INHERITS`, `LIKE`, and `OF type` (each binds a secondary relation or type the qualification never touches, so the name resolves via search_path to an existing object the absence proof does not cover); concurrent index builds; and a name claimed twice within the desired set (`ErrDuplicateCreateName` — decidable at admission, never a mid-run failure with a committed prefix). - `preflight.CheckCreatePrivileges` → `CreationRole` proof: one catalog snapshot proving CONNECT + schema USAGE + CREATE, with each missing grant a typed `*PrivilegeError` whose grantee is the engine role itself. Off the ownership tier ladder deliberately — a greenfield table has no owner to be a member of; it is born owned by its creator (`TierCreateTable`). - `statement.Op` now carries `IfNotExists`, `Inherits`, `Like`, and `OfType` for CREATE TABLE; four new outcome codes (`create-collision`, `duplicate-create-name`, `partition-of-unsupported`, `unsupported-create-step`); docs updated (ST-7 enforcement list, SAFETY.md / tcb-model.md / review-checks proof types, engine-role.md off-ladder section, capabilities/limitations/README create-path boundaries). ## Before / after ``` Before: no execution path for a desired table that does not exist yet ParseDesired ──▶ DesiredSchema ──▶ (no executor consumes it) CheckTableAbsent ──▶ AbsentTarget ──▶ (no executor consumes it) After: the create path, proof-gated end to end CheckCreatePrivileges ──▶ CreationRole (may I create here?) CheckTableAbsent ─────────▶ AbsentTarget (is the name free?) │ ParseDesired ──▶ DesiredSchema ──┤ ▼ ExecuteCreate qualify + re-parse + admit (ST-7) refuse: PARTITION OF / INHERITS / LIKE / OF / IF NOT EXISTS / CONCURRENTLY / duplicate names │ ┌──────────────┼──────────────┐ ▼ ▼ ▼ CREATE TABLE CREATE INDEX CREATE INDEX ... (always 1st) (input order, plain builds, brief budgets) 42P07 / 42710 ──▶ ErrCreateCollision ──▶ caller re-diffs the live catalog failed step ──▶ committed prefix remains ──▶ rerun refuses ErrRelationExists ──▶ re-diff ```
Desired-state execution previously refused a plan whose table does not exist. The greenfield path now verifies absence and schema CREATE privilege, then runs the create and index builds as brief bounded steps; an occupied name is the new typed create-collision refusal. Greenfield plans order CREATE TABLE first so plan order states execution order. Amp-Thread-ID: https://ampcode.com/threads/T-01a03b04-5f75-7059-b544-bb826e67db29 Co-authored-by: Amp <amp@ampcode.com>
An index-before-table desired file created the table on run 1 and then hard-errored on every rerun: the scratch-schema replay executed input order while the plan and the create path hoisted the CREATE TABLE. Ordering once in ParseDesired makes every replay site execute table-first by construction; the two per-site hoists are retired. Also sweeps the capability docs the create path made stale.
Alter attempts now run with search_path pinned to the target schema (same contract ExecuteCreate already had), with a regression test. Doc call order reconciled with runCreate (absence before privileges), if-not-exists-unsupported added to the refusal routing table, greenfield routing-class table and README Go-API pointer added, success Detail derived from the executed step count, and the parse-time statement ordering guarantee promoted to invariant ST-8.
|
🤖 Adversarial correctness review, requested by @aparajon and performed by their agent. Reviewed at head Verdict: there is nothing left to review — the tree at this PR's head is byte-identical to Findings1. The head tree and 2. The 43-file, +2247/-94 diff GitHub shows is an artifact of the base, not real content. This PR targets 3. Worth confirming nothing was dropped in the squash. Since Action items
Verified (tried to break, couldn't)
This review was generated by Claude Code (claude-opus-5). |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Desired-state execution now creates a table that does not exist yet, instead of refusing the greenfield plan.
Why
The absence preflight (
CheckTableAbsent), the creation-privilege preflight (CheckCreatePrivileges), and the executor create path (ExecuteCreate) all exist, but the declarative front door never routed to them — a desired file for a brand-new table was refused withunsupported-statement, which blocks the most common first interaction anyone has with a desired-state tool: declaring a table on a fresh database.What
migrate.RunDesiredon a greenfield plan verifies the name is free and the role holdsCREATEon the schema, then hands the desired schema toExecuteCreate(consuming both proofs); a rerun converges to an empty plan.create-collision(added toverdict.Reasons()); a privilege gap refuses withinsufficient-privileges;PARTITION OF/IF NOT EXISTSshapes keep refusing withunsupported-statementbefore anything runs.CREATE TABLEfirst (indexes keep input order after it), so plan order states execution order and per-statement verdicts map positionally.Before / after