UNPKG

@worker-tools/deno-kv-storage

Version:

An implementation of the StorageArea (1,2,3) interface for Deno with an extensible system for supporting various database backends.

402 lines 14.7 kB
import instantiate from "../build/sqlite.js"; import { setArr, setStr } from "./wasm.js"; import { OpenFlags, Status, Values } from "./constants.js"; import SqliteError from "./error.js"; import { Empty, Rows } from "./rows.js"; export class DB { /** * DB * * Create a new database. The passed * path will be opened with read/ write * permissions and created if it does not * already exist. * * The default opens an in-memory database. */ constructor(path = ":memory:", options = {}) { Object.defineProperty(this, "_wasm", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "_open", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "_statements", { enumerable: true, configurable: true, writable: true, value: void 0 }); this._wasm = instantiate().exports; this._open = false; this._statements = new Set(); // 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 let status; setStr(this._wasm, path, (ptr) => { status = this._wasm.open(ptr, flags); }); if (status !== Status.SqliteOk) { throw new SqliteError(this._wasm, status); } this._open = true; } /** * DB.query * * Run a query against the database. The query * can contain placeholder parameters, which * are bound to the values passed in 'values'. * * db.query("SELECT name, email FROM users WHERE subscribed = ? AND list LIKE ?", [true, listName]); * * This supports positional and named parameters. * Positional parameters can be set by passing an * array for values. Named parameters can be set * by passing an object for values. * * While they can be mixed in principle, this is * not recommended. * * | Parameter | Values | * |---------------|-------------------------| * | `?NNN` or `?` | NNN-th value in array | * | `:AAAA` | value `AAAA` or `:AAAA` | * | `@AAAA` | value `@AAAA` | * | `$AAAA` | value `$AAAA` | * * (see https://www.sqlite.org/lang_expr.html) * * Values may only be of the following * types and are converted as follows: * * | JS in | SQL type | JS 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 to a given parameter, * SQLite will default to NULL. * * If a `bigint` is bound, it is converted to a * signed 64 big integer, which may not be lossless. * 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. * * This always returns an iterable Rows object. * As a special case, if the query has no rows * to return this returns the Empty row (which * is also iterable, but has zero entries). * * !> Any returned Rows object needs to be fully * iterated over or discarded by calling * `.return()` or closing the iterator. * * !> To prevent SQL injections, sql queries should * never be obtained via string interpolation. Instead, * dynamic parameters should be bound using query parameters: * * db.query("SELECT name FROM users WHERE id = ?", [id]); // GOOD * db.query(`SELECT name FROM users WHERE id = ${id}`); // BAD: Potential SQL injection! */ query(sql, values) { const stmt = this.prepareStmt(sql); if (values != null) { try { this.bindStmt(stmt, values); } catch (err) { this._wasm.finalize(stmt); throw err; } } // Step once to handle case where result is empty const status = this._wasm.step(stmt); switch (status) { case Status.SqliteDone: this._wasm.finalize(stmt); return Empty; case Status.SqliteRow: this._statements.add(stmt); return new Rows(this._wasm, stmt, this._statements); default: this._wasm.finalize(stmt); throw new SqliteError(this._wasm, status); } } /** * DB.prepareQuery * * This is similar to `query()`, with the difference * that the returned function can be called multiple * times (with different values to bind each time). * * Using a prepared query instead of `query()` will * improve performance if the query is issued a lot, * e.g. when writing a web server, the queries used * by the server could be prepared once and then used * through it's runtime. * * A prepared query must be finalized when it is no * longer in used by calling `query.finalize()`. So * the complete lifetime of a query would look like * this: * * // once * const query = db.prepareQuery("INSERT INTO messages (message, author) VALUES (?, ?)"); * // many times * query([messageValueOne, authorValueOne]); * query([messageValueTwo, authorValueTwo]); * // ... * // once * query.finalize(); */ prepareQuery(sql) { const stmt = this.prepareStmt(sql); let lastRows = null; let finalized = false; this._statements.add(stmt); const query = (values) => { if (finalized) { throw new SqliteError("Query already finalized."); } // Mark previous rows object as done, such that // they won't intermingle and produce wired // results. if (lastRows != null) { lastRows.return(); lastRows = null; } this._wasm.reset(stmt); this._wasm.clear_bindings(stmt); if (values != null) { this.bindStmt(stmt, values); } // Step once to handle case where result is empty const status = this._wasm.step(stmt); switch (status) { case Status.SqliteDone: return Empty; case Status.SqliteRow: lastRows = new Rows(this._wasm, stmt); // don't pass statement set for cleanup return lastRows; default: throw new SqliteError(this._wasm, status); } }; query.finalize = () => { if (!finalized) { finalized = true; this._wasm.finalize(stmt); this._statements.delete(stmt); } }; query.columns = () => { if (finalized) { throw new SqliteError("Unable to return column names of finalized query."); } // TODO(dyedgreen): This is a bit of a hack, but moving everything // into a Statement class also doesn't feel quite right... return (new Rows(this._wasm, stmt)).columns(); }; return query; } prepareStmt(sql) { if (!this._open) { throw new SqliteError("Database was closed."); } let stmt = Values.Null; setStr(this._wasm, sql, (ptr) => { stmt = this._wasm.prepare(ptr); }); if (stmt === Values.Null) { throw new SqliteError(this._wasm); } return stmt; } bindStmt(stmt, values) { // Prepare parameter array let parameters = []; if (Array.isArray(values)) { parameters = values; } else if (typeof values === "object") { // Resolve parameter index for named values for (const key of Object.keys(values)) { let idx = Values.Error; // Prepend ':' to name, if it does not have a special starting character let name = key; if (name[0] !== ":" && name[0] !== "@" && name[0] !== "$") { name = `:${name}`; } setStr(this._wasm, name, (ptr) => { idx = this._wasm.bind_parameter_index(stmt, ptr); }); if (idx === Values.Error) { throw new SqliteError(`No parameter named '${name}'.`); } parameters[idx - 1] = values[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(stmt, i + 1, value); } else { status = this._wasm.bind_double(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(stmt, i + 1, sign, upper, lower); } break; case "string": setStr(this._wasm, value, (ptr) => { status = this._wasm.bind_text(stmt, i + 1, ptr); }); break; default: if (value instanceof Date) { // Dates are allowed and bound to TEXT, formatted `YYYY-MM-DDTHH:MM:SS.SSSZ` setStr(this._wasm, value.toISOString(), (ptr) => { status = this._wasm.bind_text(stmt, i + 1, ptr); }); } else if (value instanceof Uint8Array) { // Uint8Arrays are allowed and bound to BLOB const size = value.length; setArr(this._wasm, value, (ptr) => { status = this._wasm.bind_blob(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(stmt, i + 1); } else { throw new SqliteError(`Can not bind ${typeof value}.`); } break; } if (status !== Status.SqliteOk) { throw new SqliteError(this._wasm, status); } } } /** * DB.close * * Close database handle. This must be called if * DB is no longer used, to avoid leaking file * resources. * * If force is specified, any on-going transactions * will be closed. */ 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; } /** * DB.lastInsertRowId * * Get last inserted row id. This corresponds to * the SQLite function `sqlite3_last_insert_rowid`. * * By default, it will return 0 if there is no row * inserted yet. */ get lastInsertRowId() { return this._wasm.last_insert_rowid(); } /** * DB.changes * * 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() { return this._wasm.changes(); } /** * DB.totalChanges * * 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() { return this._wasm.total_changes(); } } //# sourceMappingURL=db.js.map