Skip to content

benchmarks: the PostgreSQL-wire twin of the served SQL cells, with an answers-identical gate - #563

Merged
tae898 merged 3 commits into
bench/arcadedb-import-26111from
bench/fourarm-pgwire
Nov 3, 2026
Merged

tae898 merged 3 commits into
bench/arcadedb-import-26111from
bench/fourarm-pgwire

Conversation

@tae898

@tae898 tae898 commented Nov 3, 2026 •

Copy link
Copy Markdown

Stacked on #554 (bench/arcadedb-import-26111), draft, never for main. Implements the PostgreSQL-wire twin of ArcadeDB's served SQL arms that DECISIONS #166, #171 and #237 (3) call for: the same served SQL cell runs a third time with its read statements on ArcadeDB's postgres plugin through psycopg, as a labelled sensitivity row beside the headline (official arcadedb-driver 0.2.0) and the lean HTTP twin. It is never a headline: the wire flatters ArcadeDB most (#166 addendum 2).

What runs on the wire. A twin job per served SQL cell, built by runner.build_jobs like the lean twin (runner.PGWIRE_SQL_ARMS, run_id ending _clientpgwire, BENCH_ARCADEDB_HTTP_CLIENT=pgwire). lean_http.PgSession sends POST /query SQL over psycopg 3 (autocommit, prepare_threshold=None, so every execution is an unnamed Parse/Bind/Execute and the prepared-statement path of upstream ArcadeData#9009 is not used; :name placeholders become bound %(name)s parameters). Everything else stays on lean HTTP and is counted by reason: /command (writes, DDL, sqlscript transactions), /batch bulk loads (#171 (c)), the server probe, Cypher, session ids, dict parameters. The twin therefore times the read and query phases only; its write-phase numbers are lean HTTP and are not to be quoted (arcadedb_pgwire_scope on every wire row, PROTOCOL.md and CAMPAIGN.md row 6 say so).

Cells covered (lane x arm, every workload the arm runs), at the small size tier only: the twin re-runs the whole cell including load and index build, so no wire twin is built at runner.LARGE_SCALES (deep10m, tpch10, sf1full), the generator's own large tier (build_jobs(..., scale=)); the lean twin is unchanged at every scale. DECISIONS #166's "tier 1 only" is the engine tier, which the twin already meets (ArcadeDB arms only); the size cut is an added cost decision. In the paper-2611 four-arm roster that leaves l1tpc tpch1 and l3d small (stages qV03, qV04 and their native twins); l1, l3s and l4 are not in that roster.

lane arms workloads
l1 arcadedb_server oltp, olap
l1tpc arcadedb_server oltp, olap
l3s arcadedb_sparse_server, arcadedb_sparse_server_fp32 search
l3d arcadedb_dense_server, arcadedb_dense_server_int8 search
l4 arcadedb_ts_doc_server, arcadedb_ts_native_server ingest (its queries are read over the wire instead of NDJSON)

Not covered, on purpose: l2 (Cypher stays on HTTP, #162), e2 (SQL and Cypher in one cell, plus session transactions), lifecycle and restart (their timed quantity is open, close and restart), e4 (it measures the client axis itself), and the JVM-defaults arm. Both chains are covered: the JVM image and the native image take the same settings.

Plugin settings. Only the twin's server gets them, as -D tokens (JAVA_OPTS on the JVM image, command line on the native one): arcadedb.server.plugins=Postgres:com.arcadedb.postgres.PostgresProtocolPlugin (the plugin is opt-in), and the engine's own defaults written out so they are recorded: arcadedb.postgres.host=0.0.0.0, port=5432, ssl=DISABLED, debug=false, quotedIdentifiers=true, queryMaxRows=1000000. They are read back from GET /server?mode=default into server_settings like the other server settings, and the cell is refused if one is unlisted or differs (the server prepends AutoBackupSchedulerPlugin, to the plugin list at start, so that one setting matches as a tail). The cell is also refused if port 5432 does not accept a connection. The client records arcadedb_pgwire_client (psycopg version, no prepare, autocommit, text results), arcadedb_pgwire_calls and arcadedb_pgwire_fallback_reasons from the lane's log.

Answers-identical gate (pgwire_gate.py). The lanes already record one res_<query>_digest per deterministic query, outside the timers, under the lane's own declared coercions; the twin runs the same lane code, so its digests carry the same coercions. The gate compares them with the headline's for the same cell: equal passes; different, or a query the headline recorded and the wire row did not, fails the row; a headline that disagrees with itself leaves that query unchecked; no headline digests leaves the row unchecked, never passed. A failed row is kept with error set and the reason recorded (never dropped). The runner stamps it as the twin cell ends against the headline rows in the results file, and python3 pgwire_gate.py --rows results/runs.jsonl re-checks a whole file and exits 1 on any failure. Honest limits: l1tpc and l4 record digests; l3s and l3d record none and are checked by recall_at_10, so their wire rows are compared within 0.01 of the headline median (stamped "not by digest"); l1 records neither, so its wire row stays unchecked until that lane records digests (follow-up, not in this PR).

Landing and export. The role stays sensitivity, so every existing filter keeps the wire rows out of every headline. arcadedb_client_wire = pgwire splits the two twins: the freeze key in make_paper_tables (otherwise the lean and wire twins of one cell would replace each other on ts_utc), the exporter (wire rows never enter the lean twin's client_twin columns), and one generated sentence on tables whose served SQL cells ran on the wire. Also fixed on the way: --skip-done-since keyed a cell by (backend, rep), so a twin counted as done when its headline was; the key now includes the client.

Tests (test_pgwire_twin.py, 34). The digest gate (equal, unequal, missing, unchecked, self-disagreeing headline, declared absence, headline-only reference, recall lanes, the lane coercions applied to Decimal and datetime wire rows, the file gate's exit status); the role and wire stamp; the stage generator and build_jobs emitting the twin for exactly the served SQL cells and none for Cypher, e2, lifecycle, restart or e4; the session routing with a fake psycopg; the settings patch and read-back; the log parse; the freeze key and exporter split. Run against the base versions of lean_http.py, runner.py, make_paper_tables.py and export_web.py (new module and tests kept), 18 of 30 fail; the gate tests (9) cannot run without pgwire_gate.py, and two (no twin for Cypher and mixed cells, the lean twin set unchanged) are regression guards that hold on both. test_instrument_method.py now selects the lean twins by client, because there are two kinds. The scale cut has its own tests (no wire twin at each large-tier scale, the lean twin kept, the generator roster wired at tpch1 and small only); 6 of the tests fail before that change. The full benchmarks/experiments suite under taskset -c 8-15 on the rebased head: 1283 passed, 27 skipped.

Smoke for the parent to queue (not run here; laptop cores are queued work). On a measurement lane, at this PR's head, once the engine image and wheel for the chain are set (the existing native-server smoke now expects the third row for l1):

cd benchmarks/experiments && bash smoke_native_server.sh

Expected: the last line SMOKE PASS native server arm and exit 0, with, for l1/arcadedb_server, three rows (one headline, two sensitivity, one of them arcadedb_client_wire=pgwire), the wire row with arcadedb_pgwire_port_open true, arcadedb_pgwire_calls above 0, server_settings["arcadedb.postgres.port"] = 5432, and a gate status of unchecked (l1 records no digest). For a digest-checked wire row, run l1tpc at its smallest staged scale with the same environment the script exports:

uv run --no-project --python 3.12 --with numpy python runner.py --lanes l1tpc --backends arcadedb_server --workloads olap --scale tpch1 --reps 1 --tier sweep --mem 3g --heap 2g --results-file smoke-pgwire.jsonl
python3 pgwire_gate.py --rows results/smoke-pgwire.jsonl

Expected: three cell lines ending -> ok (l1tpc_arcadedb_server_olap_tpch1_r1, the same with _clientlean and _clientpgwire), then ok l1tpc_arcadedb_server_olap_clientpgwire_tpch1_r1: N checked with N at least 5 and 1 pg-wire twin rows, 0 failed, exit 0. If the wire returns a different answer for a query, that row is kept with error starting pg-wire answers differ from the headline: and the command exits 1.

Not verified here: nothing in this PR has run against a live server. Two things only a live cell can answer: whether the plugin accepts psycopg's bound parameters and list parameters (vectorNeighbors(:idx, :q, ...), upstream ArcadeData#9007/ArcadeData#9008 are the known list-typing issues) and whether the settings read back as written; both surface as a failed twin row with a reason, not as a silent number.

🤖 Generated with Claude Code

tae898 and others added 3 commits November 4, 2026 01:10
… answers-identical gate

DECISIONS #166/#171/#237(3): the served SQL cells (l1, l1tpc, l3s, l3d, l4) run a third time as a
twin job whose /query SQL goes over ArcadeDB's postgres plugin through psycopg (never prepared,
autocommit); writes, DDL, transactions and bulk loads stay on HTTP. Role stays `sensitivity` so
landing and export keep the headline unchanged; `arcadedb_client_wire`=pgwire splits it from the
lean twin in the freeze key and the exporter. The twin's server starts the plugin with explicit
settings read back from the engine (the plugin list is matched as a tail: the server prepends
AutoBackupSchedulerPlugin). pgwire_gate.py compares each res_<query>_digest with the headline's
(the lane's own coercions) and keeps a mismatching row with `error` set. A resume no longer
treats a twin as done because its headline is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… digest; the native-server smoke expects the third row

l3s and l3d are checked against ground truth (recall_at_10), not by digest, so the gate compares
the wire row's recall with the headline median within 0.01 and says so; l1 records neither and
stays unchecked. smoke_native_server.sh now expects l1 to produce a headline, a lean and a wire
row and checks the wire row's port, call count, gate and read-back postgres port.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The twin re-runs the whole cell (load, index build, queries) for a sensitivity row, so at the
large tier (deep10m, tpch10, sf1full) it would add hours to a pass. build_jobs takes the
invocation's scale and builds no wire twin when it is in LARGE_SCALES, which moves from
make_2611_stages.py to runner.py (the generator reads it from there: one definition). The lean
twin is unchanged at every scale. DECISIONS #166's "tier 1 only" is the ENGINE tier, which the
twin already satisfies (ArcadeDB arms only); the size cut is an added cost decision, stated in
PROTOCOL.md and CAMPAIGN.md row 6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tae898
tae898 force-pushed the bench/fourarm-pgwire branch from 793d710 to 952ef54 Compare November 3, 2026 16:17
@tae898
tae898 marked this pull request as ready for review November 3, 2026 16:21
@tae898
tae898 merged commit 6f1db3e into bench/arcadedb-import-26111 Nov 3, 2026
4 checks passed
@tae898
tae898 deleted the bench/fourarm-pgwire branch November 3, 2026 16:21
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