@livestore/wa-sqlite 1.0.3-dev.6 → 1.0.5-dev.1

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.
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livestore/wa-sqlite",
3
- "version": "1.0.3-dev.6",
3
+ "version": "1.0.5-dev.1",
4
4
  "type": "module",
5
5
  "main": "src/sqlite-api.js",
6
6
  "types": "src/types/index.d.ts",
@@ -15,12 +15,6 @@
15
15
  "dist/*",
16
16
  "test/*"
17
17
  ],
18
- "scripts": {
19
- "build-docs": "typedoc",
20
- "start": "web-dev-server --node-resolve",
21
- "test": "web-test-runner",
22
- "test-manual": "web-test-runner --manual"
23
- },
24
18
  "devDependencies": {
25
19
  "@types/jasmine": "^5.1.4",
26
20
  "@web/dev-server": "^0.4.6",
@@ -40,5 +34,11 @@
40
34
  "web-test-runner-jasmine@0.0.6": {
41
35
  "unplugged": true
42
36
  }
37
+ },
38
+ "scripts": {
39
+ "build-docs": "typedoc",
40
+ "start": "web-dev-server --node-resolve",
41
+ "test": "web-test-runner",
42
+ "test-manual": "web-test-runner --manual"
43
43
  }
44
- }
44
+ }
@@ -82,8 +82,10 @@ export const WebLocksMixin = superclass => class extends superclass {
82
82
  */
83
83
  async jUnlock(fileId, lockType) {
84
84
  try {
85
+ // SQLite can call xUnlock() without ever calling xLock() so
86
+ // the state may not exist.
85
87
  const lockState = this.#mapIdToState.get(fileId);
86
- if (lockType >= lockState.type) return VFS.SQLITE_OK;
88
+ if (!(lockType < lockState?.type)) return VFS.SQLITE_OK;
87
89
 
88
90
  switch (this.#options.lockPolicy) {
89
91
  case 'exclusive':
@@ -122,17 +124,17 @@ export const WebLocksMixin = superclass => class extends superclass {
122
124
  }
123
125
 
124
126
  /**
125
- * @param {number} pFile
127
+ * @param {number} fileId
126
128
  * @param {number} op
127
129
  * @param {DataView} pArg
128
130
  * @returns {number|Promise<number>}
129
131
  */
130
- jFileControl(pFile, op, pArg) {
131
- const lockState = this.#mapIdToState.get(pFile) ??
132
+ jFileControl(fileId, op, pArg) {
133
+ const lockState = this.#mapIdToState.get(fileId) ??
132
134
  (() => {
133
135
  // Call jLock() to create the lock state.
134
- this.jLock(pFile, VFS.SQLITE_LOCK_NONE);
135
- return this.#mapIdToState.get(pFile);
136
+ this.jLock(fileId, VFS.SQLITE_LOCK_NONE);
137
+ return this.#mapIdToState.get(fileId);
136
138
  })();
137
139
  if (op === WebLocksMixin.WRITE_HINT_OP_CODE &&
138
140
  this.#options.lockPolicy === 'shared+hint'){
@@ -332,14 +332,7 @@ export class IDBMirrorVFS extends FacadeVFS {
332
332
  const file = this.#mapIdToFile.get(fileId);
333
333
 
334
334
  if (file.flags & VFS.SQLITE_OPEN_MAIN_DB) {
335
- if (!file.txActive) {
336
- file.txActive = {
337
- path: file.path,
338
- txId: file.viewTx.txId + 1,
339
- blocks: new Map(),
340
- fileSize: file.blockSize * file.blocks.size,
341
- };
342
- }
335
+ this.#requireTxActive(file);
343
336
  file.txActive.blocks.set(iOffset, pData.slice());
344
337
  file.txActive.fileSize = Math.max(file.txActive.fileSize, iOffset + pData.byteLength);
345
338
  file.blockSize = pData.byteLength;
@@ -375,6 +368,7 @@ export class IDBMirrorVFS extends FacadeVFS {
375
368
  const file = this.#mapIdToFile.get(fileId);
376
369
 
377
370
  if (file.flags & VFS.SQLITE_OPEN_MAIN_DB) {
371
+ this.#requireTxActive(file);
378
372
  file.txActive.fileSize = iSize;
379
373
  } else {
380
374
  // All files that are not main databases are stored in a single
@@ -717,6 +711,20 @@ export class IDBMirrorVFS extends FacadeVFS {
717
711
  file.txWriteHint = false;
718
712
  }
719
713
 
714
+ /**
715
+ * @param {File} file
716
+ */
717
+ #requireTxActive(file) {
718
+ if (!file.txActive) {
719
+ file.txActive = {
720
+ path: file.path,
721
+ txId: file.viewTx.txId + 1,
722
+ blocks: new Map(),
723
+ fileSize: file.blockSize * file.blocks.size,
724
+ };
725
+ }
726
+ }
727
+
720
728
  /**
721
729
  * @param {string} path
722
730
  * @returns {Promise}
@@ -413,7 +413,7 @@ export class OPFSAdaptiveVFS extends WebLocksMixin(FacadeVFS) {
413
413
  this.lastError = e;
414
414
  return VFS.SQLITE_IOERR;
415
415
  }
416
- return VFS.SQLITE_NOTFOUND;
416
+ return super.jFileControl(fileId, op, pArg);
417
417
  }
418
418
 
419
419
  jGetLastError(zBuf) {
@@ -463,6 +463,9 @@ export class OPFSPermutedVFS extends FacadeVFS {
463
463
  const file = this.#mapIdToFile.get(fileId);
464
464
  if ((file.flags & VFS.SQLITE_OPEN_MAIN_DB) && !file.txIsOverwrite) {
465
465
  file.abortController.signal.throwIfAborted();
466
+ if (!file.txActive) {
467
+ this.#beginTx(file);
468
+ }
466
469
  file.txActive.fileSize = iSize;
467
470
 
468
471
  // Remove now obsolete pages from file.txActive.pages
@@ -19,7 +19,7 @@ Changing the page size after the database is created is not supported (this is a
19
19
  ### IDBMirrorVFS
20
20
  This VFS keeps all files in memory, persisting database files to IndexedDB. It works on all contexts.
21
21
 
22
- IDBBatchAtomicVFS can trade durability for performance by setting `PRAGMA synchronous=normal`.
22
+ IDBMirrorVFS can trade durability for performance by setting `PRAGMA synchronous=normal`.
23
23
 
24
24
  Changing the page size after the database is created is not supported.
25
25
 
package/src/sqlite-api.js CHANGED
@@ -499,6 +499,7 @@ export function Factory(Module) {
499
499
  columns = columns ?? sqlite3.column_names(stmt);
500
500
  const row = sqlite3.row(stmt);
501
501
  callback(row, columns);
502
+ callback(row, columns);
502
503
  }
503
504
  }
504
505
  }
@@ -764,6 +765,7 @@ export function Factory(Module) {
764
765
 
765
766
  const stmts = [];
766
767
 
768
+ // return (async function*() {
767
769
  const onFinally = [];
768
770
  // try {
769
771
  // Encode SQL string to UTF-8.
@@ -802,15 +804,6 @@ export function Factory(Module) {
802
804
  // Call sqlite3_prepare_v3() for the next statement.
803
805
  // Allow retry operations.
804
806
  const zTail = Module.getValue(pzTail, '*');
805
- // const rc = await retry(() => {
806
- // return prepare(
807
- // db,
808
- // zTail,
809
- // pzEnd - pzTail,
810
- // options.flags || 0,
811
- // pStmt,
812
- // pzTail);
813
- // });
814
807
  const rc = prepare(
815
808
  db,
816
809
  zTail,
@@ -854,6 +847,11 @@ export function Factory(Module) {
854
847
  };
855
848
  })();
856
849
 
850
+ sqlite3.commit_hook = function(db, xCommitHook) {
851
+ verifyDatabase(db);
852
+ Module.commit_hook(db, xCommitHook);
853
+ };
854
+
857
855
  sqlite3.update_hook = function(db, xUpdateHook) {
858
856
  verifyDatabase(db);
859
857
 
@@ -1048,12 +1046,19 @@ export function Factory(Module) {
1048
1046
  const changesetOutPtr = Module.getValue(outPtrPtr, 'i32');
1049
1047
 
1050
1048
  // Copy the inverted changeset data
1051
- const changesetOut = new Uint8Array(Module.HEAPU8.buffer, changesetOutPtr, outLength);
1049
+ const changesetOut = new Uint8Array(Module.HEAPU8.buffer, changesetOutPtr, outLength).slice();
1052
1050
 
1053
1051
  // Free allocated memory
1054
1052
  Module._sqlite3_free(inPtr);
1055
- Module._sqlite3_free(outLengthPtr);
1056
- Module._sqlite3_free(outPtrPtr);
1053
+
1054
+ // TODO investigate why freeing these pointers causes a crash
1055
+ // RuntimeError: Out of bounds memory access (evaluating 'Module._sqlite3_free(outLengthPtr)')
1056
+ // Repro: https://gist.github.com/schickling/08b10b6fda8583601e586cb0bea333ce
1057
+
1058
+ // Module._sqlite3_free(outLengthPtr);
1059
+ // Module._sqlite3_free(outPtrPtr);
1060
+
1061
+ Module._sqlite3_free(changesetOutPtr);
1057
1062
 
1058
1063
  return changesetOut;
1059
1064
  };
@@ -525,6 +525,19 @@ interface SQLiteAPI {
525
525
  */
526
526
  column_type(stmt: number, i: number): number;
527
527
 
528
+ /**
529
+ * Register a commit hook
530
+ *
531
+ * @see https://www.sqlite.org/c3ref/commit_hook.html
532
+ *
533
+ * @param db database pointer
534
+ * @param callback If a non-zero value is returned, commit is converted into
535
+ * a rollback; disables callback when null
536
+ */
537
+ commit_hook(
538
+ db: number,
539
+ callback: (() => number) | null): void;
540
+
528
541
  /**
529
542
  * Create or redefine SQL functions
530
543
  *
@@ -422,4 +422,160 @@ for (const [key, factory] of FACTORIES) {
422
422
  expect(calls).toEqual([[23, "main", "t", 1n]]);
423
423
  });
424
424
  });
425
+
426
+ describe(`${key} commit_hook`, function() {
427
+ let db;
428
+ beforeEach(async function() {
429
+ db = await sqlite3.open_v2(':memory:');
430
+ });
431
+
432
+ afterEach(async function() {
433
+ await sqlite3.close(db);
434
+ });
435
+
436
+ it('should call commit hook', async function() {
437
+ let rc;
438
+
439
+ let callsCount = 0;
440
+ const resetCallsCount = () => callsCount = 0;
441
+
442
+ sqlite3.commit_hook(db, () => {
443
+ callsCount++;
444
+ return 0;
445
+ });
446
+ expect(callsCount).toEqual(0);
447
+ resetCallsCount();
448
+
449
+ rc = await sqlite3.exec(db, `
450
+ CREATE TABLE t(i integer primary key, x);
451
+ `);
452
+ expect(rc).toEqual(SQLite.SQLITE_OK);
453
+ expect(callsCount).toEqual(1);
454
+ resetCallsCount();
455
+
456
+ rc = await sqlite3.exec(db, `
457
+ SELECT * FROM t;
458
+ `);
459
+ expect(callsCount).toEqual(0);
460
+ resetCallsCount();
461
+
462
+ rc = await sqlite3.exec(db, `
463
+ BEGIN TRANSACTION;
464
+ INSERT INTO t VALUES (1, 'foo');
465
+ ROLLBACK;
466
+ `);
467
+ expect(callsCount).toEqual(0);
468
+ resetCallsCount();
469
+
470
+ rc = await sqlite3.exec(db, `
471
+ BEGIN TRANSACTION;
472
+ INSERT INTO t VALUES (1, 'foo');
473
+ INSERT INTO t VALUES (2, 'bar');
474
+ COMMIT;
475
+ `);
476
+ expect(callsCount).toEqual(1);
477
+ resetCallsCount();
478
+ });
479
+
480
+ it('can change commit hook', async function() {
481
+ let rc;
482
+ rc = await sqlite3.exec(db, `
483
+ CREATE TABLE t(i integer primary key, x);
484
+ `);
485
+ expect(rc).toEqual(SQLite.SQLITE_OK);
486
+
487
+ let a = 0;
488
+ let b = 0;
489
+
490
+ // set hook to increment `a` on commit
491
+ sqlite3.commit_hook(db, () => {
492
+ a++;
493
+ return 0;
494
+ });
495
+ rc = await sqlite3.exec(db, `
496
+ INSERT INTO t VALUES (1, 'foo');
497
+ `);
498
+ expect(a).toEqual(1);
499
+ expect(b).toEqual(0);
500
+
501
+ // switch to increment `b`
502
+ sqlite3.commit_hook(db, () => {
503
+ b++;
504
+ return 0;
505
+ });
506
+
507
+ rc = await sqlite3.exec(db, `
508
+ INSERT INTO t VALUES (2, 'bar');
509
+ `);
510
+ expect(rc).toEqual(SQLite.SQLITE_OK);
511
+ expect(a).toEqual(1);
512
+ expect(b).toEqual(1);
513
+
514
+ // disable hook by passing null
515
+ sqlite3.commit_hook(db, null);
516
+
517
+ rc = await sqlite3.exec(db, `
518
+ INSERT INTO t VALUES (3, 'qux');
519
+ `);
520
+ expect(rc).toEqual(SQLite.SQLITE_OK);
521
+ expect(a).toEqual(1);
522
+ expect(b).toEqual(1);
523
+ });
524
+
525
+ it('can rollback based on return value', async function() {
526
+ let rc;
527
+ rc = await sqlite3.exec(db, `
528
+ CREATE TABLE t(i integer primary key, x);
529
+ `);
530
+ expect(rc).toEqual(SQLite.SQLITE_OK);
531
+
532
+ // accept commit by returning 0
533
+ sqlite3.commit_hook(db, () => 0);
534
+ rc = await sqlite3.exec(db, `
535
+ INSERT INTO t VALUES (1, 'foo');
536
+ `);
537
+ expect(rc).toEqual(SQLite.SQLITE_OK);
538
+
539
+ // reject commit by returning 1, causing rollback
540
+ sqlite3.commit_hook(db, () => 1);
541
+ await expectAsync(
542
+ sqlite3.exec(db, `INSERT INTO t VALUES (2, 'bar');`)
543
+ ).toBeRejected();
544
+
545
+ // double-check that the insert was rolled back
546
+ let hasRow = false;
547
+ rc = await sqlite3.exec(db, `
548
+ SELECT * FROM t WHERE i = 2;
549
+ `, () => hasRow = true);
550
+ expect(rc).toEqual(SQLite.SQLITE_OK);
551
+ expect(hasRow).toBeFalse();
552
+ });
553
+
554
+ it('does not overwrite update_hook', async function() {
555
+ let rc;
556
+ rc = await sqlite3.exec(db, `
557
+ CREATE TABLE t(i integer primary key, x);
558
+ `);
559
+ expect(rc).toEqual(SQLite.SQLITE_OK);
560
+
561
+ let updateHookInvocationsCount = 0;
562
+ sqlite3.update_hook(db, (...args) => {
563
+ updateHookInvocationsCount++;
564
+ });
565
+
566
+ let commitHookInvocationsCount = 0;
567
+ sqlite3.commit_hook(db, () => {
568
+ commitHookInvocationsCount++;
569
+ return 0;
570
+ });
571
+
572
+ rc = await sqlite3.exec(db, `
573
+ INSERT INTO t VALUES (1, 'foo');
574
+ `);
575
+ expect(rc).toEqual(SQLite.SQLITE_OK);
576
+
577
+ expect(updateHookInvocationsCount).toEqual(1);
578
+ expect(commitHookInvocationsCount).toEqual(1);
579
+ });
580
+ });
425
581
  }