463 lines
13 KiB
TypeScript
463 lines
13 KiB
TypeScript
import {
|
|
assertAlmostEquals,
|
|
assertEquals,
|
|
assertThrows,
|
|
} from "https://deno.land/std@0.154.0/testing/asserts.ts";
|
|
|
|
import { DB } from "../mod.ts";
|
|
|
|
const TEST_DB = "test.db";
|
|
const LARGE_TEST_DB = "build/2GB_test.db";
|
|
|
|
async function dbPermissions(path: string): Promise<boolean> {
|
|
const query = async (name: "read" | "write") =>
|
|
(await Deno.permissions.query({ name, path })).state ===
|
|
"granted";
|
|
return await query("read") && await query("write");
|
|
}
|
|
|
|
const TEST_DB_PERMISSIONS = await dbPermissions(TEST_DB);
|
|
const LARGE_TEST_DB_PERMISSIONS = await dbPermissions(LARGE_TEST_DB);
|
|
|
|
async function deleteDatabase(file: string) {
|
|
try {
|
|
await Deno.remove(file);
|
|
} catch { /* no op */ }
|
|
try {
|
|
await Deno.remove(`${file}-journal`);
|
|
} catch { /* no op */ }
|
|
}
|
|
|
|
Deno.test("execute multiple statements", function () {
|
|
const db = new DB();
|
|
|
|
db.execute(`
|
|
CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT);
|
|
|
|
INSERT INTO test (id) VALUES (1);
|
|
INSERT INTO test (id) VALUES (2);
|
|
INSERT INTO test (id) VALUES (3);
|
|
`);
|
|
assertEquals(db.query("SELECT id FROM test"), [[1], [2], [3]]);
|
|
|
|
// table `test` already exists ...
|
|
assertThrows(function () {
|
|
db.execute(`
|
|
CREATE TABLE test2 (id INTEGER);
|
|
CREATE TABLE test (id INTEGER);
|
|
`);
|
|
});
|
|
|
|
// ... but table `test2` was created before the error
|
|
assertEquals(db.query("SELECT id FROM test2"), []);
|
|
|
|
// syntax error after first valid statement
|
|
assertThrows(() => db.execute("SELECT id FROM test; NOT SQL ANYMORE"));
|
|
});
|
|
|
|
Deno.test("foreign key constraints enabled", function () {
|
|
const db = new DB();
|
|
db.execute(`
|
|
CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT);
|
|
CREATE TABLE orders (id INTEGER PRIMARY KEY AUTOINCREMENT, user INTEGER, FOREIGN KEY(user) REFERENCES users(id));
|
|
`);
|
|
|
|
db.query("INSERT INTO users (id) VALUES (1)");
|
|
const [{ id }] = db.queryEntries<{ id: number }>("SELECT id FROM users");
|
|
|
|
// user must exist
|
|
assertThrows(() =>
|
|
db.query("INSERT INTO orders (user) VALUES (?)", [id + 1])
|
|
);
|
|
db.query("INSERT INTO orders (user) VALUES (?)", [id]);
|
|
|
|
// can't delete if that violates the constraint ...
|
|
assertThrows(() => {
|
|
db.query("DELETE FROM users WHERE id = ?", [id]);
|
|
});
|
|
|
|
// ... after deleting the order, deleting is OK
|
|
db.query("DELETE FROM orders WHERE user = ?", [id]);
|
|
db.query("DELETE FROM users WHERE id = ?", [id]);
|
|
});
|
|
|
|
Deno.test("json functions exist", function () {
|
|
const db = new DB();
|
|
|
|
// The JSON1 functions should exist and we should be able to call them without unexpected errors
|
|
db.query(`SELECT json('{"this is": ["json"]}')`);
|
|
|
|
// We should expect an error if we pass invalid JSON where valid JSON is expected
|
|
assertThrows(() => {
|
|
db.query(`SELECT json('this is not json')`);
|
|
});
|
|
|
|
// We should be able to use bound values as arguments to the JSON1 functions,
|
|
// and they should produce the expected results for these simple expressions.
|
|
const [[objectType]] = db.query(`SELECT json_type('{}')`);
|
|
assertEquals(objectType, "object");
|
|
|
|
const [[integerType]] = db.query(`SELECT json_type(?)`, ["2"]);
|
|
assertEquals(integerType, "integer");
|
|
|
|
const [[realType]] = db.query(`SELECT json_type(?)`, ["2.5"]);
|
|
assertEquals(realType, "real");
|
|
|
|
const [[stringType]] = db.query(`SELECT json_type(?)`, [`"hello"`]);
|
|
assertEquals(stringType, "text");
|
|
|
|
const [[integerTypeAtPath]] = db.query(
|
|
`SELECT json_type(?, ?)`,
|
|
[`["hello", 2, {"world": 4}]`, `$[2].world`],
|
|
);
|
|
assertEquals(integerTypeAtPath, "integer");
|
|
});
|
|
|
|
Deno.test("date time is correct", function () {
|
|
const db = new DB();
|
|
// the date/ time is passed from JS and should be current (note that it is GMT)
|
|
const [[now]] = [...db.query("SELECT STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')")];
|
|
const jsTime = new Date().getTime();
|
|
const dbTime = new Date(`${now}Z`).getTime();
|
|
// to account for runtime latency, a small difference is ok
|
|
const tolerance = 10;
|
|
assertAlmostEquals(jsTime, dbTime, tolerance);
|
|
db.close();
|
|
});
|
|
|
|
Deno.test("SQL localtime reflects system locale", function () {
|
|
const db = new DB();
|
|
const [[timeDb]] = db.query("SELECT datetime('now', 'localtime')");
|
|
const now = new Date();
|
|
|
|
const jsMonth = `${now.getMonth() + 1}`.padStart(2, "0");
|
|
const jsDate = `${now.getDate()}`.padStart(2, "0");
|
|
const jsHour = `${now.getHours()}`.padStart(2, "0");
|
|
const jsMinute = `${now.getMinutes()}`.padStart(2, "0");
|
|
const jsSecond = `${now.getSeconds()}`.padStart(2, "0");
|
|
const timeJs =
|
|
`${now.getFullYear()}-${jsMonth}-${jsDate} ${jsHour}:${jsMinute}:${jsSecond}`;
|
|
|
|
assertEquals(timeDb, timeJs);
|
|
});
|
|
|
|
Deno.test("database has correct changes and totalChanges", function () {
|
|
const db = new DB();
|
|
|
|
db.execute(
|
|
"CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)",
|
|
);
|
|
|
|
for (const name of ["a", "b", "c"]) {
|
|
db.query("INSERT INTO test (name) VALUES (?)", [name]);
|
|
assertEquals(1, db.changes);
|
|
}
|
|
|
|
assertEquals(3, db.totalChanges);
|
|
|
|
db.query("UPDATE test SET name = ?", ["new name"]);
|
|
assertEquals(3, db.changes);
|
|
assertEquals(6, db.totalChanges);
|
|
});
|
|
|
|
Deno.test("last inserted id", function () {
|
|
const db = new DB();
|
|
|
|
// By default, lastInsertRowId must be 0
|
|
assertEquals(db.lastInsertRowId, 0);
|
|
|
|
// Create table and insert value
|
|
db.query("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)");
|
|
|
|
const insertRowIds = [];
|
|
|
|
// Insert data to table and collect their ids
|
|
for (let i = 0; i < 10; i++) {
|
|
db.query("INSERT INTO users (name) VALUES ('John Doe')");
|
|
insertRowIds.push(db.lastInsertRowId);
|
|
}
|
|
|
|
// Now, the last inserted row id must be 10
|
|
assertEquals(db.lastInsertRowId, 10);
|
|
|
|
// All collected row ids must be the same as in the database
|
|
assertEquals(
|
|
insertRowIds,
|
|
[...db.query("SELECT id FROM users")].map(([i]) => i),
|
|
);
|
|
|
|
db.close();
|
|
|
|
// When the database is closed, the value
|
|
// will be reset to 0 again
|
|
assertEquals(db.lastInsertRowId, 0);
|
|
});
|
|
|
|
Deno.test("close database", function () {
|
|
const db = new DB();
|
|
db.close();
|
|
assertThrows(() => db.query("CREATE TABLE test (name TEXT PRIMARY KEY)"));
|
|
db.close(); // check close is idempotent and won't throw
|
|
});
|
|
|
|
Deno.test("open queries block close", function () {
|
|
const db = new DB();
|
|
db.query("CREATE TABLE test (name TEXT PRIMARY KEY)");
|
|
|
|
const query = db.prepareQuery("SELECT name FROM test");
|
|
assertThrows(() => db.close());
|
|
query.finalize();
|
|
|
|
db.close();
|
|
});
|
|
|
|
Deno.test("open queries cleaned up by forced close", function () {
|
|
const db = new DB();
|
|
db.query("CREATE TABLE test (name TEXT PRIMARY KEY)");
|
|
db.query("INSERT INTO test (name) VALUES (?)", ["Deno"]);
|
|
|
|
db.prepareQuery("SELECT name FROM test WHERE name like '%test%'");
|
|
|
|
assertThrows(() => db.close());
|
|
db.close(true);
|
|
});
|
|
|
|
Deno.test("invalid bind does not leak statements", function () {
|
|
const db = new DB();
|
|
db.query("CREATE TABLE test (id INTEGER)");
|
|
|
|
for (let n = 0; n < 100; n++) {
|
|
assertThrows(() => {
|
|
// deno-lint-ignore no-explicit-any
|
|
const badBinding: any = [{}];
|
|
db.query("INSERT INTO test (id) VALUES (?)", badBinding);
|
|
});
|
|
assertThrows(() => {
|
|
const badBinding = { missingKey: null };
|
|
db.query("INSERT INTO test (id) VALUES (?)", badBinding);
|
|
});
|
|
}
|
|
|
|
db.query("INSERT INTO test (id) VALUES (1)");
|
|
|
|
db.close();
|
|
});
|
|
|
|
Deno.test("transactions can be nested", function () {
|
|
const db = new DB();
|
|
db.query("CREATE TABLE test (id INTEGER PRIMARY KEY)");
|
|
|
|
db.transaction(() => {
|
|
db.query("INSERT INTO test (id) VALUES (1)");
|
|
try {
|
|
db.transaction(() => {
|
|
db.query("INSERT INTO test (id) VALUES (2)");
|
|
throw new Error("boom!");
|
|
});
|
|
} catch (_) { /* ignore */ }
|
|
});
|
|
|
|
assertEquals([{ id: 1 }], db.queryEntries("SELECT * FROM test"));
|
|
});
|
|
|
|
Deno.test("transactions commit when closure exists", function () {
|
|
const db = new DB();
|
|
db.query("CREATE TABLE test (id INTEGER PRIMARY KEY)");
|
|
|
|
db.transaction(() => {
|
|
db.query("INSERT INTO test (id) VALUES (1)");
|
|
});
|
|
assertThrows(() => db.query("ROLLBACK"));
|
|
|
|
assertEquals([{ id: 1 }], db.queryEntries("SELECT * FROM test"));
|
|
});
|
|
|
|
Deno.test("transaction rolls back on throw", function () {
|
|
const db = new DB();
|
|
db.query("CREATE TABLE test (id INTEGER PRIMARY KEY)");
|
|
|
|
assertThrows(() => {
|
|
db.transaction(() => {
|
|
db.query("INSERT INTO test (id) VALUES (1)");
|
|
throw new Error("boom!");
|
|
});
|
|
});
|
|
|
|
assertEquals([], db.query("SELECT * FROM test"));
|
|
});
|
|
|
|
Deno.test(
|
|
"persist database to file",
|
|
{
|
|
ignore: !TEST_DB_PERMISSIONS,
|
|
permissions: { read: true, write: true },
|
|
sanitizeResources: true,
|
|
},
|
|
async function () {
|
|
const data = [
|
|
"Hello World!",
|
|
"Hello Deno!",
|
|
"JavaScript <3",
|
|
"This costs 0€ / $0 / £0",
|
|
"Wéll, hällö thėrè¿",
|
|
];
|
|
|
|
// ensure the test database file does not exist
|
|
await deleteDatabase(TEST_DB);
|
|
|
|
const db = new DB(TEST_DB);
|
|
db.execute(
|
|
"CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT, val TEXT)",
|
|
);
|
|
for (const val of data) {
|
|
db.query("INSERT INTO test (val) VALUES (?)", [val]);
|
|
}
|
|
|
|
// open the same database with a separate connection
|
|
const readOnlyDb = await new DB(TEST_DB, { mode: "read" });
|
|
for (
|
|
const [id, val] of readOnlyDb.query<[number, string]>(
|
|
"SELECT * FROM test",
|
|
)
|
|
) {
|
|
assertEquals(data[id - 1], val);
|
|
}
|
|
|
|
await Deno.remove(TEST_DB);
|
|
db.close();
|
|
readOnlyDb.close();
|
|
},
|
|
);
|
|
|
|
Deno.test(
|
|
"temporary file database read / write",
|
|
{
|
|
ignore: !TEST_DB_PERMISSIONS,
|
|
permissions: { read: true, write: true },
|
|
sanitizeResources: true,
|
|
},
|
|
function () {
|
|
const data = [
|
|
"Hello World!",
|
|
"Hello Deno!",
|
|
"JavaScript <3",
|
|
"This costs 0€ / $0 / £0",
|
|
"Wéll, hällö thėrè¿",
|
|
];
|
|
|
|
const tempDb = new DB("");
|
|
tempDb.execute(
|
|
"CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT, val TEXT)",
|
|
);
|
|
for (const val of data) {
|
|
tempDb.query("INSERT INTO test (val) VALUES (?)", [val]);
|
|
}
|
|
|
|
for (
|
|
const [id, val] of tempDb.query<[number, string]>("SELECT * FROM test")
|
|
) {
|
|
assertEquals(data[id - 1], val);
|
|
}
|
|
|
|
tempDb.close();
|
|
},
|
|
);
|
|
|
|
Deno.test(
|
|
"database open options",
|
|
{
|
|
ignore: !TEST_DB_PERMISSIONS,
|
|
permissions: { read: true, write: true },
|
|
sanitizeResources: true,
|
|
},
|
|
async function () {
|
|
await deleteDatabase(TEST_DB);
|
|
|
|
// when no file exists, these should error
|
|
assertThrows(() => new DB(TEST_DB, { mode: "write" }));
|
|
assertThrows(() => new DB(TEST_DB, { mode: "read" }));
|
|
|
|
// create the database
|
|
const dbCreate = new DB(TEST_DB, { mode: "create" });
|
|
dbCreate.execute(
|
|
"CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)",
|
|
);
|
|
dbCreate.close();
|
|
|
|
// the default mode is create
|
|
await deleteDatabase(TEST_DB);
|
|
const dbCreateDefault = new DB(TEST_DB, { mode: "create" });
|
|
dbCreateDefault.execute(
|
|
"CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)",
|
|
);
|
|
dbCreateDefault.close();
|
|
|
|
// in write mode, we can run INSERT queries ...
|
|
const dbWrite = new DB(TEST_DB, { mode: "write" });
|
|
dbWrite.query("INSERT INTO test (name) VALUES (?)", ["open-options-test"]);
|
|
dbWrite.close();
|
|
|
|
// ... which we can read in read-only mode ...
|
|
const dbRead = new DB(TEST_DB, { mode: "read" });
|
|
const rows = [...dbRead.query("SELECT id, name FROM test")];
|
|
assertEquals(rows, [[1, "open-options-test"]]);
|
|
|
|
// ... but we can't write with a read-only connection
|
|
assertThrows(() =>
|
|
dbRead.query("INTERT INTO test (name) VALUES (?)", ["this-fails"])
|
|
);
|
|
dbRead.close();
|
|
},
|
|
);
|
|
|
|
Deno.test(
|
|
"create / write mode require write permissions",
|
|
{
|
|
ignore: !TEST_DB_PERMISSIONS,
|
|
permissions: { read: true, write: false },
|
|
sanitizeResources: true,
|
|
},
|
|
function () {
|
|
// opening with these modes requires write permissions ...
|
|
assertThrows(() => new DB(TEST_DB, { mode: "create" }));
|
|
assertThrows(() => new DB(TEST_DB, { mode: "write" }));
|
|
|
|
// ... and the default mode is create
|
|
assertThrows(() => new DB(TEST_DB));
|
|
|
|
// however, opening in read-only mode should work (the file was created
|
|
// in the previous test)
|
|
(new DB(TEST_DB, { mode: "read" })).close();
|
|
|
|
// with memory flag set, the database will be in memory and
|
|
// not require any permissions
|
|
(new DB(TEST_DB, { mode: "create", memory: true })).close();
|
|
|
|
// the mode can also be specified via a URI flag
|
|
(new DB(`file:${TEST_DB}?mode=memory`, { uri: true })).close();
|
|
},
|
|
);
|
|
|
|
Deno.test(
|
|
"database larger than 2GB read / write",
|
|
{
|
|
ignore: !LARGE_TEST_DB_PERMISSIONS,
|
|
permissions: { read: true, write: true },
|
|
sanitizeResources: true,
|
|
},
|
|
function () {
|
|
// generated with `cd build && make testdb`
|
|
const db = new DB(LARGE_TEST_DB, { mode: "write" });
|
|
|
|
db.query("INSERT INTO test (value) VALUES (?)", ["This is a test..."]);
|
|
|
|
const rows = [
|
|
...db.query("SELECT value FROM test ORDER BY id DESC LIMIT 10"),
|
|
];
|
|
assertEquals(rows.length, 10);
|
|
assertEquals(rows[0][0], "This is a test...");
|
|
|
|
db.close();
|
|
},
|
|
);
|