@firtoz/drizzle-sqlite-wasm 0.2.1 → 0.2.3
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/CHANGELOG.md +25 -0
- package/README.md +73 -0
- package/package.json +12 -14
- package/src/collections/sqlite-collection.ts +99 -27
- package/src/drizzle/direct.ts +1 -1
- package/src/drizzle/handle-callback.ts +1 -1
- package/src/drizzle/worker.ts +140 -0
- package/src/hooks/useDrizzleSqliteDb.ts +61 -34
- package/src/types.ts +4 -0
- package/src/worker/sqlite.worker.ts +9 -11
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# @firtoz/drizzle-sqlite-wasm
|
|
2
2
|
|
|
3
|
+
## 0.2.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [`d97681f`](https://github.com/firtoz/fullstack-toolkit/commit/d97681f56e103d033292005d31f298b03b4fa7ca) Thanks [@firtoz](https://github.com/firtoz)! - Add comprehensive Vite configuration documentation for OPFS support. Includes required COOP/COEP headers and a custom plugin to fix the sqlite-wasm 3.51.x OPFS proxy worker module format issue ("Unexpected token 'export'" error).
|
|
8
|
+
|
|
9
|
+
- Updated dependencies [[`b0f7893`](https://github.com/firtoz/fullstack-toolkit/commit/b0f789314c4ee85d8c08466b968baad2977a2581)]:
|
|
10
|
+
- @firtoz/worker-helper@1.1.0
|
|
11
|
+
|
|
12
|
+
## 0.2.2
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- [`8abab0a`](https://github.com/firtoz/fullstack-toolkit/commit/8abab0ae7a99320a4254cb128c0fd823726e58e0) Thanks [@firtoz](https://github.com/firtoz)! - Fix critical bug where debug mode prevented database operations from executing. Debug handlers now properly wrap and call the actual backend handlers instead of replacing them.
|
|
17
|
+
|
|
18
|
+
Add cursor-based and offset-based pagination support to `loadSubset` operations, enabling efficient navigation through large datasets.
|
|
19
|
+
|
|
20
|
+
Add `SQLInterceptor` support to log all SQL queries, including direct Drizzle queries, with the new `createInstrumentedDrizzle` function. This provides comprehensive query visibility for debugging and monitoring.
|
|
21
|
+
|
|
22
|
+
Add explicit return type `SqliteCollectionConfig<TTable>` to `sqliteCollectionOptions` function, improving type safety and eliminating the `any` cast at the return statement.
|
|
23
|
+
|
|
24
|
+
- Updated dependencies [[`8abab0a`](https://github.com/firtoz/fullstack-toolkit/commit/8abab0ae7a99320a4254cb128c0fd823726e58e0), [`8abab0a`](https://github.com/firtoz/fullstack-toolkit/commit/8abab0ae7a99320a4254cb128c0fd823726e58e0)]:
|
|
25
|
+
- @firtoz/maybe-error@1.5.2
|
|
26
|
+
- @firtoz/drizzle-utils@0.3.1
|
|
27
|
+
|
|
3
28
|
## 0.2.1
|
|
4
29
|
|
|
5
30
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -111,6 +111,79 @@ import SqliteWorker from "@firtoz/drizzle-sqlite-wasm/worker/sqlite.worker?worke
|
|
|
111
111
|
const { drizzle } = useDrizzleSqliteDb(SqliteWorker, "mydb", schema, migrations);
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
+
#### Required Vite Configuration for OPFS Support
|
|
115
|
+
|
|
116
|
+
To enable OPFS (Origin Private File System) persistence with SQLite WASM, you need to configure Vite properly. Add the following to your `vite.config.ts`:
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
import { defineConfig } from "vite";
|
|
120
|
+
|
|
121
|
+
export default defineConfig({
|
|
122
|
+
plugins: [
|
|
123
|
+
// Required: Set COOP/COEP headers for SharedArrayBuffer support
|
|
124
|
+
{
|
|
125
|
+
name: "configure-response-headers",
|
|
126
|
+
configureServer: (server) => {
|
|
127
|
+
server.middlewares.use((_req, res, next) => {
|
|
128
|
+
res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
|
|
129
|
+
res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
|
|
130
|
+
next();
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
// Required: Fix for sqlite-wasm OPFS proxy worker module format
|
|
135
|
+
{
|
|
136
|
+
name: "sqlite-wasm-opfs-fix",
|
|
137
|
+
enforce: "pre",
|
|
138
|
+
transform(code, id) {
|
|
139
|
+
// Transform worker creation calls to use module type
|
|
140
|
+
if (
|
|
141
|
+
id.includes("@sqlite.org/sqlite-wasm") &&
|
|
142
|
+
code.includes("new Worker")
|
|
143
|
+
) {
|
|
144
|
+
// This fixes the "Unexpected token 'export'" error in OPFS proxy worker
|
|
145
|
+
let transformed = code;
|
|
146
|
+
const workerRegex =
|
|
147
|
+
/new\s+Worker\s*\(\s*(new\s+URL\s*\([^)]+,[^)]+\))\s*\)/g;
|
|
148
|
+
transformed = transformed.replace(
|
|
149
|
+
workerRegex,
|
|
150
|
+
"new Worker($1, { type: 'module' })",
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
if (transformed !== code) {
|
|
154
|
+
return {
|
|
155
|
+
code: transformed,
|
|
156
|
+
map: null,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
server: {
|
|
164
|
+
headers: {
|
|
165
|
+
"Cross-Origin-Opener-Policy": "same-origin",
|
|
166
|
+
"Cross-Origin-Embedder-Policy": "require-corp",
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
optimizeDeps: {
|
|
170
|
+
exclude: ["@sqlite.org/sqlite-wasm"],
|
|
171
|
+
},
|
|
172
|
+
worker: {
|
|
173
|
+
format: "es",
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**Why is this needed?**
|
|
179
|
+
|
|
180
|
+
1. **COOP/COEP Headers**: Required for `SharedArrayBuffer` support, which OPFS needs for synchronous file operations
|
|
181
|
+
2. **Worker Module Fix**: sqlite-wasm 3.51.x creates workers without specifying `{ type: 'module' }`, causing syntax errors when the OPFS proxy worker (which uses ES module syntax) is loaded
|
|
182
|
+
3. **Worker Format**: Ensures all workers are treated as ES modules
|
|
183
|
+
4. **Exclude from Optimization**: Prevents Vite from pre-bundling sqlite-wasm, which can break worker creation
|
|
184
|
+
|
|
185
|
+
For a complete example, see [`tests/test-playground/vite.config.ts`](../../tests/test-playground/vite.config.ts) in the repository.
|
|
186
|
+
|
|
114
187
|
### Webpack 5+
|
|
115
188
|
|
|
116
189
|
Use `new URL()` with `import.meta.url`:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@firtoz/drizzle-sqlite-wasm",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Drizzle SQLite WASM bindings",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"module": "./src/index.ts",
|
|
@@ -43,9 +43,7 @@
|
|
|
43
43
|
"typecheck": "tsc --noEmit -p ./tsconfig.json",
|
|
44
44
|
"lint": "biome check --write src",
|
|
45
45
|
"lint:ci": "biome ci src",
|
|
46
|
-
"format": "biome format src --write"
|
|
47
|
-
"test": "bun test --pass-with-no-tests",
|
|
48
|
-
"test:watch": "bun test --watch"
|
|
46
|
+
"format": "biome format src --write"
|
|
49
47
|
},
|
|
50
48
|
"keywords": [
|
|
51
49
|
"typescript",
|
|
@@ -71,19 +69,19 @@
|
|
|
71
69
|
"access": "public"
|
|
72
70
|
},
|
|
73
71
|
"dependencies": {
|
|
74
|
-
"@firtoz/drizzle-utils": "^0.3.
|
|
75
|
-
"@firtoz/maybe-error": "^1.5.
|
|
76
|
-
"@firtoz/worker-helper": "^1.
|
|
77
|
-
"@sqlite.org/sqlite-wasm": "^3.51.
|
|
78
|
-
"@tanstack/db": "^0.5.
|
|
79
|
-
"drizzle-orm": "^0.
|
|
72
|
+
"@firtoz/drizzle-utils": "^0.3.1",
|
|
73
|
+
"@firtoz/maybe-error": "^1.5.2",
|
|
74
|
+
"@firtoz/worker-helper": "^1.1.0",
|
|
75
|
+
"@sqlite.org/sqlite-wasm": "^3.51.2-build3",
|
|
76
|
+
"@tanstack/db": "^0.5.20",
|
|
77
|
+
"drizzle-orm": "^0.45.1",
|
|
80
78
|
"drizzle-valibot": "^0.4.2",
|
|
81
|
-
"react": "^19.2.
|
|
79
|
+
"react": "^19.2.3",
|
|
82
80
|
"valibot": "^1.2.0",
|
|
83
|
-
"zod": "^4.
|
|
81
|
+
"zod": "^4.3.5"
|
|
84
82
|
},
|
|
85
83
|
"devDependencies": {
|
|
86
|
-
"@standard-schema/spec": "^1.
|
|
87
|
-
"@types/react": "^19.2.
|
|
84
|
+
"@standard-schema/spec": "^1.1.0",
|
|
85
|
+
"@types/react": "^19.2.8"
|
|
88
86
|
}
|
|
89
87
|
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
InferSchemaOutput,
|
|
3
|
+
SyncMode,
|
|
4
|
+
CollectionConfig,
|
|
5
|
+
} from "@tanstack/db";
|
|
2
6
|
import type { IR } from "@tanstack/db";
|
|
3
7
|
import {
|
|
4
8
|
eq,
|
|
@@ -31,6 +35,7 @@ import type {
|
|
|
31
35
|
TableWithRequiredFields,
|
|
32
36
|
BaseSyncConfig,
|
|
33
37
|
SyncBackend,
|
|
38
|
+
CollectionUtils,
|
|
34
39
|
} from "@firtoz/drizzle-utils";
|
|
35
40
|
import {
|
|
36
41
|
createSyncFunction,
|
|
@@ -38,6 +43,7 @@ import {
|
|
|
38
43
|
createGetKeyFunction,
|
|
39
44
|
createCollectionConfig,
|
|
40
45
|
} from "@firtoz/drizzle-utils";
|
|
46
|
+
import type * as v from "valibot";
|
|
41
47
|
|
|
42
48
|
export type AnyDrizzleDatabase = BaseSQLiteDatabase<
|
|
43
49
|
"async",
|
|
@@ -102,6 +108,16 @@ export type SQLOperation =
|
|
|
102
108
|
tableName: string;
|
|
103
109
|
sql?: string;
|
|
104
110
|
timestamp: number;
|
|
111
|
+
}
|
|
112
|
+
| {
|
|
113
|
+
/** Raw SQL query executed directly via Drizzle (not through collection) */
|
|
114
|
+
type: "raw-query";
|
|
115
|
+
sql: string;
|
|
116
|
+
params?: unknown[];
|
|
117
|
+
method: string;
|
|
118
|
+
rowCount: number;
|
|
119
|
+
context: string;
|
|
120
|
+
timestamp: number;
|
|
105
121
|
};
|
|
106
122
|
|
|
107
123
|
/**
|
|
@@ -144,6 +160,25 @@ export type ValidTableNames<TSchema extends Record<string, unknown>> = {
|
|
|
144
160
|
[K in keyof TSchema]: TSchema[K] extends TableWithRequiredFields ? K : never;
|
|
145
161
|
}[keyof TSchema];
|
|
146
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Return type for sqliteCollectionOptions - configuration object for creating SQLite collections
|
|
165
|
+
*
|
|
166
|
+
* Note: The third type parameter of CollectionConfig uses `any` to maintain compatibility with
|
|
167
|
+
* TanStack DB's type system, which expects different schema types in different contexts.
|
|
168
|
+
*/
|
|
169
|
+
export type SqliteCollectionConfig<TTable extends Table> = Omit<
|
|
170
|
+
CollectionConfig<
|
|
171
|
+
InferSchemaOutput<SelectSchema<TTable>>,
|
|
172
|
+
string,
|
|
173
|
+
// biome-ignore lint/suspicious/noExplicitAny: Required for TanStack DB type compatibility
|
|
174
|
+
any
|
|
175
|
+
>,
|
|
176
|
+
"utils"
|
|
177
|
+
> & {
|
|
178
|
+
schema: v.GenericSchema<unknown>;
|
|
179
|
+
utils: CollectionUtils<InferSchemaOutput<SelectSchema<TTable>>>;
|
|
180
|
+
};
|
|
181
|
+
|
|
147
182
|
/**
|
|
148
183
|
* Converts TanStack DB IR BasicExpression to Drizzle SQL expression
|
|
149
184
|
*
|
|
@@ -262,7 +297,9 @@ export function sqliteCollectionOptions<
|
|
|
262
297
|
const TDrizzle extends AnyDrizzleDatabase,
|
|
263
298
|
const TTableName extends string & ValidTableNames<DrizzleSchema<TDrizzle>>,
|
|
264
299
|
TTable extends DrizzleSchema<TDrizzle>[TTableName] & TableWithRequiredFields,
|
|
265
|
-
>(
|
|
300
|
+
>(
|
|
301
|
+
config: DrizzleCollectionConfig<TDrizzle, TTableName>,
|
|
302
|
+
): SqliteCollectionConfig<TTable> {
|
|
266
303
|
const tableName = config.tableName as string &
|
|
267
304
|
ValidTableNames<DrizzleSchema<TDrizzle>>;
|
|
268
305
|
|
|
@@ -286,7 +323,7 @@ export function sqliteCollectionOptions<
|
|
|
286
323
|
|
|
287
324
|
// Create backend-specific implementation
|
|
288
325
|
const backend: SyncBackend<TTable> = {
|
|
289
|
-
initialLoad: async (
|
|
326
|
+
initialLoad: async () => {
|
|
290
327
|
const items = (await config.drizzle
|
|
291
328
|
.select()
|
|
292
329
|
.from(table)) as unknown as InferSchemaOutput<SelectSchema<TTable>>[];
|
|
@@ -315,25 +352,44 @@ export function sqliteCollectionOptions<
|
|
|
315
352
|
});
|
|
316
353
|
}
|
|
317
354
|
|
|
318
|
-
|
|
319
|
-
write(item);
|
|
320
|
-
}
|
|
355
|
+
return items as unknown as InferSchemaOutput<SelectSchema<TTable>>[];
|
|
321
356
|
},
|
|
322
357
|
|
|
323
|
-
loadSubset: async (options
|
|
324
|
-
// Build the query with optional where, orderBy, and
|
|
358
|
+
loadSubset: async (options) => {
|
|
359
|
+
// Build the query with optional where, orderBy, limit, and offset
|
|
325
360
|
// Use $dynamic() to enable dynamic query building
|
|
326
361
|
let query = config.drizzle.select().from(table).$dynamic();
|
|
327
362
|
|
|
328
|
-
//
|
|
363
|
+
// Combine where with cursor expressions if present
|
|
364
|
+
// The cursor.whereFrom gives us rows after the cursor position
|
|
329
365
|
let hasWhere = false;
|
|
330
|
-
if (options.where) {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
366
|
+
if (options.where || options.cursor?.whereFrom) {
|
|
367
|
+
let drizzleWhere: SQL | undefined;
|
|
368
|
+
|
|
369
|
+
if (options.where && options.cursor?.whereFrom) {
|
|
370
|
+
// Combine main where with cursor expression using AND
|
|
371
|
+
const mainWhere = convertBasicExpressionToDrizzle(
|
|
372
|
+
options.where,
|
|
373
|
+
table,
|
|
374
|
+
);
|
|
375
|
+
const cursorWhere = convertBasicExpressionToDrizzle(
|
|
376
|
+
options.cursor.whereFrom,
|
|
377
|
+
table,
|
|
378
|
+
);
|
|
379
|
+
drizzleWhere = and(mainWhere, cursorWhere);
|
|
380
|
+
} else if (options.where) {
|
|
381
|
+
drizzleWhere = convertBasicExpressionToDrizzle(options.where, table);
|
|
382
|
+
} else if (options.cursor?.whereFrom) {
|
|
383
|
+
drizzleWhere = convertBasicExpressionToDrizzle(
|
|
384
|
+
options.cursor.whereFrom,
|
|
385
|
+
table,
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (drizzleWhere) {
|
|
390
|
+
query = query.where(drizzleWhere);
|
|
391
|
+
hasWhere = true;
|
|
392
|
+
}
|
|
337
393
|
}
|
|
338
394
|
|
|
339
395
|
if (options.orderBy) {
|
|
@@ -345,6 +401,11 @@ export function sqliteCollectionOptions<
|
|
|
345
401
|
query = query.limit(options.limit);
|
|
346
402
|
}
|
|
347
403
|
|
|
404
|
+
// Apply offset for offset-based pagination
|
|
405
|
+
if (options.offset !== undefined && options.offset > 0) {
|
|
406
|
+
query = query.offset(options.offset);
|
|
407
|
+
}
|
|
408
|
+
|
|
348
409
|
const items = (await query) as unknown as InferSchemaOutput<
|
|
349
410
|
SelectSchema<TTable>
|
|
350
411
|
>[];
|
|
@@ -358,6 +419,12 @@ export function sqliteCollectionOptions<
|
|
|
358
419
|
if (options.limit !== undefined) {
|
|
359
420
|
contextParts.push(`limit ${options.limit}`);
|
|
360
421
|
}
|
|
422
|
+
if (options.offset !== undefined && options.offset > 0) {
|
|
423
|
+
contextParts.push(`offset ${options.offset}`);
|
|
424
|
+
}
|
|
425
|
+
if (options.cursor) {
|
|
426
|
+
contextParts.push("with cursor pagination");
|
|
427
|
+
}
|
|
361
428
|
|
|
362
429
|
if (hasWhere) {
|
|
363
430
|
config.interceptor.onOperation({
|
|
@@ -393,6 +460,9 @@ export function sqliteCollectionOptions<
|
|
|
393
460
|
if (options.limit !== undefined) {
|
|
394
461
|
contextParts.push(`limit ${options.limit}`);
|
|
395
462
|
}
|
|
463
|
+
if (options.offset !== undefined && options.offset > 0) {
|
|
464
|
+
contextParts.push(`offset ${options.offset}`);
|
|
465
|
+
}
|
|
396
466
|
|
|
397
467
|
config.interceptor.onOperation({
|
|
398
468
|
type: "write",
|
|
@@ -404,22 +474,16 @@ export function sqliteCollectionOptions<
|
|
|
404
474
|
});
|
|
405
475
|
}
|
|
406
476
|
|
|
407
|
-
|
|
408
|
-
write(item);
|
|
409
|
-
}
|
|
477
|
+
return items as unknown as InferSchemaOutput<SelectSchema<TTable>>[];
|
|
410
478
|
},
|
|
411
479
|
|
|
412
|
-
handleInsert: async (
|
|
480
|
+
handleInsert: async (items) => {
|
|
413
481
|
const results: Array<InferSchemaOutput<SelectSchema<TTable>>> = [];
|
|
414
482
|
|
|
415
483
|
// Queue the transaction to serialize SQLite operations
|
|
416
484
|
await queueTransaction(async () => {
|
|
417
485
|
await config.drizzle.transaction(async (tx) => {
|
|
418
|
-
for (const
|
|
419
|
-
// TanStack DB applies schema transform (including ID default) before calling this listener
|
|
420
|
-
// So mutation.modified already has the ID from insertSchemaWithIdDefault
|
|
421
|
-
const itemToInsert = mutation.modified;
|
|
422
|
-
|
|
486
|
+
for (const itemToInsert of items) {
|
|
423
487
|
if (config.debug) {
|
|
424
488
|
console.log(
|
|
425
489
|
`[${new Date().toISOString()}] insertListener inserting`,
|
|
@@ -544,21 +608,29 @@ export function sqliteCollectionOptions<
|
|
|
544
608
|
onInsert: config.debug
|
|
545
609
|
? async (params) => {
|
|
546
610
|
console.log("onInsert", params);
|
|
611
|
+
// Call the actual handler from syncResult (always defined in createSyncFunction)
|
|
612
|
+
// biome-ignore lint/style/noNonNullAssertion: onInsert is always defined in SyncFunctionResult
|
|
613
|
+
await syncResult.onInsert!(params);
|
|
547
614
|
}
|
|
548
615
|
: undefined,
|
|
549
616
|
onUpdate: config.debug
|
|
550
617
|
? async (params) => {
|
|
551
618
|
console.log("onUpdate", params);
|
|
619
|
+
// Call the actual handler from syncResult (always defined in createSyncFunction)
|
|
620
|
+
// biome-ignore lint/style/noNonNullAssertion: onUpdate is always defined in SyncFunctionResult
|
|
621
|
+
await syncResult.onUpdate!(params);
|
|
552
622
|
}
|
|
553
623
|
: undefined,
|
|
554
624
|
onDelete: config.debug
|
|
555
625
|
? async (params) => {
|
|
556
626
|
console.log("onDelete", params);
|
|
627
|
+
// Call the actual handler from syncResult (always defined in createSyncFunction)
|
|
628
|
+
// biome-ignore lint/style/noNonNullAssertion: onDelete is always defined in SyncFunctionResult
|
|
629
|
+
await syncResult.onDelete!(params);
|
|
557
630
|
}
|
|
558
631
|
: undefined,
|
|
559
632
|
syncMode: config.syncMode,
|
|
560
633
|
});
|
|
561
634
|
|
|
562
|
-
|
|
563
|
-
return collectionConfig as any;
|
|
635
|
+
return collectionConfig;
|
|
564
636
|
}
|
package/src/drizzle/direct.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Database } from "
|
|
1
|
+
import type { Database } from "../types";
|
|
2
2
|
import type { DrizzleConfig } from "drizzle-orm";
|
|
3
3
|
import { drizzle as drizzleSqliteProxy } from "drizzle-orm/sqlite-proxy";
|
|
4
4
|
import { handleRemoteCallback } from "./handle-callback";
|
package/src/drizzle/worker.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { DrizzleConfig } from "drizzle-orm";
|
|
2
2
|
import { drizzle as drizzleSqliteProxy } from "drizzle-orm/sqlite-proxy";
|
|
3
3
|
import type { ISqliteWorkerClient } from "../worker/client";
|
|
4
|
+
import type {
|
|
5
|
+
SQLInterceptor,
|
|
6
|
+
SQLOperation,
|
|
7
|
+
} from "../collections/sqlite-collection";
|
|
4
8
|
|
|
5
9
|
export const drizzleSqliteWasmWorker = <
|
|
6
10
|
TSchema extends Record<string, unknown> = Record<string, never>,
|
|
@@ -22,3 +26,139 @@ export const drizzleSqliteWasmWorker = <
|
|
|
22
26
|
});
|
|
23
27
|
}, config);
|
|
24
28
|
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Creates an instrumented Drizzle instance that logs all SQL queries.
|
|
32
|
+
* This wraps the standard drizzleSqliteWasmWorker to intercept every query.
|
|
33
|
+
*/
|
|
34
|
+
export const createInstrumentedDrizzle = <
|
|
35
|
+
TSchema extends Record<string, unknown> = Record<string, never>,
|
|
36
|
+
>(
|
|
37
|
+
client: ISqliteWorkerClient,
|
|
38
|
+
config: DrizzleConfig<TSchema> = {},
|
|
39
|
+
interceptor?: SQLInterceptor,
|
|
40
|
+
) => {
|
|
41
|
+
return drizzleSqliteProxy<TSchema>(async (sql, params, method) => {
|
|
42
|
+
const startTime = Date.now();
|
|
43
|
+
|
|
44
|
+
const result = await new Promise<{ rows: unknown[] }>((resolve, reject) => {
|
|
45
|
+
client.performRemoteCallback(
|
|
46
|
+
{
|
|
47
|
+
sql,
|
|
48
|
+
params,
|
|
49
|
+
method,
|
|
50
|
+
},
|
|
51
|
+
resolve,
|
|
52
|
+
reject,
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Log the operation if interceptor is provided
|
|
57
|
+
if (interceptor?.onOperation) {
|
|
58
|
+
// Parse SQL to determine context
|
|
59
|
+
const sqlLower = sql.toLowerCase().trim();
|
|
60
|
+
let context = "Direct Drizzle query";
|
|
61
|
+
|
|
62
|
+
if (sqlLower.startsWith("select")) {
|
|
63
|
+
// Extract table name from SELECT query
|
|
64
|
+
const fromMatch = sql.match(/from\s+["']?(\w+)["']?/i);
|
|
65
|
+
const tableName = fromMatch?.[1] || "unknown";
|
|
66
|
+
|
|
67
|
+
// Check for LIMIT/OFFSET - handle both literal values and ? placeholders
|
|
68
|
+
// Drizzle uses parameterized queries, so we need to check for `limit ?` style
|
|
69
|
+
const hasLimit = /limit\s+(\d+|\?|\$\d+)/i.test(sql);
|
|
70
|
+
const hasOffset = /offset\s+(\d+|\?|\$\d+)/i.test(sql);
|
|
71
|
+
const hasOrderBy = /order\s+by/i.test(sql);
|
|
72
|
+
|
|
73
|
+
if (hasLimit || hasOffset) {
|
|
74
|
+
// Extract actual values from params if using placeholders
|
|
75
|
+
// Drizzle typically puts LIMIT as second-to-last param, OFFSET as last
|
|
76
|
+
let limitVal = "?";
|
|
77
|
+
let offsetVal = "0";
|
|
78
|
+
|
|
79
|
+
// Try to extract literal values first
|
|
80
|
+
const literalLimit = sql.match(/limit\s+(\d+)/i);
|
|
81
|
+
const literalOffset = sql.match(/offset\s+(\d+)/i);
|
|
82
|
+
|
|
83
|
+
if (literalLimit) {
|
|
84
|
+
limitVal = literalLimit[1];
|
|
85
|
+
} else if (params && params.length > 0) {
|
|
86
|
+
// For parameterized queries, try to infer from params
|
|
87
|
+
// LIMIT/OFFSET are usually at the end
|
|
88
|
+
if (hasLimit && hasOffset && params.length >= 2) {
|
|
89
|
+
limitVal = String(params[params.length - 2]);
|
|
90
|
+
offsetVal = String(params[params.length - 1]);
|
|
91
|
+
} else if (hasLimit && params.length >= 1) {
|
|
92
|
+
limitVal = String(params[params.length - 1]);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (literalOffset) {
|
|
97
|
+
offsetVal = literalOffset[1];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
context = `SELECT with LIMIT ${limitVal} OFFSET ${offsetVal}`;
|
|
101
|
+
} else if (hasOrderBy) {
|
|
102
|
+
context = `SELECT all from ${tableName} (ordered)`;
|
|
103
|
+
} else {
|
|
104
|
+
context = `SELECT all from ${tableName}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const operation: SQLOperation = {
|
|
108
|
+
type: "raw-query",
|
|
109
|
+
sql,
|
|
110
|
+
params: params as unknown[],
|
|
111
|
+
method,
|
|
112
|
+
rowCount: result.rows?.length ?? 0,
|
|
113
|
+
context,
|
|
114
|
+
timestamp: startTime,
|
|
115
|
+
};
|
|
116
|
+
interceptor.onOperation(operation);
|
|
117
|
+
} else if (sqlLower.startsWith("insert")) {
|
|
118
|
+
const intoMatch = sql.match(/into\s+["']?(\w+)["']?/i);
|
|
119
|
+
context = `INSERT into ${intoMatch?.[1] || "unknown"}`;
|
|
120
|
+
|
|
121
|
+
const operation: SQLOperation = {
|
|
122
|
+
type: "raw-query",
|
|
123
|
+
sql,
|
|
124
|
+
params: params as unknown[],
|
|
125
|
+
method,
|
|
126
|
+
rowCount: 0,
|
|
127
|
+
context,
|
|
128
|
+
timestamp: startTime,
|
|
129
|
+
};
|
|
130
|
+
interceptor.onOperation(operation);
|
|
131
|
+
} else if (sqlLower.startsWith("update")) {
|
|
132
|
+
const tableMatch = sql.match(/update\s+["']?(\w+)["']?/i);
|
|
133
|
+
context = `UPDATE ${tableMatch?.[1] || "unknown"}`;
|
|
134
|
+
|
|
135
|
+
const operation: SQLOperation = {
|
|
136
|
+
type: "raw-query",
|
|
137
|
+
sql,
|
|
138
|
+
params: params as unknown[],
|
|
139
|
+
method,
|
|
140
|
+
rowCount: 0,
|
|
141
|
+
context,
|
|
142
|
+
timestamp: startTime,
|
|
143
|
+
};
|
|
144
|
+
interceptor.onOperation(operation);
|
|
145
|
+
} else if (sqlLower.startsWith("delete")) {
|
|
146
|
+
const fromMatch = sql.match(/from\s+["']?(\w+)["']?/i);
|
|
147
|
+
context = `DELETE from ${fromMatch?.[1] || "unknown"}`;
|
|
148
|
+
|
|
149
|
+
const operation: SQLOperation = {
|
|
150
|
+
type: "raw-query",
|
|
151
|
+
sql,
|
|
152
|
+
params: params as unknown[],
|
|
153
|
+
method,
|
|
154
|
+
rowCount: 0,
|
|
155
|
+
context,
|
|
156
|
+
timestamp: startTime,
|
|
157
|
+
};
|
|
158
|
+
interceptor.onOperation(operation);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return result;
|
|
163
|
+
}, config);
|
|
164
|
+
};
|
|
@@ -3,12 +3,16 @@ import {
|
|
|
3
3
|
customSqliteMigrate,
|
|
4
4
|
type DurableSqliteMigrationConfig,
|
|
5
5
|
} from "@firtoz/drizzle-sqlite-wasm/sqlite-wasm-migrator";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
drizzleSqliteWasmWorker,
|
|
8
|
+
createInstrumentedDrizzle,
|
|
9
|
+
} from "@firtoz/drizzle-sqlite-wasm/drizzle-sqlite-wasm-worker";
|
|
7
10
|
import type { ISqliteWorkerClient } from "../worker/manager";
|
|
8
11
|
import {
|
|
9
12
|
initializeSqliteWorker,
|
|
10
13
|
isSqliteWorkerInitialized,
|
|
11
14
|
} from "../worker/global-manager";
|
|
15
|
+
import type { SQLInterceptor } from "../collections/sqlite-collection";
|
|
12
16
|
|
|
13
17
|
export const useDrizzleSqliteDb = <TSchema extends Record<string, unknown>>(
|
|
14
18
|
WorkerConstructor: new () => Worker,
|
|
@@ -16,6 +20,8 @@ export const useDrizzleSqliteDb = <TSchema extends Record<string, unknown>>(
|
|
|
16
20
|
schema: TSchema,
|
|
17
21
|
migrations: DurableSqliteMigrationConfig,
|
|
18
22
|
debug?: boolean,
|
|
23
|
+
/** Optional interceptor to log ALL SQL queries (including direct Drizzle queries) */
|
|
24
|
+
interceptor?: SQLInterceptor,
|
|
19
25
|
) => {
|
|
20
26
|
const resolveRef = useRef<null | (() => void)>(null);
|
|
21
27
|
const rejectRef = useRef<null | ((error: unknown) => void)>(null);
|
|
@@ -72,45 +78,66 @@ export const useDrizzleSqliteDb = <TSchema extends Record<string, unknown>>(
|
|
|
72
78
|
};
|
|
73
79
|
}, [dbName, WorkerConstructor]);
|
|
74
80
|
|
|
81
|
+
// Store interceptor in a ref to avoid recreating drizzle on interceptor changes
|
|
82
|
+
const interceptorRef = useRef(interceptor);
|
|
83
|
+
interceptorRef.current = interceptor;
|
|
84
|
+
|
|
75
85
|
// Create drizzle instance with a callback-based approach that waits for the client
|
|
86
|
+
// Use instrumented version if interceptor is provided to log ALL queries
|
|
76
87
|
const drizzle = useMemo(() => {
|
|
77
88
|
if (debug) {
|
|
78
89
|
console.log(`[DEBUG] ${dbName} - creating drizzle proxy wrapper`);
|
|
79
90
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
client.onStarted(callback);
|
|
106
|
-
},
|
|
107
|
-
terminate: () => {
|
|
108
|
-
sqliteClientRef.current?.terminate();
|
|
109
|
-
},
|
|
91
|
+
|
|
92
|
+
const client: ISqliteWorkerClient = {
|
|
93
|
+
performRemoteCallback: (data, resolve, reject) => {
|
|
94
|
+
const actualClient = sqliteClientRef.current;
|
|
95
|
+
if (!actualClient) {
|
|
96
|
+
console.error(
|
|
97
|
+
`[DEBUG] ${dbName} - performRemoteCallback called but no sqliteClient yet`,
|
|
98
|
+
);
|
|
99
|
+
reject(
|
|
100
|
+
new Error(`Database ${dbName} not ready yet - still initializing`),
|
|
101
|
+
);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
actualClient.performRemoteCallback(data, resolve, reject);
|
|
105
|
+
},
|
|
106
|
+
onStarted: (callback) => {
|
|
107
|
+
const actualClient = sqliteClientRef.current;
|
|
108
|
+
if (!actualClient) {
|
|
109
|
+
console.warn(
|
|
110
|
+
`[DEBUG] ${dbName} - onStarted called but no sqliteClient yet`,
|
|
111
|
+
);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
actualClient.onStarted(callback);
|
|
110
115
|
},
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
116
|
+
terminate: () => {
|
|
117
|
+
sqliteClientRef.current?.terminate();
|
|
118
|
+
},
|
|
119
|
+
checkpoint: () => {
|
|
120
|
+
return sqliteClientRef.current?.checkpoint() ?? Promise.resolve();
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// Use instrumented version if interceptor is provided
|
|
125
|
+
// Use a wrapper that accesses the ref so interceptor changes don't recreate drizzle
|
|
126
|
+
const interceptorWrapper: SQLInterceptor = {
|
|
127
|
+
onOperation: (op) => interceptorRef.current?.onOperation?.(op),
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// Always use instrumented if initial interceptor was provided
|
|
131
|
+
if (interceptor) {
|
|
132
|
+
return createInstrumentedDrizzle<TSchema>(
|
|
133
|
+
client,
|
|
134
|
+
{ schema },
|
|
135
|
+
interceptorWrapper,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return drizzleSqliteWasmWorker<TSchema>(client, { schema });
|
|
140
|
+
}, [schema, dbName, !!interceptor]); // Only recreate if interceptor presence changes, not on every render
|
|
114
141
|
|
|
115
142
|
useEffect(() => {
|
|
116
143
|
if (!sqliteClient) {
|
package/src/types.ts
ADDED
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
// type BindingSpec,
|
|
3
|
-
Database,
|
|
4
|
-
Sqlite3Static,
|
|
5
|
-
} from "@sqlite.org/sqlite-wasm";
|
|
6
|
-
|
|
7
1
|
import { WorkerHelper } from "@firtoz/worker-helper";
|
|
8
2
|
import {
|
|
9
3
|
SqliteWorkerClientMessageSchema,
|
|
@@ -18,6 +12,7 @@ import {
|
|
|
18
12
|
} from "./schema";
|
|
19
13
|
import { handleRemoteCallback } from "../drizzle/handle-callback";
|
|
20
14
|
import { exhaustiveGuard } from "@firtoz/maybe-error";
|
|
15
|
+
import type { Sqlite3Static, Database } from "../types";
|
|
21
16
|
|
|
22
17
|
// Declare self as DedicatedWorkerGlobalScope for TypeScript
|
|
23
18
|
declare var self: DedicatedWorkerGlobalScope;
|
|
@@ -27,7 +22,13 @@ class SqliteWorkerHelper extends WorkerHelper<
|
|
|
27
22
|
SqliteWorkerServerMessage
|
|
28
23
|
> {
|
|
29
24
|
private initPromise: Promise<Sqlite3Static>;
|
|
30
|
-
private databases = new Map<
|
|
25
|
+
private databases = new Map<
|
|
26
|
+
DbId,
|
|
27
|
+
{
|
|
28
|
+
db: Database;
|
|
29
|
+
initialized: boolean;
|
|
30
|
+
}
|
|
31
|
+
>();
|
|
31
32
|
|
|
32
33
|
constructor() {
|
|
33
34
|
super(self, SqliteWorkerClientMessageSchema, sqliteWorkerServerMessage, {
|
|
@@ -50,10 +51,7 @@ class SqliteWorkerHelper extends WorkerHelper<
|
|
|
50
51
|
|
|
51
52
|
this.initPromise = import("@sqlite.org/sqlite-wasm").then(
|
|
52
53
|
async ({ default: sqlite3InitModule }) => {
|
|
53
|
-
const result = await sqlite3InitModule(
|
|
54
|
-
print: this.log.bind(this),
|
|
55
|
-
printErr: this.error.bind(this),
|
|
56
|
-
});
|
|
54
|
+
const result = await sqlite3InitModule();
|
|
57
55
|
|
|
58
56
|
return result;
|
|
59
57
|
},
|