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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ refusal — never a silently wrong or incomplete result:
and environments it cannot run against. The shadow build also refuses a
statement the copy cannot carry: a `USING` expression it cannot reproduce,
or a dropped-and-re-added column it cannot fill.
- **Connection poolers**: point pg-sprite directly at the primary; the
application keeps whatever pooling it uses. pg-sprite's own connection
through a pooler is unsupported, except Supabase's Supavisor session
endpoint for native schema changes. Startup checks usually refuse a
transaction-mode pooler, but passing them does not make it supported
([connections and poolers](docs/limitations.md#connections-and-poolers)).
- **Row security** is included in exports when present, with reviewable before/after differences. The
[atomic executor](docs/atomic-row-security.md) applies RLS-only changes to existing tables
through `migrate --desired` or the Go API. Only the Go API supports binding execution to
Expand Down
41 changes: 41 additions & 0 deletions docs/limitations.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,47 @@ follows `bytea_output`. The copy, the verifier, and the catch-up each pin both s
(`3` and `hex`) in their own sessions, so such a conversion lands the same text from all
three whatever the database or role is configured with.

## Connections and poolers

pg-sprite keeps state on each connection's PostgreSQL session: the execution bounds
(`lock_timeout`, `statement_timeout`) every statement runs under, and the per-table
advisory lock that keeps a second run off the same table. Each of its connections must
therefore stay on one session for its whole life. This is **session affinity**: every
statement sent on a connection runs on the same PostgreSQL backend, so a setting or lock
taken by one statement is still there for the next.

A direct connection always has it. A transaction-mode or statement-mode pooler does not:
it may hand the connection's next transaction to a different backend, one another client
may have just used, so pg-sprite's timeouts could silently vanish and two runs could both
believe they hold the same table lock. pg-sprite checks rather than assumes. When it opens
its pool it reads back the timeouts it set, and on the pool and again on the table lock's
own connection it takes a throwaway advisory lock, then confirms from a second connection
that the first still holds it and that the second cannot take it. A connection that drops
the timeouts is refused with `ErrSessionSettingsDiscarded`, and one that fails the lock
check with `ErrNoSessionAffinity`, before any schema change runs.

These checks guard against session loss they can observe; they cannot prove its absence.
An idle transaction-mode pooler with spare backends can keep a connection on one backend
for the length of the checks and move it later, so a clean start does not make a pooler
path safe. Connect directly to the primary.

This constrains only pg-sprite's own connection. The application keeps whatever pooling
it uses — a client-side pool, or a server-side pooler in session, transaction, or
statement mode — and copy-and-swap reads the application's writes from the WAL through
logical decoding, whichever pooler carried them.

| pg-sprite connects through | Support |
| --- | --- |
| A direct connection to the primary | Supported, and the recommended deployment. Every schema change path, copy-and-swap included, is tested this way. |
| Supabase's Supavisor session endpoint | Supported for the native schema changes listed in [supabase.md](supabase.md#what-works-today). Copy-and-swap through it is unsupported. |
| PgBouncer, or any other server-side pooler, in session mode | Unsupported. Connect directly to the primary. |
| Any pooler in transaction mode | Unsupported. Connect directly to the primary. The startup checks usually refuse it before any work runs, with `ErrSessionSettingsDiscarded` when the pooler drops the timeouts or `ErrNoSessionAffinity` when it moves the connection; the tests pin `ErrNoSessionAffinity` for PgBouncer and Supavisor in transaction mode. A pooler that passes the checks is still unsupported. |
| Any pooler in statement mode | Unsupported. Connect directly to the primary. Opening the pool refuses it with `ErrNoSessionAffinity`: the pooler rejects the check's transaction, which the tests pin for PgBouncer in statement mode. |

The application's pooler and pg-sprite share the server's `max_connections`, which has
to cover the application's backends plus pg-sprite's pool, its dedicated sessions, and,
for copy-and-swap, its replication connections.

## Declarative model boundaries

The declarative front door (desired-state files, `diff`, and schema export)
Expand Down
Loading