Repository navigation
Conversation
The acknowledgement names a table, but on the declarative front door the derived plan decides how many fenced copies block its writes, and a value left on an embedder's configuration would take the table offline for every later plan.
Each gate had its own tests but nothing checked the set was complete; the registry's test found RunDesired passing AcceptBlocking and Restart through to planned statements, so desired-state execution now rejects every acknowledgement from the same list.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
🤖 Review 1/2 of c135be8: 0 blocking, 4 non-blocking. I read the whole diff at Invariants: extends RF-10 (no statement that discards data or leaves the table unavailable runs without a typed acknowledgement; the declarative path now takes none of the five, not only BlockingNone. Non-blocking1. The completeness test checks that entries exist, not that they are right. Of the registry's four columns, Test that passes on c135be8 and fails when either entry is mis-statedpackage migrate
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/block/pg-sprite/pkg/verdict"
)
// Each non-shape entry names the acknowledgement and refusal the real gate
// uses, not just any member of the closed sets.
func TestIrreversibleRegistryEntriesNameTheirGate(t *testing.T) {
want := map[string]struct {
ack ackKind
refusal verdict.Reason
}{
"blocking DROP INDEX": {ackAcceptBlocking, verdict.ReasonIndexStatement},
"blocking REINDEX": {ackAcceptBlocking, verdict.ReasonIndexStatement},
"submitted form that blocks the table": {ackForce, verdict.ReasonRewriteRequired},
"copy-and-swap restart": {ackRestart, ""},
"copy-and-swap with the table's writes blocked": {ackBlockWrites, verdict.ReasonCopyAndSwapRefused},
}
seen := 0
for _, op := range irreversibleOps() {
w, ok := want[op.key]
if !ok {
continue // destructive shapes are derived and checked elsewhere
}
seen++
assert.Equal(t, w.ack, op.ack, "entry %q", op.key)
assert.Equal(t, w.refusal, op.refusal, "entry %q", op.key)
}
assert.Equal(t, len(want), seen)
}Passes on 2. A new acknowledgement is only caught if it is also added to Test that passes on c135be8 and fails when a new string field is added to CopySwapOptionsfunc TestOptionsStringFieldsAreAccountedFor(t *testing.T) {
known := map[string]bool{"Force": true, "AcceptBlocking": true, "AllowDestructive": true, "CreateOwner": true}
copyKnown := map[string]bool{"BlockWrites": true, "Restart": true}
for _, tc := range []struct {
v any
known map[string]bool
}{{Options{}, known}, {CopySwapOptions{}, copyKnown}} {
ty := reflect.TypeOf(tc.v)
for i := 0; i < ty.NumField(); i++ {
f := ty.Field(i)
if f.Type.Kind() == reflect.String {
assert.True(t, tc.known[f.Name], "%s.%s", ty.Name(), f.Name)
}
}
}
}Passes on 3. "Setting any of them returns its own sentinel" is not true for combinations. Test case: two acknowledgements togetherfunc TestRunDesiredNamesTheSentinelForTwoAcknowledgements(t *testing.T) {
opts := DefaultOptions()
opts.Force = "public.t"
opts.CopySwap.Restart = "public.t"
_, err := RunDesired(t.Context(), nil, DesiredRequest{Schema: "public"}, opts)
require.ErrorIs(t, err, ErrForceNotSupported)
}
4. "Every operation that cannot be undone" is wider than the list. The file's header says the registry lists every operation pg-sprite can run that cannot be undone (L9-L18), but it lists the acknowledged ones. A copy-and-swap with Verified
This review was generated by Claude Code (claude-sonnet-5-5). |
|
🤖 Review 2/2 of c135be8: OSS adoption and SchemaBot integration lenses, 0 blocking, 3 non-blocking. This is the second of two comments; the correctness pass is the one above. The change is a deliberate, fail-closed tightening of an exported surface: BlockingNone. Non-blocking1. RF-10's Enforced entry does not name what now enforces it. RF-10 is the invariant this PR extends ("The declarative path takes no acknowledgement"), and its Enforced line names only 2. 3. The rule is now stated in three places in the integration doc. The new bullet (schemabot-integration.md#L642-L648) covers all five, while the bullet just above it still restates the Verified
This review was generated by Claude Code (claude-sonnet-5-5). |
aparajon
left a comment
There was a problem hiding this comment.
🤖 Stamping with comments: 0 blocking, 7 non-blocking (see the two review comments above).
This stamp was left by Claude Code (claude-sonnet-5-5).
One registry in
pkg/migratelists every operation pg-sprite can run that cannot be undone, with the acknowledgement that lifts its refusal, and a unit test fails CI when a new acknowledgement or destructive shape arrives without an entry.Why
"Make mistakes hard to commit" is enforced gate by gate today:
--allow-destructive,--accept-blocking,--force,--restart,--block-writes, each with its own tests. Nothing checks that the set of gates is complete, so a new irreversible path is caught only if its author remembers to add one. Writing the registry found a gap straight away:RunDesiredrejectedForce,AllowDestructiveandBlockWrites, but passedAcceptBlockingandCopySwap.Restartthrough to every statement the plan derived. The CLI already rejects both with--desired, so only library callers were exposed.What
pkg/migrate/irreversible_registry.go: the five acknowledgements (ackKind) and the registry entries. The destructive-shape entries are derived from the newplanner.DestructiveOpKinds(), so the planner's classification stays the only list of destructive shapes. The other five are listed: blockingDROP INDEX, blockingREINDEX, the submitted blocking form, copy-and-swap restart, and blocked writes.RunDesiredrejects every acknowledgement by walking that list, addingErrAcceptBlockingNotSupportedandErrRestartNotSupported.TestIrreversibleOperationRegistryIsCompletechecks that:Optionsfield, has its own sentinel, and is rejected byRunDesired;docs/schemabot-integration.md:RunDesiredtakes no acknowledgement.No CLI behaviour changes; the behaviour of each gate under a real database is the follow-up.
Before / after
A library caller reuses one
Optionsvalue, withCopySwap.Restart: "public.orders"left set from an imperative run, for a desired-state run whose plan retypespublic.orders.id.Stacked on #281; retarget to
mainonce it merges.