UNPKG

@livestore/sqlite-wasm

Version:

193 lines • 8.61 kB
import { SqliteDbHelper, SqliteError } from '@livestore/common'; import * as SqliteConstants from '@livestore/wa-sqlite/src/sqlite-constants.js'; import { makeInMemoryDb } from './in-memory-vfs.js'; export const makeSqliteDb = ({ sqlite3, metadata, }) => { const preparedStmts = []; const { dbPointer } = metadata; let isClosed = false; const sqliteDb = { _tag: 'SqliteDb', metadata, prepare: (queryStr) => { try { const stmts = sqlite3.statements(dbPointer, queryStr.trim(), { unscoped: true }); let isFinalized = false; const preparedStmt = { execute: (bindValues, options) => { for (const stmt of stmts) { if (bindValues !== undefined && Object.keys(bindValues).length > 0) { sqlite3.bind_collection(stmt, bindValues); } try { sqlite3.step(stmt); } finally { if (options?.onRowsChanged) { options.onRowsChanged(sqlite3.changes(dbPointer)); } sqlite3.reset(stmt); // Reset is needed for next execution } } }, select: (bindValues) => { if (stmts.length !== 1) { throw new SqliteError({ query: { bindValues, sql: queryStr }, code: -1, cause: 'Expected only one statement when using `select`', }); } const stmt = stmts[0]; if (bindValues !== undefined && Object.keys(bindValues).length > 0) { sqlite3.bind_collection(stmt, bindValues); } const results = []; try { // NOTE `column_names` only works for `SELECT` statements, ignoring other statements for now let columns = undefined; try { columns = sqlite3.column_names(stmt); // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (_e) { } while (sqlite3.step(stmt) === SqliteConstants.SQLITE_ROW) { if (columns !== undefined) { const obj = {}; for (let i = 0; i < columns.length; i++) { obj[columns[i]] = sqlite3.column(stmt, i); } results.push(obj); } } } catch (e) { throw new SqliteError({ query: { bindValues, sql: queryStr }, code: e.code, cause: e, }); } finally { // reset the cached statement so we can use it again in the future sqlite3.reset(stmt); } return results; }, finalize: () => { // Avoid double finalization which leads to a crash if (isFinalized) { return; } isFinalized = true; for (const stmt of stmts) { sqlite3.finalize(stmt); } }, sql: queryStr, }; preparedStmts.push(preparedStmt); return preparedStmt; } catch (e) { throw new SqliteError({ query: { sql: queryStr, bindValues: {} }, code: e.code, cause: e, }); } }, export: () => sqlite3.serialize(dbPointer, 'main'), execute: SqliteDbHelper.makeExecute((queryStr, bindValues, options) => { const stmt = sqliteDb.prepare(queryStr); stmt.execute(bindValues, options); stmt.finalize(); }), select: SqliteDbHelper.makeSelect((queryStr, bindValues) => { const stmt = sqliteDb.prepare(queryStr); const results = stmt.select(bindValues); stmt.finalize(); return results; }), destroy: () => { sqliteDb.close(); metadata.deleteDb(); // if (metadata._tag === 'opfs') { // metadata.vfs.resetAccessHandle(metadata.fileName) // } }, close: () => { if (isClosed) { return; } for (const stmt of preparedStmts) { stmt.finalize(); } sqlite3.close(dbPointer); isClosed = true; }, import: (source) => { // https://www.sqlite.org/c3ref/c_deserialize_freeonclose.html // #define SQLITE_DESERIALIZE_FREEONCLOSE 1 /* Call sqlite3_free() on close */ // #define SQLITE_DESERIALIZE_RESIZEABLE 2 /* Resize using sqlite3_realloc64() */ // #define SQLITE_DESERIALIZE_READONLY 4 /* Database is read-only */ const FREE_ON_CLOSE = 1; const RESIZEABLE = 2; // NOTE in case we'll have a future use-case where we need a read-only database, we can reuse this code below // if (readOnly === true) { // sqlite3.deserialize(db, 'main', bytes, bytes.length, bytes.length, FREE_ON_CLOSE | RESIZEABLE) // } else { if (source instanceof Uint8Array) { const tmpDb = makeInMemoryDb(sqlite3); // TODO find a way to do this more efficiently with sqlite to avoid either of the deserialize + backup call // Maybe this can be done via the VFS API sqlite3.deserialize(tmpDb.dbPointer, 'main', source, source.length, source.length, FREE_ON_CLOSE | RESIZEABLE); sqlite3.backup(dbPointer, 'main', tmpDb.dbPointer, 'main'); sqlite3.close(tmpDb.dbPointer); } else { sqlite3.backup(dbPointer, 'main', source.metadata.dbPointer, 'main'); } metadata.configureDb(sqliteDb); }, session: () => { const sessionPointer = sqlite3.session_create(dbPointer, 'main'); sqlite3.session_attach(sessionPointer, null); return { changeset: () => { const res = sqlite3.session_changeset(sessionPointer); return res.changeset ?? undefined; }, finish: () => { sqlite3.session_delete(sessionPointer); }, }; }, makeChangeset: (data) => { const changeset = { invert: () => { const inverted = sqlite3.changeset_invert(data); return sqliteDb.makeChangeset(inverted); }, apply: () => { try { sqlite3.changeset_apply(dbPointer, data); // @ts-expect-error data should be garbage collected after use // biome-ignore lint/style/noParameterAssign: data = undefined; } catch (cause) { throw new SqliteError({ code: cause.code ?? -1, cause, note: `Failed calling makeChangeset.apply`, }); } }, }; return changeset; }, }; metadata.configureDb(sqliteDb); return sqliteDb; }; //# sourceMappingURL=make-sqlite-db.js.map