Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 53 additions & 2 deletions docs/cli-output-examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ and applies to `insufficient-privileges`, both exit 2. The JSON report schema is
- [Migrate](#migrate)
- [Runs as written (`metadata-only`) — exit 0](#runs-as-written-metadata-only--exit-0)
- [Safer-sequence substitution (`safer-idiom`) — exit 0](#safer-sequence-substitution-safer-idiom--exit-0)
- [Real execution](#real-execution)
- [Real execution — exit 0](#real-execution--exit-0)
- [Accepted blocking refusal (`index-statement`) — exit 3](#accepted-blocking-refusal-index-statement--exit-3)
- [Failed mid-sequence (`execution-failed`) — exit 1](#failed-mid-sequence-execution-failed--exit-1)
- [Refused: no online rewrite exists (`rewrite-required`) — exit 2](#refused-no-online-rewrite-exists-rewrite-required--exit-2)
- [Routed to copy-and-swap (`type-rewrite`) — exit 0](#routed-to-copy-and-swap-type-rewrite--exit-0)
- [Refused: the engine does not carry the operation yet (`backend-unavailable`) — exit 2](#refused-the-engine-does-not-carry-the-operation-yet-backend-unavailable--exit-2)
Expand Down Expand Up @@ -239,7 +241,7 @@ $ pg-sprite migrate --alter 'ALTER TABLE users ADD CONSTRAINT users_email_key UN
}
```

### Real execution
### Real execution — exit 0

The same statement without `--dry-run` runs the substituted sequence for
real. The verdict names what was executed and every step that committed
Expand All @@ -259,6 +261,8 @@ $ pg-sprite migrate --alter 'ALTER TABLE users ADD CONSTRAINT users_email_key UN
}
```

### Accepted blocking refusal (`index-statement`) — exit 3

An operator-accepted blocking refusal has a distinct marked outcome and exit
3; exit 0 remains exclusive to online-safe execution. `--accept-blocking`
takes the schema-qualified table whose lock the operator accepts, must match
Expand Down Expand Up @@ -290,6 +294,53 @@ $ pg-sprite migrate --alter 'DROP INDEX public.users_email_idx' --accept-blockin
}
```

### Failed mid-sequence (`execution-failed`) — exit 1

Exit 1 means execution started and PostgreSQL refused a step, or the command
stopped before anything ran. Here the safer sequence for a `CHECK` constraint
adds it `NOT VALID`, which commits, and then fails to validate it because a
row has an upper-case email. The verdict on stdout names the failed step and
the committed prefix that remains (`executed_sql`); the PostgreSQL error,
with its SQLSTATE, goes to stderr. The constraint now exists `NOT VALID`, so
fix the rows and then run the failed step (`failed_step_sql`) to finish. Do
not rerun the original `ADD CONSTRAINT`: the constraint already exists, so it
fails with `duplicate_object` (see the
[retry paths](execution-model.md#the-committed-prefix)):

```console
$ pg-sprite migrate --alter 'ALTER TABLE users ADD CONSTRAINT users_email_lower CHECK (email = lower(email))' --json
{
"outcome": "failed",
"code": "execution-failed",
"failed_step": 2,
"failed_step_sql": "ALTER TABLE \"public\".\"users\" VALIDATE CONSTRAINT \"users_email_lower\"",
"statement": "ALTER TABLE public.users ADD CONSTRAINT users_email_lower CHECK (email = lower(email))",
"table": "public.users",
"detail": "sequence step 2 of 2 failed; the 1 committed steps' state remains — the planner sequence's partial-failure contract says how a retry resumes",
"executed_sql": [
"ALTER TABLE public.users ADD CONSTRAINT users_email_lower CHECK (email = lower(email)) NOT VALID"
]
}
```

```text
pg-sprite: error: run schema change on public.users: sequence step 2 of 2 (validate-constraint) failed; steps before it committed and their state remains: optimistic attempt: ERROR: check constraint "users_email_lower" of relation "users" is violated by some row (SQLSTATE 23514)
```

When `code` is `commit-outcome-unknown`, the failed step's `COMMIT` may have
landed; inspect the catalog before retrying.

The other exit 1 is an error before anything runs — bad flags, an unreachable
database, or an acknowledgement that names the wrong table. It prints no
verdict, even with `--json`; stdout is empty and the error is on stderr:

```console
$ pg-sprite migrate --alter 'ALTER TABLE users DROP COLUMN email' --allow-destructive public.orders
pg-sprite: error: the destructive acknowledgement must name the resolved target table "public.users", got "public.orders"; nothing was executed
$ echo $?
1
```

### Refused: no online rewrite exists (`rewrite-required`) — exit 2

The column and its constraint arrive in one statement, so no online
Expand Down
21 changes: 19 additions & 2 deletions internal/cli/docs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/block/pg-sprite/pkg/executor"
"github.com/block/pg-sprite/pkg/lint"
"github.com/block/pg-sprite/pkg/migrate"
"github.com/block/pg-sprite/pkg/plan"
Expand Down Expand Up @@ -76,7 +77,7 @@ func TestCLIOutputExamplesMatchPipelineOutput(t *testing.T) {
raw, err := os.ReadFile(cliOutputExamplesDoc)
require.NoError(t, err)
blocks := regexp.MustCompile("(?s)```console\n\\$ pg-sprite [^\n]*--json\n(.*?)```").FindAllStringSubmatch(string(raw), -1)
require.Len(t, blocks, 13, "the doc publishes thirteen captured --json outputs")
require.Len(t, blocks, 14, "the doc publishes fourteen captured --json outputs")

metadataOnly := alterReport(t, "ALTER TABLE users ADD COLUMN note text",
"public", "users", usersFacts())
Expand Down Expand Up @@ -104,6 +105,22 @@ func TestCLIOutputExamplesMatchPipelineOutput(t *testing.T) {
"public", "users", usersFacts())
copyAndSwapRouted := alterReport(t, "ALTER TABLE users ALTER COLUMN id TYPE text",
"public", "users", usersFacts())
// The failed verdict is captured from a run: a CHECK whose NOT VALID
// step commits and whose VALIDATE step then meets a violating row. Its
// detail is the migrate package's rendering of a mid-sequence failure.
failedSequence := verdict.Verdict{
Outcome: verdict.OutcomeFailed,
Code: string(executor.CodeExecutionFailed),
FailedStep: 2,
FailedStepSQL: `ALTER TABLE "public"."users" VALIDATE CONSTRAINT "users_email_lower"`,
Statement: "ALTER TABLE public.users ADD CONSTRAINT users_email_lower CHECK (email = lower(email))",
Table: "public.users",
Detail: "sequence step 2 of 2 failed; the 1 committed steps' state remains — " +
"the planner sequence's partial-failure contract says how a retry resumes",
ExecutedSQL: []string{
"ALTER TABLE public.users ADD CONSTRAINT users_email_lower CHECK (email = lower(email)) NOT VALID",
},
}
// The executed verdict is captured from a run; its detail is the
// verdict's own rendering of the verification pass and the retained
// source's fate, which the migrate package's tests pin.
Expand Down Expand Up @@ -168,7 +185,7 @@ func TestCLIOutputExamplesMatchPipelineOutput(t *testing.T) {
}
diff.Fingerprint = plan.Fingerprint(diff.Statements)

want := []any{metadataOnly, saferIdiom, executed, acceptedBlocking, rewriteRequired,
want := []any{metadataOnly, saferIdiom, executed, acceptedBlocking, failedSequence, rewriteRequired,
copyAndSwapRouted, executedViaCopyAndSwap, backendUnavailable, partitioned, destructiveRefused, destructive,
lintReport, diff}
for i, w := range want {
Expand Down
Loading