Skip to content

fix(postgres): work through connection poolers (disable prepared-stmt cache) - #85

Merged
oesukam merged 1 commit into
mainfrom
fix/pooler-prepared-statements
Aug 4, 2026
Merged

fix(postgres): work through connection poolers (disable prepared-stmt cache)#85
oesukam merged 1 commit into
mainfrom
fix/pooler-prepared-statements

Conversation

@oesukam

@oesukam oesukam commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

The bug

Running a query through a transaction/statement-pooling PgBouncer (or Supabase's pooler, RDS Proxy, etc.) failed with:

driver error: error returned from database: prepared statement "sqlx_s_1" does not exist

sqlx caches prepared statements, but a pooler in transaction mode routes consecutive queries to different backends — so a statement prepared on one isn't visible on the next. This is the same managed/pooled server (postgres.clymist.com) behind the earlier reports, and it made Nembrix unable to query through it at all.

Fix

Set statement_cache_capacity(0) on the Postgres connection options → sqlx uses the unnamed/simple protocol, which survives poolers.

Trade-off: no prepared-statement caching. For a desktop client where queries are user-paced (not a high-throughput hot path), the caching win is negligible — and correctness through the poolers that most managed Postgres providers front is worth far more.

Verified

  • cargo check + fmt + clippy clean on db-postgres.
  • No runtime .prepare()/query! calls remain that would still break.

… cache)

Queries through a transaction/statement-pooling PgBouncer (or Supabase
pooler, RDS Proxy, …) failed with 'prepared statement "sqlx_s_N" does
not exist': sqlx prepares a statement on one pooled backend, then the
next query lands on a different backend where it isn't defined.

Set statement_cache_capacity(0) so sqlx uses the unnamed/simple protocol
that survives poolers. This is a desktop client where queries are
user-paced, so the lost caching is negligible — correctness through the
poolers many managed Postgres providers put in front is worth more.
@oesukam
oesukam merged commit aa84563 into main Aug 4, 2026
6 checks passed
@oesukam
oesukam deleted the fix/pooler-prepared-statements branch August 4, 2026 15:43
oesukam added a commit that referenced this pull request Aug 4, 2026
## Why #85 wasn't enough

#85 set `statement_cache_capacity(0)`, but the user **still** hit
`prepared statement "sqlx_s_1" does not exist` through their PgBouncer
after building locally.

Root cause (from sqlx 0.8 source, `executor.rs`): the cache capacity
only disables statement **reuse**. sqlx still **creates a named**
statement (`sqlx_s_N`) per query, because `Query::persistent` defaults
to `true`. Named statements don't survive a transaction pooler — the
next query lands on a different backend where the name isn't defined.

## The real fix

Set **`.persistent(false)`** on every query → sqlx uses the **unnamed**
prepared statement (per-execution), which works through PgBouncer /
Supabase pooler / RDS Proxy.

Coverage:
- **`lib.rs`**: `bind_params()` (both the SQL editor and **JS
`db.query`** route through `stream`/`execute` → here) + the direct
`SELECT 1` / `pg_backend_pid` / `pg_cancel_backend` / `SET
statement_timeout` calls.
- **`introspect.rs`**: all 8 schema queries — these run **during
connect**, which is exactly why Connect failed.
- **`object_ops.rs`**: both.

JS scripting needs no separate change (it goes through the same driver
methods).

## Verified
- `cargo check` + `fmt` + `clippy` clean.
- Audited: every `sqlx::query*` in the crate now uses an unnamed
statement.
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