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
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,11 @@ refusal — never a silently wrong or incomplete result:
[runbook](docs/invalid-index-recovery.md) applies.
- **The accepted-blocking passthrough covers index maintenance only** —
`migrate --accept-blocking SCHEMA.TABLE` runs an otherwise-refused plain
`DROP INDEX`, `REINDEX INDEX`, or `REINDEX TABLE` deliberately under the
engine's bounded lock and statement budgets, with the verdict marked
`executed-without-online-safety` (exit 3). Every other refusal — including
a blocking index build on a partitioned parent — still exits 2. Design:
`DROP INDEX` (not `CASCADE`), `REINDEX INDEX`, or `REINDEX TABLE`
deliberately under the engine's bounded lock and statement budgets, with
the verdict marked `executed-without-online-safety` (exit 3). Every other
refusal — including a blocking index build on a partitioned parent — still
exits 2. Design:
[docs/lock-budgeted-passthrough.md](docs/lock-budgeted-passthrough.md).
- **Non-table objects** — views, standalone sequences, enums, domains,
extensions, functions, triggers — are outside the declarative model,
Expand Down
5 changes: 3 additions & 2 deletions docs/capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ or the only online form is a direct statement the operator runs).
| Operation | Status | Engine path | Online-safety problem? | Behavior and why |
| --- | --- | --- | --- | --- |
| `CREATE [UNIQUE] INDEX` on a plain table — including partial, expression, covering (`INCLUDE`), GIN/GiST/BRIN | ✅ | native, safer sequence | Yes | Executed as (or rewritten to) `CREATE INDEX CONCURRENTLY`, with validity verification, typed invalid-index outcomes, and a proven recovery for abandoned leftovers (`RebuildAbandonedIndex`, or `DropAbandonedIndex` to remove the leftover without rebuilding; both library-only; [runbook](invalid-index-recovery.md)) |
| `DROP INDEX` | ❌ | — | Yes — the online form is a direct `CONCURRENTLY` statement that `migrate` does not drive | `migrate` never runs this statement online-safely: the plain form is refused and names `DROP INDEX CONCURRENTLY`, and the concurrent form is refused as already safe — run it directly against the database. A single-index plain `DROP INDEX` can be run as-is under bounded budgets with `--accept-blocking SCHEMA.TABLE` (exit 3, never 0); `--accept-blocking` checks the replica identity under the table's lock and refuses the drop as `unsupported-statement`, naming the identity index. `diff` reports an index to remove as a **destructive** `DROP INDEX` and hints to run it through `migrate --alter`, which ends in this `index-statement` refusal — run `DROP INDEX CONCURRENTLY` directly instead. `diff` refuses dropping the table's replica identity index, or one a partition's is attached under, with `replica-identity` |
| `DROP INDEX` | ❌ | — | Yes — the online form is a direct `CONCURRENTLY` statement that `migrate` does not drive | `migrate` never runs this statement online-safely: the plain form is refused and names `DROP INDEX CONCURRENTLY`, and the concurrent form is refused as already safe — run it directly against the database. A single-index plain `DROP INDEX` without `CASCADE` can be run as-is under bounded budgets with `--accept-blocking SCHEMA.TABLE` (exit 3, never 0); `--accept-blocking` checks the replica identity under the table's lock and refuses the drop as `unsupported-statement`, naming the identity index. `diff` reports an index to remove as a **destructive** `DROP INDEX` and hints to run it through `migrate --alter`, which ends in this `index-statement` refusal — run `DROP INDEX CONCURRENTLY` directly instead. `diff` refuses dropping the table's replica identity index, or one a partition's is attached under, with `replica-identity` |
| `REINDEX` | ❌ | — | Yes — the online form is a direct `CONCURRENTLY` statement that `migrate` does not drive | `migrate` never runs this statement online-safely: the plain form is refused and names `REINDEX ... CONCURRENTLY`, and the concurrent form is refused as already safe — run it directly against the database. A plain `REINDEX INDEX` or `REINDEX TABLE` can be run as-is under bounded budgets with `--accept-blocking SCHEMA.TABLE` (exit 3, never 0). `REINDEX SCHEMA`, `DATABASE`, and `SYSTEM` stay refused. A desired schema cannot express a reindex, so `diff` never produces one |
| Index build on a **partitioned parent** | 🟡 | native, planned flow | Yes | PostgreSQL has no parent-level `CONCURRENTLY`; the blocking form is refused by policy (`--force` does not bypass it, and `--accept-blocking` does not reach it yet). The partition-aware flow — `CREATE INDEX ON ONLY` → per-partition CIC → `ATTACH PARTITION`, with crash-resume per leaf — is planned |
| `ADD CONSTRAINT ... USING INDEX` on a partitioned parent | ❌ | — | Yes — unsolvable today | PostgreSQL does not support adopting an index on a partitioned parent in any supported version; refused before execution |
Expand Down Expand Up @@ -366,7 +366,8 @@ unmistakably marked as executed without an online-safety guarantee
0). The value must name the table whose lock you accept — the owning table of the index —
resolved from the catalog, and `--statement-timeout` must be spelled on the same command
line. Only changes the engine understands but cannot run *safely* are eligible: today a
single-relation plain `DROP INDEX`, `REINDEX INDEX`, or `REINDEX TABLE`. Refusals for
single-relation plain `DROP INDEX` (never `CASCADE`, which also drops dependent objects such
as a foreign key on another table), `REINDEX INDEX`, or `REINDEX TABLE`. Refusals for
unrecognized SQL never are, and the refused statement's `blocking_passthrough_eligible`
field on the plan report says whether a refusal qualifies. An exhausted lock budget is
still a refusal (exit 2, nothing ran); a statement cancelled by its budget is a failure
Expand Down
22 changes: 17 additions & 5 deletions docs/invariants.md
Original file line number Diff line number Diff line change
Expand Up @@ -646,24 +646,36 @@ relation (a table where the statement needs an index, or the reverse) is a usage
no verdict — each its own typed error — and nothing executes. The acknowledgement is never
inferred from the statement text. The resolution and the execution are separate statements
on the pool, so a concurrent re-pointing of the index name between them is outside what this
invariant guarantees; the design records the window. *Enforced:* `pkg/migrate`
invariant guarantees; the design records the window. The statement's scope must also be
exactly that table: a `DROP INDEX ... CASCADE` also drops the objects that depend on the index,
such as a foreign key on another table, so it is not a single-relation statement and the
executor refuses it (`ErrUnsupportedAcceptedBlocking`) before opening a transaction.
*Enforced:* `pkg/migrate`
(`acceptBlocking`, `lockedTable`; `ErrAcceptBlockingMismatch`,
`ErrAcceptBlockingRelationNotFound`, `ErrAcceptBlockingWrongRelationKind`),
`TestRunAcceptBlockingRejectsFalseAcknowledgements`, `TestRelationKindMatches`.
`pkg/executor` (`acceptedBlockingIneligibility`), `pkg/statement` (`ParseOne` classifies a
cascading drop as `IndexTargetOther`),
`TestRunAcceptBlockingRejectsFalseAcknowledgements`, `TestRelationKindMatches`,
`TestAcceptedBlockingAdmission`, `TestExecuteAcceptedBlockingRefusesCascadeDrop`,
`TestExecuteAcceptedBlockingPlainDropOfReferencedIndexIsRefusedByServer`,
`TestExecuteAcceptedBlockingRestrictDropOfReferencedIndexIsRefusedByServer`.
*Source:* [lock-budgeted passthrough](lock-budgeted-passthrough.md#flag-and-front-door).

### AB-4 — Acceptance applies only to an eligible in-process refusal, and to nothing else

The acknowledgement changes the outcome of exactly one thing: a gate refusal whose in-process
proof the eligibility registry marks eligible (RF-8). Every other refusal — an ineligible
gate refusal, a planner refusal, a size-guard or preflight refusal — stands exactly as it
would without the flag, and a statement the gate admits runs its normal path. The
would without the flag — including a `DROP INDEX ... CASCADE`, whose refusal site is the
ineligible one because its scope exceeds the acknowledged table — and a statement the gate
admits runs its normal path. The
acknowledgement is not `--force`: the two cannot be combined, and a dry run reports the
refusal as it stands rather than the accepted execution. The front door requires the
statement budget to be stated on the same command line, because the accepted outage bound is
a decision, not a default. *Enforced:* `migrate.AcceptedRefusal`, `MigrateCmd.Validate`,
`TestAcceptedRefusal`, `TestAcceptBlockingFlagGrammar`,
`TestMigrateAcceptBlockingLeavesIneligibleRefusalsAlone`. *Source:*
`migrate.gateRefusal` (the refusal site), `TestAcceptedRefusal`, `TestAcceptBlockingFlagGrammar`,
`TestMigrateAcceptBlockingLeavesIneligibleRefusalsAlone`,
`TestMigrateAcceptBlockingRefusesCascadeAndKeepsDependentForeignKey`. *Source:*
[lock-budgeted passthrough](lock-budgeted-passthrough.md#flag-and-front-door).

### AB-5 — The accepted outcome is distinguishable from online-safe execution at every surface
Expand Down
10 changes: 9 additions & 1 deletion docs/lock-budgeted-passthrough.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,14 @@ and the site distinguishes the single-relation forms — one `DROP INDEX`, `REIN
`REINDEX TABLE` — from the forms that name several relations or none: `DROP INDEX a, b`,
`REINDEX SCHEMA`, `REINDEX DATABASE`, and `REINDEX SYSTEM`. Those multi-relation forms are
ineligible in v1 because one flag value cannot acknowledge several tables' locks.
`DROP INDEX ... CASCADE` is on the ineligible side too, although it names one index: it also
drops the objects that depend on the index — a foreign key on another table that references a
unique index, for one — which locks and changes tables the acknowledgement never named. The
parse boundary classifies it as a statement whose scope is wider than one relation, so the
registry refuses it, and the executor's shape check refuses it again for a library caller
that reaches `ExecuteAcceptedBlocking` directly. A plain drop or an explicit `RESTRICT` (the
default behavior) stays eligible, and is the safe retry: if anything still depends on the
index, PostgreSQL refuses the drop with SQLSTATE `2BP01` and changes nothing.

Execution is exposed only by the imperative `migrate` front door. Declarative `diff` still
classifies and reports eligibility, but it cannot accept the flag or execute this path. A
Expand Down Expand Up @@ -195,7 +203,7 @@ The v1 set is:

| Class | Typed refusal shape | v1 | Rationale |
| --- | --- | --- | --- |
| `by-design` | `index-statement` at the plain single-relation `DROP INDEX`, `REINDEX INDEX`, or `REINDEX TABLE` site, with the concurrent safer idiom | eligible | This is the core maintenance-window case: the submitted form is understood, the safer idiom is known, one table's lock is being accepted, and bounding `ACCESS EXCLUSIVE` acquisition prevents lock-queue pile-up. The multi-relation sites (`DROP INDEX a, b`, `REINDEX SCHEMA`, `DATABASE`, `SYSTEM`) stay ineligible. `REINDEX` on a partitioned table or index is eligible by shape but cannot run inside the engine-owned transaction; the executor reports it as `unsupported-accepted-blocking` (see [Engine-owned session and budgets](#engine-owned-session-and-budgets)). |
| `by-design` | `index-statement` at the plain single-relation `DROP INDEX`, `REINDEX INDEX`, or `REINDEX TABLE` site, with the concurrent safer idiom | eligible | This is the core maintenance-window case: the submitted form is understood, the safer idiom is known, one table's lock is being accepted, and bounding `ACCESS EXCLUSIVE` acquisition prevents lock-queue pile-up. The multi-relation sites (`DROP INDEX a, b`, `REINDEX SCHEMA`, `DATABASE`, `SYSTEM`) and `DROP INDEX ... CASCADE`, whose dependent drops reach tables beyond the acknowledged one, stay ineligible. `REINDEX` on a partitioned table or index is eligible by shape but cannot run inside the engine-owned transaction; the executor reports it as `unsupported-accepted-blocking` (see [Engine-owned session and budgets](#engine-owned-session-and-budgets)). |
| `capability-boundary` | `unsupported-partitioned-parent` with cause `parent-blocking-index-build` | eligible | The missing partition-aware flow does not make the plain PostgreSQL statement unknown. Its principal pre-execution hazard is acquiring the parent lock, which `lock_timeout` bounds. |
| `capability-boundary` | `not-native-safe-rewrite-required` | ineligible | A rewrite holds its strong lock for the full rewrite. Bounding acquisition alone does not bound the outage, and v1 must not imply otherwise. |
| `capability-boundary` | `backend-unavailable` and all other missing routes or backends | ineligible | A missing execution backend is not permission to substitute an unrelated blocking implementation. |
Expand Down
2 changes: 1 addition & 1 deletion docs/plan-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ statement of each class and its consumer action; this table is a copy of its row
|---|---|
| `capability-boundary` | The engine cannot do this yet, or has no implemented safe route. Wait for the capability or escalate. |
| `no-online-safety-problem` | There is nothing for an online schema-change engine to make safe; another tool class owns the work, or the operator runs it directly. Hand the statement to the named `owner`. |
| `by-design` | pg-sprite permanently refuses this form — PostgreSQL offers no online mechanism in any supported version, or a safer idiom exists and the verdict names it. Use the safer idiom; for a single-relation plain `DROP INDEX`, `REINDEX INDEX`, or `REINDEX TABLE`, `migrate --accept-blocking SCHEMA.TABLE` runs the blocking form under bounded budgets and exits 3; otherwise run it outside pg-sprite in a maintenance window. |
| `by-design` | pg-sprite permanently refuses this form — PostgreSQL offers no online mechanism in any supported version, or a safer idiom exists and the verdict names it. Use the safer idiom; for a single-relation plain `DROP INDEX` (not `CASCADE`), `REINDEX INDEX`, or `REINDEX TABLE`, `migrate --accept-blocking SCHEMA.TABLE` runs the blocking form under bounded budgets and exits 3; otherwise run it outside pg-sprite in a maintenance window. |
| `environmental` | The change is supportable, but not here, now, or as this role: a size policy, exhausted budget, privilege, server version, stale plan, or catalog collision stopped it. Retry, provision, upgrade, re-plan, or escalate. |
| `invariant-violation` | pg-sprite refused because its own input or state is incoherent — a report this build cannot have produced. Report it with the verdict; do not retry, wait, or route elsewhere. |

Expand Down
6 changes: 3 additions & 3 deletions docs/refusal-classes.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ The refusal verdict carries `class`, with exactly five values:
| --- | --- | --- |
| `capability-boundary` | The engine cannot do this yet, or has no implemented safe route. This is a T2 capability. | Wait for the capability or escalate. |
| `no-online-safety-problem` | There is nothing for an online schema-change engine to make safe; another tool class owns the work, or the operator runs it directly. | Hand the statement to the named owner. |
| `by-design` | pg-sprite permanently refuses this form — because PostgreSQL offers no online mechanism in any supported version, or because a safer idiom exists; the verdict names the idiom or the deliberate path. This is [RF-5](invariants.md#refusals-and-preflight-rf) seen from the consumer's side. | Use the safer idiom. For a single-relation plain `DROP INDEX`, `REINDEX INDEX`, or `REINDEX TABLE`, `migrate --accept-blocking SCHEMA.TABLE` runs the blocking form under bounded budgets and exits 3 ([lock-budgeted-passthrough.md](lock-budgeted-passthrough.md)); otherwise run it outside pg-sprite in a maintenance window. |
| `by-design` | pg-sprite permanently refuses this form — because PostgreSQL offers no online mechanism in any supported version, or because a safer idiom exists; the verdict names the idiom or the deliberate path. This is [RF-5](invariants.md#refusals-and-preflight-rf) seen from the consumer's side. | Use the safer idiom. For a single-relation plain `DROP INDEX` (not `CASCADE`), `REINDEX INDEX`, or `REINDEX TABLE`, `migrate --accept-blocking SCHEMA.TABLE` runs the blocking form under bounded budgets and exits 3 ([lock-budgeted-passthrough.md](lock-budgeted-passthrough.md)); otherwise run it outside pg-sprite in a maintenance window. |
| `environmental` | The change is supportable, but not here, now, or as this role: a size policy, exhausted budget, privilege, server version, stale plan, or catalog collision stopped it. | Retry, provision, upgrade, re-plan, or escalate. |
| `invariant-violation` | pg-sprite refused because its own input or state is incoherent — a report this build cannot have produced. This is a bug in pg-sprite, not a boundary of it; it is the refusal-verdict face of the fail-closed `ErrInvariantViolation` rule in [SAFETY.md](../SAFETY.md#rules-inside-the-core). | Report it with the verdict; do not retry, wait, or route elsewhere. |

Expand Down Expand Up @@ -76,8 +76,8 @@ routed to provisioning and never to the data change runner.
| `unsupported-statement` | Imperative or create admission sentinel `ErrIfNotExistsUnsupported`; create admission sentinel `ErrDuplicateCreateName` (the sentinel forms of the `if-not-exists` and `duplicate-name` causes below) | `by-design` | — |
| `unsupported-statement` | Create admission sentinel `ErrPartitionOfUnsupported` (the sentinel form of the `partition-of` cause below) | `capability-boundary` | — |
| `unsupported-statement` | `planRefusal` in `pkg/migrate/desired.go`: a statement whose disposition this build does not know, or a plan whose aggregate disposition no statement carries | `invariant-violation` | — |
| `index-statement` | Plain `DROP INDEX` or `REINDEX`, for which the verdict names the concurrent form | `by-design` | — |
| `index-statement` | An already-concurrent maintenance statement that pg-sprite need not wrap | `no-online-safety-problem` | `direct-operator` |
| `index-statement` | Plain `DROP INDEX` or `REINDEX`, for which the verdict names the concurrent form; also a `DROP INDEX CONCURRENTLY` that PostgreSQL rejects (with `CASCADE`, or naming several indexes) | `by-design` | — |
| `index-statement` | An already-concurrent maintenance statement that pg-sprite need not wrap and PostgreSQL accepts | `no-online-safety-problem` | `direct-operator` |
| `not-native-safe-table-too-large` | The size policy prevents a blind bounded attempt | `environmental` | — |
| `insufficient-privileges` | The connected role lacks a required grant | `environmental` | — |
| `not-native-safe-budget-exceeded` | The lock or statement budget cancels an attempt | `environmental` | — |
Expand Down
Loading
Loading