fix(backend): honor psycopg3 prepare_threshold=None to disable prepared statements

Why

django_vpg aims to be a drop-in for Django's stock psycopg backend — the same DATABASES dict should work on either, unchanged. The OPTIONS → driver-kwarg mapping diverged from psycopg3 in two ways that broke that promise:

  1. prepare_threshold=None was silently ignored. In psycopg3, prepare_threshold=None means disable prepared statements — the canonical setting for a transaction-mode PgBouncer without max_prepared_statements. django_vpg coerced None → the default of 5, leaving prepared statements on. A config that is safe on psycopg would send server-side prepared statements into a pooler that cannot honor them.
  2. The on/off switch was a bespoke pgbouncer bool with no psycopg/Django equivalent. libpq rejects unknown keys, so a config written for django_vpg was not portable back to the stock backend.

This surfaced from a real GlitchTip deployment: its historical psycopg config was prepare_threshold=None (prepared off, PgBouncer-safe), but under the Rust backend that spelling became a no-op.

What

  • prepare_threshold now follows psycopg3 exactly:
    • int N → prepare after N uses (default 5)
    • 0 → prepare on first use
    • None → disable prepared statements
  • The translation to the driver's prepared_statements bool + int threshold lives in one helper (_prepared_statement_params) shared by the sync and async wrappers, so they can't drift.
  • pgbouncer=True is kept as a documented legacy alias for prepare_threshold=None — existing configs keep working; docs steer to the psycopg spelling.
  • README + CHANGELOG updated.

Tests

New TestPreparedStatementOptions covers default (on, threshold 5), explicit None (off), explicit int, 0 (immediate, not disabled), and both pgbouncer alias directions. Full backend suite green (60 passed), ruff clean.

Follow-up

The matching change in gt_rust.django_backend (glitchtip-rust) and the GlitchTip settings decision (disable via prepare_threshold=None, or enable end-to-end with max_prepared_statements on the poolers) will be handled separately.

🤖 Generated with Claude Code

Merge request reports

Loading
Loading