sqlite.js (3102B)
1 // src/lib/db/sqlite.js - sqlite driver built on node:sqlite. 2 // 3 // node:sqlite is synchronous. The methods are async so callers use one 4 // interface for every driver. 5 6 import { DatabaseSync } from 'node:sqlite'; 7 import { compileCached, bindParams } from './query.js'; 8 9 export function openSqlite(cfg) { 10 const db = new DatabaseSync(cfg.database.path); 11 12 // WAL keeps readers from blocking the writer. 13 db.exec('PRAGMA journal_mode = WAL'); 14 db.exec('PRAGMA foreign_keys = ON'); 15 db.exec('PRAGMA busy_timeout = 5000'); 16 db.exec('PRAGMA synchronous = NORMAL'); 17 18 const statements = new Map(); 19 20 function prepare(sql) { 21 const compiled = compileCached(sql, 'sqlite'); 22 let stmt = statements.get(compiled.sql); 23 if (!stmt) { 24 stmt = db.prepare(compiled.sql); 25 statements.set(compiled.sql, stmt); 26 } 27 return { stmt, keys: compiled.keys }; 28 } 29 30 function fail(e, sql) { 31 const err = new Error(`sqlite: ${e.message}\n sql: ${sql.trim().split('\n')[0]}`); 32 err.cause = e; 33 err.code = e.code; 34 throw err; 35 } 36 37 // node:sqlite returns null-prototype objects. 38 const toPlain = (row) => (row === undefined ? undefined : { ...row }); 39 40 const api = { 41 dialect: 'sqlite', 42 43 async all(sql, params = {}) { 44 const { stmt, keys } = prepare(sql); 45 const values = bindParams(keys, params, sql); 46 try { 47 return stmt.all(...values).map(toPlain); 48 } catch (e) { 49 fail(e, sql); 50 } 51 }, 52 53 async get(sql, params = {}) { 54 const { stmt, keys } = prepare(sql); 55 const values = bindParams(keys, params, sql); 56 try { 57 return toPlain(stmt.get(...values)); 58 } catch (e) { 59 fail(e, sql); 60 } 61 }, 62 63 async run(sql, params = {}) { 64 const { stmt, keys } = prepare(sql); 65 const values = bindParams(keys, params, sql); 66 try { 67 const r = stmt.run(...values); 68 return { 69 changes: Number(r.changes), 70 lastInsertId: r.lastInsertRowid === undefined ? null : Number(r.lastInsertRowid), 71 }; 72 } catch (e) { 73 fail(e, sql); 74 } 75 }, 76 77 // Raw DDL, no bind markers are interpreted. 78 async exec(sql) { 79 try { 80 db.exec(sql); 81 } catch (e) { 82 fail(e, sql); 83 } 84 }, 85 86 async close() { 87 statements.clear(); 88 db.close(); 89 }, 90 }; 91 92 // Nesting uses savepoints so that a helper which opens a transaction can be 93 // composed inside a larger one. 94 let depth = 0; 95 api.transaction = async function transaction(fn) { 96 const name = `sp_${depth}`; 97 if (depth === 0) db.exec('BEGIN IMMEDIATE'); 98 else db.exec(`SAVEPOINT ${name}`); 99 depth += 1; 100 try { 101 const result = await fn(api); 102 depth -= 1; 103 if (depth === 0) db.exec('COMMIT'); 104 else db.exec(`RELEASE ${name}`); 105 return result; 106 } catch (e) { 107 depth -= 1; 108 try { 109 if (depth === 0) db.exec('ROLLBACK'); 110 else db.exec(`ROLLBACK TO ${name}`); 111 } catch { 112 // The original error is more useful than a rollback failure. 113 } 114 throw e; 115 } 116 }; 117 118 return api; 119 }