@peerbit/indexer-sqlite3 2.1.1 → 2.1.2-000e3f1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peerbit/indexer-sqlite3",
3
- "version": "2.1.1",
3
+ "version": "2.1.2-000e3f1",
4
4
  "description": "SQLite index for document store",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -61,20 +61,20 @@
61
61
  "dependencies": {
62
62
  "better-sqlite3": "^12.5.0",
63
63
  "@dao-xyz/borsh": "^6.0.0",
64
+ "@peerbit/indexer-interface": "2.1.1-000e3f1",
65
+ "@peerbit/crypto": "2.4.1-000e3f1",
66
+ "@peerbit/time": "2.3.0-000e3f1",
64
67
  "@sqlite.org/sqlite-wasm": "^3.51.1-build2",
65
68
  "p-defer": "^4.0.0",
66
69
  "uint8arrays": "^5.1.0",
67
70
  "uuid": "^10.0.0",
68
- "libsodium-wrappers": "0.7.15",
69
- "@peerbit/crypto": "2.4.1",
70
- "@peerbit/indexer-interface": "2.1.1",
71
- "@peerbit/time": "2.3.0"
71
+ "libsodium-wrappers": "0.7.15"
72
72
  },
73
73
  "devDependencies": {
74
74
  "@types/better-sqlite3": "^7.6.13",
75
- "esbuild": "0.27.0",
76
- "@peerbit/indexer-tests": "2.0.1",
77
- "@peerbit/build-assets": "1.1.0"
75
+ "@peerbit/indexer-tests": "2.0.1-000e3f1",
76
+ "@peerbit/build-assets": "1.1.0-000e3f1",
77
+ "esbuild": "0.27.0"
78
78
  },
79
79
  "scripts": {
80
80
  "clean": "aegir clean",
package/src/engine.ts CHANGED
@@ -87,6 +87,38 @@ async function getIgnoreFK(stmt: Statement, values: any[]) {
87
87
  export class SQLLiteIndex<T extends Record<string, any>>
88
88
  implements Index<T, any>
89
89
  {
90
+ // SQLite writes are inherently serialized per connection.
91
+ // We still need an explicit async barrier because our API is async and
92
+ // awaits between statements (root insert -> many child inserts). Without
93
+ // a barrier, concurrent `put()` and `del()` can interleave mid-insert and
94
+ // create large volumes of FK constraint noise (and occasional timeouts in
95
+ // browser/webworker runners).
96
+ // TODO(perf): This is intentionally coarse-grained for correctness.
97
+ // Possible optimizations:
98
+ // 1) wrap nested writes in explicit transactions to reduce lock time;
99
+ // 2) use table/key-scoped write queues when overlap detection is available.
100
+ // Any relaxation must keep concurrent put/del stability across all runners.
101
+ private _writeBarrier: Promise<void> = Promise.resolve();
102
+
103
+ private async withWriteBarrier<R>(fn: () => Promise<R>): Promise<R> {
104
+ const prev = this._writeBarrier;
105
+ let release!: () => void;
106
+ const next = new Promise<void>((r) => (release = r));
107
+ // Keep the chain alive even if `prev` rejected.
108
+ this._writeBarrier = prev.then(
109
+ () => next,
110
+ () => next,
111
+ );
112
+
113
+ // Wait for previous writer without propagating its error.
114
+ await prev.catch(() => undefined);
115
+ try {
116
+ return await fn();
117
+ } finally {
118
+ release();
119
+ }
120
+ }
121
+
90
122
  primaryKeyArr!: string[];
91
123
  primaryKeyString!: string;
92
124
  planner: QueryPlanner;
@@ -397,52 +429,54 @@ export class SQLLiteIndex<T extends Record<string, any>>
397
429
  }
398
430
 
399
431
  async put(value: T, _id?: any): Promise<void> {
400
- const classOfValue = value.constructor as Constructor<T>;
401
- return insert(
402
- async (values, table) => {
403
- let preId = values[table.primaryIndex];
404
- let statement: Statement | undefined = undefined;
405
- try {
406
- if (preId != null) {
407
- statement = this.properties.db.statements.get(
408
- replaceStatementKey(table),
409
- )!;
410
- this.fkMode === "race-tolerant"
411
- ? await runIgnoreFK(statement, values)
412
- : await statement.run(values);
413
- return preId;
414
- } else {
415
- statement = this.properties.db.statements.get(
416
- putStatementKey(table),
417
- )!;
418
- const out =
432
+ return this.withWriteBarrier(async () => {
433
+ const classOfValue = value.constructor as Constructor<T>;
434
+ return insert(
435
+ async (values, table) => {
436
+ let preId = values[table.primaryIndex];
437
+ let statement: Statement | undefined = undefined;
438
+ try {
439
+ if (preId != null) {
440
+ statement = this.properties.db.statements.get(
441
+ replaceStatementKey(table),
442
+ )!;
419
443
  this.fkMode === "race-tolerant"
420
- ? await getIgnoreFK(statement, values)
421
- : await statement.get(values);
422
-
423
- // TODO types
424
- if (out == null) {
425
- return undefined;
444
+ ? await runIgnoreFK(statement, values)
445
+ : await statement.run(values);
446
+ return preId;
447
+ } else {
448
+ statement = this.properties.db.statements.get(
449
+ putStatementKey(table),
450
+ )!;
451
+ const out =
452
+ this.fkMode === "race-tolerant"
453
+ ? await getIgnoreFK(statement, values)
454
+ : await statement.get(values);
455
+
456
+ // TODO types
457
+ if (out == null) {
458
+ return undefined;
459
+ }
460
+ return out[table.primary as string];
426
461
  }
427
- return out[table.primary as string];
462
+ } finally {
463
+ await statement?.reset?.();
428
464
  }
429
- } finally {
430
- await statement?.reset?.();
431
- }
432
- },
433
- value,
434
- this.tables,
435
- resolveTable(
436
- this.scopeString ? [this.scopeString] : [],
465
+ },
466
+ value,
437
467
  this.tables,
438
- classOfValue,
439
- true,
440
- ),
441
- getSchema(classOfValue).fields,
442
- (_fn) => {
443
- throw new Error("Unexpected");
444
- },
445
- );
468
+ resolveTable(
469
+ this.scopeString ? [this.scopeString] : [],
470
+ this.tables,
471
+ classOfValue,
472
+ true,
473
+ ),
474
+ getSchema(classOfValue).fields,
475
+ (_fn) => {
476
+ throw new Error("Unexpected");
477
+ },
478
+ );
479
+ });
446
480
  }
447
481
 
448
482
  iterate<S extends Shape | undefined>(
@@ -629,46 +663,48 @@ export class SQLLiteIndex<T extends Record<string, any>>
629
663
  }
630
664
 
631
665
  async del(query: types.DeleteOptions): Promise<types.IdKey[]> {
632
- let ret: types.IdKey[] = [];
633
- let once = false;
634
- let lastError: Error | undefined = undefined;
635
- for (const table of this._rootTables) {
636
- try {
637
- const { sql, bindable } = convertDeleteRequestToQuery(
638
- query,
639
- this.tables,
640
- table,
641
- );
642
- const stmt = await this.properties.db.prepare(sql, sql);
643
- const results: any[] = await stmt.all(bindable);
644
-
645
- // TODO types
646
- for (const result of results) {
647
- ret.push(
648
- types.toId(
649
- convertFromSQLType(
650
- result[table.primary as string],
651
- table.primaryField!.from!.type,
652
- ),
653
- ),
666
+ return this.withWriteBarrier(async () => {
667
+ let ret: types.IdKey[] = [];
668
+ let once = false;
669
+ let lastError: Error | undefined = undefined;
670
+ for (const table of this._rootTables) {
671
+ try {
672
+ const { sql, bindable } = convertDeleteRequestToQuery(
673
+ query,
674
+ this.tables,
675
+ table,
654
676
  );
655
- }
656
- once = true;
657
- } catch (error) {
658
- if (error instanceof MissingFieldError) {
659
- lastError = error;
660
- continue;
661
- }
677
+ const stmt = await this.properties.db.prepare(sql, sql);
678
+ const results: any[] = await stmt.all(bindable);
662
679
 
663
- throw error;
680
+ // TODO types
681
+ for (const result of results) {
682
+ ret.push(
683
+ types.toId(
684
+ convertFromSQLType(
685
+ result[table.primary as string],
686
+ table.primaryField!.from!.type,
687
+ ),
688
+ ),
689
+ );
690
+ }
691
+ once = true;
692
+ } catch (error) {
693
+ if (error instanceof MissingFieldError) {
694
+ lastError = error;
695
+ continue;
696
+ }
697
+
698
+ throw error;
699
+ }
664
700
  }
665
- }
666
701
 
667
- if (!once) {
668
- throw lastError!;
669
- }
702
+ if (!once) {
703
+ throw lastError!;
704
+ }
670
705
 
671
- return ret;
706
+ return ret;
707
+ });
672
708
  }
673
709
 
674
710
  async sum(query: types.SumOptions): Promise<number | bigint> {
package/src/schema.ts CHANGED
@@ -207,7 +207,14 @@ const resolvePrimaryFieldInfoFromSchema = (
207
207
  fieldType = fieldType.elementType;
208
208
  }
209
209
 
210
- fieldType = unwrapNestedType(fieldType);
210
+ // fixedArray(u8, N) represents bytes and must map to BLOB in SQL. Note that
211
+ // FixedArrayKind is also a WrappedType, so unwrapNestedType() would otherwise
212
+ // turn it into the scalar "u8" and incorrectly treat it as INTEGER.
213
+ if (fieldType instanceof FixedArrayKind && fieldType.elementType === "u8") {
214
+ fieldType = Uint8Array;
215
+ } else {
216
+ fieldType = unwrapNestedType(fieldType);
217
+ }
211
218
 
212
219
  // Arrays are always stored in separate tables.
213
220
  if (fieldType instanceof VecKind) {
@@ -215,7 +222,13 @@ const resolvePrimaryFieldInfoFromSchema = (
215
222
  }
216
223
 
217
224
  if (typeof fieldType === "string" || isUint8ArrayType(fieldType)) {
218
- const sqlField = createScalarSQLField(path, field, fieldType, primary, true);
225
+ const sqlField = createScalarSQLField(
226
+ path,
227
+ field,
228
+ fieldType,
229
+ primary,
230
+ true,
231
+ );
219
232
  if (sqlField.isPrimary) {
220
233
  return {
221
234
  name: sqlField.name,
@@ -538,7 +551,13 @@ export const getSQLFields = (
538
551
  type: FieldType,
539
552
  isOptional: boolean,
540
553
  ) => {
541
- const sqlField = createScalarSQLField(path, field, type, primary, isOptional);
554
+ const sqlField = createScalarSQLField(
555
+ path,
556
+ field,
557
+ type,
558
+ primary,
559
+ isOptional,
560
+ );
542
561
  foundPrimary = foundPrimary || sqlField.isPrimary;
543
562
  sqlFields.push(sqlField);
544
563
  };