Skip to content

Reduce round trips for prepared bulk writes - #19

Merged
quinnj merged 3 commits into
mainfrom
perf/bounded-executemany
Sep 28, 2026
Merged

quinnj merged 3 commits into
mainfrom
perf/bounded-executemany

Conversation

@quinnj

@quinnj quinnj commented Sep 26, 2026 •

Copy link
Copy Markdown
Member

Prepared DBInterface.executemany currently waits for PostgreSQL after every row and materializes results that are discarded. Group ordinary typed, no-result bulk writes into bounded Bind/Execute groups while preserving the existing transaction/savepoint and parameter conversion behavior.

The first row runs normally to confirm the actual command type; later groups contain at most 64 rows and stop after reaching 64 KiB, with a single large row allowed to exceed that target. A write-only task lets the caller drain responses concurrently, preventing bidirectional socket backpressure from deadlocking. Server errors drain through Sync before rollback, earlier errors retain precedence over later conversions, and uncertain protocol/transport state aborts and joins the writer before reuse.

Custom collections/values/styles, query logging, result-returning statements, and GSS encryption retain serial execution. The manual describes which statements and inputs are grouped. No new public API, export, or dependency.

Validation:

  • All 16 hosted checks pass on rebased head dce518da05e4b44d8bb63a2c24bd985d8bf5ec55: runtime/platform, PostgreSQL 14–18, MD5/SCRAM authentication, docs and coverage.

  • Full integration suites on the rebased head pass 8,377 checks on Julia 1.10 / DBInterface 2.5 and 8,403 on Julia 1.13 / DBInterface 2.7, including real PostgreSQL, TLS and Kerberos, with one thread. Strict docs build passes. The rebase includes main’s merged scalar-decoding and single-execute recovery changes; both incoming and bulk-execution test registrations are retained.

  • Protocol fixtures cover exact grouping, malformed/truncated responses, fatal/callback/write errors, reconnect, custom callback/input parity, statement lifetime, and error precedence. Live tests cover precision/nulls/arrays, nested and deferred failures, observed-state cancellation, and simultaneous 8 MiB notices/parameters. A synchronous-writer control deadlocks at the test deadline; the candidate completes.

  • Benchmarks on the original tested head e805207, with nine alternating warmed pairs per workload: 128 small inserts use 3 Syncs instead of 128, with 130 → 5 ReadyForQuery messages. Current Julia takes 38.68 → 4.45 ms with no injected delay and 809.48 → 36.81 ms with a nominal 4 ms acknowledgment delay; allocations fall 69.5%. Minimum Julia shows comparable timing gains and 76.3% fewer bytes. The 64 KiB-per-row control is essentially unchanged. These loopback fixtures do not establish cloud throughput; unrelated host work and timing ranges are retained in the proof.

benchmark/executemany.jl reproduces the serial-versus-grouped timing. The message counts and injected-delay figures above came from a loopback counting proxy that is not kept in the repository.

Combined validation: the published SASLAuth #6 and #7 sources pass all 690 package checks together on Julia 1.9.4 and 1.13.0. Using that combined authentication source with the published Postgres #18 and #19 sources passes the full local integration suite: 8,375 checks on Julia 1.13.0 and 8,349 on 1.10.11. Production patches apply unchanged; only overlapping test-runner includes needed composition in the temporary fixture. Dependency versions and source hashes were verified. This adds compatibility evidence, not a new timing or native-compilation claim.

Review follow-up, pushed on top:

  • Report the first failure when a group aborts. If the server sent a malformed reply, or a NOTICE log handler threw, while the writer was still sending a large row, the caller saw use of closed network connection (caused by the driver's own abort) instead of the real error. Each side now records its error before aborting, and the first one wins. The :unexpected_type and :callback fault tests now stall the fake server behind a 32 MiB row; both fail before the fix and pass after.
  • Shorten the manual section to what callers need, drop two eligibility checks that other checks already imply, and remove the Python proxy and benchmark README.
  • Local full suite with Docker PostgreSQL, TLS and Kerberos on Julia 1.12 with 10 threads: 8,405 of 8,405 pass (8,403 before the new checks).

Co-authored by Codex

🤖 Generated with Claude Code

@quinnj
quinnj force-pushed the perf/bounded-executemany branch from e805207 to dce518d Compare September 26, 2026 18:02
quinnj and others added 2 commits September 28, 2026 11:36
When the reader rejected a server reply, or a NOTICE log handler threw,
while the background writer was still sending a large group, the reader
aborted the connection and then reported the writer's resulting "use of
closed network connection" instead of its own error. Each side now
records its error before aborting, and the first one is reported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Shorten the manual section to what callers need, drop two eligibility
checks that other checks already imply, and remove the Python proxy and
benchmark README; the Julia benchmark carries its own usage line, like
decoding.jl.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@quinnj
quinnj merged commit 85e7a59 into main Sep 28, 2026
16 checks passed
quinnj referenced this pull request Sep 28, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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