Feat: model pooler mode in ConnectionParams for PgBouncer transaction-mode + custom types (tabularis-postgresql-plugin#157, #1097 residual)

Closed
#1,126 1 comment 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

A pull request for this has already been merged.

  • #1128 by @aesslinger — merged

Assessment

Difficulty
3/5
Estimated time
1-2 days
Newbie friendliness
25/100
Issue type
Feature
Clarity
Mostly clear
Activity status
Stale
Tech stack
postgresql, rust, typescript

Research direction

Start with the ConnectionParams struct in src-tauri/src/models.rs, where the new pooler_mode: Option<String> field with #[serde(default)] goes, then find the PostgreSQL connection settings form in the frontend to add the dropdown and hint text. The work is already claimed by linked PR #1128 and depends on companion issue tabularis-postgresql-plugin#157, so check those first. Done means the field round-trips through serde and the dropdown shows the max_prepared_statements hint for transaction mode.

Written by the indexing model from the issue text.

Description

Background

rc.9 of the postgresql-plugin fixed the primary PgBouncer prepared-statement failure (#1097) by routing all user-issued queries through the unnamed-statement protocol. However, a residual gap remains: when a query returns columns with non-builtin types (custom enums, composites, extension types), tokio-postgres's internal type resolution fires a named typeinfo_statement that splits across two Syncs — the same pooler-unsafe pattern. Under PgBouncer transaction mode, this fails with ERROR: prepared statement "typeinfo_statement" does not exist (SQLSTATE 26000/42P05).

The plugin can't eliminate this trigger without patching tokio-postgres, but it can adapt its own behavior (avoid its own prepare() calls, surface actionable error messages) — if it knows the connection goes through a transaction-mode pooler. Currently neither the host nor the plugin models this.

What needs to happen on the host side

1. Add pooler_mode to ConnectionParams (src-tauri/src/models.rs)
/// Connection pooler mode, set by the user when connecting through PgBouncer
/// or another transaction-mode pooler. Drives driver-side behavior:
/// the postgresql-plugin avoids its own named-statement `prepare()` calls
/// and surfaces actionable error messages for tokio-postgres's internal
/// `typeinfo_statement` (which requires `max_prepared_statements > 0` on
/// the pooler side for custom types). None for direct connections.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pooler_mode: Option<String>, // "transaction" | "session" | None

Forwarded to the plugin automatically via serde in every RPC params object. The plugin parses it in ConnectionParams::from_value and uses it to gate pooler-unsafe behavior. See companion issue tabularis-postgresql-plugin#157 for the plugin-side consumption.

2. Expose in the PostgreSQL connection UI

A "Pooler mode" dropdown in the connection settings (None / PgBouncer transaction / PgBouncer session). When set to "transaction," show a hint:

When using PgBouncer in transaction mode with custom types (enums, composites, extension types), set max_prepared_statements > 0 in pgbouncer.ini. This enables server-side prepared statement caching so named statements survive connection reassignment.

3. No host-side query-path changes needed

The query execution path is entirely in the plugin. The host's role is to model the pooler mode and forward it — the plugin does the rest. The builtin Postgres driver is frozen (deprecated, not receiving this fix per the #1097 discussion); the plugin is the forward path.

Why this modeling approach (PostgreSQL ecosystem perspective)

The PostgreSQL ecosystem converged on a two-part answer for prepared statements under transaction-mode poolers:

  • Client side: use the unnamed statement where possible (the plugin did this in rc.9).
  • Pooler side: max_prepared_statements > 0 for named statements that can't be eliminated from the client/driver (tokio-postgres's internal typeinfo_statement).

Modeling pooler mode as a first-class connection property lets the plugin adapt its behavior without unreliable runtime detection (SHOW server_version through PgBouncer returns the backend version, not PgBouncer's; SHOW CONFIG requires a separate admin connection). The user explicitly declares "this is a pooler connection," and the system adapts.

This is the same pattern as JDBC's preferQueryMode and libpq's prepare_threshold — connection-level configuration that drives prepared-statement behavior.

Scope

  • ConnectionParams gets one new Option<String> field (pooler_mode) — backward-compatible (#[serde(default)]).
  • Connection UI gets a dropdown + hint text.
  • No query-path changes on the host side.
  • Plugin-side changes tracked in tabularis-postgresql-plugin#157.

References

  • #1097 — the original PgBouncer prepared-statement bug
  • tabularis-postgresql-plugin#157 — companion issue (plugin-side consumption of pooler_mode)
  • psaus's feedback on #1097: "the limitation is triggered by any column with a non-builtin type, and enums, composites and extension types are common use cases, not rare ones"
  • PgBouncer max_prepared_statements docs: https://www.pgbouncer.org/config.html#max_prepared_statements (added in PgBouncer 1.21, 2023)
Dominant language
TypeScript
Stars
5.1k
Forks
335
Avg merge
1d 7h
Merged PRs (30d)
255

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from TabularisDB/tabularis

All issues in TabularisDB/tabularis

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.