import crypto from "crypto"; import fs from "node:fs"; import { fileURLToPath } from "url"; import path from "node:path"; import DualTest from "./dual-test.mjs"; const inMemoryTest = new DualTest(":memory:"); const foobarTest = new DualTest("foobar.db"); inMemoryTest.both("Open in-memory database", async (t) => { const db = t.context.db; t.is(db.memory, true); }); inMemoryTest.both("Property .name of in-memory database", async (t) => { const db = t.context.db; t.is(db.name, t.context.path); }); foobarTest.both("Property .name of database", async (t) => { const db = t.context.db; t.is(db.name, t.context.path); }); new DualTest("foobar.db", { readonly: true }).both( "Property .readonly of database if set", async (t) => { const db = t.context.db; t.is(db.readonly, true); }, ); const genDatabaseFilename = () => { return `test-${crypto.randomBytes(8).toString("hex")}.db`; }; new DualTest().both( "opening a read-only database fails if the file doesn't exist", async (t) => { t.throws( () => t.context.connect(genDatabaseFilename(), { readonly: true }), { any: true, code: "SQLITE_CANTOPEN", }, ); }, ); foobarTest.both("Property .readonly of database if not set", async (t) => { const db = t.context.db; t.is(db.readonly, false); }); foobarTest.both("Property .open of database", async (t) => { const db = t.context.db; t.is(db.open, true); }); inMemoryTest.both("Statement.get() returns data", async (t) => { const db = t.context.db; const stmt = db.prepare("SELECT 1"); const result = stmt.get(); t.is(result["1"], 1); const result2 = stmt.get(); t.is(result2["1"], 1); }); inMemoryTest.both( "Statement.get() returns undefined when no data", async (t) => { const db = t.context.db; const stmt = db.prepare("SELECT 1 WHERE 1 = 2"); const result = stmt.get(); t.is(result, undefined); }, ); inMemoryTest.both( "Statement.run() returns correct result object", async (t) => { const db = t.context.db; db.prepare("CREATE TABLE users (name TEXT)").run(); const rows = db.prepare("INSERT INTO users (name) VALUES (?)").run("Alice"); t.deepEqual(rows, { changes: 1, lastInsertRowid: 1 }); }, ); inMemoryTest.onlySqlitePasses( "Statment.iterate() should correctly return an iterable object", async (t) => { const db = t.context.db; db.prepare( "CREATE TABLE users (name TEXT, age INTEGER, nationality TEXT)", ).run(); db.prepare( "INSERT INTO users (name, age, nationality) VALUES (?, ?, ?)", ).run(["Alice", 42], "UK"); db.prepare( "INSERT INTO users (name, age, nationality) VALUES (?, ?, ?)", ).run("Bob", 24, "USA"); let rows = db.prepare("SELECT * FROM users").iterate(); for (const row of rows) { t.truthy(row.name); t.truthy(row.nationality); t.true(typeof row.age === "number"); } }, ); inMemoryTest.both( "Empty prepared statement should throw the correct error", async (t) => { const db = t.context.db; t.throws( () => { db.prepare(""); }, { instanceOf: RangeError, message: "The supplied SQL string contains no statements", }, ); }, ); inMemoryTest.both("Test pragma()", async (t) => { const db = t.context.db; t.deepEqual(typeof db.pragma("cache_size")[0].cache_size, "number"); t.deepEqual(typeof db.pragma("cache_size", { simple: true }), "number"); }); inMemoryTest.both("pragma query", async (t) => { const db = t.context.db; let page_size = db.pragma("page_size"); let expectedValue = [{ page_size: 4096 }]; t.deepEqual(page_size, expectedValue); }); inMemoryTest.both("pragma table_list", async (t) => { const db = t.context.db; let param = "sqlite_schema"; let actual = db.pragma(`table_info(${param})`); let expectedValue = [ { cid: 0, name: "type", type: "TEXT", notnull: 0, dflt_value: null, pk: 0 }, { cid: 1, name: "name", type: "TEXT", notnull: 0, dflt_value: null, pk: 0 }, { cid: 2, name: "tbl_name", type: "TEXT", notnull: 0, dflt_value: null, pk: 0, }, { cid: 3, name: "rootpage", type: "INT", notnull: 0, dflt_value: null, pk: 0, }, { cid: 4, name: "sql", type: "TEXT", notnull: 0, dflt_value: null, pk: 0 }, ]; t.deepEqual(actual, expectedValue); }); inMemoryTest.both("simple pragma table_list", async (t) => { const db = t.context.db; let param = "sqlite_schema"; let actual = db.pragma(`table_info(${param})`, { simple: true }); let expectedValue = 0; t.deepEqual(actual, expectedValue); }); inMemoryTest.onlySqlitePasses( "Statement shouldn't bind twice with bind()", async (t) => { const db = t.context.db; db.prepare("CREATE TABLE users (name TEXT, age INTEGER)").run(); db.prepare("INSERT INTO users (name, age) VALUES (?, ?)").run("Alice", 42); let stmt = db.prepare("SELECT * FROM users WHERE name = ?").bind("Alice"); let row = stmt.get(); t.truthy(row.name); t.true(typeof row.age === "number"); t.throws( () => { stmt.bind("Bob"); }, { instanceOf: TypeError, message: "The bind() method can only be invoked once per statement object", }, ); }, ); inMemoryTest.both( "Test pluck(): Rows should only have the values of the first column", async (t) => { const db = t.context.db; db.prepare("CREATE TABLE users (name TEXT, age INTEGER)").run(); db.prepare("INSERT INTO users (name, age) VALUES (?, ?)").run("Alice", 42); db.prepare("INSERT INTO users (name, age) VALUES (?, ?)").run("Bob", 24); let stmt = db.prepare("SELECT * FROM users").pluck(); for (const row of stmt.all()) { t.truthy(row); t.assert(typeof row === "string"); } }, ); inMemoryTest.both( "Test raw(): Rows should be returned as arrays", async (t) => { const db = t.context.db; db.prepare("CREATE TABLE users (name TEXT, age INTEGER)").run(); db.prepare("INSERT INTO users (name, age) VALUES (?, ?)").run("Alice", 42); db.prepare("INSERT INTO users (name, age) VALUES (?, ?)").run("Bob", 24); let stmt = db.prepare("SELECT * FROM users").raw(); for (const row of stmt.all()) { t.true(Array.isArray(row)); t.true(typeof row[0] === "string"); t.true(typeof row[1] === "number"); } stmt = db.prepare("SELECT * FROM users WHERE name = ?").raw(); const row = stmt.get("Alice"); t.true(Array.isArray(row)); t.is(row.length, 2); t.is(row[0], "Alice"); t.is(row[1], 42); const noRow = stmt.get("Charlie"); t.is(noRow, undefined); stmt = db.prepare("SELECT * FROM users").raw(); const rows = stmt.all(); t.true(Array.isArray(rows)); t.is(rows.length, 2); t.deepEqual(rows[0], ["Alice", 42]); t.deepEqual(rows[1], ["Bob", 24]); }, ); inMemoryTest.onlySqlitePasses( "Test expand(): Columns should be namespaced", async (t) => { const expandedResults = [ { users: { name: "Alice", type: "premium", }, addresses: { userName: "Alice", type: "home", street: "Alice's street", }, }, { users: { name: "Bob", type: "basic", }, addresses: { userName: "Bob", type: "work", street: "Bob's street", }, }, ]; let regularResults = [ { name: "Alice", street: "Alice's street", type: "home", userName: "Alice", }, { name: "Bob", street: "Bob's street", type: "work", userName: "Bob", }, ]; const db = t.context.db; db.prepare("CREATE TABLE users (name TEXT, type TEXT)").run(); db.prepare( "CREATE TABLE addresses (userName TEXT, street TEXT, type TEXT)", ).run(); db.prepare("INSERT INTO users (name, type) VALUES (?, ?)").run( "Alice", "premium", ); db.prepare("INSERT INTO users (name, type) VALUES (?, ?)").run( "Bob", "basic", ); db.prepare( "INSERT INTO addresses (userName, street, type) VALUES (?, ?, ?)", ).run("Alice", "Alice's street", "home"); db.prepare( "INSERT INTO addresses (userName, street, type) VALUES (?, ?, ?)", ).run("Bob", "Bob's street", "work"); let allRows = db .prepare( "SELECT * FROM users u JOIN addresses a ON (u.name = a.userName)", ) .expand(true) .all(); t.deepEqual(allRows, expandedResults); allRows = db .prepare( "SELECT * FROM users u JOIN addresses a ON (u.name = a.userName)", ) .expand() .all(); t.deepEqual(allRows, expandedResults); allRows = db .prepare( "SELECT * FROM users u JOIN addresses a ON (u.name = a.userName)", ) .expand(false) .all(); t.deepEqual(allRows, regularResults); }, ); inMemoryTest.both( "Presentation modes should be mutually exclusive", async (t) => { const db = t.context.db; db.prepare("CREATE TABLE users (name TEXT, age INTEGER)").run(); db.prepare("INSERT INTO users (name, age) VALUES (?, ?)").run("Alice", 42); db.prepare("INSERT INTO users (name, age) VALUES (?, ?)").run("Bob", 24); // test raw() let stmt = db.prepare("SELECT * FROM users").pluck().raw(); for (const row of stmt.all()) { t.true(Array.isArray(row)); t.true(typeof row[0] === "string"); t.true(typeof row[1] === "number"); } stmt = db.prepare("SELECT * FROM users WHERE name = ?").raw(); const row = stmt.get("Alice"); t.true(Array.isArray(row)); t.is(row.length, 2); t.is(row[0], "Alice"); t.is(row[1], 42); const noRow = stmt.get("Charlie"); t.is(noRow, undefined); stmt = db.prepare("SELECT * FROM users").raw(); let rows = stmt.all(); t.true(Array.isArray(rows)); t.is(rows.length, 2); t.deepEqual(rows[0], ["Alice", 42]); t.deepEqual(rows[1], ["Bob", 24]); // test pluck() stmt = db.prepare("SELECT * FROM users").raw().pluck(); for (const name of stmt.all()) { t.truthy(name); t.assert(typeof name === "string"); } }, ); inMemoryTest.onlySqlitePasses( "Presentation mode 'expand' should be mutually exclusive", async (t) => { // this test can be appended to the previous one when 'expand' is implemented in Turso const db = t.context.db; db.prepare("CREATE TABLE users (name TEXT, age INTEGER)").run(); db.prepare("INSERT INTO users (name, age) VALUES (?, ?)").run("Alice", 42); db.prepare("INSERT INTO users (name, age) VALUES (?, ?)").run("Bob", 24); let stmt = db.prepare("SELECT * FROM users").pluck().raw(); // test expand() stmt = db.prepare("SELECT * FROM users").raw().pluck().expand(); const rows = stmt.all(); t.true(Array.isArray(rows)); t.is(rows.length, 2); t.deepEqual(rows[0], { users: { name: "Alice", age: 42 } }); t.deepEqual(rows[1], { users: { name: "Bob", age: 24 } }); }, ); inMemoryTest.both( "Test exec(): Should correctly load multiple statements from file", async (t) => { const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const db = t.context.db; const file = fs.readFileSync( path.resolve(__dirname, "./artifacts/basic-test.sql"), "utf8", ); db.exec(file); let rows = db.prepare("SELECT * FROM users").all(); for (const row of rows) { t.truthy(row.name); t.true(typeof row.age === "number"); } }, ); inMemoryTest.both( "Test Statement.database gets the database object", async (t) => { const db = t.context.db; let stmt = db.prepare("SELECT 1"); t.is(stmt.database, db); }, ); inMemoryTest.both("Test Statement.source", async (t) => { const db = t.context.db; let sql = "CREATE TABLE t (id int)"; let stmt = db.prepare(sql); t.is(stmt.source, sql); });