feat(engine): disclose at plan time whether an apply continues or discards a copy - #1087
Conversation
dfd08bc to
40f7459
Compare
e78b99d to
ea8cf86
Compare
40f7459 to
d1604cb
Compare
ea8cf86 to
625ddea
Compare
d1604cb to
a701b89
Compare
625ddea to
70b8935
Compare
a701b89 to
9bf565c
Compare
a050715 to
645389f
Compare
…cards a copy The prediction already runs at apply time, once the decision is unavoidable. Run it at plan time too, against the target the plan reads, and carry the answer on the plan as an engine-agnostic ExistingCopy per namespace. A plan is a read: it describes the target and must never fail because of it, so a target that cannot be read is logged and the plan carries no disclosure, leaving the plan exactly as it is without the check.
645389f to
4232c06
Compare
There was a problem hiding this comment.
Pull request overview
This PR extends the Spirit engine’s planning phase to surface whether applying the planned ALTER batch would adopt (resume) or discard (restart) an unfinished Spirit row copy already present on the target, so that decision is knowable before confirmation.
Changes:
- Adds an engine-agnostic
ExistingCopiesdisclosure field toengine.PlanResult. - Implements Spirit plan-time detection/prediction of existing copy disposition and attaches it to the returned plan.
- Adds integration tests covering “no copy”, “adopt”, and “discard (statement differs)” plan disclosures.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| pkg/engine/spirit/spirit.go | Populates PlanResult.ExistingCopies during Spirit planning. |
| pkg/engine/spirit/existing_copy.go | Adds plan-time conversion and prediction helpers to disclose existing-copy disposition. |
| pkg/engine/spirit/existing_copy_plan_integration_test.go | New integration tests verifying plan-time disclosures across clean/adopt/discard cases. |
| pkg/engine/engine.go | Introduces PlanResult.ExistingCopies and the engine-agnostic ExistingCopy type. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
🤖 Review findings - created by Kiran's code review agent - for schemabot/pull/1087, 4232c06. Verdict: 8 findings — 2 blocking (namespace mislabeling, dropped multi-namespace disclosure), 5 non-blocking (plan/apply ExecutionMode drift, test-coverage gaps), 1 general suggestion (proto/API wiring). Blocking
Non-blocking
General suggestions
The one thing that could have broken, verifiedThe riskiest mechanism in this PR is whether the plan-time Verified correct
This review was generated by Claude Code (claude-sonnet-5). |
…it predicts The Namespace field promised one entry per namespace, which holds only for a target the engine plans per namespace. A connection scoped to one database whose schema subdirectories divide it only logically is read once, so the disclosure names that database: something the operator can go and look at, which is what a disclosure owes them, and not a key to group or route on. Also name the second way the plan-time prediction stays a prediction. Routing runs again at apply time, so a statement the plan reads as directly executable or refused can reach the copy path after all, in a batch that no longer matches the one predicted. Both that and an unreadable target fail quiet rather than wrong, and the apply recomputes from the routing it actually took. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The prediction's failure mode is silence, so its branches need assertions that inspect what it produced rather than what an apply did afterwards. plannedSpiritBatch gets unit coverage for every branch that decides what the batch is: the join order Spirit's checkpoint identity depends on, the directly executed statement that never reaches a checkpoint, non-ALTER changes, and the blocked statement that means nothing on the target is at stake. Swapping any of those for a fallthrough was previously invisible. At plan time, an unreadable target is proven to leave the plan as it was, and the two discards an operator cannot see coming from the schema change alone — an expired checkpoint and a copy covering only part of the batch — are proven to arrive as disclosures rather than as a promise the copy survives. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
🤖 Thanks — addressed in Namespace labeling: the code was right and the doc contract was the overclaim, so I corrected the contract. Splitting one ExecutionMode drift: agreed the Test gaps: Multi-namespace aggregation and proto/API wiring: both already land in #1088 — This reply was generated by Claude Code (Opus 5). |
…ew-drift-rollup * origin/main: (357 commits) fix(github): render each lint violation as its own bullet in unsafe-change comments (#1105) feat(engine): disclose at plan time whether an apply continues or discards a copy (#1087) fix(operator): choose the drive mode from the generation manifest, not the attached row count (#1101) feat(tern): one deployment correlates to exactly one remote apply (#1060) fix(github): record the passing check when an apply plan finds no changes (#1099) feat(spirit): detect an unfinished row copy and log what the apply will do to it (#1048) docs: reserve metrics for signals worth alerting on (#1089) feat(cli): browse stored plan history with the list-plans command (#1083) feat(cli): render status sources as OSC 8 hyperlinks on interactive terminals (#1097) feat(github): show VSchema changes in sharded apply comments (#1096) test(webhook): PostgreSQL failure-matrix row — declined stop is terminal, apply completes (#1098) feat(observability): log the delivery GUID when a goroutine panics (#1092) test(webhook): pin apply-confirm lock-path dispositions (#1091) fix(api): type terminal rollback validation errors (#1090) build(deps): pin pg-sprite to released v0.1.0 (#1093) feat(cli): show apply provenance as a clickable source in status output (#1086) fix(github): give sharded applies a real terminal summary comment (#1085) fix(vitess): gate stored-plan applies on recorded VSchema deletions and mutations (#1084) webhook: PostgreSQL failure-matrix rows — restart survival and permanent privilege refusal (#1079) fix(tern): complete a deployment-keyed apply only when its generation manifest is satisfied (#1076) ... # Conflicts: # pkg/webhook/plan.go # pkg/webhook/templates/plan.go
Why this matters
The engine can already predict whether an apply will resume unfinished work on the target or destroy it, but only at apply time — the moment the decision is unavoidable. By then the copy is gone. The point of predicting it at all is to tell someone before they confirm, and that requires the plan to carry the answer.
What it does
Runs the same prediction at plan time, against the target the plan already reads, and carries the result on the plan as an engine-agnostic
ExistingCopyper target.The shape is one entry per target read, not one per plan, so an engine spanning several targets reports which one each disclosure is about. An engine planning each namespace separately produces one entry per namespace that holds a copy; where several namespaces share one connection-scoped database, that database is read once and named once. Either way the name is something an operator can go and look at, which is what a disclosure owes them.
Two safety properties worth calling out:
The type carries no engine-specific fields — namespace, disposition, reason, tables, age — so a second engine that copies tables reports the same shape and every surface renders it one way.
How it moves us toward the northstar
This is the middle link of three. The engineer whose schema change is about to destroy a day of copying learns nothing from an engine's internal state — they read a plan, in a PR comment or on the CLI. The plan is the one object every surface already reads, so putting the disposition there is what lets any of them disclose it without reaching into an engine. #1048 makes the prediction; #1088 renders it.
Opened by Claude (Opus 5).