mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 18:38:55 +00:00
### What does this PR do? Time should be represented as HH:MM:SS or HHH:MM:SS string ### How did you verify your code works? Test --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
964 lines
37 KiB
TypeScript
964 lines
37 KiB
TypeScript
import { SQL, randomUUIDv7 } from "bun";
|
|
import { beforeAll, describe, expect, mock, test } from "bun:test";
|
|
import { describeWithContainer, isDockerEnabled, tempDirWithFiles } from "harness";
|
|
import net from "net";
|
|
import path from "path";
|
|
const dir = tempDirWithFiles("sql-test", {
|
|
"select-param.sql": `select ? as x`,
|
|
"select.sql": `select CAST(1 AS SIGNED) as x`,
|
|
});
|
|
function rel(filename: string) {
|
|
return path.join(dir, filename);
|
|
}
|
|
if (isDockerEnabled()) {
|
|
const images = [
|
|
{
|
|
name: "MySQL with TLS",
|
|
image: "mysql_tls",
|
|
},
|
|
{
|
|
name: "MySQL",
|
|
image: "mysql_plain",
|
|
},
|
|
// This image only works on x64.
|
|
process.arch === "x64" && {
|
|
name: "MySQL 9",
|
|
image: "mysql:9",
|
|
env: {
|
|
MYSQL_ROOT_PASSWORD: "bun",
|
|
},
|
|
},
|
|
].filter(Boolean);
|
|
|
|
for (const image of images) {
|
|
describeWithContainer(
|
|
image.name,
|
|
{
|
|
image: image.image,
|
|
env: image.env,
|
|
concurrent: true,
|
|
},
|
|
container => {
|
|
let sql: SQL;
|
|
const password = image.image === "mysql_plain" ? "" : "bun";
|
|
const getOptions = (): Bun.SQL.Options => ({
|
|
url: `mysql://root:${password}@${container.host}:${container.port}/bun_sql_test`,
|
|
max: 1,
|
|
tls:
|
|
image.name === "MySQL with TLS"
|
|
? Bun.file(path.join(import.meta.dir, "mysql-tls", "ssl", "ca.pem"))
|
|
: undefined,
|
|
});
|
|
|
|
beforeAll(async () => {
|
|
await container.ready;
|
|
sql = new SQL(getOptions());
|
|
});
|
|
|
|
test("should return lastInsertRowid and affectedRows", async () => {
|
|
await using db = new SQL({ ...getOptions(), max: 1, idleTimeout: 5 });
|
|
using sql = await db.reserve();
|
|
const random_name = "test_" + randomUUIDv7("hex").replaceAll("-", "");
|
|
|
|
await sql`CREATE TEMPORARY TABLE ${sql(random_name)} (id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, name text)`;
|
|
|
|
const { lastInsertRowid } = await sql`INSERT INTO ${sql(random_name)} (name) VALUES (${"test"})`;
|
|
expect(lastInsertRowid).toBe(1);
|
|
const { affectedRows } =
|
|
await sql`UPDATE ${sql(random_name)} SET name = "test2" WHERE id = ${lastInsertRowid}`;
|
|
expect(affectedRows).toBe(1);
|
|
});
|
|
describe("should work with more than the max inline capacity", () => {
|
|
for (let size of [50, 60, 62, 64, 70, 100]) {
|
|
for (let duplicated of [true, false]) {
|
|
test(`${size} ${duplicated ? "+ duplicated" : "unique"} fields`, async () => {
|
|
const longQuery = `select ${Array.from({ length: size }, (_, i) => {
|
|
if (duplicated) {
|
|
return i % 2 === 0 ? `${i + 1} as f${i}, ${i} as f${i}` : `${i} as f${i}`;
|
|
}
|
|
return `${i} as f${i}`;
|
|
}).join(",\n")}`;
|
|
await using sql = new SQL({ ...getOptions(), max: 1 });
|
|
|
|
const result = await sql.unsafe(longQuery);
|
|
let value = 0;
|
|
for (const column of Object.values(result[0])) {
|
|
expect(column?.toString()).toEqual(value.toString());
|
|
value++;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
test("Connection timeout works", async () => {
|
|
const onclose = mock();
|
|
const onconnect = mock();
|
|
await using sql = new SQL({
|
|
...getOptions(),
|
|
hostname: "example.com",
|
|
connection_timeout: 4,
|
|
onconnect,
|
|
onclose,
|
|
max: 1,
|
|
});
|
|
let error: any;
|
|
try {
|
|
await sql`select SLEEP(8)`;
|
|
} catch (e) {
|
|
error = e;
|
|
}
|
|
expect(error.code).toBe(`ERR_MYSQL_CONNECTION_TIMEOUT`);
|
|
expect(error.message).toContain("Connection timeout after 4s");
|
|
expect(onconnect).not.toHaveBeenCalled();
|
|
expect(onclose).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
test("Idle timeout works at start", async () => {
|
|
const onClosePromise = Promise.withResolvers();
|
|
const onclose = mock(err => {
|
|
onClosePromise.resolve(err);
|
|
});
|
|
const onconnect = mock();
|
|
await using sql = new SQL({
|
|
...getOptions(),
|
|
idle_timeout: 1,
|
|
onconnect,
|
|
onclose,
|
|
max: 1,
|
|
});
|
|
await sql.connect();
|
|
const err = await onClosePromise.promise;
|
|
expect(err).toBeInstanceOf(SQL.SQLError);
|
|
expect(err).toBeInstanceOf(SQL.MySQLError);
|
|
expect((err as SQL.MySQLError).code).toBe(`ERR_MYSQL_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 = new SQL({
|
|
...getOptions(),
|
|
idle_timeout: 1,
|
|
connection_timeout: 5,
|
|
onconnect,
|
|
onclose,
|
|
max: 1,
|
|
});
|
|
expect<[{ x: number }]>(await sql`select 123 as x`).toEqual([{ x: 123 }]);
|
|
expect(onconnect).toHaveBeenCalledTimes(1);
|
|
expect(onclose).not.toHaveBeenCalled();
|
|
const err = await onClosePromise.promise;
|
|
expect(err).toBeInstanceOf(SQL.SQLError);
|
|
expect(err).toBeInstanceOf(SQL.MySQLError);
|
|
expect((err as SQL.MySQLError).code).toBe(`ERR_MYSQL_IDLE_TIMEOUT`);
|
|
});
|
|
|
|
test("Max lifetime works", async () => {
|
|
const onClosePromise = Promise.withResolvers();
|
|
const onclose = mock(err => {
|
|
onClosePromise.resolve(err);
|
|
});
|
|
const onconnect = mock();
|
|
await using sql = new SQL({
|
|
...getOptions(),
|
|
max_lifetime: 1,
|
|
onconnect,
|
|
onclose,
|
|
max: 1,
|
|
});
|
|
let error: unknown;
|
|
|
|
try {
|
|
expect<[{ x: number }]>(await sql`select 1 as x`).toEqual([{ x: 1 }]);
|
|
|
|
while (true) {
|
|
for (let i = 0; i < 100; i++) {
|
|
await sql`select SLEEP(1)`;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
error = e;
|
|
}
|
|
|
|
expect(onclose).toHaveBeenCalledTimes(1);
|
|
expect(onconnect).toHaveBeenCalledTimes(1);
|
|
|
|
expect(error).toBeInstanceOf(SQL.SQLError);
|
|
expect(error).toBeInstanceOf(SQL.MySQLError);
|
|
expect((error as SQL.MySQLError).code).toBe(`ERR_MYSQL_LIFETIME_TIMEOUT`);
|
|
});
|
|
|
|
// Last one wins.
|
|
test("Handles duplicate string column names", async () => {
|
|
const result = await sql`select 1 as x, 2 as x, 3 as x`;
|
|
expect(result).toEqual([{ x: 3 }]);
|
|
});
|
|
|
|
test("should not timeout in long results", async () => {
|
|
await using db = new SQL({ ...getOptions(), max: 1, idleTimeout: 5 });
|
|
using sql = await db.reserve();
|
|
const random_name = "test_" + randomUUIDv7("hex").replaceAll("-", "");
|
|
|
|
await sql`CREATE TEMPORARY TABLE ${sql(random_name)} (id int, name text)`;
|
|
const promises: Promise<any>[] = [];
|
|
for (let i = 0; i < 10_000; i++) {
|
|
promises.push(sql`INSERT INTO ${sql(random_name)} VALUES (${i}, ${"test" + i})`);
|
|
if (i % 50 === 0 && i > 0) {
|
|
await Promise.all(promises);
|
|
promises.length = 0;
|
|
}
|
|
}
|
|
await Promise.all(promises);
|
|
await sql`SELECT * FROM ${sql(random_name)}`;
|
|
await sql`SELECT * FROM ${sql(random_name)}`;
|
|
await sql`SELECT * FROM ${sql(random_name)}`;
|
|
|
|
expect().pass();
|
|
}, 10_000);
|
|
|
|
test("Handles numeric column names", async () => {
|
|
// deliberately out of order
|
|
const result = await sql`select 1 as "1", 2 as "2", 3 as "3", 0 as "0"`;
|
|
expect(result).toEqual([{ "1": 1, "2": 2, "3": 3, "0": 0 }]);
|
|
|
|
expect(Object.keys(result[0])).toEqual(["0", "1", "2", "3"]);
|
|
// Sanity check: ensure iterating through the properties doesn't crash.
|
|
Bun.inspect(result);
|
|
});
|
|
|
|
// Last one wins.
|
|
test("Handles duplicate numeric column names", async () => {
|
|
const result = await sql`select 1 as "1", 2 as "1", 3 as "1"`;
|
|
expect(result).toEqual([{ "1": 3 }]);
|
|
// Sanity check: ensure iterating through the properties doesn't crash.
|
|
Bun.inspect(result);
|
|
});
|
|
|
|
test("Handles mixed column names", async () => {
|
|
const result = await sql`select 1 as "1", 2 as "2", 3 as "3", 4 as x`;
|
|
expect(result).toEqual([{ "1": 1, "2": 2, "3": 3, x: 4 }]);
|
|
// Sanity check: ensure iterating through the properties doesn't crash.
|
|
Bun.inspect(result);
|
|
});
|
|
|
|
test("Handles mixed column names with duplicates", async () => {
|
|
const result = await sql`select 1 as "1", 2 as "2", 3 as "3", 4 as "1", 1 as x, 2 as x`;
|
|
expect(result).toEqual([{ "1": 4, "2": 2, "3": 3, x: 2 }]);
|
|
// Sanity check: ensure iterating through the properties doesn't crash.
|
|
Bun.inspect(result);
|
|
|
|
// Named columns are inserted first, but they appear from JS as last.
|
|
expect(Object.keys(result[0])).toEqual(["1", "2", "3", "x"]);
|
|
});
|
|
|
|
test("Handles mixed column names with duplicates at the end", async () => {
|
|
const result = await sql`select 1 as "1", 2 as "2", 3 as "3", 4 as "1", 1 as x, 2 as x, 3 as x, 4 as "y"`;
|
|
expect(result).toEqual([{ "1": 4, "2": 2, "3": 3, x: 3, y: 4 }]);
|
|
|
|
// Sanity check: ensure iterating through the properties doesn't crash.
|
|
Bun.inspect(result);
|
|
});
|
|
|
|
test("Handles mixed column names with duplicates at the start", async () => {
|
|
const result = await sql`select 1 as "1", 2 as "1", 3 as "2", 4 as "3", 1 as x, 2 as x, 3 as x`;
|
|
expect(result).toEqual([{ "1": 2, "2": 3, "3": 4, x: 3 }]);
|
|
// Sanity check: ensure iterating through the properties doesn't crash.
|
|
Bun.inspect(result);
|
|
});
|
|
|
|
test("Uses default database without slash", async () => {
|
|
const sql = new SQL("mysql://localhost");
|
|
expect("mysql").toBe(sql.options.database);
|
|
});
|
|
|
|
test("Uses default database with slash", async () => {
|
|
const sql = new SQL("mysql://localhost/");
|
|
expect("mysql").toBe(sql.options.database);
|
|
});
|
|
|
|
test("Result is array", async () => {
|
|
expect(await sql`select 1`).toBeArray();
|
|
});
|
|
|
|
test("Create table", async () => {
|
|
await using sql = new SQL({ ...getOptions(), max: 1 });
|
|
await sql`create table test_my_table(id int)`;
|
|
await sql`drop table test_my_table`;
|
|
});
|
|
|
|
test("Drop table", async () => {
|
|
await using sql = new SQL({ ...getOptions(), max: 1 });
|
|
await sql`create table drop_table_test(id int)`;
|
|
await sql`drop table drop_table_test`;
|
|
// Verify that table is dropped
|
|
const result = await sql`select * from information_schema.tables where table_name = 'drop_table_test'`;
|
|
expect(result).toBeArrayOfSize(0);
|
|
});
|
|
|
|
test("null", async () => {
|
|
expect((await sql`select ${null} as x`)[0].x).toBeNull();
|
|
});
|
|
|
|
test("Unsigned Integer", async () => {
|
|
expect((await sql`select ${0x7fffffff + 2} as x`)[0].x).toBe(2147483649);
|
|
});
|
|
|
|
test("Signed Integer", async () => {
|
|
expect((await sql`select ${-1} as x`)[0].x).toBe(-1);
|
|
expect((await sql`select ${1} as x`)[0].x).toBe(1);
|
|
});
|
|
|
|
test("Double", async () => {
|
|
expect((await sql`select ${1.123456789} as x`)[0].x).toBe(1.123456789);
|
|
});
|
|
|
|
test("String", async () => {
|
|
expect((await sql`select ${"hello"} as x`)[0].x).toBe("hello");
|
|
});
|
|
|
|
test("MediumInt/Int24", async () => {
|
|
await using sql = new SQL({ ...getOptions(), max: 1 });
|
|
let random_name = ("t_" + Bun.randomUUIDv7("hex").replaceAll("-", "")).toLowerCase();
|
|
await sql`CREATE TEMPORARY TABLE ${sql(random_name)} (a mediumint unsigned)`;
|
|
await sql`INSERT INTO ${sql(random_name)} VALUES (${1})`;
|
|
const result = await sql`select * from ${sql(random_name)}`;
|
|
expect(result[0].a).toBe(1);
|
|
const result2 = await sql`select * from ${sql(random_name)}`.simple();
|
|
expect(result2[0].a).toBe(1);
|
|
});
|
|
|
|
test("Boolean/TinyInt/BIT", async () => {
|
|
await using sql = new SQL({ ...getOptions(), max: 1 });
|
|
// Protocol will always return 0 or 1 for TRUE and FALSE when not using a table.
|
|
expect((await sql`select ${false} as x`)[0].x).toBe(0);
|
|
expect((await sql`select ${true} as x`)[0].x).toBe(1);
|
|
let random_name = ("t_" + Bun.randomUUIDv7("hex").replaceAll("-", "")).toLowerCase();
|
|
await sql`CREATE TEMPORARY TABLE ${sql(random_name)} (a bool)`;
|
|
const values = [{ a: true }, { a: false }, { a: 8 }, { a: -1 }];
|
|
await sql`INSERT INTO ${sql(random_name)} ${sql(values)}`;
|
|
const [[a], [b], [c], [d]] = await sql`select * from ${sql(random_name)}`.values();
|
|
expect(a).toBe(1);
|
|
expect(b).toBe(0);
|
|
expect(c).toBe(8);
|
|
expect(d).toBe(-1);
|
|
{
|
|
random_name += "2";
|
|
await sql`CREATE TEMPORARY TABLE ${sql(random_name)} (a tinyint(1) unsigned)`;
|
|
try {
|
|
const values = [{ a: -1 }];
|
|
await sql`INSERT INTO ${sql(random_name)} ${sql(values)}`;
|
|
expect.unreachable();
|
|
} catch (e: any) {
|
|
expect(e.code).toBe("ERR_MYSQL_SERVER_ERROR");
|
|
expect(e.message).toContain("Out of range value for column 'a'");
|
|
}
|
|
|
|
const values = [{ a: 255 }];
|
|
await sql`INSERT INTO ${sql(random_name)} ${sql(values)}`;
|
|
const [[a]] = await sql`select * from ${sql(random_name)}`.values();
|
|
expect(a).toBe(255);
|
|
}
|
|
|
|
{
|
|
random_name += "3";
|
|
await sql`CREATE TEMPORARY TABLE ${sql(random_name)} (a bit(1), b bit(2))`;
|
|
const values = [
|
|
{ a: true, b: 1 },
|
|
{ a: false, b: 2 },
|
|
];
|
|
await sql`INSERT INTO ${sql(random_name)} ${sql(values)}`;
|
|
const results = await sql`select * from ${sql(random_name)}`;
|
|
// return true or false for BIT(1) and buffer for BIT(n)
|
|
expect(results[0].a).toBe(true);
|
|
expect(results[0].b).toEqual(Buffer.from([1]));
|
|
expect(results[1].a).toBe(false);
|
|
expect(results[1].b).toEqual(Buffer.from([2]));
|
|
// text protocol should behave the same
|
|
const results2 = await sql`select * from ${sql(random_name)}`.simple();
|
|
expect(results2[0].a).toBe(true);
|
|
expect(results2[0].b).toEqual(Buffer.from([1]));
|
|
expect(results2[1].a).toBe(false);
|
|
expect(results2[1].b).toEqual(Buffer.from([2]));
|
|
}
|
|
});
|
|
|
|
test("Date", async () => {
|
|
await using sql = new SQL({ ...getOptions(), max: 1 });
|
|
const now = new Date();
|
|
const then = (await sql`select ${now} as x`)[0].x;
|
|
expect(then).toEqual(now);
|
|
});
|
|
|
|
test("Timestamp", async () => {
|
|
{
|
|
const result = (await sql`select DATE_ADD(FROM_UNIXTIME(0), INTERVAL -25 SECOND) as x`)[0].x;
|
|
expect(result.getTime()).toBe(-25000);
|
|
}
|
|
{
|
|
const result = (await sql`select DATE_ADD(FROM_UNIXTIME(0), INTERVAL 25 SECOND) as x`)[0].x;
|
|
expect(result.getSeconds()).toBe(25);
|
|
}
|
|
{
|
|
const result = (await sql`select DATE_ADD(FROM_UNIXTIME(0), INTERVAL 251000 MICROSECOND) as x`)[0].x;
|
|
expect(result.getMilliseconds()).toBe(251);
|
|
}
|
|
{
|
|
const result = (await sql`select DATE_ADD(FROM_UNIXTIME(0), INTERVAL -251000 MICROSECOND) as x`)[0].x;
|
|
expect(result.getTime()).toBe(-251);
|
|
}
|
|
});
|
|
test("time", async () => {
|
|
await using sql = new SQL({ ...getOptions(), max: 1 });
|
|
const random_name = "test_" + randomUUIDv7("hex").replaceAll("-", "");
|
|
await sql`CREATE TEMPORARY TABLE ${sql(random_name)} (a TIME)`;
|
|
const times = [
|
|
{ a: "00:00:00" },
|
|
{ a: "01:01:01" },
|
|
{ a: "10:10:10" },
|
|
{ a: "12:12:59" },
|
|
{ a: "-838:59:59" },
|
|
{ a: "838:59:59" },
|
|
{ a: null },
|
|
];
|
|
await sql`INSERT INTO ${sql(random_name)} ${sql(times)}`;
|
|
const result = await sql`SELECT * FROM ${sql(random_name)}`;
|
|
expect(result).toEqual(times);
|
|
const result2 = await sql`SELECT * FROM ${sql(random_name)}`.simple();
|
|
expect(result2).toEqual(times);
|
|
});
|
|
|
|
test("date", async () => {
|
|
await using sql = new SQL({ ...getOptions(), max: 1 });
|
|
const random_name = "test_" + randomUUIDv7("hex").replaceAll("-", "");
|
|
await sql`CREATE TEMPORARY TABLE ${sql(random_name)} (a DATE)`;
|
|
const dates = [{ a: "2024-01-01" }, { a: "2024-01-02" }, { a: "2024-01-03" }, { a: null }];
|
|
await sql`INSERT INTO ${sql(random_name)} ${sql(dates)}`;
|
|
const result = await sql`SELECT * FROM ${sql(random_name)}`;
|
|
expect(result).toEqual([
|
|
{ a: new Date("2024-01-01") },
|
|
{ a: new Date("2024-01-02") },
|
|
{ a: new Date("2024-01-03") },
|
|
{ a: null },
|
|
]);
|
|
const result2 = await sql`SELECT * FROM ${sql(random_name)}`.simple();
|
|
expect(result2).toEqual([
|
|
{ a: new Date("2024-01-01") },
|
|
{ a: new Date("2024-01-02") },
|
|
{ a: new Date("2024-01-03") },
|
|
{ a: null },
|
|
]);
|
|
});
|
|
|
|
test("JSON", async () => {
|
|
await using sql = new SQL({ ...getOptions(), max: 1 });
|
|
const x = (await sql`select CAST(${{ a: "hello", b: 42 }} AS JSON) as x`)[0].x;
|
|
expect(x).toEqual({ a: "hello", b: 42 });
|
|
|
|
const y = (await sql`select CAST('{"key": "value", "number": 123}' AS JSON) as x`)[0].x;
|
|
expect(y).toEqual({ key: "value", number: 123 });
|
|
|
|
const random_name = ("t_" + Bun.randomUUIDv7("hex").replaceAll("-", "")).toLowerCase();
|
|
await sql`CREATE TEMPORARY TABLE ${sql(random_name)} (a json)`;
|
|
const values = [{ a: { b: 1 } }, { a: { b: 2 } }];
|
|
await sql`INSERT INTO ${sql(random_name)} ${sql(values)}`;
|
|
const [[a], [b]] = await sql`select * from ${sql(random_name)}`.values();
|
|
expect(a).toEqual({ b: 1 });
|
|
expect(b).toEqual({ b: 2 });
|
|
});
|
|
|
|
test("bulk insert nested sql()", async () => {
|
|
await using sql = new SQL({ ...getOptions(), max: 1 });
|
|
await sql`create temporary table test_users (name text, age int)`;
|
|
const users = [
|
|
{ name: "Alice", age: 25 },
|
|
{ name: "Bob", age: 30 },
|
|
];
|
|
try {
|
|
await sql`insert into test_users ${sql(users)}`;
|
|
const result = await sql`select * from test_users`;
|
|
expect(result).toEqual([
|
|
{ name: "Alice", age: 25 },
|
|
{ name: "Bob", age: 30 },
|
|
]);
|
|
} finally {
|
|
await sql`drop table test_users`;
|
|
}
|
|
});
|
|
|
|
test("Escapes", async () => {
|
|
expect(Object.keys((await sql`select 1 as ${sql('hej"hej')}`)[0])[0]).toBe('hej"hej');
|
|
});
|
|
|
|
test("null for int", async () => {
|
|
await using sql = new SQL({ ...getOptions(), max: 1 });
|
|
const result = await sql`create temporary table test_null_for_int (x int)`;
|
|
expect(result.count).toBe(0);
|
|
try {
|
|
await sql`insert into test_null_for_int values(${null})`;
|
|
const result2 = await sql`select * from test_null_for_int`;
|
|
expect(result2).toEqual([{ x: null }]);
|
|
} finally {
|
|
await sql`drop table test_null_for_int`;
|
|
}
|
|
});
|
|
|
|
test("should be able to execute different queries in the same connection #16774", async () => {
|
|
const sql = new SQL({ ...getOptions(), max: 1 });
|
|
const random_table_name = `test_user_${Math.random().toString(36).substring(2, 15)}`;
|
|
await sql`CREATE TEMPORARY TABLE IF NOT EXISTS ${sql(random_table_name)} (id int, name text)`;
|
|
|
|
const promises: Array<Promise<any>> = [];
|
|
// POPULATE TABLE
|
|
for (let i = 0; i < 1_000; i++) {
|
|
promises.push(sql`insert into ${sql(random_table_name)} values (${i}, ${`test${i}`})`.execute());
|
|
}
|
|
await Promise.all(promises);
|
|
|
|
// QUERY TABLE using execute() to force executing the query immediately
|
|
{
|
|
for (let i = 0; i < 1_000; i++) {
|
|
// mix different parameters
|
|
switch (i % 3) {
|
|
case 0:
|
|
promises.push(sql`select id, name from ${sql(random_table_name)} where id = ${i}`.execute());
|
|
break;
|
|
case 1:
|
|
promises.push(sql`select id from ${sql(random_table_name)} where id = ${i}`.execute());
|
|
break;
|
|
case 2:
|
|
promises.push(sql`select 1, id, name from ${sql(random_table_name)} where id = ${i}`.execute());
|
|
break;
|
|
}
|
|
}
|
|
await Promise.all(promises);
|
|
}
|
|
});
|
|
|
|
test("Prepared transaction", async () => {
|
|
await using sql = new SQL({ ...getOptions(), max: 1 });
|
|
await sql`create table test_prepared_transaction (a int)`;
|
|
|
|
try {
|
|
await sql.beginDistributed("tx1", async sql => {
|
|
await sql`insert into test_prepared_transaction values(1)`;
|
|
});
|
|
await sql.commitDistributed("tx1");
|
|
expect((await sql`select count(*) from test_prepared_transaction`).count).toBe(1);
|
|
} finally {
|
|
await sql`drop table test_prepared_transaction`;
|
|
}
|
|
});
|
|
|
|
test("Idle timeout retry works", async () => {
|
|
await using sql = new SQL({ ...getOptions(), idleTimeout: 1 });
|
|
await sql`select 1`;
|
|
await Bun.sleep(1100); // 1.1 seconds so it should retry
|
|
await sql`select 1`;
|
|
expect().pass();
|
|
});
|
|
|
|
test("Fragments in transactions", async () => {
|
|
const sql = new SQL({ ...getOptions(), debug: true, idle_timeout: 1, fetch_types: false });
|
|
expect((await sql.begin(sql => sql`select 1 as x where ${sql`1=1`}`))[0].x).toBe(1);
|
|
});
|
|
|
|
test("Helpers in Transaction", async () => {
|
|
const result = await sql.begin(async sql => await sql`select ${sql.unsafe("1 as x")}`);
|
|
expect(result[0].x).toBe(1);
|
|
});
|
|
|
|
test("Undefined values throws", async () => {
|
|
const result = await sql`select ${undefined} as x`;
|
|
expect(result[0].x).toBeNull();
|
|
});
|
|
|
|
test("Null sets to null", async () => {
|
|
expect((await sql`select ${null} as x`)[0].x).toBeNull();
|
|
});
|
|
|
|
// Add code property.
|
|
test("Throw syntax error", async () => {
|
|
await using sql = new SQL({ ...getOptions(), max: 1 });
|
|
const err = await sql`wat 1`.catch(x => x);
|
|
expect(err.code).toBe("ERR_MYSQL_SYNTAX_ERROR");
|
|
});
|
|
|
|
test("should work with fragments", async () => {
|
|
await using sql = new SQL({ ...getOptions(), max: 1 });
|
|
const random_name = sql("test_" + randomUUIDv7("hex").replaceAll("-", ""));
|
|
await sql`CREATE TEMPORARY TABLE IF NOT EXISTS ${random_name} (id int, hotel_id int, created_at timestamp)`;
|
|
await sql`INSERT INTO ${random_name} VALUES (1, 1, '2024-01-01 10:00:00')`;
|
|
// single escaped identifier
|
|
{
|
|
const results = await sql`SELECT * FROM ${random_name}`;
|
|
expect(results).toEqual([{ id: 1, hotel_id: 1, created_at: new Date("2024-01-01T10:00:00.000Z") }]);
|
|
}
|
|
// multiple escaped identifiers
|
|
{
|
|
const results = await sql`SELECT ${random_name}.* FROM ${random_name}`;
|
|
expect(results).toEqual([{ id: 1, hotel_id: 1, created_at: new Date("2024-01-01T10:00:00.000Z") }]);
|
|
}
|
|
// even more complex fragment
|
|
{
|
|
const results =
|
|
await sql`SELECT ${random_name}.* FROM ${random_name} WHERE ${random_name}.hotel_id = ${1} ORDER BY ${random_name}.created_at DESC`;
|
|
expect(results).toEqual([{ id: 1, hotel_id: 1, created_at: new Date("2024-01-01T10:00:00.000Z") }]);
|
|
}
|
|
});
|
|
test("should handle nested fragments", async () => {
|
|
await using sql = new SQL({ ...getOptions(), max: 1 });
|
|
const random_name = sql("test_" + randomUUIDv7("hex").replaceAll("-", ""));
|
|
|
|
await sql`CREATE TEMPORARY TABLE IF NOT EXISTS ${random_name} (id int, hotel_id int, created_at timestamp)`;
|
|
await sql`INSERT INTO ${random_name} VALUES (1, 1, '2024-01-01 10:00:00')`;
|
|
await sql`INSERT INTO ${random_name} VALUES (2, 1, '2024-01-02 10:00:00')`;
|
|
await sql`INSERT INTO ${random_name} VALUES (3, 2, '2024-01-03 10:00:00')`;
|
|
|
|
// fragment containing another scape fragment for the field name
|
|
const orderBy = (field_name: string) => sql`ORDER BY ${sql(field_name)} DESC`;
|
|
|
|
// dynamic information
|
|
const sortBy = { should_sort: true, field: "created_at" };
|
|
const user = { hotel_id: 1 };
|
|
|
|
// query containing the fragments
|
|
const results = await sql`
|
|
SELECT ${random_name}.*
|
|
FROM ${random_name}
|
|
WHERE ${random_name}.hotel_id = ${user.hotel_id}
|
|
${sortBy.should_sort ? orderBy(sortBy.field) : sql``}`;
|
|
expect(results).toEqual([
|
|
{ id: 2, hotel_id: 1, created_at: new Date("2024-01-02T10:00:00.000Z") },
|
|
{ id: 1, hotel_id: 1, created_at: new Date("2024-01-01T10:00:00.000Z") },
|
|
]);
|
|
});
|
|
|
|
test("Support dynamic password function", async () => {
|
|
await using sql = new SQL({ ...getOptions(), password: () => password, max: 1 });
|
|
return expect((await sql`select 1 as x`)[0].x).toBe(1);
|
|
});
|
|
|
|
test("Support dynamic async resolved password function", async () => {
|
|
await using sql = new SQL({
|
|
...getOptions(),
|
|
password: () => Promise.resolve(password),
|
|
max: 1,
|
|
});
|
|
return expect((await sql`select 1 as x`)[0].x).toBe(1);
|
|
});
|
|
|
|
test("Support dynamic async password function", async () => {
|
|
await using sql = new SQL({
|
|
...getOptions(),
|
|
max: 1,
|
|
password: async () => {
|
|
await Bun.sleep(10);
|
|
return password;
|
|
},
|
|
});
|
|
return expect((await sql`select 1 as x`)[0].x).toBe(1);
|
|
});
|
|
test("Support dynamic async rejected password function", async () => {
|
|
await using sql = new SQL({
|
|
...getOptions(),
|
|
password: () => Promise.reject(new Error("password error")),
|
|
max: 1,
|
|
});
|
|
try {
|
|
await sql`select true as x`;
|
|
expect.unreachable();
|
|
} catch (e: any) {
|
|
expect(e.message).toBe("password error");
|
|
}
|
|
});
|
|
|
|
test("Support dynamic async password function that throws", async () => {
|
|
await using sql = new SQL({
|
|
...getOptions(),
|
|
max: 1,
|
|
password: async () => {
|
|
await Bun.sleep(10);
|
|
throw new Error("password error");
|
|
},
|
|
});
|
|
try {
|
|
await sql`select true as x`;
|
|
expect.unreachable();
|
|
} catch (e: any) {
|
|
expect(e).toBeInstanceOf(Error);
|
|
expect(e.message).toBe("password error");
|
|
}
|
|
});
|
|
|
|
test("sql file", async () => {
|
|
expect((await sql.file(rel("select.sql")))[0].x).toBe(1);
|
|
});
|
|
|
|
test("sql file throws", async () => {
|
|
expect(await sql.file(rel("selectomondo.sql")).catch(x => x.code)).toBe("ENOENT");
|
|
});
|
|
test("Parameters in file", async () => {
|
|
const result = await sql.file(rel("select-param.sql"), ["hello"]);
|
|
return expect(result[0].x).toBe("hello");
|
|
});
|
|
|
|
test("Connection ended promise", async () => {
|
|
const sql = new SQL(getOptions());
|
|
|
|
await sql.end();
|
|
|
|
expect(await sql.end()).toBeUndefined();
|
|
});
|
|
|
|
test("Connection ended timeout", async () => {
|
|
const sql = new SQL(getOptions());
|
|
|
|
await sql.end({ timeout: 10 });
|
|
|
|
expect(await sql.end()).toBeUndefined();
|
|
});
|
|
|
|
test("Connection ended error", async () => {
|
|
const sql = new SQL(getOptions());
|
|
await sql.end();
|
|
return expect(await sql``.catch(x => x.code)).toBe("ERR_MYSQL_CONNECTION_CLOSED");
|
|
});
|
|
|
|
test("Connection end does not cancel query", async () => {
|
|
const sql = new SQL(getOptions());
|
|
|
|
const promise = sql`select SLEEP(1) as x`.execute();
|
|
await sql.end();
|
|
return expect(await promise).toEqual([{ x: 0 }]);
|
|
});
|
|
|
|
test("Connection destroyed", async () => {
|
|
const sql = new SQL(getOptions());
|
|
process.nextTick(() => sql.end({ timeout: 0 }));
|
|
expect(await sql``.catch(x => x.code)).toBe("ERR_MYSQL_CONNECTION_CLOSED");
|
|
});
|
|
|
|
test("Connection destroyed with query before", async () => {
|
|
const sql = new SQL(getOptions());
|
|
const error = sql`select SLEEP(0.2)`.catch(err => err.code);
|
|
|
|
sql.end({ timeout: 0 });
|
|
return expect(await error).toBe("ERR_MYSQL_CONNECTION_CLOSED");
|
|
});
|
|
|
|
test("unsafe", async () => {
|
|
await using sql = new SQL({ ...getOptions(), max: 1 });
|
|
await sql`create temporary table test_unsafe (x int)`;
|
|
try {
|
|
await sql.unsafe("insert into test_unsafe values (?)", [1]);
|
|
const [{ x }] = await sql`select * from test_unsafe`;
|
|
expect(x).toBe(1);
|
|
} finally {
|
|
await sql`drop table test_unsafe`;
|
|
}
|
|
});
|
|
|
|
test("unsafe simple", async () => {
|
|
await using sql = new SQL({ ...getOptions(), max: 1 });
|
|
expect(await sql.unsafe("select 1 as x")).toEqual([{ x: 1 }]);
|
|
});
|
|
|
|
test("simple query with multiple statements", async () => {
|
|
await using sql = new SQL({ ...getOptions(), max: 1 });
|
|
const result = await sql`select 1 as x;select 2 as x`.simple();
|
|
expect(result).toBeDefined();
|
|
expect(result.length).toEqual(2);
|
|
expect(result[0][0].x).toEqual(1);
|
|
expect(result[1][0].x).toEqual(2);
|
|
});
|
|
|
|
test("simple query using unsafe with multiple statements", async () => {
|
|
await using sql = new SQL({ ...getOptions(), max: 1 });
|
|
const result = await sql.unsafe("select 1 as x;select 2 as x");
|
|
expect(result).toBeDefined();
|
|
expect(result.length).toEqual(2);
|
|
expect(result[0][0].x).toEqual(1);
|
|
expect(result[1][0].x).toEqual(2);
|
|
});
|
|
|
|
test("only allows one statement", async () => {
|
|
expect(await sql`select 1; select 2`.catch(e => e.message)).toBe(
|
|
"You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'select 2' at line 1",
|
|
);
|
|
});
|
|
|
|
test("await sql() throws not tagged error", async () => {
|
|
try {
|
|
await sql("select 1");
|
|
expect.unreachable();
|
|
} catch (e: any) {
|
|
expect(e.code).toBe("ERR_MYSQL_NOT_TAGGED_CALL");
|
|
}
|
|
});
|
|
|
|
test("sql().then throws not tagged error", async () => {
|
|
try {
|
|
await sql("select 1").then(() => {
|
|
/* noop */
|
|
});
|
|
expect.unreachable();
|
|
} catch (e: any) {
|
|
expect(e.code).toBe("ERR_MYSQL_NOT_TAGGED_CALL");
|
|
}
|
|
});
|
|
|
|
test("sql().catch throws not tagged error", async () => {
|
|
try {
|
|
sql("select 1").catch(() => {
|
|
/* noop */
|
|
});
|
|
expect.unreachable();
|
|
} catch (e: any) {
|
|
expect(e.code).toBe("ERR_MYSQL_NOT_TAGGED_CALL");
|
|
}
|
|
});
|
|
|
|
test("sql().finally throws not tagged error", async () => {
|
|
try {
|
|
sql("select 1").finally(() => {
|
|
/* noop */
|
|
});
|
|
expect.unreachable();
|
|
} catch (e: any) {
|
|
expect(e.code).toBe("ERR_MYSQL_NOT_TAGGED_CALL");
|
|
}
|
|
});
|
|
|
|
test("little bobby tables", async () => {
|
|
await using sql = new SQL({ ...getOptions(), max: 1 });
|
|
const name = "Robert'); DROP TABLE students;--";
|
|
|
|
try {
|
|
await sql`create table students (name text, age int)`;
|
|
await sql`insert into students (name) values (${name})`;
|
|
|
|
expect((await sql`select name from students`)[0].name).toBe(name);
|
|
} finally {
|
|
await sql`drop table students`;
|
|
}
|
|
});
|
|
|
|
test("Connection errors are caught using begin()", async () => {
|
|
let error;
|
|
try {
|
|
const sql = new SQL({ host: "localhost", port: 1, adapter: "mysql" });
|
|
|
|
await sql.begin(async sql => {
|
|
await sql`insert into test_connection_errors (label, value) values (${1}, ${2})`;
|
|
});
|
|
} catch (err) {
|
|
error = err;
|
|
}
|
|
expect(error.code).toBe("ERR_MYSQL_CONNECTION_CLOSED");
|
|
});
|
|
|
|
test("dynamic table name", async () => {
|
|
await using sql = new SQL({ ...getOptions(), max: 1 });
|
|
await sql`create table test_dynamic_table_name(a int)`;
|
|
try {
|
|
return expect((await sql`select * from ${sql("test_dynamic_table_name")}`).length).toBe(0);
|
|
} finally {
|
|
await sql`drop table test_dynamic_table_name`;
|
|
}
|
|
});
|
|
|
|
test("dynamic column name", async () => {
|
|
await using sql = new SQL({ ...getOptions(), max: 1 });
|
|
const result = await sql`select 1 as ${sql("!not_valid")}`;
|
|
expect(Object.keys(result[0])[0]).toBe("!not_valid");
|
|
});
|
|
|
|
test("dynamic insert", async () => {
|
|
await using sql = new SQL({ ...getOptions(), max: 1 });
|
|
await sql`create table test_dynamic_insert (a int, b text)`;
|
|
try {
|
|
const x = { a: 42, b: "the answer" };
|
|
await sql`insert into test_dynamic_insert ${sql(x)}`;
|
|
const [{ b }] = await sql`select * from test_dynamic_insert`;
|
|
expect(b).toBe("the answer");
|
|
} finally {
|
|
await sql`drop table test_dynamic_insert`;
|
|
}
|
|
});
|
|
|
|
test("dynamic insert pluck", async () => {
|
|
await using sql = new SQL({ ...getOptions(), max: 1 });
|
|
try {
|
|
await sql`create table test_dynamic_insert_pluck (a int, b text)`;
|
|
const x = { a: 42, b: "the answer" };
|
|
await sql`insert into test_dynamic_insert_pluck ${sql(x, "a")}`;
|
|
const [{ b, a }] = await sql`select * from test_dynamic_insert_pluck`;
|
|
expect(b).toBeNull();
|
|
expect(a).toBe(42);
|
|
} finally {
|
|
await sql`drop table test_dynamic_insert_pluck`;
|
|
}
|
|
});
|
|
|
|
test("bigint is returned as String", async () => {
|
|
expect(typeof (await sql`select 9223372036854777 as x`)[0].x).toBe("string");
|
|
});
|
|
|
|
test("bigint is returned as BigInt", async () => {
|
|
await using sql = new SQL({
|
|
...getOptions(),
|
|
bigint: true,
|
|
});
|
|
expect((await sql`select 9223372036854777 as x`)[0].x).toBe(9223372036854777n);
|
|
});
|
|
|
|
test("int is returned as Number", async () => {
|
|
expect((await sql`select CAST(123 AS SIGNED) as x`)[0].x).toBe(123);
|
|
});
|
|
|
|
test("flush should work", async () => {
|
|
await sql`select 1`;
|
|
sql.flush();
|
|
});
|
|
|
|
describe("timeouts", () => {
|
|
test.each(["connect_timeout", "connectTimeout", "connectionTimeout", "connection_timeout"] as const)(
|
|
"connection timeout key %p throws",
|
|
async key => {
|
|
const server = net.createServer().listen();
|
|
|
|
const port = (server.address() as import("node:net").AddressInfo).port;
|
|
|
|
const sql = new SQL({ adapter: "mysql", port, host: "127.0.0.1", max: 1, [key]: 0.2 });
|
|
|
|
try {
|
|
await sql`select 1`;
|
|
throw new Error("should not reach");
|
|
} catch (e) {
|
|
expect(e).toBeInstanceOf(Error);
|
|
expect(e.code).toBe("ERR_MYSQL_CONNECTION_TIMEOUT" as any);
|
|
expect(e.message).toMatch(/Connection time(d out|out) after 200ms/);
|
|
} finally {
|
|
sql.close();
|
|
server.close();
|
|
}
|
|
},
|
|
{
|
|
timeout: 1000,
|
|
},
|
|
);
|
|
});
|
|
test("Array returns rows as arrays of columns", async () => {
|
|
return [(await sql`select CAST(1 AS SIGNED) as x`.values())[0][0], 1];
|
|
});
|
|
},
|
|
);
|
|
}
|
|
}
|