Skip to content

migrate: greenfield desired plans take the executor create path - #64

Closed
Kiran01bm wants to merge 4 commits into
kiran01bm/ct2-executor-create-pathfrom
kiran01bm/ct3-front-door-admission
Closed

migrate: greenfield desired plans take the executor create path#64
Kiran01bm wants to merge 4 commits into
kiran01bm/ct2-executor-create-pathfrom
kiran01bm/ct3-front-door-admission

Conversation

@Kiran01bm

Copy link
Copy Markdown
Collaborator

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 with unsupported-statement, which blocks the most common first interaction anyone has with a desired-state tool: declaring a table on a fresh database.

What

  • migrate.RunDesired on a greenfield plan verifies the name is free and the role holds CREATE on the schema, then hands the desired schema to ExecuteCreate (consuming both proofs); a rerun converges to an empty plan.
  • An occupied name is the new typed refusal reason create-collision (added to verdict.Reasons()); a privilege gap refuses with insufficient-privileges; PARTITION OF / IF NOT EXISTS shapes keep refusing with unsupported-statement before anything runs.
  • Greenfield plans order CREATE TABLE first (indexes keep input order after it), so plan order states execution order and per-statement verdicts map positionally.
  • Capability matrix, limitations, CLI output examples, and CHANGELOG updated; the greenfield row is now T1.

Before / after

Before
  desired file (table absent)
        │
        ▼
  RunDesired ──▶ refused: unsupported-statement       (create path unreachable)

After
  desired file (table absent)
        │
        ▼
  RunDesired ──▶ CheckTableAbsent ──▶ CheckCreatePrivileges ──▶ ExecuteCreate
                     │ occupied              │ no CREATE            │ per-step
                     ▼                       ▼                      ▼
               refused:                refused:               executed /
               create-collision        insufficient-          typed step
                                       privileges             refusals

Kiran01bm and others added 4 commits August 28, 2026 20:26
#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.
@aparajon

Copy link
Copy Markdown
Collaborator

🤖 Adversarial correctness review, requested by @aparajon and performed by their agent. Reviewed at head d2504e81.

Verdict: there is nothing left to review — the tree at this PR's head is byte-identical to main. This work has already landed; the PR should be closed rather than rebased.

Findings

1. The head tree and main are the same object. At head d2504e81 the tree hash is 4c38696c77c813fde856b0bed2fe37b6aac2434c; at main (c1d33b6) it is 4c38696c77c813fde856b0bed2fe37b6aac2434c. git diff main HEAD is empty — zero files, zero lines. Every change on this branch, including the two commits that look like new work (ac8129b fix(statement): order desired statements for execution at parse and d2504e8 address second-round review: pin search_path on alter path, doc fixes), is already on main via the squash merge of #63.

2. The 43-file, +2247/-94 diff GitHub shows is an artifact of the base, not real content. This PR targets kiran01bm/ct2-executor-create-path — the branch that merged as #62 — so GitHub diffs against a base that is two merges behind, and the first commit here (ab8ca3a) is the pre-squash copy of what merged as #63. That is also why it reports CONFLICTING: the same content arrives twice by two different routes. Retargeting to main would resolve the conflict by producing an empty PR, which is the honest signal.

3. Worth confirming nothing was dropped in the squash. Since ac8129b and d2504e8 were review-response commits on the #63 branch, they were folded into the #63 squash — which the identical trees confirm. No action needed beyond the close; I checked this rather than assumed it, because a review-round fix silently lost in a squash is the failure mode worth ruling out.

Action items

  1. Close this PR. Its content is on main as of c1d33b6.
  2. If any of it is meant to be further work rather than a duplicate, branch fresh from main — the current branch cannot express a delta.

Verified (tried to break, couldn't)

git fetch origin main first, so the comparison is against current main (0faa11f..c1d33b6), not a stale ref; tree equality is exact object identity, not a whitespace-insensitive diff; pkg/executor/create.go exists on main, confirming the create path landed rather than being an artifact of the merge-base; the three-dot diff against the merge-base (6f3b20e, #62's merge) shows the 24-file view that made this look like live work, and the two-dot tree comparison shows it is not; the worktree was clean throughout.

This review was generated by Claude Code (claude-opus-5).

@Kiran01bm
Kiran01bm marked this pull request as ready for review August 30, 2026 06:14
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@Kiran01bm

Copy link
Copy Markdown
Collaborator Author

Superseded by #63 — same head branch, merged to main as c1d33b6. This was the stale stacked-PR entry against the ct2 base.

@Kiran01bm Kiran01bm closed this Aug 30, 2026
@Kiran01bm
Kiran01bm deleted the kiran01bm/ct3-front-door-admission branch August 30, 2026 06:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants