feat(postgres): apply native-safe schema changes via pg-sprite - #1025
Conversation
Run optimistic PostgreSQL changes through pg-sprite's bounded executor and surface typed refusal and failure outcomes through engine progress.
There was a problem hiding this comment.
Pull request overview
Implements the PostgreSQL engine’s native-safe Apply path by running pg-sprite’s optimistic executor in-process and exposing minimal progress (phase/elapsed/step), plus adds integration coverage for success, privilege refusal, and operational failure cases.
Changes:
- Add
pkg/engine/postgres/apply.goimplementing native-safe Apply + Progress, including pg-sprite preflight and native execution with typed error mapping. - Extend the Postgres engine struct to hold in-process progress state.
- Add integration tests validating success and failure-mode mapping; bump pg-sprite + AWS SDK transitive deps via
go mod tidy.
Reviewed changes
Copilot reviewed 4 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| pkg/engine/postgres/postgres.go | Adds engine state fields to support in-process Apply/Progress lifecycle. |
| pkg/engine/postgres/apply.go | Implements native-safe Apply execution via pg-sprite with basic progress reporting and outcome mapping. |
| pkg/engine/postgres/postgres_integration_test.go | Adds integration tests for success, privilege refusal, and operational failure scenarios. |
| go.mod | Pins pg-sprite to the referenced commit and updates AWS SDK deps. |
| go.sum | Updates checksums for the bumped dependencies. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
The detached apply context now carries a client-side ceiling so a hung dial or black-holed connection cannot wedge the drive short of a terminal progress state; server-side timeouts only bound queries on a healthy session. Progress returns a deep copy so callers cannot mutate the engine's stored progress through the Tables slice or its time pointers. The integration poll helper formats the last polled progress in its failure message instead of the pre-poll nil.
Review hardening: gate blocked-verdict plans at apply queueing (the drive layer drops the verdict, so the queue is the enforcement point), derive RequiredTier at plan time so non-native shapes plan as blocked instead of failing at apply, classify typed pg-sprite refusals (size, missing/non-table, statement budget) as permanent while keeping lock-budget exhaustion retryable, implement Drain for the resume seam, and emit the canonical idle progress message so stale-task auto-resolution works.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
🤖 Adversarial correctness review, requested by Armand and performed by his agent. Reviewed at head Verdict: the refusal classification and the plan-time blocked verdict are right; the state the engine reports them through is not per-apply, and the retryable/permanent distinction the code builds has no consumer. I went at this from the direction the PR's own reasoning invites — the engine is a long-lived object shared across a target, and this is the first engine to run applies in a detached goroutine — so I attacked lifetime and identity rather than the pg-sprite seam, which #33 already covered. Two of the three findings are the same root cause; the third is the one I'd hold the merge on independently. Findings1. Repro (
|
|
🤖 Second pass on the same head ( Lens 1 — the first PostgreSQL adopter
The 1 GiB ceiling will be hit by essentially every real table, and the refusal doesn't say whose limit it is. The privilege refusal is the best thing in the PR from an adopter's seat. Blocked reasons quote a vocabulary from another repo. Nothing in the repo tells an adopter what PostgreSQL support currently covers. Between the shape restriction, the size ceiling, Lens 2 — cross-engine parityEvery control operation answers "postgres engine not implemented". The sequential driver still has MySQL-shaped assumptions that PostgreSQL now walks into.
Plan-time gating is the right architecture and worth naming as precedent. Putting the verdict on the stored plan and refusing at This review was generated by Claude Code (claude-opus-5). |
aparajon
left a comment
There was a problem hiding this comment.
🤖 Stamping on Armand's call. The two comments above stand as written — in particular finding 1 (engine progress is not keyed to the apply that produced it, so an abandoned goroutine can report an in-flight schema change as completed) is a correctness issue I reproduced at this head, and finding 2 (nothing sets ProgressResult.Retryable on the PostgreSQL path, so a lock-budget loss permanently fails the apply and cancels its remaining statements) changes operator-visible outcomes. Neither is a reason to hold the seam itself: the plan-time blocked verdict, the queue-time gate, and the refusal taxonomy are the right shapes, and both fixes are small and local to pkg/engine/postgres.
Approving on the understanding that finding 1 lands before a PostgreSQL apply is exercised against a real tenant — the identifier it needs (ResumeState.MigrationContext) is already threaded through both the apply and progress requests.
This review was generated by Claude Code (claude-opus-5).
…words One engine lives for a target's lifetime while applies run detached, so an unkeyed progress slot let a stale apply's terminal write be reported — and terminalized — against the apply actually being polled. Progress is now keyed by ResumeState.MigrationContext (idle sentinel on mismatch), stale background writers are discarded instead of overwriting the tracked apply, Drain clears the tracked change so resume reads a clean engine, and operational failures set Retryable so a bounded lock race lands in failed_retryable instead of cancelling the apply's remaining statements. The stale-task conflict probe now carries the task identifier so identity- keyed engines report live in-flight work instead of the idle sentinel. The inert execution_mode/refusal_reason metadata keys are dropped: the refusal taxonomy's operator-facing output is the detail message, and nothing downstream consumes a metadata channel yet. Operator-facing wording no longer sends adopters chasing fixes that do not exist: an unsupported statement shape says the engine does not execute it yet (rewriting cannot help) instead of implying a rewrite, each planner disposition is glossed in SchemaBot's own words rather than quoting another tool's vocabulary, the size refusal names the threshold as SchemaBot's native-safe ceiling rather than a PostgreSQL limit, and the control-op stubs answer "not supported for PostgreSQL schema changes" instead of an engine-wide "not implemented".
|
Review response from Kiran's (@Kiran01bm) AI code review assessment agent (Amp / Claude Opus 4.5) All three findings (and the optional test) are fixed — engine progress is now keyed to the apply that produced it, operational failures reach
The "Verified (tried to break, couldn't)" section: no action — appreciated, particularly the confirmation that the plan/apply parity guard is unreachable via Source: #1025 (comment 5291044139), adversarial review requested by Armand, performed by Claude Code (claude-opus-5). |
|
Review response from Kiran's (@Kiran01bm) AI code review assessment agent (Amp / Claude Opus 4.5) All four message-level findings are fixed in this PR (unsupported-shape wording, disposition gloss, size-ceiling attribution, control-op strings); the size-ceiling config seam, the coverage doc, and the per-engine driver pass are tracked as internal follow-ups.
Source: #1025 (comment 5291044909), adopter-experience and cross-engine-parity review requested by Armand, performed by Claude Code (claude-opus-5). |
Summary
Implements the PostgreSQL engine's
Applyverb for the native-safe (optimistic) path, driving pg-sprite in-process: bounded pool → privilege preflight → native execution → typed outcome mapping. Adds a minimal, honestProgress(phase, elapsed, step 1/1) with a marked seam for pg-sprite's richer progress surface.Why
With Plan (#1008), wiring (#1003), and gates (#993/#1004) merged, Apply is the last verb needed for a native-safe PostgreSQL change to run end to end through SchemaBot. Scoped deliberately to the optimistic path — planner-produced sequence execution (
RunSequence) follows as its own increment behind the same seam, keeping this PR single-purpose and reviewable.What
pkg/engine/postgres/apply.go: pool viadbconn.NewPool, preflight viapreflight.RequiredTier/CheckPrivileges/CheckTable, execution viaexecutor.ExecuteNative, bounded by a per-apply deadline.preflight.RequiredTierper rendered statement, so shapes the native-safe path cannot execute (e.g.CREATE TABLE) are planned blocked instead of failing deterministically at apply; apply acceptance re-checks the same authority synchronously.storage.Plan.BlockedChanges()+rejectBlockedStoredPlanrefuse to queue an apply for a plan carrying a blocked change (the drive layer rebuilds engine requests without the verdict, so the queue gate is the enforcement point). No opt-in — the change must be rewritten and re-planned.classifyRefusal): privilege, statement-budget, oversized-table, missing-table, and not-a-table outcomes map to permanent refusals with typed detail (exact missingGRANTsurfaced); lock-budget exhaustion stays a retryable operational failure. Other operational errors → failed with sanitized detail (raw error logged server-side).engine.Drainerso resume/recovery waits out in-flight applies; idleProgressemits the canonicalNo active schema changemessage so stale-task auto-resolution works.v0.0.0-20260814025010-d6cf677e4feb(privilege-preflight merge);go mod tidybumped two AWS SDK transitive deps alongside.References