Skip to content

Releases

v0.5.2

Choose a tag to compare

@github-actions github-actions released this 12 Nov 08:45
v0.5.2
59a6176

A release that lets a caller watch a whole migrate run, adds a percent and an ETA to progress snapshots, gives progress-tracking callers drift-checked entry points for index adoptions, constraint validations and index renames, and stops a healthy copy-and-swap stream being ended by the server's WAL release cadence. Every command and every change that ran on v0.5.1 runs the same way.

Why v0.5.2 and not v0.6.0

For a v0.5.1 caller of the CLI, migrate, diffplan or executor, everything here is additive: new fields are omitempty, a nil tracker behaves exactly as before, and no existing executor signature changed. The one exception is the low-level pkg/decode slot-lag API: decode.SlotStatus.WithinLagCeiling gives way to decode.SlotLagWatch, which keeps state across readings, and an unkeyed SlotLagExceededError struct literal stops compiling because the type gains fields. That API is the copy-and-swap catch-up's own, and pg-sprite's applier is its only known caller. Read the Upgrade notes if you call pkg/decode directly or pin the progress format_version.

Highlights

Progress

  • migrate.Options.Progress reports a whole run to one tracker — Run is one step and RunDesired has one step per planned statement. Within a step the detail shows what is running now: each statement of a safer sequence, a concurrent index build with the server's phase and counters, and a copy-and-swap's copy, catch-up and checksum phases. New step-scoped entry points (executor.RunStep, ExecuteNativeAsStep, RunSequenceAsStep, ExecuteCreateAsSteps) and schemachange.RunOptions.Tracker / Step report into a tracker the caller already started. (#353)
  • A derived percent and eta_ns — worked out on each poll for the copy, the checksum pass and a concurrent index build (one band per pg_stat_progress_create_index phase), never 100 while a step runs, and omitted where no total exists. The checksum pass now reports work.rows_to_hash. (#352)
  • Drift-checked progress entry points — ExecuteNativeAdoptingWithProgress, ExecuteNativeValidatingWithProgress and ExecuteIndexRenameWithProgress are the progress-tracked counterparts of the adopting and validating sequence paths and ExecuteIndexRename. Each re-checks the planned definition under its lock and refuses with adopted-index-drift, validated-constraint-drift or renamed-index-drift. ExecuteNativeWithProgress carries no planned definition, so a caller that runs diff-planned ADD CONSTRAINT ... USING INDEX or VALIDATE CONSTRAINT statements should switch. (#354)

Copy-and-swap

  • The slot-lag ceiling is judged on unconfirmed WAL — the stream's own lag (pg_current_wal_lsn() − confirmed_flush_lsn) over SlotLagCeiling ends the run at once with cause stream-behind; retained WAL over it ends the run with cause wal-retained only once it has stayed over for the server's release interval plus one catch-up cycle. A healthy stream under steady writes is no longer ended mid-copy because the server releases WAL only every 15 s. (#336)

Security and CI

  • Go 1.26.11 and klauspost/compress v1.20.4 clear 31 reachable advisories govulncheck reported at v0.5.1. (#349)
  • Every PR, and main daily, is scanned for reachable vulnerabilities. (#350)

Upgrade notes

  • Progress report format_version 6 → 7. Additive: detail.percent, detail.eta_ns and work.rows_to_hash. A consumer pinning 6 must reject or add support for 7; the history is in docs/progress-report.md. Plan (19), lint (10) and suggest (5) are unchanged, as is the exit-code ladder (0/1/2/3).
  • pkg/decode (breaking). SlotStatus.WithinLagCeiling(c) is removed: build decode.NewSlotLagWatch(c, grace) once per slot and call Judge(status, now) on each reading in order (not safe for concurrent use). SlotStatus gains Unconfirmed; ErrSlotLagUnknown also covers an unmeasurable unconfirmed WAL; SlotLagExceededError keeps its name for errors.As and gains Cause (SlotLagStreamBehind, SlotLagWALRetained) and Unconfirmed. A volume needs headroom above the ceiling of about the write rate times the grace.
  • Library API. New: migrate.Options.Progress, executor.RunStep, executor.ExecuteNativeAsStep, executor.RunSequenceAsStep, executor.ExecuteCreateAsSteps, executor.ExecuteNativeAdoptingWithProgress, executor.ExecuteNativeValidatingWithProgress, executor.ExecuteIndexRenameWithProgress, schemachange.RunOptions.Tracker / Step, decode.SlotLagWatch, decode.NewSlotLagWatch, decode.SlotLagCause, decode.RestartReleaseInterval. Setting both Options.Progress and CopySwap.Copier.Tracker is refused before anything runs.
  • Go toolchain. The module's go directive is now 1.26.11, so a consumer module needs Go 1.26.11 or later.

Docs

Contributors

Full changelog: v0.5.1...v0.5.2

v0.5.1

Choose a tag to compare

@github-actions github-actions released this 11 Nov 05:43
v0.5.1
ccc9e11

A release that widens what the declarative door and migrate will run, hardens the copy-and-swap route under sustained writes, and moves to pgx v5.12.0. Nothing a v0.5.0 user relies on stops working: the copy-and-swap route is still a preview you open per run, and every change that ran before runs the same way.

The declarative door now covers the shapes Supabase projects actually write: a new table created together with its row security in one transaction, table changes in a file that declares row security, policies that read another table, and extension types named unqualified. migrate runs ALTER TYPE ... ADD VALUE natively, and enum-typed columns are published as supported. On the copy-and-swap route, the decoding stream keeps itself alive while the catch-up is busy, a deadlocked flush is retried, and the live verify pass settles chunks row by row. A failed verdict now carries the server's SQLSTATE and message. Plan reports move to format_version 19; lint (10), progress (6) and suggest (5) are unchanged, as is the exit-code ladder (0/1/2/3).

Why v0.5.1 and not v0.6.0

Every change here is additive for a v0.5.0 caller: new JSON fields are omitempty, the new reason code and library symbols add arms rather than change them, and statements that used to be refused now run instead of the reverse. The one reclassification — conversion drift seen by verification or the catch-up is now refused with copy-conversion-drift instead of an unclassified failed — makes those stops match what the copier already reported. Read the Upgrade notes if you route on verdict outcomes or read connection strings with ssl=true.

Highlights

Declarative door: row security and extension schemas

  • A missing table is created together with its row security — migrate --desired on a row-security file whose table does not exist creates the table, enables RLS, and creates its policies in one transaction, so no role that new tables are granted to by default (on Supabase, anon and authenticated) can reach it in between. The verdict reports table_created: true. (#318)
  • Table changes run in a file that declares row security — when the live row security matches the file and only the table definition differs, the file plans and runs like one without row security, through diffplan.PlanWithRowSecurity and migrate.RunDesiredWithRowSecurity; it never writes a policy or a row security setting. (#332)
  • Policies may read schema-qualified tables — a membership-check policy that reads another table is admitted when the file names that table schema-qualified. An unqualified, CTE, or database-qualified name is still refused (ErrPolicyRelationDependency), as is a table carrying the policy's own table name (ErrPolicyReadsOwnTable). (#320)
  • --extension-schema / WithExtensionSchemas — names schemas such as extensions that a desired file's unqualified extension types, functions, and operators resolve from, searched after the target schema and public so they never shadow either; plans still print each such name qualified. Accepted on diff and migrate --desired, including files that declare row security, where it widens the table definition only. (#319, #333)
  • Options.WithoutAcknowledgements() — returns the policy with every typed acknowledgement cleared, so an adapter can hand RunDesired the desired-state view of the Options it hands Run, derived from the irreversible-operations registry instead of cleared field by field. (#326)

migrate and the capability matrix

  • ALTER TYPE ... ADD VALUE runs natively without locking a table — under the brief lock budget, after checking that the name resolves to an enum, the role has USAGE on its schema and membership in its owning role (else insufficient-privileges, with the grant that fixes it), and confirming before commit that the labels written belong to that type (else the new permanent enum-type-drift code, nothing added). (#334)
  • Enum-typed columns on plain tables are supported on both front doors — adding one (catalog-only with a constant default), changing its default, indexing it, and creating a table with one, with the enum in the table's schema or public. (#347)

Copy-and-swap fixes

  • The decoding stream answers the server while the catch-up is away from it — a stream-owned heartbeat reports the already-confirmed position whenever no report has left within a quarter of wal_sender_timeout, so a slow flush, snapshot, or slot read, or a pool busy with copy workers, no longer gets the stream ended mid-copy with unexpected EOF. The slot never moves past what the caller confirmed. (#331)
  • A flush rolled back as the deadlock victim is requeued — a catch-up flush the server cancels with 40P01 is flushed again on the next cycle, bounded by CatchupOptions.DeadlockAttempts, matching what a copier pass already did. (#298)
  • The live verify pass settles a differing chunk key by key — a chunk settles once every one of its rows has caught up, rather than only when no write touched it between two reads, so a chunk under sustained writes no longer ends UnresolvedError{Cause: hot}. (#322)
  • Conversion drift is copy-conversion-drift from every phase — drift in a USING expression's functions or operators noticed by verification or the flusher now refuses with the same environmental cause the copier uses, and a refused verdict from a blocked-writes run still says the table is fenced. (#287)
  • A stopped catch-up wait says what it waited on — Run returns *schemachange.CatchupWaitError with the catch-up's status and how far the capture was short of its marker, instead of a bare context error. (#292)
  • Catalog reads are pinned to pg_catalog — checkpoint.Store.Ensure qualifies every relation, function, type, and operator it reads, so a caller's own pool with a shadowing search_path cannot change what it finds. (#308)

Verdicts and schemadiff

  • A failed verdict carries sqlstate and server_error — read from the first *pgconn.PgError in the failure's chain; the server's DETAIL line stays out because it can quote row values. (#313)
  • A desired expression is compared across replays — a check whose text PostgreSQL prints differently each time it is reparsed (nested row comparisons) no longer plans a drop and an add on every export, which also unblocks unrelated changes migrate --desired refused alongside it. (#314)
  • Index constraints one CREATE TABLE would merge are refused — pull refuses a table with two PRIMARY KEY, UNIQUE, or EXCLUDE constraints declared the same way (ErrUnrenderableMergedConstraints) instead of writing a file that silently rebuilds the table without one, and a desired file whose CREATE TABLE the server would merge is refused as unsupported-statement (ErrDesiredConstraintMerged). (#315)

Connections

  • pgx v5.12.0, with the DSN scanners following its libpq grammars — pg-sprite's own reading of a connection string's sslmode, which decides between honoring the caller's mode and enforcing verify-full on RDS/Aurora, now agrees with pgx on every keyword and URL rule, checked by a differential fuzz target. (#344)
  • The session-bound read-back no longer takes a catalog lock — NewPool reads lock_timeout and statement_timeout back with SHOW, so a catalog lock held by another session's DDL can no longer cancel the read and refuse a correctly bounded session. (#335)

Testing and CI

  • make load-proof drives the built binary through a copy-and-swap over a large table under a concurrent write workload, judged by timing-free oracles. (#321)
  • Every irreversible-operations registry entry and CLI acknowledgement flag is run against its gate on a real server; the registry's completeness is stated as invariant RF-12. (#288, #291)
  • Pinning tests for the resume clear's bounds and the table lock lookup's granted predicate; the release workflow's test steps run in a read-only job; the Supabase fixture's service pins are refreshed. (#312, #325, #305, #327)

Upgrade notes

  • Plan report format_version 18 → 19. Additive: the report-level extension_schemas field, present when --extension-schema is given and covered by the fingerprint. A consumer pinning 18 must reject or add support for 19; the history is in docs/plan-report.md.
  • Verdict fields. sqlstate and server_error on failed verdicts; table_created on a desired-state run that created the table with its row security. One new executor code, enum-type-drift (permanent). No new outcome or refusal reason.
  • Conversion drift outcome. A copy-and-swap run stopped by conversion drift during verification or the catch-up now reports outcome: refused with copy-conversion-drift, where it used to report an unclassified failed. Route on the cause, as the copier's refusal already required.
  • Changes that were refused now run. ALTER TYPE ... ADD VALUE; a row-security file whose table is missing; table changes in a file that declares row security; policies reading schema-qualified tables; extension types named unqualified with --extension-schema.
  • Connection strings. In a postgres:// URL the last repeated parameter now wins and ssl=true means sslmode=require, as pgx applies them; on RDS/Aurora a URL with ssl=true is honored as require rather than forced to verify-full. Keyword strings follow libpq's backslash rules. Strings pgx rejects are rejected as before.
  • Library API. New: migrate.RunDesiredWithRowSecurity, diffplan.PlanWithRowSecurity, migrate.Options.WithoutAcknowledgements, statement.DesiredSchema.WithExtensionSchemas, statement.DesiredWithRowSecurity.WithExtensionSchemas, schemachange.CatchupWaitError, applier.CatchupOptions.DeadlockAttempts.
  • Dependencies. github.com/jackc/pgx/v5 v5.10.0 → v5.12.0 (wi...
Read more

v0.5.0

Choose a tag to compare

@github-actions github-actions released this 08 Nov 09:30
v0.5.0
c3b84bc

A breaking release for anyone who ran copy-and-swap on v0.4.1. The copy-and-swap route — the one a general ALTER COLUMN ... TYPE takes — is now a preview that stays closed until the run opens it: pg-sprite migrate --copy-and-swap on the CLI, CopySwapOptions.Enabled as a library caller. Without it a statement the planner routes there is refused before the route's preflight runs or anything is written, with exit 2, reason: copy-and-swap-refused, cause: copy-and-swap-not-enabled, class: capability-boundary. Every native change runs exactly as before. The route closes because the engine is complete end to end but has not yet been exercised under production-like load; that proof is the next piece of work, and the gate opens when it lands.

Around the gate, this release fixes four ways a copy-and-swap run could stop or confirm the wrong thing, types the outcome of a data-fault abandon, closes the last acknowledgement gap on the declarative door, and adds five planner refusals for changes PostgreSQL itself would reject after taking the table's lock. Plan reports move to format_version 18 and lint to 10; progress (6) and suggest (5) are unchanged, as is the exit-code ladder (0/1/2/3).

Why v0.5.0 and not v0.4.2

A v0.4.1 binary runs copy-and-swap whenever the planner routes a statement to it. The same command on this release exits 2 unless --copy-and-swap is on the command line, and a library caller who set CopySwapOptions.Connection alone still compiles and now gets a refusal instead of a run. That is a behaviour break, and in the 0.x line a minor bump is where breaking changes go, as the v0.4.1 notes said. Read the Upgrade notes before taking this release.

Highlights

Copy-and-swap is a preview route you open per run

  • --copy-and-swap / CopySwapOptions.Enabled — the opt-in. The zero value keeps the route closed; the CLI flag cannot be combined with --dry-run. A refused statement touched nothing: target resolution and planning read the catalog to route it, the route's preflight never runs. Every runnable example in the docs and the demo tour now carries the flag. (#301)
  • abandoned: true on the verdict — a failed copy-and-swap verdict says whether the Runner discarded the run on a data fault (nothing left behind: rerun the corrected statement plain) or could not (the checkpoint row and the run's hold on the source stand: rerun with --restart). Both carry code: data-fault; the typed field is what an adapter branches on instead of the detail's wording. (#302)

Copy-and-swap fixes

  • A chunk pass rolled back as a deadlock victim gets another pass — when the copier and the catch-up each insert a unique value the other holds uncommitted, the server breaks the cycle by cancelling one; a cancelled copier pass is now retried like a unique-value collision instead of stopping the run. (#295)
  • Reconcile clears the shadow above a partial watermark — a slot-loss reconcile now removes the shadow rows the earlier run committed but never checkpointed before it recopies, so a unique value that moved out of one of them can no longer collide with the recopy on every attempt and leave the run in reconciling. (#279)
  • The table lock holder lookup is scoped to the session's own database — TableLockSession.Confirm and LookupTableLockHolder read pg_locks with a database predicate, so a lock held under the same key in another database of the same server no longer confirms as this database's lock. (#284)
  • RunDesired rejects every acknowledgement — Restart and AcceptBlocking are now rejected with ErrRestartNotSupported and ErrAcceptBlockingNotSupported, the way Force, AllowDestructive, and BlockWrites already were, so a value left on an embedder's configuration cannot discard a stopped run's checkpoint on the first copy-and-swap statement of a later plan. One registry in pkg/migrate now lists every irreversible operation with the acknowledgement that lifts its refusal, and a unit test fails CI when a new acknowledgement or destructive shape arrives without an entry. (#300, #282)
  • The pool floor is proven, not asserted — two live tests show that with every copy worker holding a connection, a pool of CopySwapMinConns still flushes a concurrent source write into the shadow and a pool of only the workers does not; docs/schemabot-integration.md states which part of the floor each connection carries. (#304)

Planner: refuse what the server would refuse after taking the lock

  • Retyping or dropping a column a row-level security policy reads is refused with dependent-policy, naming the policies to drop and recreate around the change. The dependent-view read becomes preflight.ReadColumnDependents, which reads views and policies per column in one statement. (#303)
  • Retyping or dropping a column a trigger's UPDATE OF list or WHEN condition names, a generated column reads, or a partition key uses is refused with dependent-trigger, dependent-generated-column, or partition-key. PostgreSQL 14 would otherwise drop the generated column silently along with the dropped column. (#309)
  • Adding a NOT NULL column with no default to a table that holds rows is refused with not-null-without-default; the plan suggests a constant default or a backfill first. On an empty table it stays catalog-only. (#310)
  • Dropping the primary key of a published table under REPLICA IDENTITY DEFAULT is refused with replica-identity, as it already was under USING INDEX. Publication membership is read from the publication catalogs in the transaction's snapshot (FOR TABLE, FOR ALL TABLES, FOR TABLES IN SCHEMA on 15+, partitions through a parent); the executor's re-check under the drop's lock reads the same facts. (#307)
  • A VALIDATE CONSTRAINT inside a transaction block that already holds a blocking lock on the table is flagged by lint as validate-in-transaction, following COMMIT, savepoints, nested BEGIN, REINDEX TABLE, TRUNCATE, and LOCK TABLE the way the server does. The planner treats VALIDATE as the online idiom only beside operations that take SHARE UPDATE EXCLUSIVE alone; beside anything else the statement is refused as rewrite-required and told to split. (#311)
  • schemadiff keeps a unique index a foreign key references — a plain CREATE UNIQUE INDEX named by a foreign key's conindid was hidden from the live model, so a desired file that matched the table planned the index again and failed with 42P07. (#306)

CI

  • The integration matrix runs the race detector on the newest PostgreSQL major only, pkg/schemachange runs on its own runner in that job, and the goneLock test helpers release the dead session without waiting out the keepalive. (#297, #299, #296)

Upgrade notes

  • Copy-and-swap needs an explicit opt-in. CLI: add --copy-and-swap to every migrate invocation that may route to the shadow table; it cannot be combined with --dry-run. Library: set migrate.CopySwapOptions.Enabled; Connection alone no longer runs. Without it, exit 2 with copy-and-swap-refused / copy-and-swap-not-enabled, raised before the route's preflight. Orchestrators that leave the route closed get this refusal for every statement the router assigns to it and run every native statement as before.
  • A run stopped under v0.4.1 needs the flag too. Resuming it, starting it over with --restart SCHEMA.TABLE, or cleaning it up all go through the route, so they all need --copy-and-swap. A stopped live run keeps its slot, and the slot keeps WAL, until you do one of them.
  • Plan report format_version 15 → 18. Additive: the dependent-policy, dependent-trigger, dependent-generated-column, partition-key, and not-null-without-default reasons; the dependent_policies, dependent_triggers, dependent_generated_columns, and partition_key_tables decision fields, each present whenever its dependents block the change, whatever the decision's reason. A consumer pinning 15 must reject or add support for 18; the history is in docs/plan-report.md.
  • Lint format_version 6 → 10. The five reasons above join the embedded Reasons vocabulary (lint never reports them; they are judged from live facts) and the validate-in-transaction code is new.
  • Verdict fields. abandoned (bool, failed copy-and-swap verdicts only) and the four dependent_* / partition_key_tables lists on a refused verdict. No new outcome, refusal reason, or executor code; exhaustive switches need no new arm.
  • Changes that previously ran now refuse. Dropping the primary key of a REPLICA IDENTITY DEFAULT table a publication publishes refuses even under --allow-destructive; retyping or dropping a column a policy, trigger, generated column, or partition key depends on refuses; ADD COLUMN ... NOT NULL without a default refuses on a table with rows; an ALTER TABLE that validates a constraint beside a blocking operation refuses as rewrite-required. PostgreSQL refused each of these at the server before, after taking the lock; the difference is that the dry run now says so.
  • Library API. migrate.ErrAcceptBlockingNotSupported is new and RunDesired returns it, with ErrRestartNotSupported, for the acknowledgements it rejects; preflight.ReadColumnDependents replaces the dependent-view read; statement.Op carries the lock predicate the planner and lint share. No dependency changes.

Docs

Read more

v0.4.1

Choose a tag to compare

@github-actions github-actions released this 04 Nov 04:25
v0.4.1
22b16cb

A major release — the largest pg-sprite has shipped, and a breaking one. It ships the copy-and-swap engine: a statement PostgreSQL can only run as a table rewrite — a general ALTER COLUMN ... TYPE today — is now executed online by building a shadow table, copying and verifying it in chunks, replaying live writes from a logical replication slot, and swapping it in under a bounded lock. Around it, migrate applies declarative --desired files, refuses destructive and app-breaking changes until a typed acknowledgement names the table, and runs a two-stage stop with checkpointed resume. Plan reports move to format_version 15, progress to 6, lint to 6, suggest to 5; nine executor outcome codes and two refusal reasons are added; statements and connections that v0.3.3 accepted now refuse by default (see Upgrade notes). The exit-code ladder (0/1/2/3) is unchanged.

The version is 0.4, not 1.0, because pg-sprite is still pre-1.0: under semantic versioning a 0.x minor bump is where breaking changes go, and v0.3.3 → v0.4.x is that signal. Read the Upgrade notes before taking this release.

Why v0.4.1 and not v0.4.0

There is no v0.4.0 release. The v0.4.0 tag was pushed, but its release workflow timed out in the test step before GoReleaser ran, so no GitHub release and no binaries were ever published for it. Because the Go module proxy had already recorded the tag, it was left in place rather than moved: a moved tag would not reach module consumers and would break go.sum for anyone who had fetched it directly. #294 fixed the workflow step, and the first tag cut after that fix is v0.4.1. Treat v0.4.1 as the release that v0.4.0 was meant to be; it is v0.4.0 plus #274, #283, #286, #290, #293, and #294. Do not depend on v0.4.0.

Highlights

Copy-and-swap: a type rewrite runs online

  • Router and front door — the planner routes a general ALTER COLUMN ... TYPE to copy-and-swap instead of refusing it, migrate drives the route to one verdict, and a run that commits reports the new executed-via-copy-and-swap outcome. Static inadmissibility is reported in --dry-run, and renames never reach the route. (#184, #183, #208, #231)
  • Shadow build by execute-and-introspect — the shadow table is created under the table lock, the statement is applied to the empty shadow, and the resulting shape is introspected from the catalog; nothing is derived by AST transformation. Indexes are paired across the retyped column by definition, never by name, and renamed back at swap. A new identity or serial column, a new column the copy cannot fill, or a shadow left without the source's primary key refuses at the build. (#117, #121, #137, #138, #271, #235, #274)
  • Parallel chunked copy — a keyset chunker over the proven primary key copies the table in bounded transactions, carries a retyped column through the statement's own USING expression, pins what that conversion resolves to, and retries a chunk whose insert collides with a unique index — committing the rest of the chunk around the held-off rows and retrying only those. (#127, #128, #144, #167, #243, #239, #283)
  • Chunk checksum with live repair — every chunk is compared between source and shadow in one snapshot with SHA-256 digests, under an explicit divergence policy that mints the proofs the cutover gate consumes; a differing chunk is recopied against the catch-up and compared again when a moved unique value collides. (#134, #136, #143, #161, #191, #244)
  • Live writes through a logical replication slot — pkg/decode creates, streams, inspects, and drops the route's slot with caller-confirmed feedback; pkg/applier buffers changes per key, drains them against the copier's position, flushes column-wise with a unique-move fallback, and waits for a delivered commit until the source shows it visible. A slot-lag ceiling and an orphan-slot reaper bound what the server has to retain. (#150, #151, #148, #149, #152, #156, #241, #265)
  • Gated cutover and bounded swap — the ST-5 fidelity checklist mints CutoverReady; the swap runs at READ COMMITTED under a bounded lock, hands identities and sequences across, checks the write fence, and re-derives the swapped-table proof from the catalog for a post-swap resume. --keep-old retains the source under its _old name for a rollback window. (#137, #139, #146, #187, #252)
  • Offline mode — --block-writes=SCHEMA.TABLE fences the table with a trigger instead of streaming through a slot, for servers without logical decoding; every write that reaches the fence fails with SQLSTATE 55006 until the swap. (#163, #260, #262)

Stop, resume, start over

  • Checkpoint — one resume row per target in the engine-owned pgsprite.pgsprite_checkpoint table, written with a guarded upsert and loaded through a typed identity check; the row derives the table shape from its columns and carries the shadow proof. A row in an unknown phase reads as incompatible, not as an invariant violation. (#141, #162, #216)
  • Two-stage stop and --restart — the first Ctrl-C / SIGTERM saves where the copy reached and exits; a resume picks the run up from the checkpoint; --restart=SCHEMA.TABLE discards the leftover shadow, checkpoint, and fence or slot under the run's lock and starts over. (#227)
  • Data-fault abandon — a run that stops on a data fault (a value the USING conversion cannot produce, a unique collision the repair cannot settle) abandons itself and leaves nothing behind; the demo tour walks the fault and the rerun. (#228, #266, #267)
  • Slot-loss reconcile — when the server has dropped the slot, the run reconciles from the checkpoint instead of failing, keeping the publication when a reconcile replaces the slot. (#159, #236, #293)

Acknowledged risk is typed and named

  • --allow-destructive=SCHEMA.TABLE — DROP COLUMN, DROP CONSTRAINT, ALTER COLUMN ... DROP NOT NULL, and a DROP INDEX run under --accept-blocking refuse with reason destructive-change until the acknowledgement names the resolved table; the planner decides destructiveness for every operation shape explicitly, and a refused DROP INDEX shows the index's live usage. (#253, #273, #276, #277, #259)
  • --force=SCHEMA.TABLE covers app-breaking renames — a column rename is refused as rewrite-required unless the override names the table, on the CLI and through the library alike. (#230)
  • --accept-blocking=SCHEMA.TABLE — the bounded accepted-blocking path reserved in v0.3.3 is now on the CLI for a single-relation DROP INDEX, REINDEX INDEX, or REINDEX TABLE (exit 3); DROP INDEX ... CASCADE is refused. (#115, #189)

Declarative schema changes apply through migrate

  • migrate --desired=FILE --schema=NAME — converges one table from a desired-state file through the same planner and executor as --alter, refuses statically inadmissible steps in dry run, and reports a run executed only when a fresh plan is empty. (#126, #217)
  • Row security — schemadiff exports, compares, and shows reviewable row-security changes; the executor applies them atomically and binds execution to the reviewed SQL. (#113, #118, #119, #123, #130)
  • Model coverage — foreign keys appear in the plan and refuse for apply (declarative-foreign-key); table storage parameters, column-owned sequence declarations, and changed generated-column expressions are modelled; a live NOT VALID constraint is validated in place; a live unique index is adopted for a new key constraint; the plan resolves against the target schema's own types and functions. (#169, #176, #177, #170, #195, #180, #179)

Planner and native paths

  • Server-proven rewrite decisions — the server proves whether adding a column rewrites the table; an array type change routes to a rewrite unless the element is unchanged; the relabel route refuses type shapes it cannot fully read; column statistics are refreshed after a native type change. (#181, #196, #220, #198)
  • New refusals — dropping what the replica identity index keys on, retyping or dropping a column a view reads, and cascading drops are refused with typed reasons; an unnamed CHECK or FOREIGN KEY is named so its online sequence can be built; every attribute of a primary key or unique constraint is carried into its online sequence. (#178, #223, #219, #197)
  • ALTER INDEX ... RENAME TO — parsed as its own kind and run natively for a plain index. (#238, #248)
  • lint — flags a CONCURRENTLY operation inside a transaction block. (#222)
  • diff — reports an index left invalid by an unfinished build as an advisory rather than planning its drop. (#249)

Connections and the server boundary

  • Session affinity is required — statement-mode poolers refuse with ErrNoSessionAffinity; a copy-and-swap connection on another database refuses; the pool is sized for copy-and-swap and one too small refuses; session timeouts below one millisecond refuse. (#256, #229, #257, #201)
  • TLS and host detection — RDS host detection and sslmode discovery are hardened, and every connection attempt is verified when --ca-cert is set. (#120, #186)
  • Catalog reads are pg_catalog-qualified — every introspection and decision read across preflight, executor, migrate, schemadiff, and schemachange names pg_catalog explicitly, so a hostile search_path cannot redirect them; the scratch build's stand-in index and foreign-key reads are pinned under the build's own search_path, and every function dbconn.TerminateBlockers calls is qualified for embedders who run it under their own search_path. (#202, #204, #205, #207, #209, #210, #211, #213, #214, #290, #286)
  • Per-table advisory locks — concurrent runs against different tables no longer serialise on one lock. (#116)
  • Release workflow — the tagged tree is tested against one long-lived database, the same shape as the CI matrix, instead of per-test containers. (#294)

Observability

  • **Pro...
Read more

v0.3.3

Choose a tag to compare

@Kiran01bm Kiran01bm released this 16 Sep 09:26
v0.3.3
ecd75bc

A patch release that gives every refusal a typed routing class, publishes the support matrix from the binary, lands the bounded accepted-blocking library path, and lets greenfield tables be created under a named owner. Plan reports move to format_version 5 with additive fields; five outcome codes are added; the refusal-reason vocabulary and every existing library signature are unchanged. The accepted-blocking path is library-only — the CLI has no flag for it in this release.

Highlights

Refusals become a typed routing contract

  • verdict.Refusal — every refusal is constructed from a validated Class, Reason, and, exactly for no-online-safety-problem, Owner; Classes, Owners, ParseClass, ParseOwner, the five class constructors, Verdict.WithRefusal, and Verdict.Refusal expose the closed contract. Classification is keyed on typed causes or refusal sites rather than reason prose, and unknown or incoherent combinations fail closed under RF-7. Plan and desired-state JSON carry class and owner; plan report format_version 3 → 4. (#97)
  • Replay and docs pin the emitted vocabulary — replay expectations consume the engine-emitted class and optional owner, reject run-scoped classes, validate the complete manifest before starting the harness, and size output from its rows. Documentation guards compare every refusal reason, class, owner, progress format version, and exit-code row in both directions. (#101, #111)

One embedded capabilities matrix

  • pkg/capabilities — the embedded 53-row YAML matrix is the source for the generated support tables. Area, Tier, StatusMark, EnginePath, FrontDoorStatus, FrontDoors, and Row, with Load, Rows, Validate, and RenderDocument, expose validated machine-readable claims tied to verdict.Reasons(); make gen-capabilities renders the marked regions and the CI/release gate fails without modifying the working page when they drift. (#98, #100)
  • pg-sprite capabilities — a new offline command prints the exact matrix embedded in that binary as an aligned terminal table or stable --json, whose top-level object carries the binary version and capabilities rows. The capabilities guide adds canonical jq recipes that select by fields or ID, never array position. (#99, #104)

Accepted blocking is explicit, bounded, and distinguishable

  • verdict.AcceptedBlockingDecision / AcceptedBlockingEligible — a closed registry admits only one-relation DROP INDEX, REINDEX INDEX / REINDEX TABLE, and the blocking partitioned-parent index build represented by the same in-process typed refusal proof. Site-keyed eligibility deliberately fails closed after a JSON round trip; plan statements expose blocking_passthrough_eligible, moving plan report format_version 4 → 5. (#105)
  • executor.ExecuteAcceptedBlocking — runs one admitted blocking index statement in an engine-owned transaction under an explicit non-zero BlockingBudget{LockTimeout, StatementTimeout} and returns BlockingReport. *BlockingExecutionError separates a server failure from *BlockingOutcomeUnknownError; lock exhaustion executes nothing, while a lost outcome requires catalog inspection. AB-1 and AB-2 record both bounds and the no-retry lock-budget rule. (#107)
  • verdict.OutcomeExecutedWithoutOnlineSafety — Verdict.WithAcceptedBlocking retains the refusal identity and explicit budgets after a commit; ErrAcceptedBlocking maps it to reserved process exit 3 so it cannot look like online-safe exit 0. The four-code ladder is pinned across the public docs. This release provides the library primitive and plan eligibility only; no CLI flag invokes it. (#108, #103, #109)

Greenfield tables can be born under a named owner

  • migrate.Options.CreateOwner — when non-empty, every bounded create step runs SET LOCAL ROLE <owner> before its SQL, so the table, serial sequences, indexes, and default privileges use that owner. The option is one scalar owner per Options; a per-schema map is deferred until a real target needs it. Empty keeps the existing connected-role path unchanged. (#114)
  • preflight.CheckCreatePrivilegesAs — proves that the owner exists, the connected role can assume it, and the owner has schema USAGE and CREATE; ErrCreateOwnerNotFound is a typed insufficient-privileges refusal. The gate uses pg_has_role(..., 'MEMBER') (plus PostgreSQL 16's SET-option check), not USAGE, so a NOINHERIT engine role is admitted. CreationRole.Owner() and SetsRole() separate the intended owner from Role(). (#114)
  • Owner verification after commit — executor.ErrCreateOwnerMismatch, ErrCreateOwnerUnverified, and *CreateOwnerMismatchError fail closed when the catalog disagrees or cannot be read. The executor never repairs ownership with ALTER ... OWNER; invariant ST-9 requires the committed owner to be proven. (#114)

Connection and compatibility boundaries

  • dbconn.NewPool preserves execution bounds through poolers — lock and statement timeouts are sent as startup parameters and applied on every physical connection, then read back. ProveSessionAffinity, ErrSessionSettingsDiscarded, and ErrNoSessionAffinity refuse connections that discard settings or rebind between server sessions; session-mode pooling remains supported. (#81)
  • Supabase compatibility harness — a pinned local stack exercises native schema changes, API and pooler boundaries, Realtime continuity, privilege failures, and readable DDL cases in CI; the public guide documents direct versus pooled connections, supported workflows, and tested limits. PgBouncer test readiness now waits for a bounded login and query through the pooler rather than only an open port. (#106, #112)
  • Concurrent-index executor layout — the previous monolithic implementation is split into focused cic_* files for errors, budgets, builds, invalid-index handling, and backend classification, with no behavior, identifier, or signature change. (#110)

Upgrade notes

  • Plan report format_version 3 → 5. Version 4 adds class and owner to refused statements and aggregate target-dependent refusals; version 5 adds statements[].blocking_passthrough_eligible to every refusal. All are explanatory and excluded from the fingerprint. A consumer pinning version 3 must reject or add support for 5.
  • Five new executor outcome codes, 28 → 33. blocking-outcome-unknown (non-permanent), invalid-blocking-budget (permanent), unsupported-accepted-blocking (permanent), create-owner-mismatch (permanent), and create-owner-unverified (non-permanent) appear in executor.Codes() and Code.Permanent(). Exhaustive consumers need five arms.
  • New verdict outcome and process exit. executed-without-online-safety and exit 3 mean an explicitly accepted blocking statement committed under both budgets. Exit 0 remains online-safe success, 1 failure or usage error, and 2 refusal with nothing committed. The CLI path is reserved but not wired in this release.
  • Library API additions, no removals. New in verdict: the refusal class/owner/proof APIs, accepted-blocking eligibility APIs, OutcomeExecutedWithoutOnlineSafety, ExitCodeAcceptedBlocking, ErrAcceptedBlocking, and ErrInvalidAcceptedBlocking. New in executor: BlockingBudget, BlockingReport, BlockingExecutionError, BlockingOutcomeUnknownError, ExecuteAcceptedBlocking, the create-owner errors, and CreateOwnerMismatchError. New in preflight: CheckCreatePrivilegesAs, ErrCreateOwnerNotFound, CreationRole.Owner, and CreationRole.SetsRole. New in migrate: Options.CreateOwner. New package capabilities exports the typed matrix and renderer. New in dbconn: ProveSessionAffinity, AdvisoryLockHolder, ErrSessionSettingsDiscarded, and ErrNoSessionAffinity. No exported symbol was removed and no existing function signature changed.
  • Pooled connections are checked more strictly. dbconn.NewPool now needs a second connection while proving affinity and refuses transaction-mode endpoints or sessions that did not retain lock_timeout and statement_timeout. Use a direct or session-mode endpoint with one spare server connection.
  • No refusal-reason vocabulary change. Refusals now carry one of five classes and, where required, one of four owners; consumers should route on those typed fields rather than rebuilding classes from reason text.
  • Create-owner scope is deliberately scalar. One migrate.Options value names one create owner for its greenfield target. A per-schema owner map remains deferred until a concrete multi-schema caller requires it.

Docs

Read more

v0.3.2

Choose a tag to compare

@github-actions github-actions released this 10 Sep 05:24
v0.3.2
b641b9d

A patch release that closes the loop on the create path and on indexes left invalid by an unfinished build, and hardens every catalog read against a shadowing search_path. Plan reports move to format_version 3 with one additive field; two outcome codes are added; the refusal-reason vocabulary is unchanged. The copy-and-swap package skeletons, proof types and convergence test harness land here with no user-visible behaviour change — type rewrites still refuse as not-native-safe-rewrite-required.

Highlights

The plan carries why a create shape was refused

  • plan.Statement.Cause — a refused create statement in a diff-source plan now carries the executor's typed CreateShapeCause (json:"cause,omitempty"), stamped once at the refusal boundary by RefuseUnsupportedCreateShape; a refusal that carries no typed cause fails closed with ErrInvariantViolation. The text renderer, the greenfield refusal summary and the desired-schema path read Cause.Description() instead of each re-deriving the cause from the error. cause is explanatory and excluded from the plan fingerprint, alongside reason. Plan report format_version 2 → 3. (#84)

The create path proves the table owns the names it claimed

  • executor.ErrCreateNameMismatch — after the create path's CREATE TABLE commits and before any index build, ExecuteCreate reads the born table's owned constraint-index and sequence names and compares them with the first-choice names the desired file claimed. A claim the table does not own — a concurrent writer took t_pkey inside the probe's time-of-check window and the server silently chose t_pkey1 — fails step 1 with the permanent outcome create-name-mismatch; *CreateNameMismatchError{Missing, Unclaimed} names both sides, sorted. The table is left in place because a committed creation cannot be proven unused; unclaimed extras alone are not a mismatch. (#85)
  • executor.ErrCreateNamesUnverified — a read of the owned names that cannot complete is its own non-permanent outcome, create-names-unverified, never a pass; the read's cause stays in the chain for errors.Is but never picks the code. migrate.RunDesired sets FailedStep / FailedStepSQL for both create-path codes. The first-choice prediction in pkg/statement now mirrors the server's numbering for INCLUDE columns, repeated column and expression contributions, and identity SEQUENCE NAME, verified two-oracle against every supported shape. (#85)

Indexes left invalid by an unfinished build

  • schemadiff.Index.Invalid — pg_index.indisvalid joins the comparison on plain tables: a live index that matches the desired definition but is invalid diffs to a create-index change instead of "no change", and the diff never emits a drop for an invalid entry whatever the desired file says about its name. On a partitioned parent indisvalid = false means unattached partition indexes, so validity plays no part there. A freshly pulled baseline that re-diffs to a lone create-index is the documented signal to check indisvalid. (#86)
  • executor.DropAbandonedIndex(ctx, pool, sql, budget) — removes an abandoned invalid index with the same proof, quarantine and OID-verified DROP INDEX CONCURRENTLY sequencing as RebuildAbandonedIndex, without resuming the build. IndexRecoveryReport.Build stays zero for drop-only recovery. (#90)

Catalog reads resist a shadowing search_path

  • dbconn.NewPool un-shadows the catalog — an AfterConnect hook runs once per physical connection, reads SHOW search_path, and removes every explicit pg_catalog entry that has a non-catalog entry before it. PostgreSQL searches pg_catalog implicitly before every entry unless the path names it, so a role configured with search_path = app, pg_catalog had made every unqualified pg_class resolve to app.pg_class. Stripping, rather than prepending, leaves current_schema() and migrate's unqualified target resolution unchanged. Recorded as invariant CO-9. (#93)
  • dbconn.LocalSearchPath(schemas ...string) — the single builder for every transaction-local SET LOCAL search_path pg-sprite sets (identifiers sanitized, the same rewrite applied); schemadiff and executor use it, and a test walks the production sources and fails if any other file sets a search_path. (#93)
  • schemadiff.ListManagedTables(ctx, pool, schema) — the catalog enumeration pull baselines a schema from (ordinary and partitioned tables; partitions, extension-owned relations, views, materialized views, foreign tables and sequences excluded), exported so an owner detecting undeclared tables calls the same query instead of the SQL the docs used to show inline. Every relation, operator and cast is pg_catalog-qualified. Decision reversed from the earlier recipe: INHERITS children are listed — an undeclared table is undeclared whether or not pull can write its file. (#92)

Copy-and-swap foundations

  • Package skeletons and shared types — pkg/copier, pkg/checksum, pkg/decode, pkg/applier, pkg/checkpoint, pkg/schemachange, each with its SAFETY.md core row. copier.Chunk / copier.Watermark, decode.ChangeKind / LSN / Column / ChangeEvent, checkpoint.Phase / Checkpoint, and the proof types dbconn.TableLock, preflight.CopySwapTarget, checksum.VerifiedShadow, checksum.CleanWatermark are declared without constructors; the PR that earns each proof adds its constructor. The router still marks BackendCopyAndSwap unavailable. (#87)
  • Convergence test harness — internal/testutil gains a seeded, rate-limited DML load generator (StartLoad / LoadSpec, spec validated up front, serialization races counted) and a convergence oracle that runs both EXCEPT directions and both counts in one read-only REPEATABLE READ snapshot. A repository-wide test in internal/safety derives the proof-type registry from code shape and requires SAFETY.md, the review checklist and the TCB model to name every type. (#87, #91)

Upgrade notes

  • Plan report format_version 2 → 3. One additive field: statements[].cause, present exactly when a diff-source plan with table_exists: false refuses a statement as unsupported-statement; absent for every other refusal. A consumer pinning format_version == 2 must accept 3. Fingerprints are unchanged; cause is excluded like reason. The duplicate-name refusal detail no longer appends the colliding name; the typed cause carries it.
  • Two new outcome codes. create-name-mismatch (permanent) and create-names-unverified (non-permanent) appear in Codes() and Code.Permanent(); both mean the CREATE TABLE committed and was not rolled back. A consumer with an exhaustive switch over outcome codes needs two arms; the routing table in docs/schemabot-integration.md describes the rerun path — rename or drop the occupant, re-diff to an empty plan.
  • Library API additions, no removals. New in plan: Statement.Cause. New in executor: DropAbandonedIndex, ErrCreateNameMismatch, ErrCreateNamesUnverified, *CreateNameMismatchError. New in schemadiff: Index.Invalid, ListManagedTables. New in dbconn: LocalSearchPath, TableLock. New in preflight: CopySwapTarget, PKType. New packages copier, checksum, decode, applier, checkpoint, schemachange. No signature changed.
  • Pooled sessions may see a rewritten search_path. Only an explicit pg_catalog entry with a non-catalog entry before it is removed; a leading or sole pg_catalog and every other entry are left as configured. The rewrite is a per-connection set_config, so behind a transaction-mode pooler it applies to the server connection the hook ran on.
  • Diff round-trip exception. A table carrying an invalid index re-diffs to exactly its rebuild rather than to an empty plan, and a desired file that removes an index whose live entry is invalid diffs to an empty plan — the diff will not drop it. Both are recorded in docs/limitations.md and docs/pull.md.
  • No refusal-reason vocabulary change. Progress reports are byte-compatible with v0.3.1.

Docs

Contributors

Full changelog: v0.3.1...v0.3.2

v0.3.1

Choose a tag to compare

@Kiran01bm Kiran01bm released this 07 Sep 06:20
v0.3.1
7004102

A patch release carrying the two library additions that landed just after the v0.3.0 cut, giving orchestrators two typed signals around the create path: a closed vocabulary for why a desired statement's shape was refused, and a catalog read of the constraint-index and sequence names a table actually ended up owning. No format_version, outcome-code or refusal-reason vocabulary changes; the CLI's behaviour is unchanged.

Highlights

Create-shape refusals carry a typed cause

  • executor.CreateShapeCause — the create path's shape refusals (PARTITION OF, INHERITS, LIKE, OF type, IF NOT EXISTS, CONCURRENTLY on a table born this run, a duplicate relation name, a multi-operation statement, an unsupported kind) now carry one of nine flat kebab-case causes instead of only prose, so a consumer branches on the cause the way it branches on outcome codes. CreateShapeCauses() enumerates the closed set; Description() is the single owner of each human sentence; *CreateShapeError{Cause, Name} unwraps to the sentinel the cause belongs to, so existing errors.Is callers and the OutcomeCode mapping are untouched; CreateShapeCauseOf(err) reads the cause through wrappers, including a SequenceStepError. A cause added without a Description() or Unwrap() arm fails the suite rather than rendering "unknown create-shape refusal" at runtime. (#82)

Read back what a table owns

  • preflight.LookupOwnedRelationNames(ctx, pool, schema, table) — one catalog read returning the constraint-index names (pg_constraint, contype p/u/x; a foreign key's borrowed index on the referenced table is excluded) and column-owned sequence names (pg_depend deptype a/i, relkind S) a table owns, sorted and duplicate-free, whether the server invented the name or the desired file stated it. PostgreSQL never raises 42P07/42710 for an implicit name taken inside the absence proof's time-of-check window — it silently suffixes (t_pkey1, t_id_seq1) — and this read is what lets a caller detect that after the CREATE TABLE step. Schema is required; a missing relation is ErrTableNotFound, a non-table at the name is ErrNotTable. The create path does not yet run the comparison itself. (#83)

Upgrade notes

  • Library API additions, no removals. New in executor: CreateShapeCause and its nine constants, CreateShapeCauses(), CreateShapeCause.Description(), *CreateShapeError (Unwrap() returns the existing sentinel), CreateShapeCauseOf(err). New in preflight: OwnedRelationNames{ConstraintIndexes, Sequences}, LookupOwnedRelationNames, ErrTableNotFound, ErrNotTable. No signature changed.
  • Six create-shape sentences lost their prefix. The INHERITS, LIKE, OF, CONCURRENTLY, multiple-operations and unsupported-kind refusals no longer start with statement is not a shape the create path can run:; every renderer already says the create path refuses the statement. PARTITION OF, IF NOT EXISTS and duplicate-name text is byte-identical. A consumer matching refusal text should switch to CreateShapeCauseOf; errors.Is against ErrUnsupportedCreateStep, ErrPartitionOfUnsupported, ErrIfNotExistsUnsupported and ErrDuplicateCreateName keeps working.
  • No format_version, outcome-code or refusal-reason vocabulary change. Plan and progress reports are byte-compatible with v0.3.0; fingerprints are unchanged.

Docs

Contributors

Full changelog: v0.3.0...v0.3.1

v0.3.0

Choose a tag to compare

@Kiran01bm Kiran01bm released this 07 Sep 03:55
v0.3.0
68d1b5d

Concurrent index builds learn to run under an orchestrator — a caller-owned deadline instead of a fixed server timeout, a gated stop path for operators, lock-aware progress, and a proven recovery for the invalid index a dead build leaves behind. Around that, pg-sprite pull baselines a live schema into desired files, greenfield plans disclose exactly what the create path runs, and the create path refuses shape and name problems before anything executes.

Highlights

Concurrent index builds an orchestrator can own

  • ConcurrentBudget.CallerOwned runs CREATE INDEX CONCURRENTLY with statement_timeout = 0 and the caller's cancellable context as the statement's only bound — for a build that should live as long as a lease can be renewed rather than a fixed number of minutes. A context that cannot be cancelled is refused before any session is acquired, so the statement stays bounded by construction; lock_timeout stays disabled in both modes, and both settings are reset before the session returns to the pool. The bounded mode and every existing caller are unchanged. (#76, #78)
  • Cancellation is a three-way partition in both modes: the caller's own context ending is cancelled-by-caller whichever of the server's 57014 or the client's context error arrives first; a 57014 under a live context is cancelled-externally; a 57014 at the bounded deadline is a *BudgetError. An orchestrator can now tell my lease lapsed, an operator stopped it, and the server's budget killed it apart. (#76, #78)
  • progress.Tracker.CancelBuild(ctx) is the operator's stop path: pg_cancel_backend over the tracker's reserved session, under the lock that guards the build's lifecycle, only while the build is active and only against a backend that is running a statement. The backend PID never leaves the tracker. (#76, #78)
  • Progress snapshots report lockers: work.lockers_total, work.lockers_done, detail.current_locker_pid from pg_stat_progress_create_index, so a build stuck in the "waiting for old snapshots" phase names what it is waiting on. Snapshots also carry detail.statement, the canonical SQL of the step in flight — including on the terminal failed snapshot. (#72, #76)

Proven recovery of abandoned invalid indexes

  • executor.RebuildAbandonedIndex (library-only) removes and rebuilds the invalid index a dead concurrent build leaves behind, after proving abandonment: a bounded LOCK TABLE … IN SHARE UPDATE EXCLUSIVE MODE excludes any live concurrent index command — visible or hidden — then the entry is re-verified by OID, quarantined as pgsprite_abandoned_<oid>, dropped with DROP INDEX CONCURRENTLY under OID verification, and the index rebuilt. The result is an IndexRecoveryReport. (#79)
  • The classifier never reads an unobservable builder as absent. pg_stat_progress_create_index hides another role's command without pg_read_all_stats and records nothing under track_activities = off; that case is now its own code, invalid-index-builder-unobservable, and the lock proof decides. An invalid entry the server will not drop concurrently — a partitioned table's index (invalid by design when built ON ONLY), an index partition, or a constraint's index — is invalid-index-not-droppable and is left for an operator, never quarantined. invalid-index-preexisting is gone — see upgrade notes. (#79)

Baseline export: pg-sprite pull

  • pg-sprite pull --url=… [--schema public] [-o schema] writes one desired-state .sql file per ordinary or partitioned table in a schema, with per-table PULLED / REFUSED / ERROR outcomes and a summary — not fail-fast. Constructs the declarative model cannot express refuse per table. The demo tour proves the loop: pull, then diff --json against the export returns zero statements. (#67, #74)

Greenfield plans say what will run

  • Greenfield plan statements disclose the create path's execution: exec_sql is the statement as written, decisions are metadata-only, and no CONCURRENTLY substitution is shown for a table that does not exist yet — the create path runs the desired file's builds verbatim under bounded lock_timeout / statement_timeout. The disclosure is a no-op unless the report proves the table absent, so a live table's online sequence cannot be collapsed. (#69)
  • Shape refusals surface at plan time: PARTITION OF, INHERITS, LIKE, OF, IF NOT EXISTS, and a set that claims the same relation name twice are refused on the plan as unsupported-statement, from the same rules the executor applies at admission; diff --sql comments out the refused statement with its cause. (#70)
  • Claimed relation names are proven free before the first step: explicit index names, the first-choice name of each PRIMARY KEY / UNIQUE / EXCLUDE constraint index, and each serial / identity column's <table>_<column>_seq are probed in pg_class; an occupant is a create-collision with nothing executed. (#71)

Verified against Aurora-shaped failures

  • The AWS-boundary tier now pins reader read-only behaviour (SQLSTATE 25006 is terminal), connection loss mid-change (terminal execution-failed, no catalog trace), and metadata failover with an open writer session. (#75)

Upgrade notes

  • Progress report format_version moves from 1 to 3. New fields: detail.statement; work.lockers_total, work.lockers_done, detail.current_locker_pid. A strict consumer should re-sync from docs/progress-report.md.
  • Greenfield plan fingerprints change value (the fingerprint hashes exec_sql, which now commits to what runs). format_version stays 2. A consumer holding a pre-upgrade greenfield fingerprint gets one plan-fingerprint-mismatch and re-plans; fingerprints for existing tables are unaffected.
  • Implicit-name collisions are now refused — a policy change. A desired file whose PRIMARY KEY / UNIQUE index or serial sequence first-choice name is already taken used to converge with a server-suffixed name (t_pkey1, t_id_seq1); it now refuses with create-collision until the occupant is dropped or renamed, the constraint's index is named explicitly, or the column uses a named sequence or non-serial type.
  • Outcome-code vocabulary changed — a consumer with a total switch over executor.Codes() will not build until it routes the new members. invalid-index-preexisting is removed; invalid-index-build-in-flight, invalid-index-abandoned, invalid-index-other-table, invalid-index-not-droppable and invalid-index-builder-unobservable are added alongside the existing invalid-index-own-leftover and invalid-index-unproven, and cancelled-by-caller joins cancelled-externally. The invalid-index family is not one retry class: Code.Permanent() says which members a retry unchanged would reproduce (other-table, not-droppable) and which wait on a recovery, a builder, or a re-taken proof; see docs/invalid-index-recovery.md. cancelled-by-caller is the caller's own context ending and belongs with the operational outcomes, not the refusals.
  • Library API. progress.Tracker.StartStep takes the step's statement (StartStep(step, operation, statement)). ConcurrentBudget gains CallerOwned (mutually exclusive with Overall; ErrCallerOwnedOverallBudget, ErrCallerOwnedNeedsCancellableContext). statement.ImplicitIndexNames is renamed statement.ImplicitRelationNames and now also returns the sequence names a serial / identity column owns. New: executor.RebuildAbandonedIndex, executor.CreateShapeRefusals, preflight.CheckNamesAbsent, plan.DiscloseGreenfieldExecution, plan.RefuseUnsupportedCreateShape, progress.Tracker.CancelBuild. New error sentinels: ErrCancelledByCaller, ErrNoActiveBuild, ErrBuildNotRunning, ErrUnboundedBudget.
  • Refusal-reason vocabulary: unsupported-statement now also covers plan-time create-shape refusals; create-collision now also covers index, constraint-index and sequence names, not only the table.

Docs

Contributors

Full changelog: v0.2.0...v0.3.0

v0.2.0

Choose a tag to compare

@Kiran01bm Kiran01bm released this 30 Aug 06:09
v0.2.0
127f158

Desired-state execution grows a library front door and learns to create the table it is converging toward — declaring a brand-new table on a fresh database now works end to end under the engine's proof discipline.

Highlights

Greenfield CREATE TABLE through the declarative front door

  • A desired file whose table does not exist yet now converges — the engine proves the name is free (CheckTableAbsent, read against pg_class so a missing privilege can never masquerade as absence), proves the role holds CREATE on the schema, then runs the CREATE TABLE and its index builds as brief bounded steps under the usual lock_timeout / statement_timeout budgets. A rerun converges to an empty plan. (#60, #62, #63)
  • Refusals are typed and land before anything runs — an occupied name (relation or standalone type) is the new create-collision reason; a privilege gap is insufficient-privileges; PARTITION OF, INHERITS, LIKE, OF, and IF NOT EXISTS shapes refuse with unsupported-statement. (#63)
  • Plan order states execution order — desired-file statements are ordered at parse (the CREATE TABLE first, indexes keeping input order after it) everywhere the file replays: the greenfield plan, the create path's steps, and the scratch-schema introspection that derives a diff once the table exists. (#63)

Library-level desired-state execution

  • migrate.RunDesired converges one live table onto its parsed desired schema: derive the convergence plan, admit it as a whole (table existence, destructive guard, routed dispositions, optional ExpectedFingerprint pin), then run each planned statement back through the same migrate.Run pipeline with fresh introspection and classification, stopping at the first refusal or failure. The result carries the plan, per-statement verdicts, and an aggregate outcome with committed-prefix detail. (#49, #53)
  • Two new refusal reasons: destructive-change (the plan discards live structure — desired-state execution never runs it) and plan-fingerprint-mismatch (the plan derived at execution time is not the pinned reviewed plan). Library-only for now — the migrate --desired CLI verb follows separately. (#53)

Classification and resolution fixes

  • Alter attempts pin search_path to the target schema (then public) whenever the statement is schema-qualified — the same resolution the create path and introspection use, so unqualified secondary names (a column's type, an expression's function) can no longer silently bind a same-named object in public. (#63)
  • ALTER COLUMN ... DROP NOT NULL now classifies as destructive — dropping NOT NULL discards the same guarantee as dropping the equivalent constraint; DROP DEFAULT deliberately stays non-destructive. (#53)

Upgrade notes

  • A greenfield desired plan now executes where it previously refused — automation branching on the old unsupported-statement refusal for a not-yet-existing table sees the create run instead.
  • Callers relying on ambient search_path resolution must qualify secondary names — alter attempts on a schema-qualified statement now resolve unqualified secondary names (a column's type, an expression's function) in the target schema, not the session's search_path.
  • ALTER COLUMN ... DROP NOT NULL now reports destructive: true in plan reports, and desired-state execution refuses it like any other drop. DROP DEFAULT deliberately stays non-destructive.
  • Refusal-reason vocabulary additions: create-collision, destructive-change, plan-fingerprint-mismatch. A consumer enumerating the closed set should re-sync.

Docs

Contributors

Full changelog: v0.1.0...v0.2.0

v0.1.0

Choose a tag to compare

@github-actions github-actions released this 19 Aug 04:18
v0.1.0
7058fb1

First release of pg-sprite — an online schema-change engine for PostgreSQL that classifies every DDL operation before running it, substitutes the safer native form where one exists, and refuses honestly (with typed guidance) where it doesn't.

Highlights

Planner: classify first, never guess

  • All SQL enters through one parse boundary using the real PostgreSQL grammar — libpg_query compiled to Wasm (wasilibs/go-pgquery), so a parse failure is a typed error, never a guess, and builds stay pure Go. (#6)
  • The declarative diff is derived by execute-and-introspect — desired DDL runs in a transaction-scoped scratch schema and the model is read back from PostgreSQL's own catalogs, then rolled back; no reimplementation of PostgreSQL's semantics to drift out of date. (#6, #20)
  • Every operation is classified native-safe · needs-rewrite · refuse before anything runs — the classifier and router are the one place the safety decision lives, and both front doors (imperative --alter, declarative --desired) converge on the same pipeline. (#7)
  • One versioned plan report for both front doors — diff and migrate --dry-run emit the same machine-readable plan (format_version: 2), with typed guidance on rewrite-required statements. (#8, #37)
  • Column and table renames classify as app-breaking — a rename is an application contract change, not a metadata tweak, and the verdict says so. (#19)

Execution: the safer form by default

  • The optimistic front door commits instant changes and refuses the rest with typed verdicts — a metadata-only change just runs; anything else gets a not-native-safe verdict naming the reason. (#5)
  • Blocking forms are substituted with the safer native sequence by default — CREATE INDEX runs as CREATE INDEX CONCURRENTLY, ADD CONSTRAINT UNIQUE builds the index concurrently first; overriding requires a typed --force acknowledgement naming the exact target table. (#28, #30, #32)
  • CREATE INDEX CONCURRENTLY recovers fail-closed — a failed concurrent build detects the invalid index and cleans up rather than leaving it behind. (#15)
  • Every statement runs under bounded lock_timeout with bounded retry — DDL that cannot get its lock backs off and retries a bounded number of times instead of queueing behind traffic. (#27)
  • Preflight names the exact missing GRANTs — an engine role without the needed privileges gets a typed refusal listing the GRANT statements to run, before any DDL is attempted. (#31)
  • Parent-level index builds on partitioned tables are refused — the unsupported partitioned-parent case fails closed instead of taking locks it cannot honor. (#33)
  • Execution progress is machine-readable across strategies — a strategy-wide Progress() contract backed by pg_stat_progress_create_index and sequence step position. (#41)

CLI: compiler-grade diagnostics

  • migrate --dry-run, diff, lint, and suggest render as colored compiler-style diagnostic reports — with a stable exit-code contract, and --json everywhere for machines; the raw SQL script moved behind --sql. (#36, #43, #46)
  • lint produces offline typed findings — file/line/column, severity, reason codes, and the safer form inline. (#9)
  • suggest recommends safer native forms with typed caveats — and is explicit that a safer form is not a semantic equivalent. (#13, #14)
  • A runnable demo tour doubles as the artifact smoke test — make demo walks the CLI the way a user invokes it; VHS-generated terminal GIFs are committed with their tapes. (#39, #42)

Testing: real databases, all supported majors

  • The full suite runs against real PostgreSQL 14 → 18 — integration tests via testcontainers, with the version matrix as a merge gate. (#3, #4, #21)
  • An AWS-boundary test tier via Ministack — RDS/Aurora API fidelity, managed-password rotation reconnects, and control-plane error contracts are exercised in CI without touching AWS. (#11, #12, #25, #29)
  • Docs-only changes skip the test matrix — with exclusion patterns that are themselves tested. (#23)

Docs

  • Vision, architecture, and low-level design ship with the code — including the SchemaBot integration contract, the engine-role provisioning contract (tiered access for schema changes), and a README written as decided outcomes with demo GIFs. (#2, #10, #17, #18, #22, #24, #35, #40, #44, #45)

Upgrade notes

  • First tagged release. The module is github.com/block/pg-sprite; the CLI installs via the attached binaries or go install github.com/block/pg-sprite/cmd/[email protected].
  • The plan report is format_version: 2 and verdicts carry typed guidance. Consumers pinning pre-release pseudo-versions should re-validate their plan/verdict mapping when moving to the tag. (#37, #38)
  • This release requires Go 1.26. Consumers embedding the module must be on that toolchain or newer.

Contributors

Full Changelog: https://github.com/block/pg-sprite/commits/v0.1.0