Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions test/docker/prestart-map.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
53 changes: 33 additions & 20 deletions test/js/sql/postgres-bytea-bind.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<unknown>): Promise<any> =>
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"],
Expand All @@ -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(
Expand All @@ -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 });
});
});