orez 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/admin/server.d.ts.map +1 -1
- package/dist/admin/server.js +1 -0
- package/dist/admin/server.js.map +1 -1
- package/dist/admin/ui.d.ts.map +1 -1
- package/dist/admin/ui.js +12 -0
- package/dist/admin/ui.js.map +1 -1
- package/dist/cli.js +5 -5
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +44 -64
- package/dist/index.js.map +1 -1
- package/dist/log.d.ts +1 -1
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +2 -2
- package/dist/log.js.map +1 -1
- package/dist/sqlite-mode/apply-mode.d.ts +49 -0
- package/dist/sqlite-mode/apply-mode.d.ts.map +1 -0
- package/dist/sqlite-mode/apply-mode.js +190 -0
- package/dist/sqlite-mode/apply-mode.js.map +1 -0
- package/dist/sqlite-mode/index.d.ts +14 -0
- package/dist/sqlite-mode/index.d.ts.map +1 -0
- package/dist/sqlite-mode/index.js +14 -0
- package/dist/sqlite-mode/index.js.map +1 -0
- package/dist/sqlite-mode/resolve-mode.d.ts +24 -0
- package/dist/sqlite-mode/resolve-mode.d.ts.map +1 -0
- package/dist/sqlite-mode/resolve-mode.js +61 -0
- package/dist/sqlite-mode/resolve-mode.js.map +1 -0
- package/dist/sqlite-mode/shim-template.d.ts +20 -0
- package/dist/sqlite-mode/shim-template.d.ts.map +1 -0
- package/dist/sqlite-mode/shim-template.js +144 -0
- package/dist/sqlite-mode/shim-template.js.map +1 -0
- package/dist/sqlite-mode/types.d.ts +16 -0
- package/dist/sqlite-mode/types.d.ts.map +1 -0
- package/dist/sqlite-mode/types.js +17 -0
- package/dist/sqlite-mode/types.js.map +1 -0
- package/package.json +3 -2
- package/src/admin/server.ts +1 -0
- package/src/admin/ui.ts +12 -0
- package/src/cli.ts +5 -5
- package/src/index.ts +79 -63
- package/src/log.ts +2 -2
- package/src/shim/hooks.mjs +42 -18
- package/src/sqlite-mode/apply-mode.ts +224 -0
- package/src/sqlite-mode/index.ts +14 -0
- package/src/sqlite-mode/resolve-mode.ts +71 -0
- package/src/sqlite-mode/shim-template.ts +158 -0
- package/src/sqlite-mode/sqlite-mode.test.ts +421 -0
- package/src/sqlite-mode/types.ts +29 -0
package/src/shim/hooks.mjs
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
// esm loader hooks — intercept @rocicorp/zero-sqlite3 with bedrock-sqlite wasm.
|
|
2
|
+
//
|
|
3
|
+
// NOTE: this file is currently UNUSED. orez uses in-place CJS patching via
|
|
4
|
+
// src/sqlite-mode/apply-mode.ts instead. this file is kept for potential future
|
|
5
|
+
// use with Node.js ESM loader hooks (--import flag).
|
|
6
|
+
//
|
|
2
7
|
// __BEDROCK_PATH__ is replaced at runtime by orez before writing to tmpdir.
|
|
3
8
|
|
|
4
9
|
const SHIM_URL = 'orez-sqlite-shim://shim'
|
|
5
10
|
const BEDROCK_PATH = '__BEDROCK_PATH__'
|
|
11
|
+
// __JOURNAL_MODE__ would be replaced at runtime (delete for wasm, wal2 for native)
|
|
12
|
+
const JOURNAL_MODE = '__JOURNAL_MODE__'
|
|
6
13
|
|
|
7
14
|
export function resolve(specifier, context, nextResolve) {
|
|
8
15
|
if (
|
|
@@ -16,10 +23,18 @@ export function resolve(specifier, context, nextResolve) {
|
|
|
16
23
|
|
|
17
24
|
export function load(url, context, nextLoad) {
|
|
18
25
|
if (url === SHIM_URL) {
|
|
26
|
+
// journal mode differs between native and wasm:
|
|
27
|
+
// - native: wal2 for better concurrency
|
|
28
|
+
// - wasm: delete for compatibility (wal2 can corrupt wasm vfs)
|
|
29
|
+
const journalMode = JOURNAL_MODE === '__JOURNAL_MODE__' ? 'delete' : JOURNAL_MODE
|
|
30
|
+
|
|
19
31
|
return {
|
|
20
32
|
format: 'module',
|
|
21
33
|
shortCircuit: true,
|
|
22
34
|
source: `
|
|
35
|
+
// orez sqlite shim - wraps bedrock-sqlite for zero-cache compatibility
|
|
36
|
+
// journal_mode: ${journalMode}
|
|
37
|
+
|
|
23
38
|
// catch uncaught exceptions from bedrock-sqlite wasm clearly
|
|
24
39
|
process.on('uncaughtException', (err) => {
|
|
25
40
|
console.error('[orez-shim] UNCAUGHT EXCEPTION:', err?.message || err);
|
|
@@ -27,49 +42,55 @@ process.on('uncaughtException', (err) => {
|
|
|
27
42
|
console.error('[orez-shim] stack:', err?.stack?.split('\\n').slice(0, 5).join('\\n'));
|
|
28
43
|
process.exit(1);
|
|
29
44
|
});
|
|
45
|
+
|
|
30
46
|
import { createRequire } from 'node:module';
|
|
31
47
|
const require = createRequire('${BEDROCK_PATH}');
|
|
32
48
|
const mod = require('${BEDROCK_PATH}');
|
|
33
49
|
const OrigDatabase = mod.Database;
|
|
34
50
|
const SqliteError = mod.SqliteError;
|
|
51
|
+
|
|
35
52
|
function Database(...args) {
|
|
36
53
|
const db = new OrigDatabase(...args);
|
|
37
|
-
try {
|
|
54
|
+
try {
|
|
55
|
+
db.pragma('journal_mode = ${journalMode}');
|
|
56
|
+
db.pragma('busy_timeout = 30000');
|
|
57
|
+
db.pragma('synchronous = normal');
|
|
58
|
+
} catch(e) {}
|
|
38
59
|
return db;
|
|
39
60
|
}
|
|
61
|
+
|
|
40
62
|
Database.prototype = OrigDatabase.prototype;
|
|
41
63
|
Database.prototype.constructor = Database;
|
|
42
64
|
Object.keys(OrigDatabase).forEach(k => { Database[k] = OrigDatabase[k]; });
|
|
65
|
+
|
|
66
|
+
// api polyfills for better-sqlite3 compatibility
|
|
43
67
|
Database.prototype.unsafeMode = function() { return this; };
|
|
44
|
-
|
|
68
|
+
if (!Database.prototype.defaultSafeIntegers) {
|
|
69
|
+
Database.prototype.defaultSafeIntegers = function() { return this; };
|
|
70
|
+
}
|
|
71
|
+
if (!Database.prototype.serialize) {
|
|
72
|
+
Database.prototype.serialize = function() { throw new Error('not supported in wasm'); };
|
|
73
|
+
}
|
|
74
|
+
if (!Database.prototype.backup) {
|
|
75
|
+
Database.prototype.backup = function() { throw new Error('not supported in wasm'); };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// wrap pragma to skip optimize (can corrupt wasm vfs) and swallow sqlite errors
|
|
45
79
|
const origPragma = OrigDatabase.prototype.pragma;
|
|
46
80
|
Database.prototype.pragma = function(str, opts) {
|
|
47
81
|
if (str && str.trim().toLowerCase().startsWith('optimize')) return [];
|
|
48
82
|
try { return origPragma.call(this, str, opts); }
|
|
49
83
|
catch(e) { if (e && (e.code === 'SQLITE_CORRUPT' || e.code === 'SQLITE_IOERR')) return []; throw e; }
|
|
50
84
|
};
|
|
85
|
+
|
|
51
86
|
// wrap close to swallow wasm errors during shutdown
|
|
52
87
|
const origClose = OrigDatabase.prototype.close;
|
|
53
88
|
Database.prototype.close = function() {
|
|
54
89
|
try { return origClose.call(this); }
|
|
55
90
|
catch(e) { console.error('[orez-shim] close error (swallowed):', e?.message || e); }
|
|
56
91
|
};
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
Database.prototype.run = function(sql, ...args) {
|
|
60
|
-
if (typeof sql === 'string') {
|
|
61
|
-
if (sql.includes('_zero.changeLog')) {
|
|
62
|
-
console.info('[orez-shim] changeLog write:', sql.slice(0, 120), args.length ? JSON.stringify(args[0]).slice(0, 80) : '');
|
|
63
|
-
}
|
|
64
|
-
if (sql.includes('_zero.replicationState') && (sql.includes('UPDATE') || sql.includes('INSERT'))) {
|
|
65
|
-
console.info('[orez-shim] replicationState update:', sql.slice(0, 120), args.length ? JSON.stringify(args[0]).slice(0, 80) : '');
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
return origRun.call(this, sql, ...args);
|
|
69
|
-
};
|
|
70
|
-
if (!Database.prototype.defaultSafeIntegers) Database.prototype.defaultSafeIntegers = function() { return this; };
|
|
71
|
-
if (!Database.prototype.serialize) Database.prototype.serialize = function() { throw new Error('not supported in wasm'); };
|
|
72
|
-
if (!Database.prototype.backup) Database.prototype.backup = function() { throw new Error('not supported in wasm'); };
|
|
92
|
+
|
|
93
|
+
// statement prototype polyfills
|
|
73
94
|
const tmpDb = new OrigDatabase(':memory:');
|
|
74
95
|
const tmpStmt = tmpDb.prepare('SELECT 1');
|
|
75
96
|
const SP = Object.getPrototypeOf(tmpStmt);
|
|
@@ -78,6 +99,8 @@ SP.scanStatus = function() { return undefined; };
|
|
|
78
99
|
SP.scanStatusV2 = function() { return []; };
|
|
79
100
|
SP.scanStatusReset = function() {};
|
|
80
101
|
tmpDb.close();
|
|
102
|
+
|
|
103
|
+
// scanstat constants for query planner compatibility
|
|
81
104
|
Database.SQLITE_SCANSTAT_NLOOP = 0;
|
|
82
105
|
Database.SQLITE_SCANSTAT_NVISIT = 1;
|
|
83
106
|
Database.SQLITE_SCANSTAT_EST = 2;
|
|
@@ -87,6 +110,7 @@ Database.SQLITE_SCANSTAT_SELECTID = 5;
|
|
|
87
110
|
Database.SQLITE_SCANSTAT_PARENTID = 6;
|
|
88
111
|
Database.SQLITE_SCANSTAT_NCYCLE = 7;
|
|
89
112
|
Database.SQLITE_SCANSTAT_COMPLEX = 8;
|
|
113
|
+
|
|
90
114
|
export default Database;
|
|
91
115
|
export { SqliteError };
|
|
92
116
|
`,
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* apply sqlite mode - handles shim installation with backup/restore lifecycle
|
|
3
|
+
*
|
|
4
|
+
* this module manages the in-place patching of @rocicorp/zero-sqlite3 with proper
|
|
5
|
+
* backup/restore to prevent mode contamination when switching between wasm and native.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
9
|
+
import { resolve } from 'node:path'
|
|
10
|
+
|
|
11
|
+
import { generateCjsShim } from './shim-template.js'
|
|
12
|
+
import { BACKUP_MARKER, type SqliteMode, type SqliteModeConfig } from './types.js'
|
|
13
|
+
|
|
14
|
+
interface ApplyResult {
|
|
15
|
+
success: boolean
|
|
16
|
+
shimPath?: string
|
|
17
|
+
error?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* find the index.js file to shim in @rocicorp/zero-sqlite3
|
|
22
|
+
*/
|
|
23
|
+
function findZeroSqliteIndex(zeroSqlitePath: string): string | null {
|
|
24
|
+
// find package root (contains package.json)
|
|
25
|
+
let dir = zeroSqlitePath
|
|
26
|
+
while (dir && !existsSync(resolve(dir, 'package.json'))) {
|
|
27
|
+
const parent = resolve(dir, '..')
|
|
28
|
+
if (parent === dir) break
|
|
29
|
+
dir = parent
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// try lib/index.js first, then root index.js
|
|
33
|
+
const libIndex = resolve(dir, 'lib', 'index.js')
|
|
34
|
+
if (existsSync(libIndex)) return libIndex
|
|
35
|
+
|
|
36
|
+
const rootIndex = resolve(dir, 'index.js')
|
|
37
|
+
if (existsSync(rootIndex)) return rootIndex
|
|
38
|
+
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* get backup path for an index file
|
|
44
|
+
*/
|
|
45
|
+
function getBackupPath(indexPath: string): string {
|
|
46
|
+
return indexPath + BACKUP_MARKER
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* check if a file is already shimmed by orez
|
|
51
|
+
*/
|
|
52
|
+
function isOrezShimmed(indexPath: string): boolean {
|
|
53
|
+
if (!existsSync(indexPath)) return false
|
|
54
|
+
const content = readFileSync(indexPath, 'utf-8')
|
|
55
|
+
return content.includes('orez sqlite shim')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* check what mode an existing shim is configured for
|
|
60
|
+
*/
|
|
61
|
+
export function getShimMode(indexPath: string): SqliteMode | null {
|
|
62
|
+
if (!existsSync(indexPath)) return null
|
|
63
|
+
const content = readFileSync(indexPath, 'utf-8')
|
|
64
|
+
if (!content.includes('orez sqlite shim')) return null
|
|
65
|
+
|
|
66
|
+
// extract mode from comment: "// mode: wasm, journal_mode: delete"
|
|
67
|
+
const match = content.match(/\/\/ mode: (native|wasm),/)
|
|
68
|
+
return match ? (match[1] as SqliteMode) : null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* backup the original @rocicorp/zero-sqlite3 index.js
|
|
73
|
+
* only backs up if not already backed up and not already shimmed
|
|
74
|
+
*/
|
|
75
|
+
export function backupOriginal(indexPath: string): boolean {
|
|
76
|
+
const backupPath = getBackupPath(indexPath)
|
|
77
|
+
|
|
78
|
+
// already have a backup
|
|
79
|
+
if (existsSync(backupPath)) return true
|
|
80
|
+
|
|
81
|
+
// don't backup if file is already shimmed (would backup the shim)
|
|
82
|
+
if (isOrezShimmed(indexPath)) {
|
|
83
|
+
return false
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!existsSync(indexPath)) return false
|
|
87
|
+
|
|
88
|
+
const content = readFileSync(indexPath, 'utf-8')
|
|
89
|
+
writeFileSync(backupPath, content)
|
|
90
|
+
return true
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* restore the original @rocicorp/zero-sqlite3 from backup
|
|
95
|
+
*/
|
|
96
|
+
export function restoreOriginal(indexPath: string): boolean {
|
|
97
|
+
const backupPath = getBackupPath(indexPath)
|
|
98
|
+
|
|
99
|
+
if (!existsSync(backupPath)) {
|
|
100
|
+
// no backup to restore from
|
|
101
|
+
return false
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const content = readFileSync(backupPath, 'utf-8')
|
|
105
|
+
writeFileSync(indexPath, content)
|
|
106
|
+
rmSync(backupPath)
|
|
107
|
+
return true
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* check if backup exists
|
|
112
|
+
*/
|
|
113
|
+
export function hasBackup(indexPath: string): boolean {
|
|
114
|
+
return existsSync(getBackupPath(indexPath))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* apply wasm mode shim to @rocicorp/zero-sqlite3
|
|
119
|
+
* backs up original first, writes shim in place
|
|
120
|
+
*/
|
|
121
|
+
export function applyWasmShim(config: SqliteModeConfig): ApplyResult {
|
|
122
|
+
if (config.mode !== 'wasm') {
|
|
123
|
+
return { success: false, error: 'applyWasmShim called with non-wasm mode' }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!config.zeroSqlitePath) {
|
|
127
|
+
return { success: false, error: '@rocicorp/zero-sqlite3 not found' }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!config.bedrockPath) {
|
|
131
|
+
return { success: false, error: 'bedrock-sqlite not found' }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const indexPath = findZeroSqliteIndex(config.zeroSqlitePath)
|
|
135
|
+
if (!indexPath) {
|
|
136
|
+
return { success: false, error: 'could not find @rocicorp/zero-sqlite3 index.js' }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// check if already shimmed for wasm mode
|
|
140
|
+
const existingMode = getShimMode(indexPath)
|
|
141
|
+
if (existingMode === 'wasm') {
|
|
142
|
+
return { success: true, shimPath: indexPath }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// backup original before shimming - must succeed
|
|
146
|
+
const backedUp = backupOriginal(indexPath)
|
|
147
|
+
if (!backedUp && !hasBackup(indexPath)) {
|
|
148
|
+
// file is shimmed (possibly by another mode) but no backup exists
|
|
149
|
+
// cannot safely proceed - would lose ability to restore
|
|
150
|
+
return {
|
|
151
|
+
success: false,
|
|
152
|
+
error:
|
|
153
|
+
'cannot apply wasm shim: file is already shimmed with no backup. reinstall @rocicorp/zero-sqlite3',
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// generate and write shim
|
|
158
|
+
const shimCode = generateCjsShim({
|
|
159
|
+
mode: 'wasm',
|
|
160
|
+
bedrockPath: config.bedrockPath,
|
|
161
|
+
includeTracing: false,
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
writeFileSync(indexPath, shimCode)
|
|
165
|
+
|
|
166
|
+
return { success: true, shimPath: indexPath }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* restore native mode by removing shim and restoring original
|
|
171
|
+
*/
|
|
172
|
+
export function restoreNativeMode(zeroSqlitePath: string): ApplyResult {
|
|
173
|
+
const indexPath = findZeroSqliteIndex(zeroSqlitePath)
|
|
174
|
+
if (!indexPath) {
|
|
175
|
+
return { success: false, error: 'could not find @rocicorp/zero-sqlite3 index.js' }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// check if currently shimmed
|
|
179
|
+
if (!isOrezShimmed(indexPath)) {
|
|
180
|
+
// not shimmed, nothing to do
|
|
181
|
+
return { success: true }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// restore from backup
|
|
185
|
+
if (restoreOriginal(indexPath)) {
|
|
186
|
+
return { success: true }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// no backup available - this is a problem
|
|
190
|
+
return {
|
|
191
|
+
success: false,
|
|
192
|
+
error:
|
|
193
|
+
'cannot restore native mode: no backup found. reinstall @rocicorp/zero-sqlite3',
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* apply sqlite mode - main entry point
|
|
199
|
+
* handles both installing wasm shim and restoring to native
|
|
200
|
+
*/
|
|
201
|
+
export function applySqliteMode(config: SqliteModeConfig): ApplyResult {
|
|
202
|
+
if (config.mode === 'native') {
|
|
203
|
+
// for native mode, restore original if shimmed
|
|
204
|
+
if (config.zeroSqlitePath) {
|
|
205
|
+
return restoreNativeMode(config.zeroSqlitePath)
|
|
206
|
+
}
|
|
207
|
+
return { success: true }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return applyWasmShim(config)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* cleanup any shim artifacts - used during shutdown or cleanup
|
|
215
|
+
*/
|
|
216
|
+
export function cleanupShim(zeroSqlitePath: string | undefined): void {
|
|
217
|
+
if (!zeroSqlitePath) return
|
|
218
|
+
|
|
219
|
+
const indexPath = findZeroSqliteIndex(zeroSqlitePath)
|
|
220
|
+
if (!indexPath) return
|
|
221
|
+
|
|
222
|
+
// restore original if we have a backup
|
|
223
|
+
restoreOriginal(indexPath)
|
|
224
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sqlite-mode module - unified sqlite mode handling for orez
|
|
3
|
+
*
|
|
4
|
+
* this module provides:
|
|
5
|
+
* - type definitions for sqlite modes
|
|
6
|
+
* - mode resolution from config
|
|
7
|
+
* - shim generation (single source of truth)
|
|
8
|
+
* - safe mode application with backup/restore
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export * from './types.js'
|
|
12
|
+
export * from './resolve-mode.js'
|
|
13
|
+
export * from './apply-mode.js'
|
|
14
|
+
export * from './shim-template.js'
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mode resolution - canonical place to determine sqlite mode from config/env
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createRequire } from 'node:module'
|
|
6
|
+
|
|
7
|
+
import type { SqliteMode, SqliteModeConfig } from './types.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* resolve a package entry path
|
|
11
|
+
* import.meta.resolve doesn't work in vitest, so we fall back to require.resolve
|
|
12
|
+
*/
|
|
13
|
+
export function resolvePackage(pkg: string): string {
|
|
14
|
+
try {
|
|
15
|
+
const resolved = import.meta.resolve(pkg)
|
|
16
|
+
if (resolved) return resolved.replace('file://', '')
|
|
17
|
+
} catch {}
|
|
18
|
+
try {
|
|
19
|
+
const require = createRequire(import.meta.url)
|
|
20
|
+
return require.resolve(pkg)
|
|
21
|
+
} catch {}
|
|
22
|
+
return ''
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* resolve sqlite mode from config
|
|
27
|
+
* single source of truth for mode selection
|
|
28
|
+
*/
|
|
29
|
+
export function resolveSqliteMode(disableWasmSqlite: boolean): SqliteMode {
|
|
30
|
+
return disableWasmSqlite ? 'native' : 'wasm'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* resolve full sqlite mode config including paths
|
|
35
|
+
* returns null if required packages aren't installed
|
|
36
|
+
*/
|
|
37
|
+
export function resolveSqliteModeConfig(
|
|
38
|
+
disableWasmSqlite: boolean
|
|
39
|
+
): SqliteModeConfig | null {
|
|
40
|
+
const mode = resolveSqliteMode(disableWasmSqlite)
|
|
41
|
+
const zeroSqlitePath = resolvePackage('@rocicorp/zero-sqlite3') || undefined
|
|
42
|
+
|
|
43
|
+
// native mode may still need zero-sqlite3 path for restoring from a prior shim
|
|
44
|
+
if (mode === 'native') {
|
|
45
|
+
return { mode, zeroSqlitePath }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// wasm mode needs bedrock-sqlite and zero-sqlite3 paths
|
|
49
|
+
const bedrockPath = resolvePackage('bedrock-sqlite')
|
|
50
|
+
|
|
51
|
+
if (!bedrockPath) {
|
|
52
|
+
return null // bedrock-sqlite not installed
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!zeroSqlitePath) {
|
|
56
|
+
return null // zero-sqlite3 not installed
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
mode,
|
|
61
|
+
bedrockPath,
|
|
62
|
+
zeroSqlitePath,
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* get mode display string for logging
|
|
68
|
+
*/
|
|
69
|
+
export function getModeDisplayString(mode: SqliteMode): string {
|
|
70
|
+
return mode
|
|
71
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* shim template generator - single source of truth for both cjs and esm shims.
|
|
3
|
+
* generates the shim code that wraps bedrock-sqlite to be compatible with
|
|
4
|
+
* @rocicorp/zero-sqlite3 (better-sqlite3 api).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { COMMON_PRAGMAS, JOURNAL_MODE, type SqliteMode } from './types.js'
|
|
8
|
+
|
|
9
|
+
export interface ShimOptions {
|
|
10
|
+
mode: SqliteMode
|
|
11
|
+
bedrockPath: string
|
|
12
|
+
// include debug tracing for changeLog/replicationState writes
|
|
13
|
+
includeTracing?: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* generate the core shim code (shared between cjs and esm)
|
|
18
|
+
* this is the constructor wrapper and api polyfills
|
|
19
|
+
*/
|
|
20
|
+
function generateShimCore(opts: ShimOptions): string {
|
|
21
|
+
const journalMode = JOURNAL_MODE[opts.mode]
|
|
22
|
+
const { busy_timeout, synchronous } = COMMON_PRAGMAS
|
|
23
|
+
|
|
24
|
+
return `
|
|
25
|
+
var OrigDatabase = mod.Database;
|
|
26
|
+
var SqliteError = mod.SqliteError;
|
|
27
|
+
|
|
28
|
+
function Database() {
|
|
29
|
+
var db = new OrigDatabase(...arguments);
|
|
30
|
+
try {
|
|
31
|
+
db.pragma('journal_mode = ${journalMode}');
|
|
32
|
+
db.pragma('busy_timeout = ${busy_timeout}');
|
|
33
|
+
db.pragma('synchronous = ${synchronous}');
|
|
34
|
+
} catch(e) {}
|
|
35
|
+
return db;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
Database.prototype = OrigDatabase.prototype;
|
|
39
|
+
Database.prototype.constructor = Database;
|
|
40
|
+
Object.keys(OrigDatabase).forEach(function(k) { Database[k] = OrigDatabase[k]; });
|
|
41
|
+
|
|
42
|
+
// api polyfills for better-sqlite3 compatibility
|
|
43
|
+
Database.prototype.unsafeMode = function() { return this; };
|
|
44
|
+
if (!Database.prototype.defaultSafeIntegers) {
|
|
45
|
+
Database.prototype.defaultSafeIntegers = function() { return this; };
|
|
46
|
+
}
|
|
47
|
+
if (!Database.prototype.serialize) {
|
|
48
|
+
Database.prototype.serialize = function() { throw new Error('not supported in wasm'); };
|
|
49
|
+
}
|
|
50
|
+
if (!Database.prototype.backup) {
|
|
51
|
+
Database.prototype.backup = function() { throw new Error('not supported in wasm'); };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// wrap pragma to skip optimize (can corrupt wasm vfs) and swallow sqlite errors
|
|
55
|
+
var origPragma = OrigDatabase.prototype.pragma;
|
|
56
|
+
Database.prototype.pragma = function(str, opts) {
|
|
57
|
+
if (str && str.trim().toLowerCase().startsWith('optimize')) return [];
|
|
58
|
+
try { return origPragma.call(this, str, opts); }
|
|
59
|
+
catch(e) { if (e && (e.code === 'SQLITE_CORRUPT' || e.code === 'SQLITE_IOERR')) return []; throw e; }
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// wrap close to swallow wasm errors during shutdown
|
|
63
|
+
var origClose = OrigDatabase.prototype.close;
|
|
64
|
+
Database.prototype.close = function() {
|
|
65
|
+
try { return origClose.call(this); }
|
|
66
|
+
catch(e) { console.error('[orez-shim] close error (swallowed):', e?.message || e); }
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// statement prototype polyfills
|
|
70
|
+
var tmpDb = new OrigDatabase(':memory:');
|
|
71
|
+
var tmpStmt = tmpDb.prepare('SELECT 1');
|
|
72
|
+
var SP = Object.getPrototypeOf(tmpStmt);
|
|
73
|
+
if (!SP.safeIntegers) SP.safeIntegers = function() { return this; };
|
|
74
|
+
SP.scanStatus = function() { return undefined; };
|
|
75
|
+
SP.scanStatusV2 = function() { return []; };
|
|
76
|
+
SP.scanStatusReset = function() {};
|
|
77
|
+
tmpDb.close();
|
|
78
|
+
|
|
79
|
+
// scanstat constants for query planner compatibility
|
|
80
|
+
Database.SQLITE_SCANSTAT_NLOOP = 0;
|
|
81
|
+
Database.SQLITE_SCANSTAT_NVISIT = 1;
|
|
82
|
+
Database.SQLITE_SCANSTAT_EST = 2;
|
|
83
|
+
Database.SQLITE_SCANSTAT_NAME = 3;
|
|
84
|
+
Database.SQLITE_SCANSTAT_EXPLAIN = 4;
|
|
85
|
+
Database.SQLITE_SCANSTAT_SELECTID = 5;
|
|
86
|
+
Database.SQLITE_SCANSTAT_PARENTID = 6;
|
|
87
|
+
Database.SQLITE_SCANSTAT_NCYCLE = 7;
|
|
88
|
+
Database.SQLITE_SCANSTAT_COMPLEX = 8;
|
|
89
|
+
`.trim()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* generate debug tracing code for run() method
|
|
94
|
+
*/
|
|
95
|
+
function generateTracing(): string {
|
|
96
|
+
return `
|
|
97
|
+
// trace writes to _zero.changeLog and _zero.replicationState for debugging
|
|
98
|
+
var origRun = OrigDatabase.prototype.run;
|
|
99
|
+
Database.prototype.run = function(sql) {
|
|
100
|
+
var args = Array.prototype.slice.call(arguments, 1);
|
|
101
|
+
if (typeof sql === 'string') {
|
|
102
|
+
if (sql.includes('_zero.changeLog')) {
|
|
103
|
+
console.info('[orez-shim] changeLog write:', sql.slice(0, 120), args.length ? JSON.stringify(args[0]).slice(0, 80) : '');
|
|
104
|
+
}
|
|
105
|
+
if (sql.includes('_zero.replicationState') && (sql.includes('UPDATE') || sql.includes('INSERT'))) {
|
|
106
|
+
console.info('[orez-shim] replicationState update:', sql.slice(0, 120), args.length ? JSON.stringify(args[0]).slice(0, 80) : '');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return origRun.apply(this, arguments);
|
|
110
|
+
};
|
|
111
|
+
`.trim()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* generate commonjs shim for in-place patching of @rocicorp/zero-sqlite3
|
|
116
|
+
*/
|
|
117
|
+
export function generateCjsShim(opts: ShimOptions): string {
|
|
118
|
+
const core = generateShimCore(opts)
|
|
119
|
+
const tracing = opts.includeTracing ? '\n' + generateTracing() : ''
|
|
120
|
+
|
|
121
|
+
return `'use strict';
|
|
122
|
+
// orez sqlite shim - wraps bedrock-sqlite for zero-cache compatibility
|
|
123
|
+
// mode: ${opts.mode}, journal_mode: ${JOURNAL_MODE[opts.mode]}
|
|
124
|
+
var mod = require('${opts.bedrockPath}');
|
|
125
|
+
${core}
|
|
126
|
+
${tracing}
|
|
127
|
+
module.exports = Database;
|
|
128
|
+
module.exports.SqliteError = SqliteError;
|
|
129
|
+
`
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* generate esm shim for loader hooks
|
|
134
|
+
*/
|
|
135
|
+
export function generateEsmShim(opts: ShimOptions): string {
|
|
136
|
+
const core = generateShimCore(opts)
|
|
137
|
+
const tracing = opts.includeTracing ? '\n' + generateTracing() : ''
|
|
138
|
+
|
|
139
|
+
return `// orez sqlite shim - wraps bedrock-sqlite for zero-cache compatibility
|
|
140
|
+
// mode: ${opts.mode}, journal_mode: ${JOURNAL_MODE[opts.mode]}
|
|
141
|
+
|
|
142
|
+
// catch uncaught exceptions from bedrock-sqlite wasm clearly
|
|
143
|
+
process.on('uncaughtException', (err) => {
|
|
144
|
+
console.error('[orez-shim] UNCAUGHT EXCEPTION:', err?.message || err);
|
|
145
|
+
console.error('[orez-shim] code:', err?.code, 'name:', err?.name);
|
|
146
|
+
console.error('[orez-shim] stack:', err?.stack?.split('\\n').slice(0, 5).join('\\n'));
|
|
147
|
+
process.exit(1);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
import { createRequire } from 'node:module';
|
|
151
|
+
const require = createRequire('${opts.bedrockPath}');
|
|
152
|
+
var mod = require('${opts.bedrockPath}');
|
|
153
|
+
${core}
|
|
154
|
+
${tracing}
|
|
155
|
+
export default Database;
|
|
156
|
+
export { SqliteError };
|
|
157
|
+
`
|
|
158
|
+
}
|