@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.
- package/README.md +2 -1
- package/dist/fts5/wa-sqlite.mjs +5 -5
- package/dist/fts5/wa-sqlite.node.mjs +976 -1155
- package/dist/fts5/wa-sqlite.node.wasm +0 -0
- package/dist/fts5/wa-sqlite.wasm +0 -0
- package/dist/wa-sqlite.mjs +5 -5
- package/dist/wa-sqlite.node.mjs +974 -1153
- package/dist/wa-sqlite.node.wasm +0 -0
- package/dist/wa-sqlite.wasm +0 -0
- package/package.json +8 -8
- package/src/WebLocksMixin.js +8 -6
- package/src/examples/IDBMirrorVFS.js +16 -8
- package/src/examples/OPFSAdaptiveVFS.js +1 -1
- package/src/examples/OPFSPermutedVFS.js +3 -0
- package/src/examples/README.md +1 -1
- package/src/sqlite-api.js +17 -12
- package/src/types/index.d.ts +13 -0
- package/test/callbacks.test.js +156 -0
package/dist/wa-sqlite.node.wasm
CHANGED
|
Binary file
|
package/dist/wa-sqlite.wasm
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livestore/wa-sqlite",
|
|
3
|
-
"version": "1.0.
|
|
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
|
+
}
|
package/src/WebLocksMixin.js
CHANGED
|
@@ -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
|
|
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}
|
|
127
|
+
* @param {number} fileId
|
|
126
128
|
* @param {number} op
|
|
127
129
|
* @param {DataView} pArg
|
|
128
130
|
* @returns {number|Promise<number>}
|
|
129
131
|
*/
|
|
130
|
-
jFileControl(
|
|
131
|
-
const lockState = this.#mapIdToState.get(
|
|
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(
|
|
135
|
-
return this.#mapIdToState.get(
|
|
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
|
-
|
|
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}
|
|
@@ -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
|
package/src/examples/README.md
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
1056
|
-
|
|
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
|
};
|
package/src/types/index.d.ts
CHANGED
|
@@ -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
|
*
|
package/test/callbacks.test.js
CHANGED
|
@@ -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
|
}
|