Skip to content

feat(plan): carry the create-shape cause on refused statements - #84

Merged
Kiran01bm merged 3 commits into
mainfrom
kiran01bm/ct14b-plan-statement-cause
Sep 8, 2026
Merged

feat(plan): carry the create-shape cause on refused statements#84
Kiran01bm merged 3 commits into
mainfrom
kiran01bm/ct14b-plan-statement-cause

Conversation

@Kiran01bm

@Kiran01bm Kiran01bm commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

Refused create statements in a plan now carry the executor's typed CreateShapeCause, so every renderer describes the same cause instead of re-deriving it.

Why

The executor already decides why a create shape is refused (IF NOT EXISTS, PARTITION OF, a duplicate relation name, …) and returns it as a typed cause. The plan dropped that type at the refusal boundary, so the text renderer, the greenfield refusal summary, and the desired-schema path each recomputed a cause from the error. Three recomputations of one fact drift; the plan is the single place the verdict should live.

What

  • plan.Statement.Cause (executor.CreateShapeCause, json:"cause,omitempty"), stamped by RefuseUnsupportedCreateShape from executor.CreateShapeCauseOf(err). First refusal wins; a refusal that carries no typed cause fails closed with ErrInvariantViolation.
  • cause is explanatory: excluded from the plan fingerprint, alongside reason. Present exactly when a diff-source plan with table_exists: false refuses a statement as unsupported-statement; absent for every other refusal, including an alter-source refusal against an absent table.
  • The three cause recomputations are deleted; renderers read Cause.Description().
  • Plan report format_version 2 → 3; docs/plan-report.md gains a cause row, a Causes vocabulary section, and a third pinned example (greenfield IF NOT EXISTS refusal alongside an admitted index), all checked by the docs tests. The SchemaBot integration notes point the adapter at statements[].cause instead of the executor's sentinels.
  • Demo tour gains a refused-diff step (IF NOT EXISTS fixture) asserting disposition, reason, and cause in check mode.

Trade-off: the duplicate-name refusal detail no longer appends the colliding name (the plan carries the typed cause only). A cause_detail field is the natural follow-up if the name proves necessary for operators.

Before / after

Before

  executor
    CreateShapeRefusals ──► []error   (typed cause inside each error)
                               │
                               ▼
  plan.Statement
    reason: unsupported-statement
    cause dropped at the refusal boundary
                               │
              ┌────────────────┼────────────────┐
              ▼                ▼                ▼
       diff_text.go         diff.go         desired.go
       re-derives cause     re-derives      re-derives
       from the error       cause           cause
After

  executor
    CreateShapeRefusals ──► []error   (typed cause inside each error)
                               │
                               ▼  CreateShapeCauseOf(err), stamped once
  plan.Statement
    reason: unsupported-statement
    cause:  if-not-exists
                               │
              ┌────────────────┼────────────────┐
              ▼                ▼                ▼
       diff_text.go         diff.go         desired.go
       Cause.Description()  — all three read the same field

@Kiran01bm
Kiran01bm marked this pull request as ready for review September 7, 2026 10:22
@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
Kiran01bm marked this pull request as draft September 7, 2026 10:48
Renderers previously re-derived why a create was refused from the
error, in three places that could drift from the executor's verdict.
The plan now records the typed cause once, where the refusal is
decided, and readers only describe it. Report format moves to v3.
The plan report and the Cause field comment stated cause as present iff a
statement is refused as unsupported-statement on a plan whose table does not
exist. That holds only for a diff-source report: an alter-source dry run
against an absent table can carry the same refusal with no create-shape
cause. Both now scope the rule to the diff source and name the alter-source
exception.

Pin the two halves of the contract that had no test: the fingerprint test
sets Reason and Cause on the explained statement, and the partitioned-parent
refusal test asserts the statement is causeless. The create-shape corpus test
asserts every refusal names a cause, and the refuse-create-shapes guard wraps
ErrInvariantViolation so a refusal the vocabulary cannot name fails closed the
way the rest of the core does.

Publish a third pinned example, a greenfield diff whose CREATE TABLE carries
IF NOT EXISTS, so the doc shows cause alongside the fields it withdraws. Note
in the Causes section that concurrently, multiple-operations, and
unsupported-kind are rejected by ParseDesired before a report exists, align
the cause rows with the execution model, and point the SchemaBot integration
notes and the verdict reason table at the typed cause so an adapter reads the
field instead of deriving the shape from sentinels or prose.

Reword the PARTITION OF and IF NOT EXISTS descriptions so they read as one
clause when spliced after "the create path refuses this statement:"; the
sentinels derive from Description() and follow.
@Kiran01bm
Kiran01bm force-pushed the kiran01bm/ct14b-plan-statement-cause branch from 4df1647 to f456fff Compare September 8, 2026 01:27
@Kiran01bm
Kiran01bm marked this pull request as ready for review September 8, 2026 01:27
@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.

The plan report's Causes table copies the execution model's vocabulary rows,
and three of them had already been paraphrased apart before the docs test
noticed only that each value had a row. TestDocCauseRowsMatchExecutionModel
compares the Meaning cell of every create-shape cause across the two pages,
modulo the sentence-ending period the plan report's table style adds, so an
integrator reading either page learns the same meaning for each value.
@Kiran01bm
Kiran01bm merged commit 28d8808 into main Sep 8, 2026
14 checks passed
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.

1 participant