Support jsonb, idle_timeout, connection_timeout, max_lifetime timeouts in bun:sql. Add onopen and onclose callbacks. Fix missing "code" property appearing in errors. Add error codes for postgres. (#16045)

This commit is contained in:
Jarred Sumner
2024-12-30 13:25:01 -08:00
committed by GitHub
parent f0073bfa81
commit 76bfceae81
15 changed files with 1152 additions and 347 deletions

View File

@@ -1,5 +1,5 @@
import { postgres, sql } from "bun:sql";
import { expect, test } from "bun:test";
import { expect, test, mock } from "bun:test";
import { $ } from "bun";
import { bunExe, isCI, withoutAggressiveGC } from "harness";
import path from "path";
@@ -13,18 +13,20 @@ if (!isCI) {
// local all postgres trust
// local all bun_sql_test_scram scram-sha-256
// local all bun_sql_test trust
//
// local all bun_sql_test_md5 md5
// # IPv4 local connections:
// host all ${USERNAME} 127.0.0.1/32 trust
// host all postgres 127.0.0.1/32 trust
// host all bun_sql_test_scram 127.0.0.1/32 scram-sha-256
// host all bun_sql_test 127.0.0.1/32 trust
// host all bun_sql_test_md5 127.0.0.1/32 md5
// # IPv6 local connections:
// host all ${USERNAME} ::1/128 trust
// host all postgres ::1/128 trust
// host all bun_sql_test ::1/128 trust
// host all bun_sql_test_scram ::1/128 scram-sha-256
//
// host all bun_sql_test_md5 ::1/128 md5
// # Allow replication connections from localhost, by a user with the
// # replication privilege.
// local replication all trust
@@ -33,9 +35,6 @@ if (!isCI) {
// --- Expected pg_hba.conf ---
process.env.DATABASE_URL = "postgres://bun_sql_test@localhost:5432/bun_sql_test";
const delay = ms => Bun.sleep(ms);
const rel = x => new URL(x, import.meta.url);
const login = {
username: "bun_sql_test",
};
@@ -54,8 +53,8 @@ if (!isCI) {
db: "bun_sql_test",
username: login.username,
password: login.password,
idle_timeout: 1,
connect_timeout: 1,
idle_timeout: 0,
connect_timeout: 0,
max: 1,
};
@@ -67,6 +66,97 @@ if (!isCI) {
expect(result).toBe(1);
});
test("Connection timeout works", async () => {
const onclose = mock();
const onconnect = mock();
await using sql = postgres({
...options,
hostname: "unreachable_host",
connection_timeout: 1,
onconnect,
onclose,
});
let error: any;
try {
await sql`select pg_sleep(2)`;
} catch (e) {
error = e;
}
expect(error.code).toBe(`ERR_POSTGRES_CONNECTION_TIMEOUT`);
expect(error.message).toContain("Connection timeout after 1ms");
expect(onconnect).not.toHaveBeenCalled();
expect(onclose).toHaveBeenCalledTimes(1);
});
test("Idle timeout works at start", async () => {
const onclose = mock();
const onconnect = mock();
await using sql = postgres({
...options,
idle_timeout: 1,
onconnect,
onclose,
});
let error: any;
try {
await sql`select pg_sleep(2)`;
} catch (e) {
error = e;
}
expect(error.code).toBe(`ERR_POSTGRES_IDLE_TIMEOUT`);
expect(onconnect).toHaveBeenCalled();
expect(onclose).toHaveBeenCalledTimes(1);
});
test("Idle timeout is reset when a query is run", async () => {
const onClosePromise = Promise.withResolvers();
const onclose = mock(err => {
onClosePromise.resolve(err);
});
const onconnect = mock();
await using sql = postgres({
...options,
idle_timeout: 100,
onconnect,
onclose,
});
expect(await sql`select 123 as x`).toEqual([{ x: 123 }]);
expect(onconnect).toHaveBeenCalledTimes(1);
expect(onclose).not.toHaveBeenCalled();
const err = await onClosePromise.promise;
expect(err.code).toBe(`ERR_POSTGRES_IDLE_TIMEOUT`);
});
test("Max lifetime works", async () => {
const onClosePromise = Promise.withResolvers();
const onclose = mock(err => {
onClosePromise.resolve(err);
});
const onconnect = mock();
const sql = postgres({
...options,
max_lifetime: 64,
onconnect,
onclose,
});
let error: any;
expect(await sql`select 1 as x`).toEqual([{ x: 1 }]);
expect(onconnect).toHaveBeenCalledTimes(1);
try {
while (true) {
for (let i = 0; i < 100; i++) {
await sql`select pg_sleep(1)`;
}
}
} catch (e) {
error = e;
}
expect(onclose).toHaveBeenCalledTimes(1);
expect(error.code).toBe(`ERR_POSTGRES_LIFETIME_TIMEOUT`);
});
test("Uses default database without slash", async () => {
const sql = postgres("postgres://localhost");
expect(sql.options.username).toBe(sql.options.database);
@@ -145,10 +235,9 @@ if (!isCI) {
expect(x).toEqual({ a: "hello", b: 42 });
});
// It's treating as a string.
test.todo("implicit jsonb", async () => {
test("implicit jsonb", async () => {
const x = (await sql`select ${{ a: "hello", b: 42 }}::jsonb as x`)[0].x;
expect([x.a, x.b].join(",")).toBe("hello,42");
expect(x).toEqual({ a: "hello", b: 42 });
});
test("bulk insert nested sql()", async () => {
@@ -428,9 +517,11 @@ if (!isCI) {
test("Null sets to null", async () => expect((await sql`select ${null} as x`)[0].x).toBeNull());
// Add code property.
test.todo("Throw syntax error", async () => {
const code = await sql`wat 1`.catch(x => x);
console.log({ code });
test("Throw syntax error", async () => {
const err = await sql`wat 1`.catch(x => x);
expect(err.code).toBe("ERR_POSTGRES_SYNTAX_ERROR");
expect(err.errno).toBe(42601);
expect(err).toBeInstanceOf(SyntaxError);
});
// t('Connect using uri', async() =>
@@ -502,13 +593,26 @@ if (!isCI) {
// return [1, (await sql`select 1 as x`)[0].x]
// })
// t('Login without password', async() => {
// return [true, (await postgres({ ...options, ...login })`select true as x`)[0].x]
// })
test("Login without password", async () => {
await using sql = postgres({ ...options, ...login });
expect((await sql`select true as x`)[0].x).toBe(true);
});
// t('Login using MD5', async() => {
// return [true, (await postgres({ ...options, ...login_md5 })`select true as x`)[0].x]
// })
test("Login using MD5", async () => {
await using sql = postgres({ ...options, ...login_md5 });
expect(await sql`select true as x`).toEqual([{ x: true }]);
});
test("Login with bad credentials propagates error from server", async () => {
const sql = postgres({ ...options, ...login_md5, username: "bad_user", password: "bad_password" });
let err;
try {
await sql`select true as x`;
} catch (e) {
err = e;
}
expect(err.code).toBe("ERR_POSTGRES_SERVER_ERROR");
});
test("Login using scram-sha-256", async () => {
await using sql = postgres({ ...options, ...login_scram });
@@ -1159,9 +1263,10 @@ if (!isCI) {
// ]
// })
// t('dynamic column name', async() => {
// return ['!not_valid', Object.keys((await sql`select 1 as ${ sql('!not_valid') }`)[0])[0]]
// })
test.todo("dynamic column name", async () => {
const result = await sql`select 1 as ${"\\!not_valid"}`;
expect(Object.keys(result[0])[0]).toBe("!not_valid");
});
// t('dynamic select as', async() => {
// return ['2', (await sql`select ${ sql({ a: 1, b: 2 }) }`)[0].b]
@@ -1178,12 +1283,12 @@ if (!isCI) {
// return ['the answer', (await sql`insert into test ${ sql(x) } returning *`)[0].b, await sql`drop table test`]
// })
// t('dynamic insert pluck', async() => {
// await sql`create table test (a int, b text)`
// const x = { a: 42, b: 'the answer' }
// return [null, (await sql`insert into test ${ sql(x, 'a') } returning *`)[0].b, await sql`drop table test`]
// })
// test.todo("dynamic insert pluck", async () => {
// await sql`create table test (a int, b text)`;
// const x = { a: 42, b: "the answer" };
// const [{ b }] = await sql`insert into test ${sql(x, "a")} returning *`;
// expect(b).toBe("the answer");
// });
// t('dynamic in with empty array', async() => {
// await sql`create table test (a int)`