Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
9f819e6
docs(test): SP-3 implementation plan — driver matrix expansion
renecannao Jul 8, 2026
4bd0f37
test(pg-compat): multi-language runner image + behavior CLI scaffoldi…
renecannao Jul 8, 2026
feaab3a
fix(pg-compat): sentinel-based not-implemented dispatch + SHOW encodi…
renecannao Jul 8, 2026
e0cdcea
test(pg-compat): Go/pgx behavior program (full contract)
renecannao Jul 8, 2026
da7c6a1
test(pg-compat): Java/pgjdbc behavior program (full contract)
renecannao Jul 8, 2026
4cfeaba
test(pg-compat): Node/node-postgres behavior program (full contract)
renecannao Jul 8, 2026
5295364
test(pg-compat): Prisma behavior program (ORM tier, findings catalogued)
renecannao Jul 8, 2026
d167731
fix(pg-compat): pin client_encoding in Prisma URL (Task 5 review) + d…
renecannao Jul 8, 2026
45b34d0
docs(pg-compat): SP-3 driver matrix docs + spec sync (+ CI budget evi…
renecannao Jul 8, 2026
acd6f0b
docs(pg-compat): correct pgx Parse-once semantics in table + source c…
renecannao Jul 8, 2026
4db6cb4
docs(pg-compat): fix stale plan path in three driver headers (final r…
renecannao Jul 8, 2026
8d2f338
Merge branch 'test/pgsql-sp2-polyglot-foundation' into test/pgsql-sp3…
renecannao Jul 8, 2026
f26787b
test(pg-compat): review follow-ups — Go cleanup parity, connect encod…
renecannao Jul 8, 2026
26fc586
Merge branch 'test/pgsql-sp2-polyglot-foundation' into test/pgsql-sp3…
renecannao Jul 10, 2026
0be6b4b
fix(pg-compat): compile Java behaviors with --release 17 (runtime com…
renecannao Jul 10, 2026
ba207ea
Merge branch 'test/pgsql-sp2-polyglot-foundation' into test/pgsql-sp3…
renecannao Jul 10, 2026
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
249 changes: 249 additions & 0 deletions docs/superpowers/plans/2026-07-08-pgsql-sp3-driver-matrix.md

Large diffs are not rendered by default.

44 changes: 40 additions & 4 deletions docs/superpowers/specs/2026-07-08-pgsql-protocol-testing-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ Adapted from pgcat. `harness/oracle.py`:

- **New workflow** (e.g. `.github/workflows/CI-pg-compat.yml` caller on `v3.0`, reusable on `GH-Actions` per the two-branch split in `doc/GH-Actions/README.md`).
- **Triggers:** nightly `schedule` + `pull_request` gated on the `pg-compat` label.
- **Shape:** build proxysql (debug, `PROXYSQL31=1`) once → cache → job spins up `infra-dbdeployer-pgsql17-repl` (+ Toxiproxy) via the standard `test/infra/control/` runners → runs `pytest test/pg-compat` across both backend modes (§2.2). SP-3 will fan out per-language matrix jobs from the same cached binary.
- **Shape:** build proxysql (debug, `PROXYSQL31=1`) once → cache → job spins up `infra-dbdeployer-pgsql17-repl` (+ Toxiproxy) via the standard `test/infra/control/` runners → runs `pytest test/pg-compat` across both backend modes (§2.2). SP-3 (as built — see §6) runs all languages from one multi-language runner image in the same job; a per-language matrix fan-out remains an option at promote-to-gating.
- **Not gating** on normal PRs (heavy, multi-toolchain) — and, per §2.1, **reporting-oriented** in the discovery phase: the job publishes the failure inventory / xfail summary rather than going red on expected divergences. Nightly failures triaged per `CLAUDE.md`'s "never dismiss as flaky" policy.

---
Expand All @@ -220,9 +220,45 @@ Adapted from pgcat. `harness/oracle.py`:

---

## 6. Roadmap — SP-3 and SP-4 (not in this spec)

- **SP-3 — Driver matrix expansion.** Add adapters under `test/pg-compat/drivers/`: **Java** (pgjdbc, +HikariCP), **Go** (pgx native), **Node.js** (node-postgres, postgres.js, Prisma). Each runs the existing `behaviors/` set + differential cases. CI fans out one matrix job per language from the cached binary. Prisma/pgjdbc are the highest-value targets (aggressive server-side prepared statements historically break poolers).
## 6. Roadmap — SP-3, SP-3b and SP-4

- **SP-3 — Driver matrix expansion (AS BUILT, complete 2026-07-08).** Ran the
existing SP-2 `behaviors/` contract (`connect`, `transactions`, `prepared`,
`session_isolation` — frozen, unchanged) through four more driver stacks:
**Go** (pgx v5.7.5), **Java** (pgjdbc 42.7.4), **Node.js** (pg/node-postgres
8.13.1), and **Node.js/Prisma** (5.22.0, raw-query API only). Each ships one
self-contained CLI program (`<prog> <behavior>`, exit 0/1/2) built into the
pg-compat image by a multi-stage `Dockerfile` extension, invoked from pytest
via subprocess wrappers (`tests/test_behaviors_<lang>.py` +
`tests/_subproc.py::run_behavior`) so the existing xfail catalogue, junit
report, and CI wiring apply unchanged.
- **Scope decision (user-approved 2026-07-08): behaviors only.** The
differential engine (§4.4) stays Python/psycopg-only — its comparison
unit is psycopg's row/type decode semantics, which the other languages
don't share — so it was NOT extended to the new drivers in SP-3. See
SP-3b below for that follow-up.
- CI fans out via a **single fat multi-language image**, not a one-job-
per-language matrix as originally sketched below: same coverage (all
five drivers run every CI invocation), no matrix-job complexity. Revisit
the split if/when this suite is promoted to gating.
- **Result: all five driver stacks pass the full behavior contract with
zero `xfail.toml` entries added** — four distinct prepared-statement
strategies (psycopg auto-prepare@5, pgx's default statement-cache,
pgjdbc's server-side NAMED statements after `prepareThreshold=5` — the
classic connection-pooler breaker — and Prisma's always-prepared Rust
engine) all stay transparent through ProxySQL's connection multiplexing.
See `test/pg-compat/README.md`'s "Driver matrix (SP-3)" section for the
full per-language table (versions, placeholder syntax, encoding-pin
mechanism) and the Prisma raw-vs-ORM caveat.
- **SP-3b — Per-language differential runners (stub, deferred).** Extend each
non-Python driver's behavior program with a differential-case runner that
executes the same case files as §4.4 and emits a normalized result
(status, column names, OIDs/type tags, decoded row values) on stdout for
Python's `compare()` to consume — so the differential engine's comparisons
gain Go/Java/Node/Prisma coverage without reimplementing the comparator
once per language. Deferred pending nightly stability of the SP-3
behaviors-only suite (see `ci-pg-compat.yml`'s non-gating `|| true`); not
scheduled against a specific SP number yet.
- **SP-4 — Chaos & resilience suite.** Build on SP-2's Toxiproxy layer: failover/shunning (1-byte `limit_data` slow-loris), latency toxics, reset-peer, health-check detection and auto-recovery — with **bounded-error-rate assertions** (pgdog/pgcat style: "≤N errors of M", "reroute within T"), exercising the automatic `pgsql_replication_hostgroups` monitor path from §4.1.

---
Expand Down
7 changes: 7 additions & 0 deletions test/pg-compat/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Keep dependency trees and generated Prisma artifacts out of the build
# context: every driver's node_modules (and Prisma's generated client) are
# produced INSIDE their multi-stage build steps, so shipping a host-built
# copy in via `COPY . .` would bloat the image and risk a wrong-platform
# query-engine binary. Report output is host-only too.
**/node_modules
pg-compat-reports
71 changes: 70 additions & 1 deletion test/pg-compat/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,76 @@
# ---- Go builder: static behavior binary (no runtime needed in final) ----
FROM golang:1.23-bookworm AS gobuild
WORKDIR /src
COPY drivers/go/ .
RUN CGO_ENABLED=0 go build -o /out/behaviors-go .

# ---- Java builder: compile against a pinned pgjdbc jar ----
FROM eclipse-temurin:21-jdk AS javabuild
WORKDIR /src
# Pin the driver version explicitly; record bumps in README's driver table.
ARG PGJDBC_VERSION=42.7.4
RUN curl -fsSLo /pgjdbc.jar "https://repo1.maven.org/maven2/org/postgresql/postgresql/${PGJDBC_VERSION}/postgresql-${PGJDBC_VERSION}.jar"
COPY drivers/java/Behaviors.java .
RUN javac --release 17 -cp /pgjdbc.jar Behaviors.java -d /out

# ---- Node deps: install node-postgres against the lockfile ----
FROM node:22-bookworm-slim AS nodebuild
WORKDIR /app
COPY drivers/node/package.json drivers/node/package-lock.json* ./
# The `npm install` fallback must never be reached in normal operation
# (package-lock.json is committed, so `npm ci` succeeds); it exists only
# for first-bootstrap before a lockfile exists.
RUN npm ci --omit=dev || npm install --omit=dev
COPY drivers/node/behaviors.js .

# ---- Prisma deps + client generation (ORM tier, SP3-Task 5) ----
# npm ci pulls prisma (CLI, a devDependency) AND @prisma/client, then
# `prisma generate` produces the client + downloads the query-engine binary
# for binaryTargets=["debian-openssl-3.0.x"] (matching the bookworm/OpenSSL-3
# final image). This stage is bookworm/OpenSSL-3 too, so the engine it
# fetches is the exact one the final stage runs. The engine download needs
# network egress -> the build runs with --network=host (see
# run-pg-compat.bash). PGCOMPAT_PRISMA_URL only has to EXIST for `generate`
# (it does not connect); behaviors.mjs overwrites it at runtime.
FROM node:22-bookworm-slim AS prismabuild
WORKDIR /app
COPY drivers/prisma/package.json drivers/prisma/package-lock.json* ./
# devDependencies (the prisma CLI) are REQUIRED here for `prisma generate`,
# so this is a full install, not --omit=dev. The `npm install` fallback must
# never be reached in normal operation (package-lock.json is committed).
RUN npm ci || npm install
COPY drivers/prisma/schema.prisma drivers/prisma/behaviors.mjs ./
ENV PGCOMPAT_PRISMA_URL="postgresql://build:build@localhost:5432/build?sslmode=disable"
RUN npx prisma generate

# ---- Final: python base + JRE + node runtime + artifacts ----
FROM python:3.11-slim
RUN apt-get update && apt-get install -y --no-install-recommends libpq5 curl && rm -rf /var/lib/apt/lists/*
# openssl/libssl3: the Prisma query engine (debian-openssl-3.0.x) links
# against libssl.so.3 / libcrypto.so.3 at load time.
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq5 curl default-jre-headless openssl \
&& rm -rf /var/lib/apt/lists/*
# Node runtime copied from the official image (bookworm-glibc compatible).
COPY --from=nodebuild /usr/local/bin/node /usr/local/bin/node
WORKDIR /pg-compat
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# Language artifacts under /pg-compat/bin with uniform CLI wrappers.
COPY --from=gobuild /out/behaviors-go /pg-compat/bin/behaviors-go
COPY --from=javabuild /out/ /pg-compat/bin/java-classes/
COPY --from=javabuild /pgjdbc.jar /pg-compat/bin/pgjdbc.jar
COPY --from=nodebuild /app /pg-compat/node-app
# Prisma app: node_modules (with the generated @prisma/client + .prisma
# client + query-engine binary), schema.prisma, behaviors.mjs.
# Known size trade-off: this copies the whole prismabuild dev tree,
# including the prisma CLI devDependency and its non-query engines
# (schema/format engines), not just what behaviors.mjs needs at runtime --
# simple and correct over minimal. Task 6's image-size measurement accounts
# for it; prune here if the numbers demand it.
COPY --from=prismabuild /app /pg-compat/prisma-app
RUN printf '#!/bin/sh\nexec java -cp /pg-compat/bin/java-classes:/pg-compat/bin/pgjdbc.jar Behaviors "$@"\n' > /pg-compat/bin/behaviors-java \

Check warning on line 72 in test/pg-compat/Dockerfile

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Line is too long. Split it into multiple lines using backslash continuations.

See more on https://sonarcloud.io/project/issues?id=sysown_proxysql&issues=AZ9CTMsHrXty-HiHlM3j&open=AZ9CTMsHrXty-HiHlM3j&pullRequest=5910
&& printf '#!/bin/sh\nexec node /pg-compat/node-app/behaviors.js "$@"\n' > /pg-compat/bin/behaviors-node \
&& printf '#!/bin/sh\nexec node /pg-compat/prisma-app/behaviors.mjs "$@"\n' > /pg-compat/bin/behaviors-prisma \
&& chmod +x /pg-compat/bin/behaviors-*
ENTRYPOINT ["pytest", "-q"]
94 changes: 94 additions & 0 deletions test/pg-compat/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,100 @@ WORKSPACE=$(pwd) INFRA_ID=<infra-id> test/pg-compat/run-pg-compat.bash \
# report lands at: ${WORKSPACE}/pg-compat-reports/pg-compat.xml
```

## Driver matrix (SP-3)

Beyond the reference Python/psycopg3 harness, the suite runs the same
4-behavior contract (`connect`, `transactions`, `prepared`,
`session_isolation` — see `behaviors/*.py`, the FROZEN cross-driver
contract) through four more real-world driver stacks, each its own
self-contained CLI program compiled/installed into the pg-compat image by
the multi-stage `Dockerfile` (`drivers/<lang>/`). All five stacks pass the
full behavior contract through ProxySQL with **zero `xfail.toml` entries
added** — every pass below is a genuine pass, not a catalogued divergence.

| Language | Driver | Version | Placeholders | Prepared-statement strategy | Encoding pin |
|---|---|---|---|---|---|
| Python | psycopg3 | 3.2.* | `%s` (client-side) | auto-prepare after `prepare_threshold=5` (driver default) | DSN `client_encoding=UTF8` |
| Go | pgx | v5.7.5 | `$1, $2` | default `QueryExecMode=cache_statement` — Parse once per distinct SQL text (extended protocol), then Bind/Execute-only on every subsequent call via the server-side statement cache | DSN param `client_encoding=UTF8` |
| Java | pgjdbc | 42.7.4 | `?` | server-side NAMED statement after `prepareThreshold=5` (driver default); one `PreparedStatement` object reused for all 50 iterations | `options=-c client_encoding=UTF8` connection property |
| Node | pg (node-postgres) | 8.13.1 | `$1, $2` | UNCONDITIONAL named statements — `Parse` sent once at iteration 0 via `{name, text, values}`, every later call is `Bind`/`Execute` only | `client_encoding` config key |
| Node | Prisma | 5.22.0 | tagged-template (`$queryRaw`) | always-prepared — the Rust query engine has no simple-query mode; every `$queryRaw`/`$executeRawUnsafe` call is a real Parse/Bind/Execute; `connection_limit=1` pins the client to one backend connection | URL param `client_encoding` is accepted but IGNORED by the Rust engine (verified: `LATIN1` in the URL still yields UTF8) — the factory issues an explicit `SET client_encoding TO 'UTF8'` instead |

**Headline finding:** four distinct prepared-statement strategies — including
pgjdbc's server-side NAMED statements (the classic connection-pooler
breaker: `prepared statement "S_1" does not exist`) and Prisma's
always-prepared Rust engine — all stay transparent through ProxySQL's
connection multiplexing.

**Prisma caveat:** the Prisma behavior program (`drivers/prisma/behaviors.mjs`)
exercises only the **raw-query API** (`$queryRaw`/`$executeRawUnsafe`/
`$transaction`), not Prisma's model/ORM query path (`prisma.model.findMany()`
etc.) — there are no real models in `schema.prisma` (a single unused dummy
model exists only to satisfy `prisma generate`). The ORM query path is a
possible future extension, not covered here.

### Running one language

Extra arguments to `run-pg-compat.bash` are forwarded to `pytest`, so a
single language's wrapper file (or `-k`) selects just that driver, e.g.:

```bash
WORKSPACE=$(pwd) INFRA_ID=<infra-id> test/pg-compat/run-pg-compat.bash tests/test_behaviors_go.py -v
```

Per-language wrapper files: `tests/test_behaviors_go.py`,
`tests/test_behaviors_java.py`, `tests/test_behaviors_node.py`,
`tests/test_behaviors_prisma.py` (Python's own behaviors run via
`tests/test_behaviors.py`, in-process rather than as a subprocess).

### Behavior-CLI contract

Every language ships ONE compiled/installed binary at
`/pg-compat/bin/behaviors-<lang>` implementing the same CLI:

```
behaviors-<lang> <behavior> # <behavior> ∈ {connect, transactions, prepared, session_isolation}
```

- **exit 0** — behavior passed.
- **exit 1** — behavior assertion failed; a human-readable reason on stderr.
- **exit 2** — usage or infra error (unknown behavior name, not-yet-implemented
behavior, missing/invalid env); never a behavior-contract failure.
- No stdout output is required on pass.

`tests/_subproc.py::run_behavior(program, behavior)` runs
`[program, behavior]`, translates exit 0/1/2 into pytest pass/fail, and
`pytest.skip`s if the binary is absent from the image (so a partial image
still runs the languages it does have).

### Adding a language

1. Implement the 4 behaviors (`connect`, `transactions`, `prepared`,
`session_isolation`) against `behaviors/*.py` as the frozen reference —
same assertions, same trap adaptations (session-isolation probe is
`SET TimeZone = 'Antarctica/Troll'` / `SHOW TimeZone`, **never**
`application_name` — it's in ProxySQL's `ignore_vars`; every transaction
verification read runs inside its own `BEGIN`/`COMMIT` so it pins to the
writer instead of racing replica lag; every connection pins
`client_encoding=UTF8`; placeholders are driver-native, not psycopg's `%s`).
2. Expose them behind the uniform CLI contract above, in its own
`drivers/<lang>/` directory. Use a per-language table name for the
transactions behavior (`behavior_tx_t_<lang>`) so runs never collide
with another language's.
3. Add a build stage to `Dockerfile` that produces
`/pg-compat/bin/behaviors-<lang>` (a compiled binary, or a thin shell
wrapper invoking an interpreter — see the Java/Node stages for both
patterns) and pin the exact driver version in both the Dockerfile
(`ARG`/lockfile) and this table.
4. Add `tests/test_behaviors_<lang>.py` — a thin subprocess wrapper using
`tests/_subproc.py::run_behavior` and
`@pytest.mark.parametrize("behavior", BEHAVIORS, ids=BEHAVIORS)` so
nodeids stay stable (`tests/test_behaviors_<lang>.py::test_behavior_<lang>[<behavior>]`)
for the `xfail.toml` exact-nodeid catalogue.
5. A behavior that genuinely fails through ProxySQL is a FINDING, not a bug
to hide — add an `[[xfail]]` entry (or a `[[finding]]` if nothing fails
but a divergence was neutralized), never weaken the assertion.

## CI

The suite is wired into CI as `CI-pg-compat` (`.github/workflows/CI-pg-compat.yml`
Expand Down
5 changes: 5 additions & 0 deletions test/pg-compat/behaviors/connect.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,10 @@ def run(Adapter):
a = Adapter()
try:
assert a.exec_simple("SELECT 1")[0][0] == 1
# Uniform with the four SP-3 driver ports (go/java/node/prisma):
# assert the client_encoding=UTF8 pin took effect -- see
# harness/targets.py's encoding rationale (backend DBs default to
# SQL_ASCII; ProxySQL imposes UTF8).
assert a.exec_simple("SHOW client_encoding")[0][0] == "UTF8"
finally:
a.close()
16 changes: 16 additions & 0 deletions test/pg-compat/conftest.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import warnings

import psycopg
import pytest
Expand Down Expand Up @@ -48,11 +49,26 @@ def proxy_conn():


def pytest_collection_modifyitems(config, items):
matched_ids = set()
for item in items:
entry = _XFAILS.get(item.nodeid)
if entry:
matched_ids.add(item.nodeid)
item.add_marker(
pytest.mark.xfail(
reason=f'{entry["reason"]} ({entry["ref"]})', strict=False
)
)

# Catalogue hygiene: a [[xfail]] entry whose test_id matched NO collected
# item is currently a silent no-op (e.g. a typo'd nodeid, or a test that
# was renamed/removed without updating xfail.toml). Warn -- don't fail
# collection -- so a stale/typo'd entry is visible in the run instead of
# quietly doing nothing forever.
for test_id in _XFAILS:
if test_id not in matched_ids:
warnings.warn(
f"xfail.toml entry test_id={test_id!r} matched no collected "
f"test item -- stale or typo'd entry?",
stacklevel=1,
)
Loading
Loading