Skip to content

postgres: serialize tracked schema apply with an advisory lock#1846

Draft
ns-kosti wants to merge 2 commits into
postgres/skip-unchanged-schemafrom
postgres/schema-advisory-lock
Draft

postgres: serialize tracked schema apply with an advisory lock#1846
ns-kosti wants to merge 2 commits into
postgres/skip-unchanged-schemafrom
postgres/schema-advisory-lock

Conversation

@ns-kosti

Copy link
Copy Markdown
Contributor

Builds on #1844 (checksum ledger). Targets that branch so the diff is advisory-lock only.

What

When track_schema_checksums is enabled, concurrent deploys of the same database could race while creating the checksum ledger and applying DDL. This serializes the tracked schema apply with a Postgres session advisory lock:

  • Pins a single pooled connection and holds a foundation-namespaced advisory lock (pg_advisory_lock) across the ledger ensure, checksum read, DDL apply, and checksum write.
  • The lock is session-scoped on a pinned connection (not pg_advisory_xact_lock) so DDL stays non-transactional — CREATE INDEX CONCURRENTLY and similar remain possible.
  • On unlock failure, or when the acquire result is uncertain, the connection's session is ended (removed from the pool) so Postgres releases the lock rather than stranding it on a pooled connection.
  • Acquire is not retried: pg_advisory_lock stacks per session, and a dead connection can't be reused. The wait is bounded by context.
  • Only the opt-in tracked path takes the lock; default behavior is unchanged.

Notes

Test

  • go build ./library/oss/..., go vet, gofmt
  • go test ./library/oss/postgres/prepare/database/helpers/ (incl. new TestAdvisoryLockKey)

ns-kosti added 2 commits June 24, 2026 15:00
## What

Adds an opt-in `track_schema_checksums` intent flag to the OSS Postgres database provider. When enabled, each schema file's checksum is recorded in a `foundation.schema_checksums` table and the file is only re-applied when its contents change, instead of being re-applied on every `ns deploy`.

## Why

The provider currently re-applies every `.sql` file on every deploy. Schema files are expected to be idempotent (e.g. `fn_ensure_table`, `CREATE INDEX IF NOT EXISTS`), so this is safe but wasteful. The checksum table lets us skip files that haven't changed.

## Design

- **Opt-in, default false** — existing databases are unaffected until they enable it.
- **Checksum** = `sha256:` + hex of the raw file bytes. No canonicalization, so any byte change re-applies; this avoids the correctness risk of a SQL-aware normalizer at the cost of an occasional harmless re-run.
- **Table** = `foundation.schema_checksums(path TEXT PRIMARY KEY, checksum TEXT, updated_at TIMESTAMPTZ)`.
- **Written only after a successful apply**, so `updated_at` stays constant across redeploys when nothing changed.
- **Path validation** — empty and duplicate paths are rejected, since `path` is the primary key.
- **Single batched read** of stored checksums (`WHERE path = ANY($1)`); reads and writes retry on retryable errors.
- **Logs** each schema file it applies (skipped files are silent).

## Caveat

The table records that a file's bytes were applied successfully once; it is not an assertion that the live schema matches the file. With the `fn_ensure_*` helpers, editing a definition changes the checksum (so the file re-runs) but the helper may still no-op against an existing object. It's a re-execution cache.

## Tests

Unit tests cover checksum determinism, byte-sensitivity, the `sha256:` prefix, and path validation. End-to-end skip/re-apply coverage is tracked in #1845.
When track_schema_checksums is enabled, concurrent deploys of the same
database could race while creating the checksum ledger and applying DDL.
Pin a single pooled connection and hold a foundation-namespaced session
advisory lock for the ledger ensure, checksum read, DDL apply and
checksum write, so only one deploy mutates a given database's schema at a
time.

The lock is session-scoped on a pinned connection to keep DDL
non-transactional (e.g. CREATE INDEX CONCURRENTLY remains possible). On
unlock failure, or when the acquire result is uncertain, the connection's
session is ended so Postgres releases the lock rather than stranding it
on a pooled connection. Only the opt-in tracked path takes the lock;
default behavior is unchanged.
@ns-kosti
ns-kosti marked this pull request as draft June 24, 2026 13:57
@ns-kosti
ns-kosti force-pushed the postgres/skip-unchanged-schema branch from 09b7528 to a46ed8e Compare June 24, 2026 14:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant