|
| 1 | +/// Database bindings for sql.js (SQLite compiled to WebAssembly). |
| 2 | +library; |
| 3 | + |
| 4 | +import 'dart:js_interop'; |
| 5 | +import 'dart:js_interop_unsafe'; |
| 6 | + |
| 7 | +import 'package:dart_node_core/dart_node_core.dart'; |
| 8 | +import 'package:dart_node_sql_js/src/statement.dart'; |
| 9 | +import 'package:nadz/nadz.dart'; |
| 10 | + |
| 11 | +/// Pre-initialized sql.js runtime. |
| 12 | +/// |
| 13 | +/// Obtained from [initializeSqlJs], passed to [openDatabase]. |
| 14 | +typedef SqlJsRuntime = ({JSFunction databaseConstructor}); |
| 15 | + |
| 16 | +/// Initialize sql.js. Call once at startup. |
| 17 | +/// |
| 18 | +/// Returns a [SqlJsRuntime] that must be passed to [openDatabase]. |
| 19 | +Future<Result<SqlJsRuntime, String>> initializeSqlJs() async { |
| 20 | + try { |
| 21 | + final initFn = requireModule('sql.js') as JSFunction; |
| 22 | + final promise = initFn.callAsFunction(null) as JSPromise<JSAny?>; |
| 23 | + final sqlJs = await promise.toDart as JSObject; |
| 24 | + final dbConstructor = sqlJs['Database'] as JSFunction; |
| 25 | + return Success((databaseConstructor: dbConstructor)); |
| 26 | + } catch (e) { |
| 27 | + return Error('Failed to initialize sql.js: $e'); |
| 28 | + } |
| 29 | +} |
| 30 | + |
| 31 | +/// A sql.js database connection. |
| 32 | +typedef Database = ({ |
| 33 | + /// Prepare a SQL statement. |
| 34 | + Result<Statement, String> Function(String sql) prepare, |
| 35 | + |
| 36 | + /// Execute raw SQL (no results). |
| 37 | + Result<void, String> Function(String sql) exec, |
| 38 | + |
| 39 | + /// Close the database. |
| 40 | + Result<void, String> Function() close, |
| 41 | + |
| 42 | + /// Set a pragma value. |
| 43 | + Result<void, String> Function(String pragmaValue) pragma, |
| 44 | + |
| 45 | + /// Check if database is open. |
| 46 | + bool Function() isOpen, |
| 47 | +}); |
| 48 | + |
| 49 | +/// Open a sql.js database. |
| 50 | +/// |
| 51 | +/// If [path] points to an existing file, loads it. |
| 52 | +/// Otherwise creates a new empty database. |
| 53 | +/// Auto-persists to disk after write operations. |
| 54 | +Result<Database, String> openDatabase( |
| 55 | + String path, { |
| 56 | + required SqlJsRuntime sqlJs, |
| 57 | +}) { |
| 58 | + try { |
| 59 | + final fs = requireModule('fs') as JSObject; |
| 60 | + final existsSyncFn = fs['existsSync'] as JSFunction; |
| 61 | + final readFileSyncFn = fs['readFileSync'] as JSFunction; |
| 62 | + |
| 63 | + JSObject jsDb; |
| 64 | + final fileExists = |
| 65 | + (existsSyncFn.callAsFunction(fs, path.toJS) as JSBoolean).toDart; |
| 66 | + |
| 67 | + if (fileExists) { |
| 68 | + final buffer = readFileSyncFn.callAsFunction(fs, path.toJS); |
| 69 | + jsDb = sqlJs.databaseConstructor |
| 70 | + .callAsConstructor<JSObject>(buffer); |
| 71 | + } else { |
| 72 | + jsDb = sqlJs.databaseConstructor.callAsConstructor<JSObject>(); |
| 73 | + } |
| 74 | + |
| 75 | + // sql.js is in-memory; WAL and busy_timeout do not apply. |
| 76 | + // Enable foreign keys for referential integrity. |
| 77 | + _dbRun(jsDb, 'PRAGMA foreign_keys = ON'); |
| 78 | + |
| 79 | + return Success(_createDatabase(jsDb, path, fs)); |
| 80 | + } catch (e) { |
| 81 | + return Error('Failed to open database: $e'); |
| 82 | + } |
| 83 | +} |
| 84 | + |
| 85 | +/// Run a SQL statement directly on the JS database object. |
| 86 | +void _dbRun(JSObject jsDb, String sql) { |
| 87 | + (jsDb['run'] as JSFunction).callAsFunction(jsDb, sql.toJS); |
| 88 | +} |
| 89 | + |
| 90 | +/// Persist the in-memory database to disk. |
| 91 | +void _save(JSObject jsDb, String path, JSObject fs) { |
| 92 | + final exportFn = jsDb['export'] as JSFunction; |
| 93 | + final data = exportFn.callAsFunction(jsDb); |
| 94 | + |
| 95 | + final bufferClass = requireModule('buffer') as JSObject; |
| 96 | + final bufferFrom = |
| 97 | + (bufferClass['Buffer'] as JSObject)['from'] as JSFunction; |
| 98 | + final nodeBuffer = bufferFrom.callAsFunction(null, data); |
| 99 | + |
| 100 | + final writeFileSyncFn = fs['writeFileSync'] as JSFunction; |
| 101 | + writeFileSyncFn.callAsFunction(fs, path.toJS, nodeBuffer); |
| 102 | +} |
| 103 | + |
| 104 | +Database _createDatabase(JSObject jsDb, String path, JSObject fs) { |
| 105 | + var open = true; |
| 106 | + |
| 107 | + return ( |
| 108 | + prepare: (sql) => _dbPrepare(jsDb, sql, path, fs), |
| 109 | + exec: (sql) => _dbExec(jsDb, sql, path, fs), |
| 110 | + close: () => _dbClose(jsDb, path, fs, () => open = false), |
| 111 | + pragma: (pragmaValue) => _dbPragma(jsDb, pragmaValue), |
| 112 | + isOpen: () => open, |
| 113 | + ); |
| 114 | +} |
| 115 | + |
| 116 | +Result<Statement, String> _dbPrepare( |
| 117 | + JSObject jsDb, |
| 118 | + String sql, |
| 119 | + String path, |
| 120 | + JSObject fs, |
| 121 | +) { |
| 122 | + try { |
| 123 | + final prepareFn = jsDb['prepare'] as JSFunction; |
| 124 | + final jsStmt = |
| 125 | + prepareFn.callAsFunction(jsDb, sql.toJS) as JSObject; |
| 126 | + return Success( |
| 127 | + createStatement(jsStmt, jsDb, onWrite: () => _save(jsDb, path, fs)), |
| 128 | + ); |
| 129 | + } catch (e) { |
| 130 | + return Error('Failed to prepare statement: $e'); |
| 131 | + } |
| 132 | +} |
| 133 | + |
| 134 | +Result<void, String> _dbExec( |
| 135 | + JSObject jsDb, |
| 136 | + String sql, |
| 137 | + String path, |
| 138 | + JSObject fs, |
| 139 | +) { |
| 140 | + try { |
| 141 | + // sql.js exec() handles multiple statements separated by ; |
| 142 | + (jsDb['exec'] as JSFunction).callAsFunction(jsDb, sql.toJS); |
| 143 | + _save(jsDb, path, fs); |
| 144 | + return const Success(null); |
| 145 | + } catch (e) { |
| 146 | + return Error('Failed to exec: $e'); |
| 147 | + } |
| 148 | +} |
| 149 | + |
| 150 | +Result<void, String> _dbClose( |
| 151 | + JSObject jsDb, |
| 152 | + String path, |
| 153 | + JSObject fs, |
| 154 | + void Function() markClosed, |
| 155 | +) { |
| 156 | + try { |
| 157 | + _save(jsDb, path, fs); |
| 158 | + (jsDb['close'] as JSFunction).callAsFunction(jsDb); |
| 159 | + markClosed(); |
| 160 | + return const Success(null); |
| 161 | + } catch (e) { |
| 162 | + return Error('Failed to close database: $e'); |
| 163 | + } |
| 164 | +} |
| 165 | + |
| 166 | +Result<void, String> _dbPragma(JSObject jsDb, String pragmaValue) { |
| 167 | + try { |
| 168 | + _dbRun(jsDb, 'PRAGMA $pragmaValue'); |
| 169 | + return const Success(null); |
| 170 | + } catch (e) { |
| 171 | + return Error('Failed to set pragma: $e'); |
| 172 | + } |
| 173 | +} |
0 commit comments