UNPKG

sqlocal

Version:

SQLocal makes it easy to run SQLite3 in the browser, backed by the origin private file system.

246 lines 8.36 kB
import { normalizeDatabaseFile } from '../lib/normalize-database-file.js'; /** * A SQLocal driver that implements the interface needed for * interacting with SQLite databases in memory. */ export class SQLiteMemoryDriver { constructor(sqlite3InitModule) { Object.defineProperty(this, "sqlite3InitModule", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "sqlite3", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "db", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "config", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "pointers", { enumerable: true, configurable: true, writable: true, value: [] }); Object.defineProperty(this, "writeCallbacks", { enumerable: true, configurable: true, writable: true, value: new Set() }); Object.defineProperty(this, "storageType", { enumerable: true, configurable: true, writable: true, value: 'memory' }); this.sqlite3InitModule = sqlite3InitModule; } async init(config) { const { databasePath } = config; const flags = this.getFlags(config); if (!this.sqlite3InitModule) { const { default: sqlite3InitModule } = await import('@sqlite.org/sqlite-wasm'); this.sqlite3InitModule = sqlite3InitModule; } if (!this.sqlite3) { this.sqlite3 = await this.sqlite3InitModule(); } if (this.db) { await this.destroy(); } this.db = new this.sqlite3.oo1.DB(databasePath, flags); this.config = config; this.initWriteHook(); } onWrite(callback) { this.writeCallbacks.add(callback); return () => { this.writeCallbacks.delete(callback); }; } async exec(statement) { if (!this.db) throw new Error('Driver not initialized'); return this.execOnDb(this.db, statement); } async execBatch(statements, method = 'transaction') { if (!this.db) throw new Error('Driver not initialized'); const results = []; this.db[method]((tx) => { const prepared = new Map(); try { for (let statement of statements) { let stmt = prepared.get(statement.sql); if (!stmt) { const newStmt = tx.prepare(statement.sql); prepared.set(statement.sql, newStmt); stmt = newStmt; } if (statement.params?.length) { stmt.bind(statement.params); } let columns = []; let rows = []; while (stmt.step()) { columns = stmt.getColumnNames([]); rows.push(stmt.get([])); } results.push({ columns, rows }); stmt.reset(); } } finally { prepared.forEach((stmt) => { stmt.finalize(); }); } }); return results; } async isDatabasePersisted() { return false; } async getDatabaseSizeBytes() { const sizeResult = await this.exec({ sql: `SELECT page_count * page_size AS size FROM pragma_page_count(), pragma_page_size()`, method: 'get', }); const size = sizeResult?.rows?.[0]; if (typeof size !== 'number') { throw new Error('Failed to query database size'); } return size; } async createFunction(fn) { if (!this.db) throw new Error('Driver not initialized'); switch (fn.type) { case 'callback': case 'scalar': this.db.createFunction({ name: fn.name, xFunc: (_, ...args) => fn.func(...args), arity: -1, }); break; case 'aggregate': this.db.createFunction({ name: fn.name, xStep: (_, ...args) => fn.func.step(...args), xFinal: (_, ...args) => fn.func.final(...args), arity: -1, }); break; case 'window': this.db.createFunction({ name: fn.name, xStep: (_, ...args) => fn.func.step(...args), xValue: (_, ...args) => fn.func.value(...args), xInverse: (_, ...args) => fn.func.inverse(...args), xFinal: (_, ...args) => fn.func.final(...args), arity: -1, }); break; } } async import(database) { if (!this.sqlite3 || !this.db || !this.config) { throw new Error('Driver not initialized'); } const data = await normalizeDatabaseFile(database, 'buffer'); const dataPointer = this.sqlite3.wasm.allocFromTypedArray(data); this.pointers.push(dataPointer); const resultCode = this.sqlite3.capi.sqlite3_deserialize(this.db, 'main', dataPointer, data.byteLength, data.byteLength, this.config.readOnly ? this.sqlite3.capi.SQLITE_DESERIALIZE_READONLY : this.sqlite3.capi.SQLITE_DESERIALIZE_RESIZEABLE); this.db.checkRc(resultCode); } async export() { if (!this.sqlite3 || !this.db) { throw new Error('Driver not initialized'); } return { name: 'database.sqlite3', data: this.sqlite3.capi.sqlite3_js_db_export(this.db), }; } async clear() { } async destroy() { this.closeDb(); this.pointers.forEach((pointer) => this.sqlite3?.wasm.dealloc(pointer)); this.pointers = []; this.writeCallbacks.clear(); } getFlags(config) { const { readOnly, verbose } = config; const parts = [readOnly === true ? 'r' : 'cw', verbose === true ? 't' : '']; return parts.join(''); } execOnDb(db, statement) { const changesBefore = db.changes(true, true); const statementData = { rows: [], columns: [], }; const rows = db.exec({ sql: statement.sql, bind: statement.params, returnValue: 'resultRows', rowMode: 'array', columnNames: statementData.columns, }); switch (statement.method) { case 'run': break; case 'get': statementData.rows = rows[0] ?? []; break; case 'all': default: statementData.rows = rows; break; } statementData.numAffectedRows = db.changes(true, true) - changesBefore; return statementData; } initWriteHook() { if (!this.config?.reactive) return; if (!this.sqlite3 || !this.db) { throw new Error('Driver not initialized'); } const opMap = { [this.sqlite3.capi.SQLITE_INSERT]: 'insert', [this.sqlite3.capi.SQLITE_UPDATE]: 'update', [this.sqlite3.capi.SQLITE_DELETE]: 'delete', }; this.sqlite3.capi.sqlite3_update_hook(this.db, (_ctx, opId, _db, table, rowid) => { this.writeCallbacks.forEach((cb) => { cb({ table, rowid, operation: opMap[opId] }); }); }, 0); } closeDb() { if (this.db) { this.db.close(); this.db = undefined; } } } //# sourceMappingURL=sqlite-memory-driver.js.map