Skip to content

test(sql): prestart postgres for postgres-bytea-bind, run its tests concurrently, assert the bytes read back - #42587

Open
robobun wants to merge 2 commits into
mainfrom
robobun/a62f5b84/speed-up-postgres-bytea-bind-test
Open

robobun wants to merge 2 commits into
mainfrom
robobun/a62f5b84/speed-up-postgres-bytea-bind-test

Conversation

@robobun

@robobun robobun commented Sep 13, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • test/js/sql/postgres-bytea-bind.test.ts took 18.46 s on alpine 3.23 x64 (build 114889). On the same lane sql-prepare-false.test.ts (eight tests, same container) took 183 ms.
  • The file has no entry in test/docker/prestart-map.mjs. Its shard logged coordinator: ensuring postgres_plain only when this file asked, so the cold start ran inside the file's beforeAll.

Fix

Background

  • describeWithContainer (test/harness.ts) is a describe block whose beforeAll asks the docker coordinator for a service. The first file to ask pays the container start.
  • test/docker/prestart-map.mjs maps test-path prefixes to services. test/docker/coordinator.ts starts mapped services at shard launch. scripts/runner.node.mjs runs mapped files last, outside the parallel bucket.
  • The tests do not share one connection. The query after a rejected bind on the same connection fails with ERR_POSTGRES_CONNECTION_CLOSED (see open PR sql(postgres): discard a partial Bind when a parameter fails to encode #34732).
Notes

CI attribution. Build 114889, job 01a097c4-9deb-426c-8da6-65829219437c (:alpine: 3.23 x64 - test-bun, shard 2). At launch the log has coordinator: ensuring minio and coordinator: ensuring redis_unified. coordinator: ensuring postgres_plain appears about 1200 lines later, and coordinator: postgres_plain ready is the line before the file's header. The file is reported as [78/295] test/js/sql/postgres-bytea-bind.test.ts (18.46s). It ran inside the parallel bucket (Ran 1258 tests across 80 files. [47.99s]).

Same build, same lane, for comparison:

file in the map coordinator log time
sql-prepare-false.test.ts (shard 18) yes postgres_plain ready (cached) Ran 8 tests across 1 file. [183.00ms]
postgres-multi-statement-fields.test.ts (shard 19) yes postgres_plain ready (cached) Ran 5 tests across 1 file. [103.00ms]
postgres-bytea-bind.test.ts (shard 2) no ensuring postgres_plain on request 18.46 s
sql-reserve-abort.test.ts (shard 17, #41104) no ensuring postgres_plain on request 19.02 s

How the entry removes the wait. coordinator.ts calls testPath.startsWith(prefix) on each test path of the shard and starts every matched service when the shard starts. runner.node.mjs uses the same check in needsDockerService. That check excludes the file from isBucketCandidate and sorts it to the end of the serial list. The container start then overlaps with the tests that run first. I checked the new key against both path separator styles. It matches this file and does not match postgres-bool-bind.test.ts.

The runner runs modified test files first in their shard. So in this PR's own CI run the file can still wait for the container. The gain shows on later builds.

test/parallel-allowlist.json was generated on 2026-08-17, before this file existed, so the file is not in excludeFiles. The generator excludes every describeWithContainer caller. The map entry gives the same result at run time.

Local timing. Postgres already runs on 127.0.0.1 (BUN_TEST_SERVICE_postgres_plain is set), so no number here contains a container start. The host had a load average near 100 on 16 cores, so absolute values are high. Runs are interleaved (before, after, before, after). Values are the runner's own Ran N tests time, median (minimum) of 9 rounds.

before after
debug ASAN build (bun bd test) 4630 ms (4000) 4410 ms (3840)
release build (USE_SYSTEM_BUN=1, 1.4.3-canary.1+b99371011) 159 ms (147) 130 ms (106)

The debug number is all fixed cost. A describeWithContainer file with one test and no query took 3.0 to 3.2 s at a time when the old file took 2.9 to 3.0 s. On the release build that empty file takes 103 ms. Without concurrent: true the new file measures 4430 ms debug and 150 ms release. With it, the release run was faster in 9 of 9 rounds.

Levers I measured and did not apply.

  • One connection in beforeAll. After a rejected bind on a max: 1 pool, select 1 fails with PostgresError: Connection closed. The same happens for an older case (an object whose toString throws, bound to text) and on the 1.4.3 canary. sql(postgres): discard a partial Bind when a parameter fails to encode #34732 has the cause and a fix.
  • One test per accepted value, each with its own connection (13 tests). Release: 138 ms. Debug: 100 to 170 ms slower than before. Seven more connections cost more under ASAN than concurrency saves.

Assertion details.

  • hex is encode(value, 'hex'), computed by the server, so it does not depend on how Bun decodes a bytea result. bytes is the same parameter selected back. It arrives as a Buffer. NULL gives { hex: null, bytes: null }.
  • The Uint16Array is a view over the bytes 10 11 12 13, so the expected value does not depend on endianness.
  • expect(query).rejects is not used. A Bun SQL query starts on .then() or .execute(). The matcher calls neither, and the test hangs past its timeout (related: event loop: never re-enter from inside a dispatch callback (de-block expect().resolves and HTMLRewriter.transform, then assert) #33261). The rejection helper calls .then().
  • toMatchObject({ code, message }) is not used. On a code mismatch its diff prints the error as [TypeError: message] and hides the received code.
  • Mutation checks, release build. Each of these made exactly one test fail: one extra byte in the expected bytes, an empty Buffer expected for NULL, a valid Buffer in the rejected table, a cut tail of the position message, a swapped Uint16Array byte order.

Open PRs #35912, #40193, #41104, #40537 and #41176 add their own lines to prestart-map.mjs. This PR inserts at a different line. No other open PR touches this test file.


[auto-merge] gate passed · iteration 1 · 2 files touched

passes on PR (with fix)
Test-only change.

Debug/ASAN (expected pass):
$ bun bd test 'test/js/sql/postgres-bytea-bind.test.ts'
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test test/js/sql/postgres-bytea-bind.test.ts
bun test v1.4.3 (b99371011)

test/js/sql/postgres-bytea-bind.test.ts:
Container ready via docker-compose: postgres_plain at 127.0.0.1:5432
(pass) postgres > number[] bound to a bytea parameter rejects [431.00ms]
(pass) postgres > JSON-revived Buffer bound to a bytea parameter rejects [284.41ms]
(pass) postgres > the error names the position of the offending parameter [261.12ms]
(pass) postgres > Date bound to a bytea parameter rejects [274.24ms]
(pass) postgres > plain object bound to a bytea parameter rejects [267.77ms]
(pass) postgres > BufferSource, string and null values bound to a bytea parameter round-trip [214.90ms]

 6 pass
 0 fail
 23 expect() calls
Ran 6 tests across 1 file. [6.78s]
Exit: 0
diff hotspot
test/docker/prestart-map.mjs            |  1 +
 test/js/sql/postgres-bytea-bind.test.ts | 53 ++++++++++++++++++++-------------
 2 files changed, 34 insertions(+), 20 deletions(-)

gate history · 2 passed · 0 rejected · iteration 1

evidence per changed file
file                                     reads  edits  tests
test/docker/prestart-map.mjs                 1      1     19
test/js/sql/postgres-bytea-bind.test.ts      2      5     19

…oncurrently, assert the bytes read back

Add the file to test/docker/prestart-map.mjs so the CI coordinator starts
postgres_plain when the shard starts and the runner orders the file late.
Without the entry the container cold start ran inside this file's
beforeAll (18s on alpine 3.23 x64).

Each test owns its connection, so the describe block is concurrent.

Assertions: the position test checks the whole message. The round-trip
test also reads the bytea value back and compares the bytes, and covers a
Uint16Array. A rejected case that wrongly resolves now prints the rows.
@coderabbitai

coderabbitai Bot commented Sep 13, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Essentials

Run ID: bcd3b93c-101a-4ef0-bd28-def0f9929d80

📥 Commits

Reviewing files that changed from the base of the PR and between 09bb546 and c6d9831.

📒 Files selected for processing (2)
  • test/docker/prestart-map.mjs
  • test/js/sql/postgres-bytea-bind.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 3 remain after this review.


Walkthrough

The PostgreSQL bytea tests now use concurrent per-test connections, shared rejection handling, complete error-message checks, and expanded round-trip assertions. The Docker prestart map assigns the test path to postgres_plain.

Changes

PostgreSQL bytea test coverage

Layer / File(s) Summary
Docker test mapping
test/docker/prestart-map.mjs
The js/sql/postgres-bytea-bind test path maps to the postgres_plain Docker service.
Concurrent bytea validation
test/js/sql/postgres-bytea-bind.test.ts
The tests use per-test connections and a shared helper for resolved or rejected queries. Assertions now check the complete invalid-parameter message and validate encoded server bytes and returned values for typed arrays, empty values, strings, and null values.

Priority: ➖ Normal

Severity of issue fixed: High

Merge Risk: ⚪ Minimal · up to c6d98

The test and Docker prestart updates have no identified merge-blocking behavior risk.

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The pull request does not implement the core requirements in issue #34732. The whole diff changes only test/docker/prestart-map.mjs and test/js/sql/postgres-bytea-bind.test.ts. At the reviewed hea… Implement atomic writer rollback in NewWriter and WriterContext. Apply it to bind_and_execute, prepare_and_query_with_signature, and parse_and_bind_and_execute, including all relevant call paths. Add regression tests for prepared …
✅ Passed checks (3 passed)
Check name Status Explanation
Out of Scope Changes check ✅ Passed The changes stay within PostgreSQL bind-test scope. The prestart-map entry supports the changed PostgreSQL test file. The test changes cover rejected bytea bindings, error details, and bytea round tri…
Title check ✅ Passed The title clearly summarizes the main changes: prestarting PostgreSQL, running the tests concurrently, and strengthening bytea assertions.
Description check ✅ Passed The description explains the problem, implementation, verification steps, test results, and relevant context. It uses different headings from the template, but it provides the required information for…
Full details: Linked Issues check

Explanation

The pull request does not implement the core requirements in issue #34732. The whole diff changes only test/docker/prestart-map.mjs and test/js/sql/postgres-bytea-bind.test.ts. At the reviewed head, WriterContext has no truncate method, NewWriter has no atomically helper, and write_bind still writes directly to the writer. The three batch writers are not wrapped in rollback behavior. The changed tests do not cover throwing encoders, prepared and first executions, partial-wire validation, or pipelined follow-up queries.

Resolution

Implement atomic writer rollback in NewWriter and WriterContext. Apply it to bind_and_execute, prepare_and_query_with_signature, and parse_and_bind_and_execute, including all relevant call paths. Add regression tests for prepared and first-execution encoding failures, verify that no partial Bind reaches the wire, and verify that subsequent and pipelined queries remain usable.

  • Fix all pre-merge checks with AI

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Sep 13, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: ready for review. The diff is green. CI is red only on tests that this PR does not touch.

Reproduced from CI build 114889 (alpine 3.23 x64, shard 2). The file reported 18.46 s. The shard log has coordinator: ensuring postgres_plain only when this file asks for the service, so the time is the postgres_plain cold start in beforeAll. On the same lane a mapped file (sql-prepare-false.test.ts) ran in 183 ms.

Locally Postgres already runs on 127.0.0.1, so there is no container start to measure. Interleaved medians: debug build 4.63 s before and 4.41 s after, release build 159 ms before and 130 ms after. bun bd test test/js/sql/postgres-bytea-bind.test.ts passed in 11 of 11 debug runs.

CI on this PR, builds 115105 and 115109 (the second is a rerun of the same diff). Both are finished.

  • test/js/sql/postgres-bytea-bind.test.ts has no failure annotation and no retry annotation in either build.
  • 115105 has one red test: test/integration/next-pages/test/dev-server-ssr-100.test.ts on Windows 2019 x64. Two requests get ConnectionRefused after the dev server stops. test/flaky-tests.txt lists this failure, and test(next-pages): wait for every request before the dev server is killed #40061 targets it. It passed in 115109.
  • 115109 has two red tests. They explain all 7 failed jobs. The other 174 jobs passed.
    • test/js/bun/s3/s3.test.ts on six lanes, Windows included. The two R2 tests named "should be able to upload large files in one go using Bun.write" time out after 15 s. They upload to the remote R2 bucket, not to the MinIO container. Four more lanes needed 2 or 3 retries for the same tests.
    • test/js/bun/spawn/spawn-pipe-leak.test.ts on Ubuntu 25.04 x64. Peak RSS reached 719 MB in one batch. The same file needed retries on macOS 13 x64 in both builds.
  • Every other entry passed on a retry.

This PR changes one Postgres test file and adds one line to test/docker/prestart-map.mjs. The js/bun/s3/ entry of that map is unchanged. No red test can depend on this diff. I will not push another rerun.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — test-only tightening plus a one-line prestart-map entry, matching the sibling js/sql/* entries exactly.

What was reviewed:

  • describeWithContainer accepts concurrent, and each test opens its own await using sql = connect(), so concurrency is safe; the header comment cites #34732 for why a shared connection isn't hoisted.
  • rejection() calls .then() on the lazy query thenable and surfaces { resolved: rows } on unexpected success, so no assertion is vacuous; error checks still assert class + .code + full message.
  • The Uint16Array case is built over a Uint8Array buffer, so the expected 10111213 is endianness-independent; stored("") and the explicit { hex: null, bytes: null } cover empty and NULL correctly.
Extended reasoning...

Overview

Two files changed. test/docker/prestart-map.mjs gains one entry mapping js/sql/postgres-bytea-bind to postgres_plain, inserted alphabetically among identical sibling entries so the CI coordinator warms the container before the shard reaches this file. test/js/sql/postgres-bytea-bind.test.ts is refactored: concurrent: true on describeWithContainer, a rejection() helper replacing inline .then(() => null, e => e), toStartWith upgraded to an exact toBe on the full message, and the round-trip test now selects both encode(..., 'hex') and the raw ::bytea back, asserting a combined { hex, bytes } object and adding a Uint16Array variant.

Security risks

None. This is test code and a static string→string[] map consumed by the test runner. No user input, no auth/crypto/permissions paths, no runtime code touched.

Level of scrutiny

Low. The prestart-map change is a mechanical config addition following the exact pattern of a dozen neighboring lines. The test changes strengthen assertions (exact message, bytes read back, resolved-rows-on-failure diagnostic) and add a variant without removing any existing case — squarely what REVIEW.md asks for. I confirmed in test/harness.ts that describeWithContainer supports concurrent: boolean and routes to describe.concurrent, and that other test/js/sql/ files already use this option, so the pattern is established.

Other factors

Checked correctness details: rejection() invokes .then() so the lazy SQL query actually executes (avoiding the expect().rejects hang the PR notes); the Uint16Array is constructed over a byte buffer so the expected hex is host-endianness-independent; stored("") yields an empty Buffer and NULL is handled as its own branch. Each test still owns its connection via await using, so concurrent execution can't share a poisoned pool. No existing test was weakened or deleted. The timeline shows no outstanding third-party objections and the bug hunt exited on dry_streak with no findings.

@robobun

robobun commented Sep 13, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 4:34 AM PT - Sep 13th, 2026

❌ @robobun, your commit c6d9831 has 2 failures in Build #115109 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 42587

That installs a local version of the PR into your bun-42587 executable, so you can run:

bun-42587 --bun

@robobun

robobun commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator Author

The "Linked Issues check" warning in the bot summary above is a false positive. #34732 is a separate open PR. This PR only points to it, to explain why the tests do not share one connection. This PR is test-only and does not try to implement that change.

One sentence in the description read like a closing keyword (fix: #34732). I reworded it. GitHub lists no closing reference for this PR.

@robobun

robobun commented Sep 24, 2026

Copy link
Copy Markdown
Collaborator Author

Order note from the review of #34732: that PR lands first. It adds tests to test/js/sql/postgres-bytea-bind.test.ts (a rejected value leaves the connection usable, pipelined siblings, a transaction), and this branch conflicts with it in that file today (git merge-tree). Rebase this PR after #34732 merges and keep those tests.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant