@livestore/sqlite-wasm
Version:
193 lines • 8.61 kB
JavaScript
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