From a1d7f7d91625fbf57667d6b1506751aef08b8579 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 13 Sep 2026 10:41:54 +0000 Subject: [PATCH 1/2] test(sql): prestart postgres for postgres-bytea-bind, run its tests concurrently, 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. --- test/docker/prestart-map.mjs | 1 + test/js/sql/postgres-bytea-bind.test.ts | 53 +++++++++++++++---------- 2 files changed, 34 insertions(+), 20 deletions(-) diff --git a/test/docker/prestart-map.mjs b/test/docker/prestart-map.mjs index a8c0c0d9b5bd..17e844f08d73 100644 --- a/test/docker/prestart-map.mjs +++ b/test/docker/prestart-map.mjs @@ -17,6 +17,7 @@ export const prestartMap = { "js/sql/sql.test": ["postgres_plain"], "js/sql/sql-postgres-datetime": ["postgres_plain"], "js/sql/postgres-binary-numeric": ["postgres_plain"], + "js/sql/postgres-bytea-bind": ["postgres_plain"], "js/sql/postgres-multi-statement-fields": ["postgres_plain"], "js/sql/postgres-simple-query-pipeline": ["postgres_plain"], "js/sql/sql-onconnect-onclose-throw": ["postgres_plain", "mysql_plain"], diff --git a/test/js/sql/postgres-bytea-bind.test.ts b/test/js/sql/postgres-bytea-bind.test.ts index 172a381e05fb..599be5f92e24 100644 --- a/test/js/sql/postgres-bytea-bind.test.ts +++ b/test/js/sql/postgres-bytea-bind.test.ts @@ -4,18 +4,30 @@ // parsed by the server). Anything else (a `number[]` of bytes, a // JSON-revived `{ type: "Buffer", data }`, a Date, a plain object) used to be // written as a zero-length value and stored as `\x` with no error. +// +// Each test opens its own connection, so the tests run concurrently. Do not +// hoist one connection into beforeAll: the query that follows a rejected bind +// on the same connection fails with ERR_POSTGRES_CONNECTION_CLOSED (#34732). import { SQL } from "bun"; import { expect, test } from "bun:test"; import { describeWithContainer } from "harness"; -describeWithContainer("postgres", { image: "postgres_plain" }, container => { +describeWithContainer("postgres", { image: "postgres_plain", concurrent: true }, container => { const connect = () => new SQL({ url: `postgres://bun_sql_test@${container.host}:${container.port}/bun_sql_test`, max: 1, }); + // Settles to the error. If the server accepted the value instead, settles to + // the rows, so the failed assertion shows what was stored. + const rejection = (query: Promise): Promise => + query.then( + rows => ({ resolved: rows }), + err => err, + ); + const rejected: [string, unknown, string][] = [ ["number[]", [1, 2, 3], "an instance of Array"], ["JSON-revived Buffer", JSON.parse(JSON.stringify(Buffer.from([1, 2]))), "an instance of Object"], @@ -26,10 +38,7 @@ describeWithContainer("postgres", { image: "postgres_plain" }, container => { test.each(rejected)("%s bound to a bytea parameter rejects", async (_, value, received) => { await container.ready; await using sql = connect(); - const err: any = await sql`select ${value}::bytea as x`.then( - () => null, - e => e, - ); + const err = await rejection(sql`select ${value}::bytea as x`); expect(err).toBeInstanceOf(TypeError); expect(err.code).toBe("ERR_INVALID_ARG_TYPE"); expect(err.message).toBe( @@ -40,28 +49,32 @@ describeWithContainer("postgres", { image: "postgres_plain" }, container => { test("the error names the position of the offending parameter", async () => { await container.ready; await using sql = connect(); - const err: any = await sql.unsafe("select $1::int4 as a, $2::bytea as b", [7, [1, 2, 3]]).then( - () => null, - e => e, - ); + const err = await rejection(sql.unsafe("select $1::int4 as a, $2::bytea as b", [7, [1, 2, 3]])); expect(err).toBeInstanceOf(TypeError); expect(err.code).toBe("ERR_INVALID_ARG_TYPE"); - expect(err.message).toStartWith("Query parameter $2 of type bytea must be"); + expect(err.message).toBe( + "Query parameter $2 of type bytea must be a Buffer, TypedArray, ArrayBuffer or string. Received an instance of Array", + ); }); test("BufferSource, string and null values bound to a bytea parameter round-trip", async () => { await container.ready; await using sql = connect(); - const hex = async (value: unknown) => { - const [row] = await sql`select encode(${value}::bytea, 'hex') as hex`; - return row.hex; + // `hex` is the server's own rendering of the bytes it received, so it does + // not depend on how Bun decodes a bytea result. `bytes` is the value read back. + const roundTrip = async (value: unknown) => { + const [row] = await sql`select encode(${value}::bytea, 'hex') as hex, ${value}::bytea as bytes`; + return row; }; - expect(await hex(new Uint8Array([1, 2, 3]))).toBe("010203"); - expect(await hex(Buffer.from([4, 5]))).toBe("0405"); - expect(await hex(new Uint8Array([6, 7, 8, 9]).buffer)).toBe("06070809"); - expect(await hex(new Uint8Array([0, 10, 11, 0]).subarray(1, 3))).toBe("0a0b"); - expect(await hex(new Uint8Array(0))).toBe(""); - expect(await hex("\\x0c0d")).toBe("0c0d"); - expect(await hex(null)).toBe(null); + const stored = (hex: string) => ({ hex, bytes: Buffer.from(hex, "hex") }); + expect(await roundTrip(new Uint8Array([1, 2, 3]))).toEqual(stored("010203")); + expect(await roundTrip(Buffer.from([4, 5]))).toEqual(stored("0405")); + expect(await roundTrip(new Uint8Array([6, 7, 8, 9]).buffer)).toEqual(stored("06070809")); + expect(await roundTrip(new Uint8Array([0, 10, 11, 0]).subarray(1, 3))).toEqual(stored("0a0b")); + // a wider element type goes out as its underlying bytes, not as element values + expect(await roundTrip(new Uint16Array(new Uint8Array([16, 17, 18, 19]).buffer))).toEqual(stored("10111213")); + expect(await roundTrip(new Uint8Array(0))).toEqual(stored("")); + expect(await roundTrip("\\x0c0d")).toEqual(stored("0c0d")); + expect(await roundTrip(null)).toEqual({ hex: null, bytes: null }); }); }); From c6d9831308a9b3ab6e947f70d68811a82aa4ea48 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 13 Sep 2026 11:05:53 +0000 Subject: [PATCH 2/2] ci: retrigger