Skip to content

cli: substitute safer sequences by default, gated --force, stable executor codes - #30

Merged
Kiran01bm merged 4 commits into
kiran01bm/p3-2-native-idiomsfrom
kiran01bm/p3-4-substitution
Aug 13, 2026
Merged

cli: substitute safer sequences by default, gated --force, stable executor codes#30
Kiran01bm merged 4 commits into
kiran01bm/p3-2-native-idiomsfrom
kiran01bm/p3-4-substitution

Conversation

@Kiran01bm

Copy link
Copy Markdown
Collaborator

Summary

migrate now executes the planner's classified route instead of a blind optimistic attempt: when the submitted form blocks and a safer native sequence exists, the engine substitutes and runs the safer form by default; the rare run-the-literal case goes through a gated, audited --force. Every typed executor outcome now carries a stable string code, the same treatment pkg/lint gave its findings.

What

  • Substitution by default. migrate classifies and routes the statement exactly as dry-run would, then executes the routed SQL through the sequence executor. A blocking CREATE INDEX runs as its concurrent build; a CHECK/FK constraint runs as NOT VALID + VALIDATE; the verdict's new executed_sql field reports the substituted steps so automation sees exactly what ran. A planner recommendation the engine cannot construct executable SQL for refuses (not-native-safe-rewrite-required) rather than running the form the plan itself flagged.
  • Gated --force. The flag's value must name the resolved schema-qualified target table exactly — a typed acknowledgement, not a -y. The override is warn-logged unconditionally and recorded machine-readably in the verdict's forced field. Force overrides routing only: budgets, the statement gate, preflight, and the size guard still apply, and planner refusals (no known safe path) are not forceable.
  • Search-path resolution. The CLI resolves an unqualified table once against the session's search_path and re-emits the qualified statement; the library-level ErrUnqualifiedTable refusal stays.
  • Stable executor outcome codes. executor.Code is a closed kebab-case vocabulary; OutcomeCode maps any executor error to its code (a SequenceStepError carries its failed step's cause), and the sequence/index reports gain JSON tags with a pinned wire-shape test.
  • Size-guard scope. preflight.NoSizeLimit lets planner-proven online sequences run on large tables — long work there is the pattern's purpose — while blind and forced attempts of the submitted form stay guarded.
  • Docs updated to describe routed execution and the force gate as current behavior (README status, HLD force section and diagram, LLD remaining-work list, architecture table).

Why

Rendering safer sequences without executing them left the engine advisory at exactly the moment it matters: the classifier knows the safe idiom, so the default path should run it. The stable code vocabulary is the seam an orchestrator consumes — branch on codes, never prose.

Before / after

before:                                after:
migrate --alter 'CREATE INDEX …'       migrate --alter 'CREATE INDEX …'
  └─ blind bounded attempt               └─ classify → route
     (blocks writes for the                 ├─ safer form exists:
      whole budget window)                  │    run CONCURRENTLY build,
                                            │    verdict.executed_sql = steps
                                            ├─ rewrite, no safer form: refuse
                                            └─ --force <schema.table>:
                                                 run literal under budgets,
                                                 audit-logged, verdict.forced

migrate now executes the planner's classified route: a blocking
submitted form runs as the safer native sequence (reported in the
verdict's executed_sql), --force requires typing the resolved
schema-qualified table and is audit-logged, and every typed executor
outcome carries a stable string code (executor.OutcomeCode) so
orchestrators branch on one vocabulary, not prose.
@Kiran01bm
Kiran01bm force-pushed the kiran01bm/p3-4-substitution branch from 0468131 to fa357bd Compare August 12, 2026 07:58
…ptNative

A forced submitted form was routed through RunSequence, whose shape
admission categorically refused it before any DB access — the documented
--force CREATE INDEX case could never execute. Forced runs now bypass
sequence admission and execute as one blind bounded attempt.

Executor admission refusals (unnamed index, IF NOT EXISTS, undriven
DETACH PARTITION CONCURRENTLY) now surface as typed refusal verdicts
instead of raw operational errors, invalid-index debris is never masked
as a plain budget refusal, refused forced attempts keep the
machine-readable forced flag, and the statement-budget advice is
tailored for submitted online idioms.
…to kiran01bm/p3-4-substitution

* origin/kiran01bm/p3-2-native-idioms:
  surface retry attempts and prove lock-retry flag wiring
  feat(executor): lock_timeout + bounded retry for native DDL

# Conflicts:
#	internal/cli/cli.go
#	internal/cli/migrate.go
#	internal/cli/migrate_test.go
@Kiran01bm
Kiran01bm marked this pull request as ready for review August 13, 2026 05:50
@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.

@morgo morgo left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Approving on Morgan's behalf (automated review; pg-sprite liberal bar).

The core safety story checks out: substituted sequences are semantically equivalent-or-safer idioms (CREATE INDEX → CONCURRENTLY; SET NOT NULL → NOT VALID CHECK + VALIDATE + SET NOT NULL + scaffold drop), disclosed in the verdict's executed_sql; rewrite-requiring changes refuse up front instead of burning an ACCESS EXCLUSIVE attempt; every refusal fails closed. The --force gate only unlocks the substitution-override/rewrite/backend-unavailable paths (planner refusals stay unforceable), keeps the size guard and budgets, and always leaves an audit trail — each of these is pinned by an integration test. The new executor codes are additive (no renumbering; refusal is still exit 2), and SQL construction follows the repo's quoting conventions (parameterized catalog lookup + AST-deparser re-qualification with the comment guard).

Two non-blocking notes worth follow-ups:

  1. Partial-sequence state isn't machine-readable yet. A substituted sequence that fails mid-way leaves a committed prefix (e.g. a surviving CHECK … NOT VALID scaffold that does enforce on new writes if VALIDATE fails). SequenceStepError discloses the failed step and committed prefix, but only as a typed operational error on stderr — the verdict JSON doesn't carry the new stable executor codes yet (the docs list this as remaining work). Until that lands, automation can't distinguish "nothing happened" from "scaffold left behind."

  2. Observable-outcome changes for automation callers. Plain CREATE INDEX used to refuse (exit 2, ReasonIndexStatement) and now succeeds via CIC substitution (exit 0); ALTER COLUMN TYPE used to blind-attempt then budget-cancel and now returns backend-unavailable with no attempt. Both are intended and strictly safer, but anything branching on the old outcomes will see different results for identical inputs — worth a changelog callout.

Nits (no action needed): checkForceAck requires the internally-folded identifier form (fails closed on mismatch); --force that turns out unnecessary is a silent no-op with forced:false; JSON tags on the (unreleased) executor report structs rename their marshaled keys, now pinned by a contract test.

…angelog

Review follow-ups on the substitution PR: a mid-sequence failure was only
a typed error on stderr, so automation could not distinguish nothing
committed from a scaffold left behind. Execution failures now end in a
third verdict outcome (failed, still exit 1) carrying executor.OutcomeCode,
the failed step, and the committed prefix. The changelog calls out the
intended outcome changes for automation callers.
@Kiran01bm

Copy link
Copy Markdown
Collaborator Author

Review response from Kiran's (@Kiran01bm) AI code review assessment agent (Amp, Claude Opus 4.5)

Summary: Both non-blocking follow-ups are fixed on this branch — execution failures now emit a machine-readable failed verdict carrying the stable executor code, failed step, and committed prefix, and a CHANGELOG.md now calls out the observable-outcome changes. Nits acknowledged, no action needed.

# Finding Status Explanation
1 Partial-sequence state isn't machine-readable — verdict JSON doesn't carry the stable executor codes, so automation can't distinguish "nothing happened" from "scaffold left behind" fixed Added a third verdict outcome failed (still exit 1; refusals keep exit 2) with code (executor.OutcomeCode), failed_step, failed_step_sql, and the committed prefix in executed_sql (empty prefix = nothing committed). The CLI now keeps the SequenceReport that RunSequence already returned alongside *SequenceStepError. Pinned by a JSON-key contract test, failureVerdict unit tests, and an integration test that fails VALIDATE mid-sequence and asserts the verdict against the surviving catalog scaffold.
2 Observable-outcome changes (plain CREATE INDEX now exit 0 via CIC; ALTER COLUMN TYPE now backend-unavailable with no attempt) deserve a changelog callout fixed Added CHANGELOG.md (Keep a Changelog, Unreleased) with a "Changed — observable outcomes for automation callers" section naming both switches, plus the substitution-by-default, --force, and failed-verdict entries.

Confirmations (no action): the review's verified-correct items — equivalent-or-safer substitutions disclosed in executed_sql, up-front refusal of rewrite-requiring changes, fail-closed refusals, the --force gate's scope/audit/budgets, additive executor codes, and quoting conventions — are noted with thanks; no changes required. The three nits (folded-identifier --force ack failing closed, unnecessary --force as silent no-op with forced:false, JSON tags on unreleased report structs pinned by contract test) are intended behavior, as the review observes.

@Kiran01bm
Kiran01bm merged commit 58fe67d into kiran01bm/p3-2-native-idioms Aug 13, 2026
13 checks passed
Kiran01bm added a commit that referenced this pull request Aug 13, 2026
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