Fixes #97: SQLite is now async, optimized, tests
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Status codes which can be returned
|
||||
* by SQLite.
|
||||
*
|
||||
* Also see https://www.sqlite.org/rescode.html.
|
||||
*/
|
||||
export enum Status {
|
||||
Unknown = -1, // Unknown status
|
||||
|
||||
SqliteOk = 0, // Successful result
|
||||
SqliteError = 1, // Generic error
|
||||
SqliteInternal = 2, // Internal logic error in SQLite
|
||||
SqlitePerm = 3, // Access permission denied
|
||||
SqliteAbort = 4, // Callback routine requested an abort
|
||||
SqliteBusy = 5, // The database file is locked
|
||||
SqliteLocked = 6, // A table in the database is locked
|
||||
SqliteNoMem = 7, // A malloc() failed
|
||||
SqliteReadOnly = 8, // Attempt to write a readonly database
|
||||
SqliteInterrupt = 9, // Operation terminated by sqlite3_interrupt()
|
||||
SqliteIOErr = 10, // Some kind of disk I/O error occurred
|
||||
SqliteCorrupt = 11, // The database disk image is malformed
|
||||
SqliteNotFound = 12, // Unknown opcode in sqlite3_file_control()
|
||||
SqliteFull = 13, // Insertion failed because database is full
|
||||
SqliteCantOpen = 14, // Unable to open the database file
|
||||
SqliteProtocol = 15, // Database lock protocol error
|
||||
SqliteEmpty = 16, // Internal use only
|
||||
SqliteSchema = 17, // The database schema changed
|
||||
SqliteTooBig = 18, // String or BLOB exceeds size limit
|
||||
SqliteConstraint = 19, // Abort due to constraint violation
|
||||
SqliteMismatch = 20, // Data type mismatch
|
||||
SqliteMisuse = 21, // Library used incorrectly
|
||||
SqliteNoLFS = 22, // Uses OS features not supported on host
|
||||
SqliteAuth = 23, // Authorization denied
|
||||
SqliteFormat = 24, // Not used
|
||||
SqliteRange = 25, // 2nd parameter to sqlite3_bind out of range
|
||||
SqliteNotADB = 26, // File opened that is not a database file
|
||||
SqliteNotice = 27, // Notifications from sqlite3_log()
|
||||
SqliteWarning = 28, // Warnings from sqlite3_log()
|
||||
SqliteRow = 100, // sqlite3_step() has another row ready
|
||||
SqliteDone = 101, // sqlite3_step() has finished executing
|
||||
}
|
||||
|
||||
export enum OpenFlags {
|
||||
ReadOnly = 0x00000001,
|
||||
ReadWrite = 0x00000002,
|
||||
Create = 0x00000004,
|
||||
Uri = 0x00000040,
|
||||
Memory = 0x00000080,
|
||||
}
|
||||
|
||||
export enum Types {
|
||||
Integer = 1,
|
||||
Float = 2,
|
||||
Text = 3,
|
||||
Blob = 4,
|
||||
Null = 5,
|
||||
BigInteger = 6,
|
||||
}
|
||||
|
||||
export enum Values {
|
||||
Error = -1,
|
||||
Null = 0,
|
||||
}
|
||||
@@ -0,0 +1,462 @@
|
||||
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();
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,393 @@
|
||||
import { instantiate, StatementPtr, Wasm } from "../build/sqlite.js";
|
||||
import { setStr } from "./wasm.ts";
|
||||
import { OpenFlags, Status, Values } from "./constants.ts";
|
||||
import { SqliteError } from "./error.ts";
|
||||
import { PreparedQuery, QueryParameterSet, Row, RowObject } from "./query.ts";
|
||||
|
||||
/**
|
||||
* Options for opening a database.
|
||||
*/
|
||||
export interface SqliteOptions {
|
||||
/**
|
||||
* Mode in which to open the database.
|
||||
*
|
||||
* - `read`: read-only, throws an error if
|
||||
* the database file does not exists
|
||||
* - `write`: read-write, throws an error
|
||||
* if the database file does not exists
|
||||
* - `create`: read-write, create the database
|
||||
* if the file does not exist
|
||||
*
|
||||
* `create` is the default if no mode is
|
||||
* specified.
|
||||
*/
|
||||
mode?: "read" | "write" | "create";
|
||||
/**
|
||||
* Force the database to be in-memory. When
|
||||
* this option is set, the database is opened
|
||||
* in memory, regardless of the specified
|
||||
* filename.
|
||||
*/
|
||||
memory?: boolean;
|
||||
/**
|
||||
* Interpret the file name as a URI.
|
||||
* See https://sqlite.org/uri.html
|
||||
* for more information.
|
||||
*/
|
||||
uri?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A database handle that can be used to run
|
||||
* queries.
|
||||
*/
|
||||
export class DB {
|
||||
private _wasm: Wasm;
|
||||
private _open: boolean;
|
||||
private _statements: Set<StatementPtr>;
|
||||
private _transactionDepth: number;
|
||||
|
||||
/**
|
||||
* Create a new database. The file at the
|
||||
* given path will be opened with the
|
||||
* mode specified in options. The default
|
||||
* mode is `create`.
|
||||
*
|
||||
* If no path is given, or if the `memory`
|
||||
* option is set, the database is opened in
|
||||
* memory.
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* Create an in-memory database.
|
||||
* ```typescript
|
||||
* const db = new DB();
|
||||
* ```
|
||||
*
|
||||
* Open a database backed by a file on disk.
|
||||
* ```typescript
|
||||
* const db = new DB("path/to/database.sqlite");
|
||||
* ```
|
||||
*
|
||||
* Pass options to open a read-only database.
|
||||
* ```typescript
|
||||
* const db = new DB("path/to/database.sqlite", { mode: "read" });
|
||||
* ```
|
||||
*/
|
||||
constructor(path: string = ":memory:", options: SqliteOptions = {}) {
|
||||
this._wasm = instantiate().exports;
|
||||
this._open = false;
|
||||
this._statements = new Set();
|
||||
this._transactionDepth = 0;
|
||||
|
||||
// Configure flags
|
||||
let flags = 0;
|
||||
switch (options.mode) {
|
||||
case "read":
|
||||
flags = OpenFlags.ReadOnly;
|
||||
break;
|
||||
case "write":
|
||||
flags = OpenFlags.ReadWrite;
|
||||
break;
|
||||
case "create": // fall through
|
||||
default:
|
||||
flags = OpenFlags.ReadWrite | OpenFlags.Create;
|
||||
break;
|
||||
}
|
||||
if (options.memory === true) {
|
||||
flags |= OpenFlags.Memory;
|
||||
}
|
||||
if (options.uri === true) {
|
||||
flags |= OpenFlags.Uri;
|
||||
}
|
||||
|
||||
// Try to open the database
|
||||
const status = setStr(
|
||||
this._wasm,
|
||||
path,
|
||||
(ptr) => this._wasm.open(ptr, flags),
|
||||
);
|
||||
if (status !== Status.SqliteOk) {
|
||||
throw new SqliteError(this._wasm, status);
|
||||
}
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the database and return all matching
|
||||
* rows.
|
||||
*
|
||||
* This is equivalent to calling `all` on
|
||||
* a prepared query which is then immediately
|
||||
* finalized.
|
||||
*
|
||||
* The type parameter `R` may be supplied by
|
||||
* the user to indicated the type for the rows returned
|
||||
* by the query. Notice that the user is responsible
|
||||
* for ensuring the correctness of the supplied type.
|
||||
*
|
||||
* To avoid SQL injection, user-provided values
|
||||
* should always be passed to the database through
|
||||
* a query parameter.
|
||||
*
|
||||
* See `QueryParameterSet` for documentation on
|
||||
* how values can be bound to SQL statements.
|
||||
*
|
||||
* See `QueryParameter` for documentation on how
|
||||
* values are returned from the database.
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```typescript
|
||||
* const rows = db.query<[string, number]>("SELECT name, age FROM people WHERE city = ?", [city]);
|
||||
* // rows = [["Peter Parker", 21], ...]
|
||||
* ```
|
||||
*
|
||||
* ```typescript
|
||||
* const rows = db.query<[string, number]>(
|
||||
* "SELECT name, age FROM people WHERE city = :city",
|
||||
* { city },
|
||||
* );
|
||||
* // rows = [["Peter Parker", 21], ...]
|
||||
* ```
|
||||
*/
|
||||
query<R extends Row = Row>(
|
||||
sql: string,
|
||||
params?: QueryParameterSet,
|
||||
): Array<R> {
|
||||
const query = this.prepareQuery<R>(sql);
|
||||
try {
|
||||
const rows = query.all(params);
|
||||
query.finalize();
|
||||
return rows;
|
||||
} catch (err) {
|
||||
query.finalize();
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Like `query` except each row is returned
|
||||
* as an object containing key-value pairs.
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```typescript
|
||||
* const rows = db.queryEntries<{ name: string, age: number }>("SELECT name, age FROM people");
|
||||
* // rows = [{ name: "Peter Parker", age: 21 }, ...]
|
||||
* ```
|
||||
*
|
||||
* ```typescript
|
||||
* const rows = db.queryEntries<{ name: string, age: number }>(
|
||||
* "SELECT name, age FROM people WHERE age >= :minAge",
|
||||
* { minAge },
|
||||
* );
|
||||
* // rows = [{ name: "Peter Parker", age: 21 }, ...]
|
||||
* ```
|
||||
*/
|
||||
queryEntries<O extends RowObject = RowObject>(
|
||||
sql: string,
|
||||
params?: QueryParameterSet,
|
||||
): Array<O> {
|
||||
const query = this.prepareQuery<Row, O>(sql);
|
||||
try {
|
||||
const rows = query.allEntries(params);
|
||||
query.finalize();
|
||||
return rows;
|
||||
} catch (err) {
|
||||
query.finalize();
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the given SQL query, so that it
|
||||
* can be run multiple times and potentially
|
||||
* with different parameters.
|
||||
*
|
||||
* If a query will be issued a lot, this is more
|
||||
* efficient than using `query`. A prepared
|
||||
* query also provides more control over how
|
||||
* the query is run, as well as access to meta-data
|
||||
* about the issued query.
|
||||
*
|
||||
* The returned `PreparedQuery` object must be
|
||||
* finalized by calling its `finalize` method
|
||||
* once it is no longer needed.
|
||||
*
|
||||
* # Typing Queries
|
||||
*
|
||||
* Prepared query objects accept three type parameters
|
||||
* to specify precise types for returned data and
|
||||
* query parameters.
|
||||
*
|
||||
* + The first type parameter `R` indicates the tuple type
|
||||
* for rows returned by the query.
|
||||
*
|
||||
* + The second type parameter `O` indicates the record type
|
||||
* for rows returned as entries (mappings from column names
|
||||
* to values).
|
||||
*
|
||||
* + The third type parameter `P` indicates the type this query
|
||||
* accepts as parameters.
|
||||
*
|
||||
* Note, that the correctness of those types must
|
||||
* be guaranteed by the caller of this function.
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```typescript
|
||||
* const query = db.prepareQuery<
|
||||
* [string, number],
|
||||
* { name: string, age: number },
|
||||
* { city: string },
|
||||
* >("SELECT name, age FROM people WHERE city = :city");
|
||||
*
|
||||
* // use query ...
|
||||
*
|
||||
* query.finalize();
|
||||
* ```
|
||||
*/
|
||||
prepareQuery<
|
||||
R extends Row = Row,
|
||||
O extends RowObject = RowObject,
|
||||
P extends QueryParameterSet = QueryParameterSet,
|
||||
>(
|
||||
sql: string,
|
||||
): PreparedQuery<R, O, P> {
|
||||
if (!this._open) {
|
||||
throw new SqliteError("Database was closed.");
|
||||
}
|
||||
|
||||
const stmt = setStr(
|
||||
this._wasm,
|
||||
sql,
|
||||
(ptr) => this._wasm.prepare(ptr),
|
||||
);
|
||||
if (stmt === Values.Null) {
|
||||
throw new SqliteError(this._wasm);
|
||||
}
|
||||
|
||||
this._statements.add(stmt);
|
||||
return new PreparedQuery<R, O, P>(this._wasm, stmt, this._statements);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run multiple semicolon-separated statements from a single
|
||||
* string.
|
||||
*
|
||||
* This method cannot bind any query parameters, and any
|
||||
* result rows are discarded. It is only for running a chunk
|
||||
* of raw SQL; for example, to initialize a database.
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```typescript
|
||||
* db.execute(`
|
||||
* CREATE TABLE people (
|
||||
* id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
* name TEXT,
|
||||
* age REAL,
|
||||
* city TEXT
|
||||
* );
|
||||
* INSERT INTO people (name, age, city) VALUES ("Peter Parker", 21, "nyc");
|
||||
* `);
|
||||
* ```
|
||||
*/
|
||||
execute(sql: string) {
|
||||
const status = setStr(
|
||||
this._wasm,
|
||||
sql,
|
||||
(ptr) => this._wasm.exec(ptr),
|
||||
);
|
||||
|
||||
if (status !== Status.SqliteOk) {
|
||||
throw new SqliteError(this._wasm, status);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a function within the context of a database
|
||||
* transaction. If the function throws an error,
|
||||
* the transaction is rolled back. Otherwise, the
|
||||
* transaction is committed when the function returns.
|
||||
*
|
||||
* Calls to `transaction` may be nested. Nested transactions
|
||||
* behave like SQLite save points.
|
||||
*/
|
||||
transaction<V>(closure: () => V): V {
|
||||
this._transactionDepth += 1;
|
||||
this.query(`SAVEPOINT _deno_sqlite_sp_${this._transactionDepth}`);
|
||||
let value;
|
||||
try {
|
||||
value = closure();
|
||||
} catch (err) {
|
||||
this.query(`ROLLBACK TO _deno_sqlite_sp_${this._transactionDepth}`);
|
||||
this._transactionDepth -= 1;
|
||||
throw err;
|
||||
}
|
||||
this.query(`RELEASE _deno_sqlite_sp_${this._transactionDepth}`);
|
||||
this._transactionDepth -= 1;
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the database. This must be called if
|
||||
* the database is no longer used to avoid leaking
|
||||
* open file descriptors.
|
||||
*
|
||||
* If `force = true` is passed, any non-finalized
|
||||
* `PreparedQuery` objects will be finalized. Otherwise,
|
||||
* this throws if there are active queries.
|
||||
*
|
||||
* `close` may safely be called multiple
|
||||
* times.
|
||||
*/
|
||||
close(force = false) {
|
||||
if (!this._open) {
|
||||
return;
|
||||
}
|
||||
if (force) {
|
||||
for (const stmt of this._statements) {
|
||||
if (this._wasm.finalize(stmt) !== Status.SqliteOk) {
|
||||
throw new SqliteError(this._wasm);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this._wasm.close() !== Status.SqliteOk) {
|
||||
throw new SqliteError(this._wasm);
|
||||
}
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get last inserted row id. This corresponds to
|
||||
* the SQLite function `sqlite3_last_insert_rowid`.
|
||||
*
|
||||
* Before a row is inserted for the first time (since
|
||||
* the database was opened), this returns `0`.
|
||||
*/
|
||||
get lastInsertRowId(): number {
|
||||
return this._wasm.last_insert_rowid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the number of rows modified, inserted or
|
||||
* deleted by the most recently completed query.
|
||||
* This corresponds to the SQLite function
|
||||
* `sqlite3_changes`.
|
||||
*/
|
||||
get changes(): number {
|
||||
return this._wasm.changes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the number of rows modified, inserted or
|
||||
* deleted since the database was opened.
|
||||
* This corresponds to the SQLite function
|
||||
* `sqlite3_total_changes`.
|
||||
*/
|
||||
get totalChanges(): number {
|
||||
return this._wasm.total_changes();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
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",
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Wasm } from "../build/sqlite.js";
|
||||
import { getStr } from "./wasm.ts";
|
||||
import { Status } from "./constants.ts";
|
||||
|
||||
/**
|
||||
* Errors which can occur while interacting with
|
||||
* a database.
|
||||
*/
|
||||
export class SqliteError extends Error {
|
||||
/**
|
||||
* Extension over the standard JS Error object
|
||||
* to also contain class members for error code
|
||||
* and error code name.
|
||||
*
|
||||
* Instances of this class should not be constructed
|
||||
* directly and should only be obtained
|
||||
* from exceptions raised in this module.
|
||||
*/
|
||||
constructor(context: Wasm | string, code?: Status) {
|
||||
let message;
|
||||
let status;
|
||||
if (typeof context === "string") {
|
||||
message = context;
|
||||
status = Status.Unknown;
|
||||
} else {
|
||||
message = getStr(context, context.get_sqlite_error_str());
|
||||
status = context.get_status();
|
||||
}
|
||||
super(message);
|
||||
this.code = code ?? status;
|
||||
this.name = "SqliteError";
|
||||
}
|
||||
|
||||
/**
|
||||
* The SQLite status code which caused this error.
|
||||
*
|
||||
* Errors that originate in the JavaScript part of
|
||||
* the library will not have an associated status
|
||||
* code. For these errors, the code will be
|
||||
* `Status.Unknown`.
|
||||
*
|
||||
* These codes are accessible via
|
||||
* the exported `Status` object.
|
||||
*/
|
||||
code: Status;
|
||||
|
||||
/**
|
||||
* Key of code in exported `status`
|
||||
* object.
|
||||
*
|
||||
* E.g. if `code` is `19`,
|
||||
* `codeName` would be `SqliteConstraint`.
|
||||
*/
|
||||
get codeName(): keyof typeof Status {
|
||||
return Status[this.code] as keyof typeof Status;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,521 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,692 @@
|
||||
import { StatementPtr, Wasm } from "../build/sqlite.js";
|
||||
import { getStr, setArr, setStr } from "./wasm.ts";
|
||||
import { Status, Types, Values } from "./constants.ts";
|
||||
import { SqliteError } from "./error.ts";
|
||||
|
||||
/**
|
||||
* The default type for returned rows.
|
||||
*/
|
||||
export type Row = Array<unknown>;
|
||||
|
||||
/**
|
||||
* The default type for row returned
|
||||
* as objects.
|
||||
*/
|
||||
export type RowObject = Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* Possible parameter values to be bound to a query.
|
||||
*
|
||||
* When values are bound to a query, they are
|
||||
* converted between JavaScript and SQLite types
|
||||
* in the following way:
|
||||
*
|
||||
* | JS type in | SQL type | JS type out |
|
||||
* |------------|-----------------|------------------|
|
||||
* | number | INTEGER or REAL | number or bigint |
|
||||
* | bigint | INTEGER | number or bigint |
|
||||
* | boolean | INTEGER | number |
|
||||
* | string | TEXT | string |
|
||||
* | Date | TEXT | string |
|
||||
* | Uint8Array | BLOB | Uint8Array |
|
||||
* | null | NULL | null |
|
||||
* | undefined | NULL | null |
|
||||
*
|
||||
* If no value is provided for a given parameter,
|
||||
* SQLite will default to NULL.
|
||||
*
|
||||
* If a `bigint` is bound, it is converted to a
|
||||
* signed 64 bit integer, which may overflow.
|
||||
*
|
||||
* If an integer value is read from the database, which
|
||||
* is too big to safely be contained in a `number`, it
|
||||
* is automatically returned as a `bigint`.
|
||||
*
|
||||
* If a `Date` is bound, it will be converted to
|
||||
* an ISO 8601 string: `YYYY-MM-DDTHH:MM:SS.SSSZ`.
|
||||
* This format is understood by built-in SQLite
|
||||
* date-time functions. Also see https://sqlite.org/lang_datefunc.html.
|
||||
*/
|
||||
export type QueryParameter =
|
||||
| boolean
|
||||
| number
|
||||
| bigint
|
||||
| string
|
||||
| null
|
||||
| undefined
|
||||
| Date
|
||||
| Uint8Array;
|
||||
|
||||
/**
|
||||
* A set of query parameters.
|
||||
*
|
||||
* When a query is constructed, it can contain
|
||||
* either positional or named parameters. For
|
||||
* more information see https://www.sqlite.org/lang_expr.html#parameters.
|
||||
*
|
||||
* A set of parameters can be passed to
|
||||
* a query method either as an array of
|
||||
* parameters (in positional order), or
|
||||
* as an object which maps parameter names
|
||||
* to their values:
|
||||
*
|
||||
* | SQL Parameter | QueryParameterSet |
|
||||
* |---------------|-------------------------|
|
||||
* | `?NNN` or `?` | NNN-th value in array |
|
||||
* | `:AAAA` | value `AAAA` or `:AAAA` |
|
||||
* | `@AAAA` | value `@AAAA` |
|
||||
* | `$AAAA` | value `$AAAA` |
|
||||
*
|
||||
* See `QueryParameter` for documentation on
|
||||
* how values are converted between SQL
|
||||
* and JavaScript types.
|
||||
*/
|
||||
export type QueryParameterSet =
|
||||
| Record<string, QueryParameter>
|
||||
| Array<QueryParameter>;
|
||||
|
||||
/**
|
||||
* Name of a column in a database query.
|
||||
*/
|
||||
export interface ColumnName {
|
||||
/**
|
||||
* Name of the returned column.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Name of the database column that stores
|
||||
* the data returned from this query.
|
||||
*
|
||||
* This might be different from `name` if a
|
||||
* columns was renamed using e.g. as in
|
||||
* `SELECT foo AS bar FROM table`.
|
||||
*/
|
||||
originName: string;
|
||||
/**
|
||||
* Name of the table that stores the data
|
||||
* returned from this query.
|
||||
*/
|
||||
tableName: string;
|
||||
}
|
||||
|
||||
interface RowsIterator<R> {
|
||||
next: () => IteratorResult<R>;
|
||||
[Symbol.iterator]: () => RowsIterator<R>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A prepared query which can be executed many
|
||||
* times.
|
||||
*/
|
||||
export class PreparedQuery<
|
||||
R extends Row = Row,
|
||||
O extends RowObject = RowObject,
|
||||
P extends QueryParameterSet = QueryParameterSet,
|
||||
> {
|
||||
private _wasm: Wasm;
|
||||
private _stmt: StatementPtr;
|
||||
private _openStatements: Set<StatementPtr>;
|
||||
|
||||
private _status: number;
|
||||
private _iterKv: boolean;
|
||||
private _rowKeys?: Array<string>;
|
||||
private _finalized: boolean;
|
||||
|
||||
/**
|
||||
* This constructor should never be used directly.
|
||||
* Instead a prepared query can be obtained by
|
||||
* calling `DB.prepareQuery`.
|
||||
*/
|
||||
constructor(
|
||||
wasm: Wasm,
|
||||
stmt: StatementPtr,
|
||||
openStatements: Set<StatementPtr>,
|
||||
) {
|
||||
this._wasm = wasm;
|
||||
this._stmt = stmt;
|
||||
this._openStatements = openStatements;
|
||||
|
||||
this._status = Status.Unknown;
|
||||
this._iterKv = false;
|
||||
this._finalized = false;
|
||||
}
|
||||
|
||||
private startQuery(params?: P) {
|
||||
if (this._finalized) {
|
||||
throw new SqliteError("Query is finalized.");
|
||||
}
|
||||
|
||||
// Reset query
|
||||
this._wasm.reset(this._stmt);
|
||||
this._wasm.clear_bindings(this._stmt);
|
||||
|
||||
// Prepare parameter array
|
||||
let parameters = [];
|
||||
if (Array.isArray(params)) {
|
||||
parameters = params;
|
||||
} else if (typeof params === "object") {
|
||||
// Resolve parameter index for named parameter
|
||||
for (const key of Object.keys(params)) {
|
||||
let name = key;
|
||||
// blank names default to ':'
|
||||
if (name[0] !== ":" && name[0] !== "@" && name[0] !== "$") {
|
||||
name = `:${name}`;
|
||||
}
|
||||
const idx = setStr(
|
||||
this._wasm,
|
||||
name,
|
||||
(ptr) => this._wasm.bind_parameter_index(this._stmt, ptr),
|
||||
);
|
||||
if (idx === Values.Error) {
|
||||
throw new SqliteError(`No parameter named '${name}'.`);
|
||||
}
|
||||
parameters[idx - 1] = params[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Bind parameters
|
||||
for (let i = 0; i < parameters.length; i++) {
|
||||
let value = parameters[i];
|
||||
let status;
|
||||
switch (typeof value) {
|
||||
case "boolean":
|
||||
value = value ? 1 : 0;
|
||||
// fall through
|
||||
case "number":
|
||||
if (Number.isSafeInteger(value)) {
|
||||
status = this._wasm.bind_int(this._stmt, i + 1, value);
|
||||
} else {
|
||||
status = this._wasm.bind_double(this._stmt, i + 1, value);
|
||||
}
|
||||
break;
|
||||
case "bigint":
|
||||
// bigint is bound as two 32bit integers and reassembled on the C side
|
||||
if (value > 9223372036854775807n || value < -9223372036854775808n) {
|
||||
throw new SqliteError(
|
||||
`BigInt value ${value} overflows 64 bit integer.`,
|
||||
);
|
||||
} else {
|
||||
const posVal = value >= 0n ? value : -value;
|
||||
const sign = value >= 0n ? 1 : -1;
|
||||
const upper = Number(BigInt.asUintN(32, posVal >> 32n));
|
||||
const lower = Number(BigInt.asUintN(32, posVal));
|
||||
status = this._wasm.bind_big_int(
|
||||
this._stmt,
|
||||
i + 1,
|
||||
sign,
|
||||
upper,
|
||||
lower,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case "string":
|
||||
status = setStr(
|
||||
this._wasm,
|
||||
value,
|
||||
(ptr) => this._wasm.bind_text(this._stmt, i + 1, ptr),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
if (value instanceof Date) {
|
||||
// Dates are allowed and bound to TEXT, formatted `YYYY-MM-DDTHH:MM:SS.SSSZ`
|
||||
status = setStr(
|
||||
this._wasm,
|
||||
value.toISOString(),
|
||||
(ptr) => this._wasm.bind_text(this._stmt, i + 1, ptr),
|
||||
);
|
||||
} else if (value instanceof Uint8Array) {
|
||||
// Uint8Arrays are allowed and bound to BLOB
|
||||
const size = value.length;
|
||||
status = setArr(
|
||||
this._wasm,
|
||||
value,
|
||||
(ptr) => this._wasm.bind_blob(this._stmt, i + 1, ptr, size),
|
||||
);
|
||||
} else if (value === null || value === undefined) {
|
||||
// Both null and undefined result in a NULL entry
|
||||
status = this._wasm.bind_null(this._stmt, i + 1);
|
||||
} else {
|
||||
throw new SqliteError(`Can not bind ${typeof value}.`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (status !== Status.SqliteOk) {
|
||||
throw new SqliteError(this._wasm, status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getQueryRow(): R {
|
||||
if (this._finalized) {
|
||||
throw new SqliteError("Query is finalized.");
|
||||
}
|
||||
|
||||
const columnCount = this._wasm.column_count(this._stmt);
|
||||
const row: Row = [];
|
||||
for (let i = 0; i < columnCount; i++) {
|
||||
switch (this._wasm.column_type(this._stmt, i)) {
|
||||
case Types.Integer:
|
||||
row.push(this._wasm.column_int(this._stmt, i));
|
||||
break;
|
||||
case Types.Float:
|
||||
row.push(this._wasm.column_double(this._stmt, i));
|
||||
break;
|
||||
case Types.Text:
|
||||
row.push(
|
||||
getStr(
|
||||
this._wasm,
|
||||
this._wasm.column_text(this._stmt, i),
|
||||
),
|
||||
);
|
||||
break;
|
||||
case Types.Blob: {
|
||||
const ptr = this._wasm.column_blob(this._stmt, i);
|
||||
if (ptr === 0) {
|
||||
// Zero pointer results in null
|
||||
row.push(null);
|
||||
} else {
|
||||
const length = this._wasm.column_bytes(this._stmt, i);
|
||||
// Slice should copy the bytes, as it makes a shallow copy
|
||||
row.push(
|
||||
new Uint8Array(this._wasm.memory.buffer, ptr, length).slice(),
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Types.BigInteger: {
|
||||
const ptr = this._wasm.column_text(this._stmt, i);
|
||||
row.push(BigInt(getStr(this._wasm, ptr)));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// TODO(dyedgreen): Differentiate between NULL and not-recognized?
|
||||
row.push(null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return row as R;
|
||||
}
|
||||
|
||||
private makeRowObject(row: Row): O {
|
||||
if (this._rowKeys == null) {
|
||||
const rowCount = this._wasm.column_count(this._stmt);
|
||||
this._rowKeys = [];
|
||||
for (let i = 0; i < rowCount; i++) {
|
||||
this._rowKeys.push(
|
||||
getStr(this._wasm, this._wasm.column_name(this._stmt, i)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const obj = row.reduce<RowObject>((obj, val, idx) => {
|
||||
obj[this._rowKeys![idx]] = val;
|
||||
return obj;
|
||||
}, {});
|
||||
return obj as O;
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds the given parameters to the query
|
||||
* and returns an iterator over rows.
|
||||
*
|
||||
* Using an iterator avoids loading all returned
|
||||
* rows into memory and hence allows to process a large
|
||||
* number of rows.
|
||||
*
|
||||
* Calling `iter`, `all`, or `first` invalidates any iterators
|
||||
* previously returned from this prepared query.
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```typescript
|
||||
* const query = db.prepareQuery<[number, string]>("SELECT id, name FROM people");
|
||||
* for (const [id, name] of query.iter()) {
|
||||
* // ...
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* To avoid SQL injection, user-provided values
|
||||
* should always be passed to the database through
|
||||
* a query parameter.
|
||||
*
|
||||
* ```typescript
|
||||
* const query = db.prepareQuery("SELECT id FROM people WHERE name = ?");
|
||||
* preparedQuery.iter([name]);
|
||||
* ```
|
||||
*
|
||||
* ```typescript
|
||||
* const query = db.prepareQuery("SELECT id FROM people WHERE name = :name");
|
||||
* preparedQuery.iter({ name });
|
||||
* ```
|
||||
*
|
||||
* See `QueryParameterSet` for documentation on
|
||||
* how values can be bound to SQL statements.
|
||||
*
|
||||
* See `QueryParameter` for documentation on how
|
||||
* values are returned from the database.
|
||||
*/
|
||||
iter(params?: P): RowsIterator<R> {
|
||||
this.startQuery(params);
|
||||
this._status = this._wasm.step(this._stmt);
|
||||
if (
|
||||
this._status !== Status.SqliteRow && this._status !== Status.SqliteDone
|
||||
) {
|
||||
throw new SqliteError(this._wasm, this._status);
|
||||
}
|
||||
this._iterKv = false;
|
||||
return this as RowsIterator<R>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Like `iter` except each row is returned
|
||||
* as an object containing key-value pairs.
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```typescript
|
||||
* const query = db.prepareQuery<_, { id: number, name: string }>("SELECT id, name FROM people");
|
||||
* for (const { id, name } of query.iter()) {
|
||||
* // ...
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
iterEntries(params?: P): RowsIterator<O> {
|
||||
this.iter(params);
|
||||
this._iterKv = true;
|
||||
return this as RowsIterator<O>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*
|
||||
* Implements the iterable protocol. It is
|
||||
* a bug to call this method directly.
|
||||
*/
|
||||
[Symbol.iterator](): RowsIterator<R | O> {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*
|
||||
* Implements the iterator protocol. It is
|
||||
* a bug to call this method directly.
|
||||
*/
|
||||
next(): IteratorResult<R | O> {
|
||||
if (this._status === Status.SqliteRow) {
|
||||
const value = this.getQueryRow();
|
||||
this._status = this._wasm.step(this._stmt);
|
||||
if (this._iterKv) {
|
||||
return { value: this.makeRowObject(value), done: false };
|
||||
} else {
|
||||
return { value, done: false };
|
||||
}
|
||||
} else if (this._status === Status.SqliteDone) {
|
||||
return { value: null, done: true };
|
||||
} else {
|
||||
throw new SqliteError(this._wasm, this._status);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds the given parameters to the query
|
||||
* and returns an array containing all resulting
|
||||
* rows.
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```typescript
|
||||
* const query = db.prepareQuery<[number, string]>("SELECT id, name FROM people");
|
||||
* const rows = query.all();
|
||||
* // [[1, "Peter"], ...]
|
||||
* ```
|
||||
*
|
||||
* To avoid SQL injection, user-provided values
|
||||
* should always be passed to the database through
|
||||
* a query parameter.
|
||||
*
|
||||
* ```typescript
|
||||
* const query = db.prepareQuery("SELECT id FROM people WHERE name = ?");
|
||||
* preparedQuery.all([name]);
|
||||
* ```
|
||||
*
|
||||
* ```typescript
|
||||
* const query = db.prepareQuery("SELECT id FROM people WHERE name = :name");
|
||||
* preparedQuery.all({ name });
|
||||
* ```
|
||||
*
|
||||
* See `QueryParameterSet` for documentation on
|
||||
* how values can be bound to SQL statements.
|
||||
*
|
||||
* See `QueryParameter` for documentation on how
|
||||
* values are returned from the database.
|
||||
*/
|
||||
all(params?: P): Array<R> {
|
||||
this.startQuery(params);
|
||||
const rows: Array<R> = [];
|
||||
this._status = this._wasm.step(this._stmt);
|
||||
while (this._status === Status.SqliteRow) {
|
||||
rows.push(this.getQueryRow());
|
||||
this._status = this._wasm.step(this._stmt);
|
||||
}
|
||||
if (this._status !== Status.SqliteDone) {
|
||||
throw new SqliteError(this._wasm, this._status);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Like `all` except each row is returned
|
||||
* as an object containing key-value pairs.
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```typescript
|
||||
* const query = db.prepareQuery<_, { id: number, name: string }>("SELECT id, name FROM people");
|
||||
* const rows = query.all();
|
||||
* // [{ id: 1, name: "Peter" }, ...]
|
||||
* ```
|
||||
*/
|
||||
allEntries(params?: P): Array<O> {
|
||||
return this.all(params).map((row) => this.makeRowObject(row));
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds the given parameters to the query
|
||||
* and returns the first resulting row or
|
||||
* `undefined` when there are no rows returned
|
||||
* by the query.
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```typescript
|
||||
* const query = db.prepareQuery<[number, string]>("SELECT id, name FROM people");
|
||||
* const person = query.first();
|
||||
* // [1, "Peter"]
|
||||
* ```
|
||||
*
|
||||
* ```typescript
|
||||
* const query = db.prepareQuery("SELECT id, name FROM people WHERE name = ?");
|
||||
* const person = query.first(["not a name"]);
|
||||
* // undefined
|
||||
* ```
|
||||
*
|
||||
* To avoid SQL injection, user-provided values
|
||||
* should always be passed to the database through
|
||||
* a query parameter.
|
||||
*
|
||||
* ```typescript
|
||||
* const query = db.prepareQuery("SELECT id FROM people WHERE name = ?");
|
||||
* preparedQuery.first([name]);
|
||||
* ```
|
||||
*
|
||||
* ```typescript
|
||||
* const query = db.prepareQuery("SELECT id FROM people WHERE name = :name");
|
||||
* preparedQuery.first({ name });
|
||||
* ```
|
||||
*
|
||||
* See `QueryParameterSet` for documentation on
|
||||
* how values can be bound to SQL statements.
|
||||
*
|
||||
* See `QueryParameter` for documentation on how
|
||||
* values are returned from the database.
|
||||
*/
|
||||
first(params?: P): R | undefined {
|
||||
this.startQuery(params);
|
||||
|
||||
this._status = this._wasm.step(this._stmt);
|
||||
let row = undefined;
|
||||
if (this._status === Status.SqliteRow) {
|
||||
row = this.getQueryRow();
|
||||
}
|
||||
|
||||
while (this._status === Status.SqliteRow) {
|
||||
this._status = this._wasm.step(this._stmt);
|
||||
}
|
||||
if (this._status !== Status.SqliteDone) {
|
||||
throw new SqliteError(this._wasm, this._status);
|
||||
}
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
/**
|
||||
* Like `first` except the row is returned
|
||||
* as an object containing key-value pairs.
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```typescript
|
||||
* const query = db.prepareQuery<_, { id: number, name: string }>("SELECT id, name FROM people");
|
||||
* const person = query.first();
|
||||
* // { id: 1, name: "Peter" }
|
||||
* ```
|
||||
*/
|
||||
firstEntry(params?: P): O | undefined {
|
||||
const row = this.first(params);
|
||||
return row === undefined ? undefined : this.makeRowObject(row);
|
||||
}
|
||||
|
||||
/**
|
||||
* **Deprecated:** prefer `first`.
|
||||
*/
|
||||
one(params?: P): R {
|
||||
const rows = this.all(params);
|
||||
|
||||
if (rows.length === 0) {
|
||||
throw new SqliteError("The query did not return any rows.");
|
||||
} else if (rows.length > 1) {
|
||||
throw new SqliteError("The query returned more than one row.");
|
||||
} else {
|
||||
return rows[0];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* **Deprecated:** prefer `firstEntry`.
|
||||
*/
|
||||
oneEntry(params?: P): O {
|
||||
return this.makeRowObject(this.one(params));
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds the given parameters to the query and
|
||||
* executes the query, ignoring any rows which
|
||||
* might be returned.
|
||||
*
|
||||
* Using this method is more efficient when the
|
||||
* rows returned by a query are not needed or
|
||||
* the query does not return any rows.
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```typescript
|
||||
* const query = db.prepareQuery<_, _, [string]>("INSERT INTO people (name) VALUES (?)");
|
||||
* query.execute(["Peter"]);
|
||||
* ```
|
||||
*
|
||||
* ```typescript
|
||||
* const query = db.prepareQuery<_, _, { name: string }>("INSERT INTO people (name) VALUES (:name)");
|
||||
* query.execute({ name: "Peter" });
|
||||
* ```
|
||||
*
|
||||
* See `QueryParameterSet` for documentation on
|
||||
* how values can be bound to SQL statements.
|
||||
*
|
||||
* See `QueryParameter` for documentation on how
|
||||
* values are returned from the database.
|
||||
*/
|
||||
execute(params?: P) {
|
||||
this.startQuery(params);
|
||||
this._status = this._wasm.step(this._stmt);
|
||||
while (this._status === Status.SqliteRow) {
|
||||
this._status = this._wasm.step(this._stmt);
|
||||
}
|
||||
if (this._status !== Status.SqliteDone) {
|
||||
throw new SqliteError(this._wasm, this._status);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the prepared query. This must be
|
||||
* called once the query is no longer needed
|
||||
* to avoid leaking resources.
|
||||
*
|
||||
* After a prepared query has been finalized,
|
||||
* calls to `iter`, `all`, `first`, `execute`,
|
||||
* or `columns` will fail.
|
||||
*
|
||||
* Using iterators which were previously returned
|
||||
* from the finalized query will fail.
|
||||
*
|
||||
* `finalize` may safely be called multiple
|
||||
* times.
|
||||
*/
|
||||
finalize() {
|
||||
if (!this._finalized) {
|
||||
this._wasm.finalize(this._stmt);
|
||||
this._openStatements.delete(this._stmt);
|
||||
this._finalized = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the column names for the query
|
||||
* results.
|
||||
*
|
||||
* This method returns an array of objects,
|
||||
* where each object has the following properties:
|
||||
*
|
||||
* | Property | Value |
|
||||
* |--------------|--------------------------------------------|
|
||||
* | `name` | the result of `sqlite3_column_name` |
|
||||
* | `originName` | the result of `sqlite3_column_origin_name` |
|
||||
* | `tableName` | the result of `sqlite3_column_table_name` |
|
||||
*/
|
||||
columns(): Array<ColumnName> {
|
||||
if (this._finalized) {
|
||||
throw new SqliteError(
|
||||
"Unable to retrieve column names from finalized transaction.",
|
||||
);
|
||||
}
|
||||
|
||||
const columnCount = this._wasm.column_count(this._stmt);
|
||||
const columns: Array<ColumnName> = [];
|
||||
for (let i = 0; i < columnCount; i++) {
|
||||
const name = getStr(
|
||||
this._wasm,
|
||||
this._wasm.column_name(this._stmt, i),
|
||||
);
|
||||
const originName = getStr(
|
||||
this._wasm,
|
||||
this._wasm.column_origin_name(this._stmt, i),
|
||||
);
|
||||
const tableName = getStr(
|
||||
this._wasm,
|
||||
this._wasm.column_table_name(this._stmt, i),
|
||||
);
|
||||
columns.push({ name, originName, tableName });
|
||||
}
|
||||
return columns;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
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;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
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));
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
import { Wasm } from "../build/sqlite.js";
|
||||
import { SqliteError } from "./error.ts";
|
||||
|
||||
// Move string to C
|
||||
export function setStr<T>(
|
||||
wasm: Wasm,
|
||||
str: string,
|
||||
closure: (ptr: number) => T,
|
||||
): T {
|
||||
const bytes = new TextEncoder().encode(str);
|
||||
const ptr = wasm.malloc(bytes.length + 1);
|
||||
if (ptr === 0) {
|
||||
throw new SqliteError("Out of memory.");
|
||||
}
|
||||
const mem = new Uint8Array(wasm.memory.buffer, ptr, bytes.length + 1);
|
||||
mem.set(bytes);
|
||||
mem[bytes.length] = 0; // \0 terminator
|
||||
try {
|
||||
const result = closure(ptr);
|
||||
wasm.free(ptr);
|
||||
return result;
|
||||
} catch (error) {
|
||||
wasm.free(ptr);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Move Uint8Array to C
|
||||
export function setArr<T>(
|
||||
wasm: Wasm,
|
||||
arr: Uint8Array,
|
||||
closure: (ptr: number) => T,
|
||||
): T {
|
||||
const ptr = wasm.malloc(arr.length);
|
||||
if (ptr === 0) {
|
||||
throw new SqliteError("Out of memory.");
|
||||
}
|
||||
const mem = new Uint8Array(wasm.memory.buffer, ptr, arr.length);
|
||||
mem.set(arr);
|
||||
try {
|
||||
const result = closure(ptr);
|
||||
wasm.free(ptr);
|
||||
return result;
|
||||
} catch (error) {
|
||||
wasm.free(ptr);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Read string from C
|
||||
export function getStr(wasm: Wasm, ptr: number): string {
|
||||
const len = wasm.str_len(ptr);
|
||||
const bytes = new Uint8Array(wasm.memory.buffer, ptr, len);
|
||||
if (len > 16) {
|
||||
return new TextDecoder().decode(bytes);
|
||||
} else {
|
||||
// This optimization is lifted from EMSCRIPTEN's glue code
|
||||
let str = "";
|
||||
let idx = 0;
|
||||
while (idx < len) {
|
||||
let u0 = bytes[idx++];
|
||||
if (!(u0 & 0x80)) {
|
||||
str += String.fromCharCode(u0);
|
||||
continue;
|
||||
}
|
||||
const u1 = bytes[idx++] & 63;
|
||||
if ((u0 & 0xE0) == 0xC0) {
|
||||
str += String.fromCharCode(((u0 & 31) << 6) | u1);
|
||||
continue;
|
||||
}
|
||||
const u2 = bytes[idx++] & 63;
|
||||
if ((u0 & 0xF0) == 0xE0) {
|
||||
u0 = ((u0 & 15) << 12) | (u1 << 6) | u2;
|
||||
} else {
|
||||
// cut warning
|
||||
u0 = ((u0 & 7) << 18) | (u1 << 12) | (u2 << 6) | (bytes[idx++] & 63);
|
||||
}
|
||||
if (u0 < 0x10000) {
|
||||
str += String.fromCharCode(u0);
|
||||
} else {
|
||||
const ch = u0 - 0x10000;
|
||||
str += String.fromCharCode(0xD800 | (ch >> 10), 0xDC00 | (ch & 0x3FF));
|
||||
}
|
||||
}
|
||||
return str;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user