import { spawnSync } from "bun"; import { constants, Database, SQLiteError } from "bun:sqlite"; import { describe, expect, it } from "bun:test"; import { existsSync, readdirSync, realpathSync, writeFileSync } from "fs"; import { bunExe, isMacOS, isMacOSVersionAtLeast, isWindows, tempDirWithFiles } from "harness"; import { tmpdir } from "os"; import path from "path"; const tmpbase = tmpdir() + path.sep; describe("as", () => { it("should return an implementation of the class", () => { const db = new Database(":memory:"); db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)"); db.run("INSERT INTO test (name) VALUES ('Hello')"); db.run("INSERT INTO test (name) VALUES ('World')"); const q = db.query("SELECT * FROM test WHERE name = ?"); class MyTest { name; get isHello() { return this.name === "Hello"; } } expect(q.get("Hello")).not.toBeInstanceOf(MyTest); q.as(MyTest); expect(q.get("Hello")).toBeInstanceOf(MyTest); expect(q.get("Hello").isHello).toBe(true); const list = db.query("SELECT * FROM test"); list.as(MyTest); const all = list.all(); expect(all[0]).toBeInstanceOf(MyTest); expect(all[0].isHello).toBe(true); expect(all[1]).toBeInstanceOf(MyTest); expect(all[1].isHello).toBe(false); }); it("should work with more complicated getters", () => { class User { rawBirthdate; get birthdate() { return new Date(this.rawBirthdate); } } const db = new Database(":memory:"); db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, rawBirthdate TEXT)"); db.run("INSERT INTO users (rawBirthdate) VALUES ('1995-12-19')"); const query = db.query("SELECT * FROM users"); query.as(User); const user = query.get(); expect(user.birthdate.getTime()).toBe(new Date("1995-12-19").getTime()); }); it("validates the class", () => { const db = new Database(":memory:"); db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)"); db.run("INSERT INTO test (name) VALUES ('Hello')"); expect(() => db.query("SELECT * FROM test").as(null)).toThrow("Expected class to be a constructor or undefined"); expect(() => db.query("SELECT * FROM test").as(() => {})).toThrow("Expected a constructor"); function BadClass() {} BadClass.prototype = 123; expect(() => db.query("SELECT * FROM test").as(BadClass)).toThrow( "Expected a constructor prototype to be an object", ); }); }); describe("safeIntegers", () => { it("should default to false", () => { const db = Database.open(":memory:"); db.exec("CREATE TABLE foo (id INTEGER PRIMARY KEY AUTOINCREMENT, age INTEGER NOT NULL)"); db.run("INSERT INTO foo (age) VALUES (?)", BigInt(Number.MAX_SAFE_INTEGER) + 10n); const query = db.query("SELECT * FROM foo"); expect(query.all()).toEqual([{ id: 1, age: Number.MAX_SAFE_INTEGER + 10 }]); query.safeIntegers(true); expect(query.all()).toEqual([ { id: 1n, age: BigInt(Number.MAX_SAFE_INTEGER) + 10n, }, ]); }); it("should allow overwriting default", () => { const db = Database.open(":memory:", { safeIntegers: true }); db.exec("CREATE TABLE foo (id INTEGER PRIMARY KEY AUTOINCREMENT, age INTEGER NOT NULL)"); db.run("INSERT INTO foo (age) VALUES (?)", BigInt(Number.MAX_SAFE_INTEGER) + 10n); const query = db.query("SELECT * FROM foo"); expect(query.all()).toEqual([ { id: 1n, age: BigInt(Number.MAX_SAFE_INTEGER) + 10n, }, ]); query.safeIntegers(false); query.as; expect(query.all()).toEqual([{ id: 1, age: Number.MAX_SAFE_INTEGER + 10 }]); }); it("should throw range error if value is out of range", () => { const db = new Database(":memory:", { safeIntegers: true }); db.run("CREATE TABLE test (id INTEGER PRIMARY KEY, value INTEGER)"); const query = db.query("INSERT INTO test (value) VALUES ($value)"); expect(() => query.run({ $value: BigInt(Number.MAX_SAFE_INTEGER) ** 2n })).toThrow(RangeError); query.safeIntegers(false); expect(() => query.run({ $value: BigInt(Number.MAX_SAFE_INTEGER) ** 2n })).not.toThrow(RangeError); }); }); { const strictInputs = [ { name: "myname", age: 42 }, { age: 42, name: "myname" }, ["myname", 42], { 0: "myname", 1: 42 }, { 1: "myname", 0: 42 }, ]; const queries = ["$name, $age", "$name, $age", "?, ?", "?1, ?2", "?2, ?1"]; const uglyInputs = [ { $name: "myname", $age: 42 }, { $age: 42, $name: "myname" }, ["myname", 42], { "?1": "myname", "?2": 42 }, { "?2": "myname", "?1": 42 }, ]; for (const strict of [true, false]) { describe(strict ? "strict" : "default", () => { const inputs = strict ? strictInputs : uglyInputs; for (let i = 0; i < strictInputs.length; i++) { const input = inputs[i]; const query = queries[i]; it(`${JSON.stringify(input)} -> ${query}`, () => { const db = Database.open(":memory:", { strict }); db.exec( "CREATE TABLE cats (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE NOT NULL, age INTEGER NOT NULL)", ); const { changes, lastInsertRowid } = db.run(`INSERT INTO cats (name, age) VALUES (${query})`, input); expect(changes).toBe(1); expect(lastInsertRowid).toBe(1); expect(db.query("SELECT * FROM cats").all()).toStrictEqual([ { id: 1, name: "myname", age: 42, }, ]); expect(db.query(`SELECT * FROM cats WHERE (name, age) = (${query})`).all(input)).toStrictEqual([ { id: 1, name: "myname", age: 42 }, ]); expect(db.query(`SELECT * FROM cats WHERE (name, age) = (${query})`).get(input)).toStrictEqual({ id: 1, name: "myname", age: 42, }); expect(db.query(`SELECT * FROM cats WHERE (name, age) = (${query})`).values(input)).toStrictEqual([ [1, "myname", 42], ]); }); } if (strict) { describe("throws missing parameter error in", () => { for (let method of ["all", "get", "values", "run"]) { it(`${method}()`, () => { const db = Database.open(":memory:", { strict: true }); db.exec("CREATE TABLE cats (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, age INTEGER)"); expect(() => { const query = db.query("INSERT INTO cats (name, age) VALUES (@name, @age)"); query[method]({ "name": "Joey", }); }).toThrow('Missing parameter "age"'); }); } }); } }); } } var encode = text => new TextEncoder().encode(text); // Use different numbers of columns to ensure we crash if using initializeIndex() on a large array can cause bugs. // https://github.com/oven-sh/bun/issues/11747 it.each([1, 16, 256, 512, 768])("should work with duplicate columns in values() of length %d", columnCount => { const db = new Database(":memory:"); db.prepare( `create table \`users\` ( id integer primary key autoincrement, name text, reportTo integer, ${Array.from( { length: columnCount, }, (_, i) => `column${i} text DEFAULT "make GC happen!!" NOT NULL${i === columnCount - 1 ? "" : ","}`, ).join("")} );`, ).run(); const names = [ ["dan", null], ["alef", 1], ["bob", 2], ["carl", 3], ["dave", 4], ["eve", 5], ["fred", 6], ["george", 7], ["harry", 8], ["isaac", 9], ["jacob", 10], ["kevin", 11], ["larry", 12], ["mike", 13], ["nathan", 14], ["oscar", 15], ["peter", 16], ["qwerty", 17], ["robert", 18], ["samuel", 19], ["tom", 20], ["william", 21], ["xavier", 22], ["yanny", 23], ["zachary", 24], ]; for (const [name, reportTo] of names) { db.prepare("insert into `users` (name, reportTo) values (?, ?);").run(name, reportTo); } const results = db .prepare("select * from 'users' left join 'users' reportee on `users`.id = reportee.reportTo; ") .values(); expect(results).toHaveLength(names.length); expect(results[0]).toHaveLength((columnCount + 3) * 2); let prevResult; for (let result of results) { expect(result).toHaveLength((columnCount + 3) * 2); if (prevResult) { expect(prevResult.slice(columnCount + 3, (columnCount + 3) * 2)).toEqual(result.slice(0, columnCount + 3)); } prevResult = result; } }); it("Database.open", () => { // in a folder which doesn't exist try { Database.open("/this/database/does/not/exist.sqlite", constants.SQLITE_OPEN_READWRITE); throw new Error("Expected an error to be thrown"); } catch (error) { expect(error.message).toBe("unable to open database file"); } // in a file which doesn't exist try { Database.open(tmpbase + `database-${Math.random()}.sqlite`, constants.SQLITE_OPEN_READWRITE); throw new Error("Expected an error to be thrown"); } catch (error) { expect(error.message).toBe("unable to open database file"); } // in a file which doesn't exist try { Database.open(tmpbase + `database-${Math.random()}.sqlite`, { readonly: true, }); throw new Error("Expected an error to be thrown"); } catch (error) { expect(error.message).toBe("unable to open database file"); } // in a file which doesn't exist try { Database.open(tmpbase + `database-${Math.random()}.sqlite`, { readwrite: true, }); throw new Error("Expected an error to be thrown"); } catch (error) { expect(error.message).toBe("unable to open database file"); } // create works { var db = Database.open(tmpbase + `database-${Math.random()}.sqlite`, { create: true, }); db.close(); } // this should not throw // it creates an in-memory db new Database().close(); }); it("upsert cross-process, see #1366", () => { const dir = realpathSync(tmpdir()) + "/"; const { exitCode } = spawnSync([bunExe(), import.meta.dir + "/sqlite-cross-process.js"], { env: { SQLITE_DIR: dir, }, stderr: "inherit", }); expect(exitCode).toBe(0); const db2 = Database.open(dir + "get-persist.sqlite"); expect(db2.query(`SELECT id FROM examples`).all()).toEqual([ { id: "hello" }, { id: "world", }, ]); }); it("creates", () => { const db = Database.open(":memory:"); db.exec( "CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT, value INTEGER, created TEXT, deci FLOAT, blobby BLOB)", ); const stmt = db.prepare("INSERT INTO test (name, value, deci, created, blobby) VALUES (?, ?, ?, ?, ?)"); stmt.run(["foo", 1, Math.fround(1.111), new Date(1995, 12, 19).toISOString(), encode("Hello World")]); stmt.run(["bar", 2, Math.fround(2.222), new Date(1995, 12, 19).toISOString(), encode("Hello World")]); stmt.run(["baz", 3, Math.fround(3.333), new Date(1995, 12, 19).toISOString(), encode("Hello World")]); stmt.finalize(); const stmt2 = db.prepare("SELECT * FROM test"); expect(JSON.stringify(stmt2.get())).toBe( JSON.stringify({ id: 1, name: "foo", value: 1, created: new Date(1995, 12, 19).toISOString(), deci: Math.fround(1.111), blobby: encode("Hello World"), }), ); expect(JSON.stringify(stmt2.all())).toBe( JSON.stringify([ { id: 1, name: "foo", value: 1, created: new Date(1995, 12, 19).toISOString(), deci: Math.fround(1.111), blobby: encode("Hello World"), }, { id: 2, name: "bar", value: 2, created: new Date(1995, 12, 19).toISOString(), deci: Math.fround(2.222), blobby: encode("Hello World"), }, { id: 3, name: "baz", value: 3, created: new Date(1995, 12, 19).toISOString(), deci: Math.fround(3.333), blobby: encode("Hello World"), }, ]), ); expect(stmt2.run()).toStrictEqual({ changes: 0, lastInsertRowid: 3, }); // not necessary to run but it's a good practice stmt2.finalize(); }); it("int52", () => { const db = Database.open(":memory:"); db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, int64 INTEGER)"); db.run("INSERT INTO test (int64) VALUES (?)", Number.MAX_SAFE_INTEGER); expect(db.query("SELECT * FROM test").get().int64).toBe(Number.MAX_SAFE_INTEGER); }); it("typechecks", () => { const db = Database.open(":memory:"); db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)"); db.exec('INSERT INTO test (name) VALUES ("Hello")'); db.exec('INSERT INTO test (name) VALUES ("World")'); const q = db.prepare("SELECT * FROM test WHERE (name = ?)"); var expectfail = val => { try { q.run([val]); throw new Error("Expected error"); } catch (e) { expect(e.message !== "Expected error").toBe(true); expect(e.name).toBe("TypeError"); } try { q.all([val]); throw new Error("Expected error"); } catch (e) { expect(e.message !== "Expected error").toBe(true); expect(e.name).toBe("TypeError"); } try { q.get([val]); throw new Error("Expected error"); } catch (e) { expect(e.message !== "Expected error").toBe(true); expect(e.name).toBe("TypeError"); } }; expectfail(Symbol("oh hai")); expectfail(new Date()); expectfail(class Foo {}); expectfail(() => class Foo {}); expectfail(new RangeError("what")); expectfail(new Map()); expectfail(new Map([["foo", "bar"]])); expectfail(new Set()); expectfail(new Set([1, 2, 3])); }); it("db.query supports TypedArray", () => { const db = Database.open(":memory:"); db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, blobby BLOB)"); const stmt = db.prepare("INSERT INTO test (blobby) VALUES (?)"); stmt.run([encode("Hello World")]); stmt.finalize(); const stmt2 = db.prepare("SELECT * FROM test"); expect(JSON.stringify(stmt2.get())).toBe( JSON.stringify({ id: 1, blobby: encode("Hello World"), }), ); const stmt3 = db.prepare("SELECT * FROM test WHERE (blobby = ?)"); expect(JSON.stringify(stmt3.get([encode("Hello World")]))).toBe( JSON.stringify({ id: 1, blobby: encode("Hello World"), }), ); expect(JSON.stringify(db.query("SELECT * FROM test WHERE (blobby = ?)").get([encode("Hello World")]))).toBe( JSON.stringify({ id: 1, blobby: encode("Hello World"), }), ); expect(stmt3.get([encode("Hello World NOT")])).toBe(null); }); it("supports serialize/deserialize", () => { const db = Database.open(":memory:"); db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)"); db.exec('INSERT INTO test (name) VALUES ("Hello")'); db.exec('INSERT INTO test (name) VALUES ("World")'); const input = db.serialize(); const db2 = new Database(input); const stmt = db2.prepare("SELECT * FROM test"); expect(JSON.stringify(stmt.get())).toBe( JSON.stringify({ id: 1, name: "Hello", }), ); expect(JSON.stringify(stmt.all())).toBe( JSON.stringify([ { id: 1, name: "Hello", }, { id: 2, name: "World", }, ]), ); db2.exec("insert into test (name) values ('foo')"); expect(JSON.stringify(stmt.all())).toBe( JSON.stringify([ { id: 1, name: "Hello", }, { id: 2, name: "World", }, { id: 3, name: "foo", }, ]), ); const db3 = new Database(input, { readonly: true }); try { db3.exec("insert into test (name) values ('foo')"); throw new Error("Expected error"); } catch (e) { expect(e.message).toBe("attempt to write a readonly database"); } // https://github.com/oven-sh/bun/issues/3712#issuecomment-1725259824 expect(Database.deserialize(input)).toBeInstanceOf(Database); }); it("Database.deserialize should support strict mode", () => { const db1 = new Database(":memory:"); db1.run("CREATE TABLE test (name TEXT)"); db1.run("INSERT INTO test VALUES (:name)", { ":name": "test1" }); // Deserialize the database with strict mode const serialized = db1.serialize(); const db2 = Database.deserialize(serialized, { strict: true, readonly: false, }); // Use strict mode db2.run("CREATE TABLE test2 (name TEXT)"); db2.run("INSERT INTO test2 VALUES ($name)", { name: "test2" }); // Verify the data was inserted correctly const result = db2.query("SELECT * FROM test2").all(); expect(result).toEqual([{ name: "test2" }]); // Also verify we can access the data from the original database const result1 = db2.query("SELECT * FROM test").all(); expect(result1).toEqual([{ name: "test1" }]); }); it("Database.deserialize should support readonly when passed as a flag or boolean", () => { expect.assertions(2); const db1 = new Database(":memory:"); db1.run("CREATE TABLE test (name TEXT)"); db1.run("INSERT INTO test VALUES (:name)", { ":name": "test1" }); // Deserialize the database with readonly as flag const serialized = db1.serialize(); const db2 = Database.deserialize(serialized, { readonly: true, }); // Create another table // It should fail because it is readonly. try { db2.run("CREATE TABLE test2 (name TEXT)"); } catch (e) { expect(e.message).toContain("attempt to write a readonly database"); } // Deserialize the database with readonly as boolean const db3 = Database.deserialize(serialized, true); try { db3.run("CREATE TABLE test2 (name TEXT)"); } catch (e) { expect(e.message).toContain("attempt to write a readonly database"); } }); it("db.query()", () => { const db = Database.open(":memory:"); db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)"); expect(db[Symbol.for("Bun.Database.cache.count")]).toBe(0); var q = db.query("SELECT * FROM test WHERE name = ?"); expect(q.get("Hello") === null).toBe(true); db.exec('INSERT INTO test (name) VALUES ("Hello")'); db.exec('INSERT INTO test (name) VALUES ("World")'); var rows = db.query("SELECT * FROM test WHERE name = ?").all(["Hello"]); expect(JSON.stringify(rows)).toBe(JSON.stringify([{ id: 1, name: "Hello" }])); rows = db.query("SELECT * FROM test WHERE name = ?").all(["World"]); // if this fails, it means the query caching failed to update expect(JSON.stringify(rows)).toBe(JSON.stringify([{ id: 2, name: "World" }])); rows = db.query("SELECT * FROM test WHERE name = ?").all(["Hello"]); expect(JSON.stringify(rows)).toBe(JSON.stringify([{ id: 1, name: "Hello" }])); // check that the query is cached expect(db[Symbol.for("Bun.Database.cache.count")]).toBe(1); db.clearQueryCache(); // check clearing the cache decremented the counter expect(db[Symbol.for("Bun.Database.cache.count")]).toBe(0); q.finalize(); try { // check clearing the cache decremented the counter q.all(["Hello"]); throw new Error("Should have thrown"); } catch (e) { expect(e.message !== "Should have thrown").toBe(true); } // check that invalid queries are not cached // and invalid queries throw try { db.query("SELECT * FROM BACON", ["Hello"]).all(); throw new Error("Should have thrown"); } catch (e) { expect(e.message !== "Should have thrown").toBe(true); expect(db[Symbol.for("Bun.Database.cache.count")]).toBe(0); } // check that it supports multiple arguments expect(JSON.stringify(db.query("SELECT * FROM test where (name = ? OR name = ?)").all(["Hello", "Fooooo"]))).toBe( JSON.stringify([{ id: 1, name: "Hello" }]), ); expect(JSON.stringify(db.query("SELECT * FROM test where (name = ? OR name = ?)").all("Hello", "Fooooo"))).toBe( JSON.stringify([{ id: 1, name: "Hello" }]), ); // throws if insufficeint arguments try { db.query("SELECT * FROM test where (name = ? OR name = ?)").all("Hello"); } catch (e) { expect(e.message).toBe("SQLite query expected 2 values, received 1"); } // named parameters expect( JSON.stringify( db.query("SELECT * FROM test where (name = $hello OR name = $goodbye)").all({ $hello: "Hello", $goodbye: "Fooooo", }), ), ).toBe(JSON.stringify([{ id: 1, name: "Hello" }])); const domjit = db.query("SELECT * FROM test"); (function (domjit) { for (let i = 0; i < 100000; i++) { domjit.get().name; } })(domjit); // statement iterator let i; i = 0; for (const row of db.query("SELECT * FROM test")) { i === 0 && expect(JSON.stringify(row)).toBe(JSON.stringify({ id: 1, name: "Hello" })); i === 1 && expect(JSON.stringify(row)).toBe(JSON.stringify({ id: 2, name: "World" })); i++; } expect(i).toBe(2); // iterate (no args) i = 0; for (const row of db.query("SELECT * FROM test").iterate()) { i === 0 && expect(JSON.stringify(row)).toBe(JSON.stringify({ id: 1, name: "Hello" })); i === 1 && expect(JSON.stringify(row)).toBe(JSON.stringify({ id: 2, name: "World" })); i++; } expect(i).toBe(2); // iterate (args) i = 0; for (const row of db.query("SELECT * FROM test WHERE name = $name").iterate({ $name: "World", })) { i === 0 && expect(JSON.stringify(row)).toBe(JSON.stringify({ id: 2, name: "World" })); i++; } expect(i).toBe(1); // interrupted iterating, then call all() const stmt = db.query("SELECT * FROM test"); i = 0; for (const row of stmt) { i === 0 && expect(JSON.stringify(row)).toBe(JSON.stringify({ id: 1, name: "Hello" })); i++; break; } expect(i).toBe(1); rows = stmt.all(); expect(JSON.stringify(rows)).toBe( JSON.stringify([ { id: 1, name: "Hello" }, { id: 2, name: "World" }, ]), ); db.close(); // Check that a closed database doesn't crash // and does throw an error when trying to run a query try { db.query("SELECT * FROM test WHERE name = ?").all(["Hello"]); throw new Error("Should have thrown"); } catch (e) { expect(e.message !== "Should have thrown").toBe(true); } // check that we can call close multiple times // it should not throw so that your code doesn't break db.close(); db.close(); db.close(); }); it("db.run()", () => { const db = Database.open(":memory:"); db.exec("CREATE TABLE cats (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE NOT NULL, age INTEGER NOT NULL)"); const insert = db.query("INSERT INTO cats (name, age) VALUES (@name, @age) RETURNING name").all({ "@name": "Joey", "@age": 2, }); }); for (let strict of [false, true]) { it(`strict: ${strict}`, () => { const db = Database.open(":memory:", { strict }); db.exec("CREATE TABLE cats (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE, age INTEGER)"); const result = db.query("INSERT INTO cats (name, age) VALUES (@name, @age) RETURNING name").all({ [(!strict ? "@" : "") + "name"]: "Joey", [(!strict ? "@" : "") + "age"]: 2, }); expect(result).toStrictEqual([{ name: "Joey" }]); }); } it("strict: true", () => { const db = Database.open(":memory:", { strict: true }); db.exec("CREATE TABLE cats (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE NOT NULL, age INTEGER NOT NULL)"); const insert = db.query("INSERT INTO cats (name, age) VALUES (@name, @age) RETURNING name").all({ "name": "Joey", "age": 2, }); }); describe("does not throw missing parameter error in", () => { for (let method of ["all", "get", "values", "run"]) { it(`${method}()`, () => { const db = Database.open(":memory:"); db.exec("CREATE TABLE cats (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, age INTEGER)"); expect(() => { const query = db.query("INSERT INTO cats (name, age) VALUES (@name, @age) RETURNING name"); const result = query[method]({ "@name": "Joey", }); switch (method) { case "all": expect(result).toHaveLength(1); expect(result[0]).toStrictEqual({ name: "Joey" }); break; case "get": expect(result).toStrictEqual({ name: "Joey" }); break; case "values": expect(result).toStrictEqual([["Joey"]]); break; case "run": expect(result).toEqual({ changes: 1, lastInsertRowid: 1 }); break; } }).not.toThrow(); }); } }); it("db.transaction()", () => { const db = Database.open(":memory:"); db.exec("CREATE TABLE cats (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE, age INTEGER)"); const insert = db.prepare("INSERT INTO cats (name, age) VALUES (@name, @age)"); expect(db.inTransaction).toBe(false); const insertMany = db.transaction(cats => { expect(db.inTransaction).toBe(true); try { for (const cat of cats) insert.run(cat); } catch (exception) { throw exception; } }); try { insertMany([ { "@name": "Joey", "@age": 2 }, { "@name": "Sally", "@age": 4 }, { "@name": "Junior", "@age": 1 }, { "@name": "Sally", "@age": 4 }, ]); throw new Error("Should have thrown"); } catch (exception) { expect(exception.message).toEqual("UNIQUE constraint failed: cats.name"); expect(exception.code).toEqual("SQLITE_CONSTRAINT_UNIQUE"); expect(exception.errno).toEqual(2067); expect(exception.byteOffset).toEqual(-1); } expect(db.inTransaction).toBe(false); expect(db.query("SELECT * FROM cats").all().length).toBe(0); expect(db.inTransaction).toBe(false); insertMany([ { "@name": "Joey", "@age": 2 }, { "@name": "Sally", "@age": 4 }, { "@name": "Junior", "@age": 1 }, ]); expect(db.inTransaction).toBe(false); expect(db.query("SELECT * FROM cats").all().length).toBe(3); expect(db.inTransaction).toBe(false); }); // this bug was fixed by ensuring FinalObject has no more than 64 properties it("inlineCapacity #987", async () => { const path = tmpbase + "bun-987.db"; if (!existsSync(path)) { const arrayBuffer = await (await fetch("https://github.com/oven-sh/bun/files/9265429/logs.log")).arrayBuffer(); writeFileSync(path, arrayBuffer); } const db = new Database(path); const query = `SELECT media.mid, UPPER(media.name) as name, media.url, media.duration, time(media.duration, 'unixepoch') AS durationStr, sum(totalDurations) AS totalDurations, sum(logs.views) AS views, total.venues, total.devices, SUM(CASE WHEN day = '01' THEN logs.views ELSE 0 END) as 'vi01', SUM(CASE WHEN day = '02' THEN logs.views ELSE 0 END) as 'vi02', SUM(CASE WHEN day = '03' THEN logs.views ELSE 0 END) as 'vi03', SUM(CASE WHEN day = '04' THEN logs.views ELSE 0 END) as 'vi04', SUM(CASE WHEN day = '05' THEN logs.views ELSE 0 END) as 'vi05', SUM(CASE WHEN day = '06' THEN logs.views ELSE 0 END) as 'vi06', SUM(CASE WHEN day = '07' THEN logs.views ELSE 0 END) as 'vi07', SUM(CASE WHEN day = '08' THEN logs.views ELSE 0 END) as 'vi08', SUM(CASE WHEN day = '09' THEN logs.views ELSE 0 END) as 'vi09', SUM(CASE WHEN day = '10' THEN logs.views ELSE 0 END) as 'vi10', SUM(CASE WHEN day = '11' THEN logs.views ELSE 0 END) as 'vi11', SUM(CASE WHEN day = '12' THEN logs.views ELSE 0 END) as 'vi12', SUM(CASE WHEN day = '13' THEN logs.views ELSE 0 END) as 'vi13', SUM(CASE WHEN day = '14' THEN logs.views ELSE 0 END) as 'vi14', SUM(CASE WHEN day = '15' THEN logs.views ELSE 0 END) as 'vi15', SUM(CASE WHEN day = '16' THEN logs.views ELSE 0 END) as 'vi16', SUM(CASE WHEN day = '17' THEN logs.views ELSE 0 END) as 'vi17', SUM(CASE WHEN day = '18' THEN logs.views ELSE 0 END) as 'vi18', SUM(CASE WHEN day = '19' THEN logs.views ELSE 0 END) as 'vi19', SUM(CASE WHEN day = '20' THEN logs.views ELSE 0 END) as 'vi20', SUM(CASE WHEN day = '21' THEN logs.views ELSE 0 END) as 'vi21', SUM(CASE WHEN day = '22' THEN logs.views ELSE 0 END) as 'vi22', SUM(CASE WHEN day = '23' THEN logs.views ELSE 0 END) as 'vi23', SUM(CASE WHEN day = '24' THEN logs.views ELSE 0 END) as 'vi24', SUM(CASE WHEN day = '25' THEN logs.views ELSE 0 END) as 'vi25', SUM(CASE WHEN day = '26' THEN logs.views ELSE 0 END) as 'vi26', SUM(CASE WHEN day = '27' THEN logs.views ELSE 0 END) as 'vi27', SUM(CASE WHEN day = '28' THEN logs.views ELSE 0 END) as 'vi28', SUM(CASE WHEN day = '29' THEN logs.views ELSE 0 END) as 'vi29', SUM(CASE WHEN day = '30' THEN logs.views ELSE 0 END) as 'vi30', MAX(CASE WHEN day = '01' THEN logs.venues ELSE 0 END) as 've01', MAX(CASE WHEN day = '02' THEN logs.venues ELSE 0 END) as 've02', MAX(CASE WHEN day = '03' THEN logs.venues ELSE 0 END) as 've03', MAX(CASE WHEN day = '04' THEN logs.venues ELSE 0 END) as 've04', MAX(CASE WHEN day = '05' THEN logs.venues ELSE 0 END) as 've05', MAX(CASE WHEN day = '06' THEN logs.venues ELSE 0 END) as 've06', MAX(CASE WHEN day = '07' THEN logs.venues ELSE 0 END) as 've07', MAX(CASE WHEN day = '08' THEN logs.venues ELSE 0 END) as 've08', MAX(CASE WHEN day = '09' THEN logs.venues ELSE 0 END) as 've09', MAX(CASE WHEN day = '10' THEN logs.venues ELSE 0 END) as 've10', MAX(CASE WHEN day = '11' THEN logs.venues ELSE 0 END) as 've11', MAX(CASE WHEN day = '12' THEN logs.venues ELSE 0 END) as 've12', MAX(CASE WHEN day = '13' THEN logs.venues ELSE 0 END) as 've13', MAX(CASE WHEN day = '14' THEN logs.venues ELSE 0 END) as 've14', MAX(CASE WHEN day = '15' THEN logs.venues ELSE 0 END) as 've15', MAX(CASE WHEN day = '16' THEN logs.venues ELSE 0 END) as 've16', MAX(CASE WHEN day = '17' THEN logs.venues ELSE 0 END) as 've17', MAX(CASE WHEN day = '18' THEN logs.venues ELSE 0 END) as 've18', MAX(CASE WHEN day = '19' THEN logs.venues ELSE 0 END) as 've19', MAX(CASE WHEN day = '20' THEN logs.venues ELSE 0 END) as 've20', MAX(CASE WHEN day = '21' THEN logs.venues ELSE 0 END) as 've21', MAX(CASE WHEN day = '22' THEN logs.venues ELSE 0 END) as 've22', MAX(CASE WHEN day = '23' THEN logs.venues ELSE 0 END) as 've23', MAX(CASE WHEN day = '24' THEN logs.venues ELSE 0 END) as 've24', MAX(CASE WHEN day = '25' THEN logs.venues ELSE 0 END) as 've25', MAX(CASE WHEN day = '26' THEN logs.venues ELSE 0 END) as 've26', MAX(CASE WHEN day = '27' THEN logs.venues ELSE 0 END) as 've27', MAX(CASE WHEN day = '28' THEN logs.venues ELSE 0 END) as 've28', MAX(CASE WHEN day = '29' THEN logs.venues ELSE 0 END) as 've29', MAX(CASE WHEN day = '30' THEN logs.venues ELSE 0 END) as 've30', MAX(CASE WHEN day = '01' THEN logs.devices ELSE 0 END) as 'de01', MAX(CASE WHEN day = '02' THEN logs.devices ELSE 0 END) as 'de02', MAX(CASE WHEN day = '03' THEN logs.devices ELSE 0 END) as 'de03', MAX(CASE WHEN day = '04' THEN logs.devices ELSE 0 END) as 'de04', MAX(CASE WHEN day = '05' THEN logs.devices ELSE 0 END) as 'de05', MAX(CASE WHEN day = '06' THEN logs.devices ELSE 0 END) as 'de06', MAX(CASE WHEN day = '07' THEN logs.devices ELSE 0 END) as 'de07', MAX(CASE WHEN day = '08' THEN logs.devices ELSE 0 END) as 'de08', MAX(CASE WHEN day = '09' THEN logs.devices ELSE 0 END) as 'de09', MAX(CASE WHEN day = '10' THEN logs.devices ELSE 0 END) as 'de10', MAX(CASE WHEN day = '11' THEN logs.devices ELSE 0 END) as 'de11', MAX(CASE WHEN day = '12' THEN logs.devices ELSE 0 END) as 'de12', MAX(CASE WHEN day = '13' THEN logs.devices ELSE 0 END) as 'de13', MAX(CASE WHEN day = '14' THEN logs.devices ELSE 0 END) as 'de14', MAX(CASE WHEN day = '15' THEN logs.devices ELSE 0 END) as 'de15', MAX(CASE WHEN day = '16' THEN logs.devices ELSE 0 END) as 'de16', MAX(CASE WHEN day = '17' THEN logs.devices ELSE 0 END) as 'de17', MAX(CASE WHEN day = '18' THEN logs.devices ELSE 0 END) as 'de18', MAX(CASE WHEN day = '19' THEN logs.devices ELSE 0 END) as 'de19', MAX(CASE WHEN day = '20' THEN logs.devices ELSE 0 END) as 'de20', MAX(CASE WHEN day = '21' THEN logs.devices ELSE 0 END) as 'de21', MAX(CASE WHEN day = '22' THEN logs.devices ELSE 0 END) as 'de22', MAX(CASE WHEN day = '23' THEN logs.devices ELSE 0 END) as 'de23', MAX(CASE WHEN day = '24' THEN logs.devices ELSE 0 END) as 'de24', MAX(CASE WHEN day = '25' THEN logs.devices ELSE 0 END) as 'de25', MAX(CASE WHEN day = '26' THEN logs.devices ELSE 0 END) as 'de26', MAX(CASE WHEN day = '27' THEN logs.devices ELSE 0 END) as 'de27', MAX(CASE WHEN day = '28' THEN logs.devices ELSE 0 END) as 'de28', MAX(CASE WHEN day = '29' THEN logs.devices ELSE 0 END) as 'de29', MAX(CASE WHEN day = '30' THEN logs.devices ELSE 0 END) as 'de30' FROM ( SELECT logs.mid, sum(logs.duration) AS totalDurations, strftime ('%d', START, 'unixepoch', 'localtime') AS day, count(*) AS views, count(DISTINCT did) AS devices, count(DISTINCT vid) AS venues FROM logs WHERE strftime('%m-%Y', start, 'unixepoch', 'localtime')='06-2022' GROUP BY day, logs.mid ) logs INNER JOIN media ON media.id = logs.mid INNER JOIN ( SELECT mid, count(DISTINCT vid) as venues, count(DISTINCT did) as devices FROM logs WHERE strftime('%m-%Y', start, 'unixepoch', 'localtime')='06-2022' GROUP by mid ) total ON logs.mid = total.mid ORDER BY name`; expect(Object.keys(db.query(query).all()[0]).length).toBe(99); }); // https://github.com/oven-sh/bun/issues/1553 it("latin1 supplement chars", () => { const db = new Database(); db.run("CREATE TABLE IF NOT EXISTS foo (id INTEGER PRIMARY KEY AUTOINCREMENT, greeting TEXT)"); db.run("INSERT INTO foo (greeting) VALUES (?)", "Welcome to bun!"); db.run("INSERT INTO foo (greeting) VALUES (?)", "Español"); db.run("INSERT INTO foo (greeting) VALUES (?)", "¿Qué sucedió?"); expect(db.query("SELECT * FROM foo").all()).toEqual([ { id: 1, greeting: "Welcome to bun!", }, { id: 2, greeting: "Español", }, { id: 3, greeting: "¿Qué sucedió?", }, ]); // test that it doesn't break when we do a structure transition db.query("SELECT * FROM foo").all()[0].booop = true; db.query("SELECT * FROM foo").all()[0].beep = true; expect(db.query("SELECT * FROM foo").all()).toEqual([ { id: 1, greeting: "Welcome to bun!", }, { id: 2, greeting: "Español", }, { id: 3, greeting: "¿Qué sucedió?", }, ]); expect(db.query("SELECT * FROM foo").values()).toEqual([ [1, "Welcome to bun!"], [2, "Español"], [3, "¿Qué sucedió?"], ]); expect(db.query("SELECT * FROM foo WHERE id > 9999").all()).toEqual([]); expect(db.query("SELECT * FROM foo WHERE id > 9999").values()).toEqual([]); }); it("supports FTS5", () => { const db = new Database(); db.run("CREATE VIRTUAL TABLE movies USING fts5(title, tokenize='trigram')"); const insert = db.prepare("INSERT INTO movies VALUES ($title)"); const insertMovies = db.transaction(movies => { for (const movie of movies) insert.run(movie); }); insertMovies([ { $title: "The Shawshank Redemption" }, { $title: "WarGames" }, { $title: "Interstellar" }, { $title: "Se7en" }, { $title: "City of God" }, { $title: "Spirited Away" }, ]); expect(db.query("SELECT * FROM movies('game')").all()).toEqual([ { title: "WarGames", }, ]); }); describe("Database.run", () => { it("should not throw error `not an error` when provided query containing only whitespace", () => { const db = Database.open(":memory:"); db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)"); expect(db[Symbol.for("Bun.Database.cache.count")]).toBe(0); var q = db.query("SELECT * FROM test WHERE name = ?"); expect(q.get("Hello") === null).toBe(true); db.exec('INSERT INTO test (name) VALUES ("Hello")'); db.exec('INSERT INTO test (name) VALUES ("World")'); try { db.run(" "); expect(true).toBeFalsy(); } catch (e) { expect(e.message).not.toBe("not an error"); expect(e.message).toBe("Query contained no valid SQL statement; likely empty query."); } }); }); it("#3991", () => { const db = new Database(":memory:"); db.prepare( `CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, xx TEXT) `, ).run(); db.prepare( `insert into users (id, xx) values ( 'foobar', '{ "links": [{"1": { "2": "https://foobar.to/123", "3": "4" }}] }' )`, ).run(); let x = db .query( `SELECT * FROM users WHERE users.id = 'foobar' limit 1`, ) .get(); // Check we don't crash when a column with a string value greater than 64 characters is present. expect(x.abc).toBeUndefined(); expect(x.id).toBe("foobar"); }); it("#5872", () => { const db = new Database(":memory:"); db.run("CREATE TABLE foo (id INTEGER PRIMARY KEY AUTOINCREMENT, greeting TEXT)"); const query = db.query("INSERT INTO foo (greeting) VALUES ($greeting);"); const result = query.all({ $greeting: "sup" }); expect(result).toEqual([]); }); it("latin1 sqlite3 column name", () => { const db = new Database(":memory:"); db.run("CREATE TABLE foo (id INTEGER PRIMARY KEY AUTOINCREMENT, copyright© TEXT)"); db.run("INSERT INTO foo (id, copyright©) VALUES (?, ?)", [1, "© 2021 The Authors. All rights reserved."]); expect(db.query("SELECT * FROM foo").all()).toEqual([ { id: 1, "copyright©": "© 2021 The Authors. All rights reserved.", }, ]); }); it("syntax error sets the byteOffset", () => { const db = new Database(":memory:"); try { db.query("SELECT * FROM foo!!").all(); throw new Error("Expected error"); } catch (error) { if (isMacOS && !isMacOSVersionAtLeast(13)) { // older versions of macOS don't have the function which returns the byteOffset // we internally use a polyfill, so we need to allow that. expect(error.byteOffset).toBe(-1); } else { expect(error.byteOffset).toBe(17); } } }); it("Missing DB throws SQLITE_CANTOPEN", () => { try { new Database("./definitely/not/found"); expect.unreachable(); } catch (error) { expect(error.code).toBe("SQLITE_CANTOPEN"); expect(error).toBeInstanceOf(SQLiteError); } }); it("empty blob", () => { const db = new Database(":memory:"); db.run("CREATE TABLE foo (id INTEGER PRIMARY KEY AUTOINCREMENT, blob BLOB)"); db.run("INSERT INTO foo (blob) VALUES (?)", [new Uint8Array()]); expect(db.query("SELECT * FROM foo").all()).toEqual([ { id: 1, blob: new Uint8Array(), }, ]); }); it("multiple statements with a schema change", () => { const db = new Database(":memory:"); db.run( ` CREATE TABLE foo (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT); CREATE TABLE bar (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT); INSERT INTO foo (name) VALUES ('foo'); INSERT INTO foo (name) VALUES ('bar'); INSERT INTO bar (name) VALUES ('foo'); INSERT INTO bar (name) VALUES ('bar'); `, ); expect(db.query("SELECT * FROM foo").all()).toEqual([ { id: 1, name: "foo", }, { id: 2, name: "bar", }, ]); expect(db.query("SELECT * FROM bar").all()).toEqual([ { id: 1, name: "foo", }, { id: 2, name: "bar", }, ]); }); it("multiple statements", () => { const fixtures = [ "INSERT INTO foo (name) VALUES ('foo')", "INSERT INTO foo (name) VALUES ('barabc')", "INSERT INTO foo (name) VALUES ('!bazaspdok')", ]; for (let separator of [";", ";\n", "\n;", "\r\n;", ";\r\n", ";\t", "\t;", "\r\n;"]) { for (let spaceOffset of [1, 0, -1]) { for (let spacesCount = 0; spacesCount < 8; spacesCount++) { const db = new Database(":memory:"); db.run("CREATE TABLE foo (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)"); const prefix = spaceOffset < 0 ? " ".repeat(spacesCount) : ""; const suffix = spaceOffset > 0 ? " ".repeat(spacesCount) : ""; const query = fixtures.join(prefix + separator + suffix); db.run(query); expect(db.query("SELECT * FROM foo").all()).toEqual([ { id: 1, name: "foo", }, { id: 2, name: "barabc", }, { id: 3, name: "!bazaspdok", }, ]); } } } }); it.skipIf( // We use the system version, which may or may not have math functions process.platform === "darwin", )("math functions", () => { const db = new Database(":memory:"); expect(db.prepare("SELECT ABS(-243.5)").all()).toEqual([ { "ABS(-243.5)": 243.5, }, ]); expect(db.prepare("SELECT ACOS(0.25)").all()).toEqual([ { "ACOS(0.25)": 1.318116071652818, }, ]); expect(db.prepare("SELECT ASIN(0.25)").all()).toEqual([ { "ASIN(0.25)": 0.25268025514207865, }, ]); expect(db.prepare("SELECT ATAN(0.25)").all()).toEqual([ { "ATAN(0.25)": 0.24497866312686414, }, ]); db.exec( ` CREATE TABLE num_table (value TEXT NOT NULL); INSERT INTO num_table values (1), (2), (6); `, ); expect(db.prepare(`SELECT AVG(value) as value FROM num_table`).all()).toEqual([{ value: 3 }]); expect(db.prepare("SELECT CEILING(0.25)").all()).toEqual([ { "CEILING(0.25)": 1, }, ]); expect(db.prepare("SELECT COUNT(*) FROM num_table").all()).toEqual([ { "COUNT(*)": 3, }, ]); expect(db.prepare("SELECT COS(0.25)").all()).toEqual([ { "COS(0.25)": 0.9689124217106447, }, ]); expect(db.prepare("SELECT DEGREES(0.25)").all()).toEqual([ { "DEGREES(0.25)": 14.32394487827058, }, ]); expect(db.prepare("SELECT EXP(0.25)").all()).toEqual([ { "EXP(0.25)": 1.2840254166877414, }, ]); expect(db.prepare("SELECT FLOOR(0.25)").all()).toEqual([ { "FLOOR(0.25)": 0, }, ]); expect(db.prepare("SELECT LOG10(0.25)").all()).toEqual([ { "LOG10(0.25)": -0.6020599913279624, }, ]); expect(db.prepare("SELECT PI()").all()).toEqual([ { "PI()": 3.141592653589793, }, ]); expect(db.prepare("SELECT POWER(0.25, 3)").all()).toEqual([ { "POWER(0.25, 3)": 0.015625, }, ]); expect(db.prepare("SELECT RADIANS(0.25)").all()).toEqual([ { "RADIANS(0.25)": 0.004363323129985824, }, ]); expect(db.prepare("SELECT ROUND(0.25)").all()).toEqual([ { "ROUND(0.25)": 0, }, ]); expect(db.prepare("SELECT SIGN(0.25)").all()).toEqual([{ "SIGN(0.25)": 1 }]); expect(db.prepare("SELECT SIN(0.25)").all()).toEqual([ { "SIN(0.25)": 0.24740395925452294, }, ]); expect(db.prepare("SELECT SQRT(0.25)").all()).toEqual([ { "SQRT(0.25)": 0.5, }, ]); expect(db.prepare("SELECT TAN(0.25)").all()).toEqual([ { "TAN(0.25)": 0.25534192122103627, }, ]); }); it("issue#6597", () => { // better-sqlite3 returns the last value of duplicate fields const db = new Database(":memory:"); db.run("CREATE TABLE Users (Id INTEGER PRIMARY KEY, Name VARCHAR(255), CreatedAt TIMESTAMP)"); db.run( "CREATE TABLE Cars (Id INTEGER PRIMARY KEY, Driver INTEGER, CreatedAt TIMESTAMP, FOREIGN KEY (Driver) REFERENCES Users(Id))", ); db.run('INSERT INTO Users (Id, Name, CreatedAt) VALUES (1, "Alice", "2022-01-01");'); db.run('INSERT INTO Cars (Id, Driver, CreatedAt) VALUES (2, 1, "2023-01-01");'); const result = db.prepare("SELECT * FROM Cars JOIN Users ON Driver=Users.Id").get(); expect(result).toStrictEqual({ Id: 1, Driver: 1, CreatedAt: "2022-01-01", Name: "Alice", }); db.close(); }); it("issue#6597 with many columns", () => { // better-sqlite3 returns the last value of duplicate fields const db = new Database(":memory:"); const count = 100; const columns = Array.from({ length: count }, (_, i) => `col${i}`); const values_foo = Array.from({ length: count }, (_, i) => `'foo${i}'`); const values_bar = Array.from({ length: count }, (_, i) => `'bar${i}'`); values_bar[0] = values_foo[0]; db.run(`CREATE TABLE foo (${columns.join(",")})`); db.run(`CREATE TABLE bar (${columns.join(",")})`); db.run(`INSERT INTO foo (${columns.join(",")}) VALUES (${values_foo.join(",")})`); db.run(`INSERT INTO bar (${columns.join(",")}) VALUES (${values_bar.join(",")})`); const result = db.prepare("SELECT * FROM foo JOIN bar ON foo.col0 = bar.col0").get(); expect(result.col0).toBe("foo0"); for (let i = 1; i < count; i++) { expect(result[`col${i}`]).toBe(`bar${i}`); } db.close(); }); it("issue#7147", () => { const db = new Database(":memory:"); db.exec("CREATE TABLE foos (foo_id INTEGER NOT NULL PRIMARY KEY, foo_a TEXT, foo_b TEXT)"); db.exec( "CREATE TABLE bars (bar_id INTEGER NOT NULL PRIMARY KEY, foo_id INTEGER NOT NULL, bar_a INTEGER, bar_b INTEGER, FOREIGN KEY (foo_id) REFERENCES foos (foo_id))", ); db.exec("INSERT INTO foos VALUES (1, 'foo_1', 'foo_2')"); db.exec("INSERT INTO bars VALUES (1, 1, 'bar_1', 'bar_2')"); db.exec("INSERT INTO bars VALUES (2, 1, 'baz_3', 'baz_4')"); const query = db.query("SELECT f.*, b.* FROM foos f JOIN bars b ON b.foo_id = f.foo_id"); const result = query.all(); expect(result).toStrictEqual([ { foo_id: 1, foo_a: "foo_1", foo_b: "foo_2", bar_id: 1, bar_a: "bar_1", bar_b: "bar_2", }, { foo_id: 1, foo_a: "foo_1", foo_b: "foo_2", bar_id: 2, bar_a: "baz_3", bar_b: "baz_4", }, ]); db.close(); }); it("should close with WAL enabled", () => { const dir = tempDirWithFiles("sqlite-wal-test", { "empty.txt": "" }); const file = path.join(dir, "my.db"); const db = new Database(file); db.exec("PRAGMA journal_mode = WAL"); db.fileControl(constants.SQLITE_FCNTL_PERSIST_WAL, 0); db.exec("CREATE TABLE foo (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)"); db.exec("INSERT INTO foo (name) VALUES ('foo')"); expect(db.query("SELECT * FROM foo").all()).toEqual([{ id: 1, name: "foo" }]); db.exec("PRAGMA wal_checkpoint(truncate)"); db.close(); expect(readdirSync(dir).sort()).toEqual(["empty.txt", "my.db"]); }); it("close(true) should throw an error if the database is in use", () => { const db = new Database(":memory:"); db.exec("CREATE TABLE foo (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)"); db.exec("INSERT INTO foo (name) VALUES ('foo')"); const prepared = db.prepare("SELECT * FROM foo"); expect(() => db.close(true)).toThrow("database is locked"); prepared.finalize(); expect(() => db.close(true)).not.toThrow(); }); it("close() should NOT throw an error if the database is in use", () => { const db = new Database(":memory:"); db.exec("CREATE TABLE foo (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)"); db.exec("INSERT INTO foo (name) VALUES ('foo')"); const prepared = db.prepare("SELECT * FROM foo"); expect(() => db.close()).not.toThrow("database is locked"); }); it("should dispose AND throw an error if the database is in use", () => { expect(() => { let prepared; { using db = new Database(":memory:"); db.exec("CREATE TABLE foo (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)"); db.exec("INSERT INTO foo (name) VALUES ('foo')"); prepared = db.prepare("SELECT * FROM foo"); } }).toThrow("database is locked"); }); it("should dispose", () => { expect(() => { { using db = new Database(":memory:"); db.exec("CREATE TABLE foo (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)"); db.exec("INSERT INTO foo (name) VALUES ('foo')"); } }).not.toThrow(); }); it("can continue to use existing statements after database has been GC'd", async () => { let called = false; async function run() { const registry = new FinalizationRegistry(() => { called = true; }); function leakTheStatement() { const db = new Database(":memory:"); console.log("---"); db.exec("CREATE TABLE foo (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)"); db.exec("INSERT INTO foo (name) VALUES ('foo')"); const prepared = db.prepare("SELECT * FROM foo"); registry.register(db); return prepared; } const stmt = leakTheStatement(); Bun.gc(true); await Bun.sleep(1); Bun.gc(true); expect(stmt.all()).toEqual([{ id: 1, name: "foo" }]); stmt.finalize(); expect(() => stmt.all()).toThrow(); } await run(); Bun.gc(true); await Bun.sleep(1); Bun.gc(true); if (!isWindows) { // on Windows, FinalizationRegistry is more flaky than on POSIX. expect(called).toBe(true); } }); it("statements should be disposable", () => { { using db = new Database("mydb.sqlite"); using query = db.query("select 'Hello world' as message;"); console.log(query.get()); // => { message: "Hello world" } } }); it("query should work if the cached statement was finalized", () => { { let prevQuery; using db = new Database("mydb.sqlite"); { using query = db.query("select 'Hello world' as message;"); prevQuery = query; query.get(); } { using query = db.query("select 'Hello world' as message;"); expect(() => query.get()).not.toThrow(); } expect(() => prevQuery.get()).toThrow(); } }); // https://github.com/oven-sh/bun/issues/12012 it("reports changes in Statement#run", () => { const db = new Database(":memory:"); db.exec("CREATE TABLE cats (id INTEGER PRIMARY KEY, name TEXT)"); const sql = "INSERT INTO cats (name) VALUES ('Fluffy'), ('Furry')"; expect(db.run(sql).changes).toBe(2); expect(db.prepare(sql).run().changes).toBe(2); expect(db.query(sql).run().changes).toBe(2); }); it("#13082", async () => { async function run() { const stmt = (() => { const db = new Database(":memory:"); let stmt = db.prepare("select 1"); db.close(); return stmt; })(); Bun.gc(true); await Bun.sleep(100); Bun.gc(true); stmt.all(); stmt.get(); stmt.run(); } const count = 100; const runs = new Array(count); for (let i = 0; i < count; i++) { runs[i] = run(); } await Promise.allSettled(runs); });