Skip to content

v0.3.0

Choose a tag to compare

@Kiran01bm Kiran01bm released this 07 Sep 03:55
· 3 commits to main since this release
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