Sync engine (#298)

Fixes #261
This commit is contained in:
Zef Hemel
2023-01-13 15:41:29 +01:00
committed by GitHub
parent de6f531e91
commit a56e14bff1
51 changed files with 1033 additions and 1418 deletions
-462
View File
@@ -1,462 +0,0 @@
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();
},
);
@@ -1,55 +0,0 @@
import {
assertEquals,
assertInstanceOf,
assertThrows,
} from "https://deno.land/std@0.154.0/testing/asserts.ts";
import { DB, SqliteError, Status } from "../mod.ts";
Deno.test("invalid SQL", function () {
const db = new DB();
const queries = [
"INSERT INTO does_not_exist (balance) VALUES (5)",
"this is not sql",
";;;",
];
for (const query of queries) assertThrows(() => db.query(query));
db.close();
});
Deno.test("constraint error code is correct", function () {
const db = new DB();
db.query("CREATE TABLE test (name TEXT PRIMARY KEY)");
db.query("INSERT INTO test (name) VALUES (?)", ["A"]);
assertThrows(
() => db.query("INSERT INTO test (name) VALUES (?)", ["A"]),
(e: Error) => {
assertInstanceOf(e, SqliteError);
assertEquals(e.code, Status.SqliteConstraint, "Got wrong error code");
assertEquals(
Status[e.codeName],
Status.SqliteConstraint,
"Got wrong error code name",
);
},
);
});
Deno.test("syntax error code is correct", function () {
const db = new DB();
assertThrows(
() => db.query("CREATE TABLEX test (name TEXT PRIMARY KEY)"),
(e: Error) => {
assertInstanceOf(e, SqliteError);
assertEquals(e.code, Status.SqliteError, "Got wrong error code");
assertEquals(
Status[e.codeName],
Status.SqliteError,
"Got wrong error code name",
);
},
);
});
-521
View File
@@ -1,521 +0,0 @@
import {
assertEquals,
assertThrows,
} from "https://deno.land/std@0.154.0/testing/asserts.ts";
import { DB, QueryParameter } from "../mod.ts";
function roundTripValues<T extends QueryParameter>(values: T[]): unknown[] {
const db = new DB();
db.execute(
"CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT, datum ANY)",
);
for (const value of values) {
db.query("INSERT INTO test (datum) VALUES (?)", [value]);
}
return db
.queryEntries<{ datum: unknown }>("SELECT datum FROM test")
.map(({ datum }) => datum);
}
Deno.test("bind string values", function () {
const values = ["Hello World!", "I love Deno.", "Täst strüng..."];
assertEquals(values, roundTripValues(values));
});
Deno.test("bind integer values", function () {
const values = [42, 1, 2, 3, 4, 3453246, 4536787093, 45536787093];
assertEquals(values, roundTripValues(values));
});
Deno.test("bind float values", function () {
const values = [42.1, 1.235, 2.999, 1 / 3, 4.2345, 345.3246, 4536787.953e-8];
assertEquals(values, roundTripValues(values));
});
Deno.test("bind boolean values", function () {
assertEquals([1, 0], roundTripValues([true, false]));
});
Deno.test("bind date values", function () {
const values = [new Date(), new Date("2018-11-20"), new Date(123456789)];
assertEquals(
values.map((date) => date.toISOString()),
roundTripValues(values),
);
});
Deno.test("bind blob values", function () {
const values = [
new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 0]),
new Uint8Array([3, 57, 45]),
];
assertEquals(values, roundTripValues(values));
});
Deno.test("blobs are copies", function () {
const db = new DB();
db.query(
"CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT, val BLOB)",
);
const data = new Uint8Array([1, 2, 3, 4, 5]);
db.query("INSERT INTO test (val) VALUES (?)", [data]);
const [[a]] = db.query<[Uint8Array]>("SELECT val FROM test");
const [[b]] = db.query<[Uint8Array]>("SELECT val FROM test");
assertEquals(data, a);
assertEquals(data, b);
assertEquals(a, b);
a[0] = 100;
assertEquals(a[0], 100);
assertEquals(b[0], 1);
assertEquals(data[0], 1);
data[0] = 5;
const [[c]] = db.query<[Uint8Array]>("SELECT val FROM test");
assertEquals(c[0], 1);
});
Deno.test("bind bigint values", function () {
assertEquals(
[9007199254741991n, 100],
roundTripValues([9007199254741991n, 100n]),
);
});
Deno.test("bind null / undefined", function () {
assertEquals([null, null], roundTripValues([null, undefined]));
});
Deno.test("bind mixed values", function () {
const values = [42, "Hello World!", 0.33333, null];
assertEquals(values, roundTripValues(values));
});
Deno.test("omitting a value binds NULL", function () {
const db = new DB();
db.execute("CREATE TABLE test (id INTEGER PRIMARY KEY, datum ANY)");
const insert = db.prepareQuery(
"INSERT INTO test (datum) VALUES (?) RETURNING datum",
);
assertEquals([null], insert.first());
assertEquals([null], insert.first([]));
assertEquals([null], insert.first({}));
// previously bound values are cleared
insert.execute(["this is not null"]);
assertEquals([null], insert.first());
});
Deno.test("prepared query clears bindings before reused", function () {
const db = new DB();
db.query("CREATE TABLE test (id INTEGER PRIMARY KEY, value INTEGER)");
const query = db.prepareQuery("INSERT INTO test (value) VALUES (?)");
query.execute([1]);
query.execute();
assertEquals([[1], [null]], db.query("SELECT value FROM test"));
query.finalize();
db.close();
});
Deno.test("bind very large floating point numbers", function () {
const db = new DB();
db.query("CREATE TABLE numbers (id INTEGER PRIMARY KEY, number REAL)");
db.query("INSERT INTO numbers (number) VALUES (?)", [+Infinity]);
db.query("INSERT INTO numbers (number) VALUES (?)", [-Infinity]);
db.query("INSERT INTO numbers (number) VALUES (?)", [+20e20]);
db.query("INSERT INTO numbers (number) VALUES (?)", [-20e20]);
const [
[positiveInfinity],
[negativeInfinity],
[positiveTwentyTwenty],
[negativeTwentyTwenty],
] = db.query("SELECT number FROM numbers");
assertEquals(negativeInfinity, -Infinity);
assertEquals(positiveInfinity, +Infinity);
assertEquals(positiveTwentyTwenty, +20e20);
assertEquals(negativeTwentyTwenty, -20e20);
});
Deno.test("big very large integers", function () {
const db = new DB();
db.query(
"CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT, val INTEGER)",
);
const goodValues = [
0n,
42n,
-42n,
9223372036854775807n,
-9223372036854775808n,
];
const overflowValues = [
9223372036854775807n + 1n,
-9223372036854775808n - 1n,
2352359223372036854775807n,
-32453249223372036854775807n,
];
const query = db.prepareQuery("INSERT INTO test (val) VALUES (?)");
for (const val of goodValues) {
query.execute([val]);
}
const dbValues = db.query<[number | bigint]>(
"SELECT val FROM test ORDER BY id",
).map((
[id],
) => BigInt(id));
assertEquals(goodValues, dbValues);
for (const bigVal of overflowValues) {
assertThrows(() => {
query.execute([bigVal]);
});
}
query.finalize();
db.close();
});
Deno.test("bind named parameters", function () {
const db = new DB();
db.query(
"CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT, val TEXT)",
);
// :name
db.query("INSERT INTO test (val) VALUES (:val)", { val: "value" });
db.query(
"INSERT INTO test (val) VALUES (:otherVal)",
{ otherVal: "value other" },
);
db.query(
"INSERT INTO test (val) VALUES (:explicitColon)",
{ ":explicitColon": "value explicit" },
);
// @name
db.query(
"INSERT INTO test (val) VALUES (@someName)",
{ "@someName": "@value" },
);
// $name
db.query(
"INSERT INTO test (val) VALUES ($var::Name)",
{ "$var::Name": "$value" },
);
// explicit positional syntax
db.query("INSERT INTO test (id, val) VALUES (?2, ?1)", ["this-is-it", 1000]);
// names must exist
assertThrows(() => {
db.query(
"INSERT INTO test (val) VALUES (:val)",
{ Val: "miss-spelled name" },
);
});
// make sure the data came through correctly
const vals = [...db.query("SELECT val FROM test ORDER BY id ASC")]
.map(([datum]) => datum);
assertEquals(
vals,
[
"value",
"value other",
"value explicit",
"@value",
"$value",
"this-is-it",
],
);
});
Deno.test("iterate from prepared query", function () {
const db = new DB();
db.execute("CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT)");
db.execute("INSERT INTO test (id) VALUES (1), (2), (3)");
const res = [];
const query = db.prepareQuery<[number]>("SELECT id FROM test");
for (const [id] of query.iter()) {
res.push(id);
}
assertEquals(res, [1, 2, 3]);
query.finalize();
db.close();
});
Deno.test("query all from prepared query", function () {
const db = new DB();
db.query("CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT)");
const query = db.prepareQuery("SELECT id FROM test");
assertEquals(query.all(), []);
db.query("INSERT INTO test (id) VALUES (1), (2), (3)");
assertEquals(query.all(), [[1], [2], [3]]);
query.finalize();
db.close();
});
Deno.test("query first from prepared query", function () {
const db = new DB();
db.query("CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT)");
db.query("INSERT INTO test (id) VALUES (1), (2), (3)");
const querySingle = db.prepareQuery("SELECT id FROM test WHERE id = ?");
assertEquals(querySingle.first([42]), undefined);
assertEquals(querySingle.first([2]), [2]);
const queryAll = db.prepareQuery("SELECT id FROM test ORDER BY id ASC");
assertEquals(queryAll.first(), [1]);
querySingle.finalize();
queryAll.finalize();
db.close();
});
Deno.test("query one from prepared query", function () {
const db = new DB();
db.query("CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT)");
db.query("INSERT INTO test (id) VALUES (1), (2), (3)");
const queryOne = db.prepareQuery<[number]>(
"SELECT id FROM test WHERE id = ?",
);
assertThrows(() => queryOne.one([42]));
assertEquals(queryOne.one([2]), [2]);
const queryAll = db.prepareQuery("SELECT id FROM test");
assertThrows(() => queryAll.one());
queryOne.finalize();
queryAll.finalize();
db.close();
});
Deno.test("execute from prepared query", function () {
const db = new DB();
db.query("CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT)");
const insert = db.prepareQuery("INSERT INTO test (id) VALUES (:id)");
for (const id of [1, 2, 3]) {
insert.execute({ id });
}
insert.finalize();
assertEquals(db.query("SELECT id FROM test"), [[1], [2], [3]]);
db.close();
});
Deno.test("empty query returns empty array", function () {
const db = new DB();
db.query("CREATE TABLE test (id INTEGER PRIMARY KEY)");
assertEquals([], db.query("SELECT * FROM test"));
db.close();
});
Deno.test("query entries returns correct object shapes", function () {
const db = new DB();
db.query(
"CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, height REAL)",
);
const rowsOrig = [
{ id: 1, name: "Peter Parker", height: 1.5 },
{ id: 2, name: "Clark Kent", height: 1.9 },
{ id: 3, name: "Robert Paar", height: 2.1 },
];
const insertQuery = db.prepareQuery(
"INSERT INTO test (id, name, height) VALUES (:id, :name, :height)",
);
for (const row of rowsOrig) {
insertQuery.execute(row);
}
insertQuery.finalize();
const query = db.prepareQuery("SELECT * FROM test");
assertEquals(rowsOrig, [...query.iterEntries()]);
assertEquals(rowsOrig, query.allEntries());
assertEquals(rowsOrig[0], query.firstEntry());
assertEquals(rowsOrig, db.queryEntries("SELECT * FROM test"));
query.finalize();
db.close();
});
Deno.test("prepared query can be reused", function () {
const db = new DB();
db.query("CREATE TABLE test (id INTEGER PRIMARY KEY)");
const query = db.prepareQuery("INSERT INTO test (id) VALUES (?)");
query.execute([1]);
query.execute([2]);
query.execute([3]);
assertEquals([[1], [2], [3]], db.query("SELECT id FROM test"));
query.finalize();
db.close();
});
Deno.test("get columns from select query", function () {
const db = new DB();
db.query(
"CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)",
);
const query = db.prepareQuery("SELECT id, name from test");
assertEquals(query.columns(), [
{ name: "id", originName: "id", tableName: "test" },
{ name: "name", originName: "name", tableName: "test" },
]);
});
Deno.test("get columns from returning query", function () {
const db = new DB();
db.query(
"CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)",
);
const query = db.prepareQuery(
"INSERT INTO test (name) VALUES (?) RETURNING *",
);
assertEquals(query.columns(), [
{ name: "id", originName: "id", tableName: "test" },
{ name: "name", originName: "name", tableName: "test" },
]);
assertEquals(query.all(["name"]), [[1, "name"]]);
});
Deno.test("get columns with renamed column", function () {
const db = new DB();
db.query(
"CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)",
);
db.query("INSERT INTO test (name) VALUES (?)", ["name"]);
const query = db.prepareQuery(
"SELECT id AS test_id, name AS test_name from test",
);
const columns = query.columns();
assertEquals(columns, [
{ name: "test_id", originName: "id", tableName: "test" },
{ name: "test_name", originName: "name", tableName: "test" },
]);
});
Deno.test("columns can be obtained from empty prepared query", function () {
const db = new DB();
db.query(
"CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEST, age INTEGER)",
);
db.query("INSERT INTO test (name, age) VALUES (?, ?)", ["Peter Parker", 21]);
const query = db.prepareQuery("SELECT * FROM test");
const columnsFromPreparedQuery = query.columns();
query.finalize();
const queryEmpty = db.prepareQuery("SELECT * FROM test WHERE 1 = 0");
const columnsFromPreparedQueryWithEmptyQuery = queryEmpty.columns();
assertEquals(queryEmpty.all(), []);
query.finalize();
assertEquals(
[{ name: "id", originName: "id", tableName: "test" }, {
name: "name",
originName: "name",
tableName: "test",
}, { name: "age", originName: "age", tableName: "test" }],
columnsFromPreparedQuery,
);
assertEquals(
columnsFromPreparedQueryWithEmptyQuery,
columnsFromPreparedQuery,
);
});
Deno.test("invalid number of bound parameters throws", function () {
const db = new DB();
db.execute("CREATE TABLE test (id INTEGER PRIMARY KEY)");
// too many
assertThrows(() => {
db.query("SELECT * FROM test", [null]);
});
assertThrows(() => {
db.query("SELECT * FROM test LIMIT ?", [5, "extra"]);
});
// too few
assertThrows(() => db.query("SELECT * FROM test LIMIT ?", []));
assertThrows(() => {
db.query(
"SELECT * FROM test WHERE id >= ? AND id <= ? LIMIT ?",
[42],
);
});
});
Deno.test("using finalized prepared query throws", function () {
const db = new DB();
db.query("CREATE TABLE test (name TEXT)");
const query = db.prepareQuery("INSERT INTO test (name) VALUES (?)");
query.finalize();
assertThrows(() => query.execute(["test"]));
db.close();
});
Deno.test("invalid binding throws", function () {
const db = new DB();
db.query("CREATE TABLE test (id INTEGER)");
assertThrows(() => {
// deno-lint-ignore no-explicit-any
const badBinding: any = [{}];
db.query("SELECT * FORM test WHERE id = ?", badBinding);
});
db.close();
});
Deno.test("get columns from finalized query throws", function () {
const db = new DB();
db.query("CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT)");
const query = db.prepareQuery("SELECT id from test");
query.finalize();
// after iteration is done
assertThrows(() => {
query.columns();
});
});
@@ -1,80 +0,0 @@
import {
assertEquals,
assertMatch,
} from "https://deno.land/std@0.154.0/testing/asserts.ts";
import { DB } from "../mod.ts";
Deno.test("README example", function () {
const db = new DB(/* in memory */);
db.execute(`
CREATE TABLE IF NOT EXISTS people (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT
)
`);
const name =
["Peter Parker", "Clark Kent", "Bruce Wane"][Math.floor(Math.random() * 3)];
// Run a simple query
db.query("INSERT INTO people (name) VALUES (?)", [name]);
// Print out data in table
for (const [_name] of db.query("SELECT name FROM people")) continue; // no console.log ;)
db.close();
});
Deno.test("old README example", function () {
const db = new DB();
const first = ["Bruce", "Clark", "Peter"];
const last = ["Wane", "Kent", "Parker"];
db.query(
"CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, email TEXT, subscribed INTEGER)",
);
for (let i = 0; i < 100; i++) {
const name = `${first[Math.floor(Math.random() * first.length)]} ${
last[
Math.floor(
Math.random() * last.length,
)
]
}`;
const email = `${name.replace(" ", "-")}@deno.land`;
const subscribed = Math.random() > 0.5 ? true : false;
db.query("INSERT INTO users (name, email, subscribed) VALUES (?, ?, ?)", [
name,
email,
subscribed,
]);
}
for (
const [
name,
email,
] of db.query<[string, string]>(
"SELECT name, email FROM users WHERE subscribed = ? LIMIT 100",
[true],
)
) {
assertMatch(name, /(Bruce|Clark|Peter) (Wane|Kent|Parker)/);
assertEquals(email, `${name.replace(" ", "-")}@deno.land`);
}
const res = db.query("SELECT email FROM users WHERE name LIKE ?", [
"Robert Parr",
]);
assertEquals(res, []);
const subscribers = db.query(
"SELECT name, email FROM users WHERE subscribed = ?",
[true],
);
for (const [_name, _email] of subscribers) {
if (Math.random() > 0.5) continue;
break;
}
});
@@ -1,40 +0,0 @@
import {
assertEquals,
assertThrows,
} from "https://deno.land/std@0.154.0/testing/asserts.ts";
import { Wasm } from "../build/sqlite.js";
import * as wasm from "./wasm.ts";
function mock(
malloc: () => number = () => 1,
free: (pts: number) => void = () => {},
): Wasm {
const memory = new Uint8Array(2048);
return {
malloc,
free,
str_len: (ptr: number) => {
let len = 0;
for (let idx = ptr; memory.at(idx) != 0; idx++) len++;
return len;
},
memory,
} as unknown as Wasm;
}
Deno.test("round trip string", function () {
const mockWasm = mock();
const testCases = ["Hello world!", "Söme, fünky lëttêrß", "你好👋"];
for (const input of testCases) {
const output = wasm.setStr(mockWasm, input, (ptr) => {
return wasm.getStr(mockWasm, ptr);
});
assertEquals(input, output);
}
});
Deno.test("throws on allocation error", function () {
const mockWasm = mock(() => 0);
assertThrows(() => wasm.setStr(mockWasm, "Hello world!", (_) => null));
});