experiments in a post-browser web
1/**
2 * better-sqlite3 Adapter Implementation
3 *
4 * Wraps the better-sqlite3 library to conform to the SqlAdapter interface.
5 * Provides statement caching for improved performance.
6 */
7
8const Database = require("better-sqlite3");
9
10/**
11 * @typedef {import('./types').SqlAdapter} SqlAdapter
12 * @typedef {import('./types').SqlAdapterFactory} SqlAdapterFactory
13 * @typedef {import('./types').RunResult} RunResult
14 */
15
16/**
17 * @implements {SqlAdapter}
18 */
19class BetterSqlite3Adapter {
20 /**
21 * @param {import('better-sqlite3').Database} db
22 */
23 constructor(db) {
24 /** @private */
25 this.db = db;
26 /** @private @type {Map<string, import('better-sqlite3').Statement>} */
27 this.stmtCache = new Map();
28 }
29
30 /**
31 * @param {string} sql
32 */
33 exec(sql) {
34 this.db.exec(sql);
35 }
36
37 /**
38 * @param {string} sql
39 * @param {unknown[]} [params=[]]
40 * @returns {RunResult}
41 */
42 run(sql, params = []) {
43 const stmt = this.getOrPrepare(sql);
44 const result = stmt.run(...params);
45 return {
46 changes: result.changes,
47 lastInsertRowid: Number(result.lastInsertRowid),
48 };
49 }
50
51 /**
52 * @template T
53 * @param {string} sql
54 * @param {unknown[]} [params=[]]
55 * @returns {T|null}
56 */
57 get(sql, params = []) {
58 const stmt = this.getOrPrepare(sql);
59 return stmt.get(...params) ?? null;
60 }
61
62 /**
63 * @template T
64 * @param {string} sql
65 * @param {unknown[]} [params=[]]
66 * @returns {T[]}
67 */
68 all(sql, params = []) {
69 const stmt = this.getOrPrepare(sql);
70 return stmt.all(...params);
71 }
72
73 /**
74 * @template T
75 * @param {function(): T} fn
76 * @returns {T}
77 */
78 transaction(fn) {
79 return this.db.transaction(fn)();
80 }
81
82 close() {
83 this.stmtCache.clear();
84 this.db.close();
85 }
86
87 /**
88 * @private
89 * @param {string} sql
90 * @returns {import('better-sqlite3').Statement}
91 */
92 getOrPrepare(sql) {
93 let stmt = this.stmtCache.get(sql);
94 if (!stmt) {
95 stmt = this.db.prepare(sql);
96 this.stmtCache.set(sql, stmt);
97 }
98 return stmt;
99 }
100}
101
102/**
103 * @type {SqlAdapterFactory}
104 */
105const factory = {
106 /**
107 * @param {string} path
108 * @param {{ readonly?: boolean }} [options]
109 * @returns {SqlAdapter}
110 */
111 open(path, options) {
112 const dbOptions = options?.readonly ? { readonly: true } : {};
113 const db = new Database(path, dbOptions);
114 return new BetterSqlite3Adapter(db);
115 },
116
117 /**
118 * @param {SqlAdapter} adapter
119 */
120 init(adapter) {
121 adapter.exec("PRAGMA journal_mode = WAL");
122 },
123};
124
125module.exports = { factory, BetterSqlite3Adapter };