@effect-agent/storage-sqlite 0.1.0-beta.42 → 0.1.0-beta.45

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.
@@ -139,6 +139,7 @@ type CheckpointError =
139
139
  | SqliteStorageCorruptionError
140
140
  | SqliteStorageError
141
141
  | SqliteWriteContention;
142
+
142
143
  const storageError =
143
144
  (operation: string) =>
144
145
  (error: SqlError): SqliteStorageError =>
@@ -194,21 +195,25 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
194
195
  const sql = yield* SqlClient.SqlClient;
195
196
  const { hit: failpoint } = yield* SqliteStorageFailpoint;
196
197
  const { busyTimeout } = yield* SqliteStorageConfig;
198
+
197
199
  yield* sql`PRAGMA foreign_keys = ON`.pipe(Effect.mapError(storageError("enable foreign keys")));
198
200
  // PRAGMA statements do not accept bound parameters; the value is a schema-validated
199
201
  // non-negative integer, never caller-controlled text.
200
202
  yield* sql
201
203
  .unsafe(`PRAGMA busy_timeout = ${busyTimeout}`)
202
204
  .pipe(Effect.mapError(storageError("configure busy timeout")));
205
+
203
206
  const journalModeRows = yield* sql<Record<string, unknown>>`PRAGMA journal_mode`.pipe(
204
207
  Effect.mapError(storageError("read journal mode")),
205
208
  );
209
+
206
210
  const journalMode = yield* decodeSingleRow(
207
211
  Schema.Array(SqliteJournalModeRow),
208
212
  "pragma_journal_mode",
209
213
  "singleton",
210
214
  journalModeRows,
211
215
  );
216
+
212
217
  if (journalMode.journal_mode.toLowerCase() !== "wal") {
213
218
  return yield* SqliteStorageCompatibilityError.make({
214
219
  actualVersion: 0,
@@ -220,6 +225,7 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
220
225
  const versionRows = yield* sql<Record<string, unknown>>`PRAGMA user_version`.pipe(
221
226
  Effect.mapError(storageError("read storage version")),
222
227
  );
228
+
223
229
  const version = yield* decodeSingleRow(
224
230
  Schema.Array(SqliteVersionRow),
225
231
  "pragma_user_version",
@@ -249,6 +255,7 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
249
255
  AND name LIKE 'effect_agent_%'
250
256
  ORDER BY name
251
257
  `.pipe(Effect.mapError(storageError("inspect unversioned storage")));
258
+
252
259
  const existing = yield* decodeRows(
253
260
  Schema.Array(SqliteNameRow),
254
261
  "sqlite_master",
@@ -296,12 +303,14 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
296
303
  )
297
304
  ORDER BY name
298
305
  `.pipe(Effect.mapError(storageError("verify storage tables")));
306
+
299
307
  const required = yield* decodeRows(
300
308
  Schema.Array(SqliteNameRow),
301
309
  "sqlite_master",
302
310
  "required_tables",
303
311
  requiredRows,
304
312
  );
313
+
305
314
  if (required.length !== 12) {
306
315
  return yield* SqliteStorageCompatibilityError.make({
307
316
  actualVersion: CurrentSqliteStorageVersion,
@@ -345,19 +354,24 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
345
354
  const connection = yield* sql.reserve.pipe(
346
355
  Effect.mapError(classifyWriteFailure(operation)),
347
356
  );
357
+
348
358
  yield* connection
349
359
  .executeUnprepared("BEGIN IMMEDIATE", [], undefined)
350
360
  .pipe(Effect.mapError(classifyWriteFailure(operation)));
361
+
351
362
  const exit = yield* restore(
352
363
  Effect.provideService(effect, sql.transactionService, [connection, 0] as const),
353
364
  ).pipe(Effect.exit);
365
+
354
366
  if (Exit.isSuccess(exit)) {
355
367
  yield* connection
356
368
  .executeUnprepared("COMMIT", [], undefined)
357
369
  .pipe(Effect.mapError(classifyWriteFailure(operation)));
370
+
358
371
  return exit.value;
359
372
  }
360
373
  yield* Effect.orDie(connection.executeUnprepared("ROLLBACK", [], undefined));
374
+
361
375
  return yield* exit;
362
376
  }),
363
377
  ).pipe(
@@ -380,19 +394,24 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
380
394
  Effect.scoped(
381
395
  Effect.gen(function* () {
382
396
  const connection = yield* sql.reserve.pipe(Effect.mapError(storageError(operation)));
397
+
383
398
  yield* connection
384
399
  .executeUnprepared("BEGIN", [], undefined)
385
400
  .pipe(Effect.mapError(storageError(operation)));
401
+
386
402
  const exit = yield* restore(
387
403
  Effect.provideService(effect, sql.transactionService, [connection, 0] as const),
388
404
  ).pipe(Effect.exit);
405
+
389
406
  if (Exit.isSuccess(exit)) {
390
407
  yield* connection
391
408
  .executeUnprepared("COMMIT", [], undefined)
392
409
  .pipe(Effect.mapError(storageError(operation)));
410
+
393
411
  return exit.value;
394
412
  }
395
413
  yield* Effect.orDie(connection.executeUnprepared("ROLLBACK", [], undefined));
414
+
396
415
  return yield* exit;
397
416
  }),
398
417
  ).pipe(Effect.withSpan("SqliteJournal.withReadTransaction", { attributes: { operation } })),
@@ -428,12 +447,14 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
428
447
  FROM effect_agent_threads
429
448
  WHERE thread_id = ${threadId}
430
449
  `.pipe(Effect.mapError(storageError("read materialized thread")));
450
+
431
451
  const existing = yield* decodeRows(
432
452
  Schema.Array(ThreadRow),
433
453
  "effect_agent_threads",
434
454
  threadId,
435
455
  existingRows,
436
456
  );
457
+
437
458
  if (existing.length > 1) {
438
459
  return yield* SqliteStorageCorruptionError.make({
439
460
  table: "effect_agent_threads",
@@ -457,6 +478,7 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
457
478
  ${producerEpoch}
458
479
  )
459
480
  `.pipe(Effect.mapError(storageError("materialize thread")));
481
+
460
482
  return;
461
483
  }
462
484
  if (producerEpoch < existing[0].producer_epoch) {
@@ -488,6 +510,7 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
488
510
  FROM effect_agent_threads
489
511
  WHERE thread_id = ${threadId}
490
512
  `.pipe(Effect.mapError(storageError("read thread")));
513
+
491
514
  return yield* decodeRows(Schema.Array(ThreadRow), "effect_agent_threads", threadId, rows);
492
515
  });
493
516
 
@@ -511,9 +534,11 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
511
534
  message: "Canonical identifiers or encoded JSON exceed the SQLite storage bounds.",
512
535
  });
513
536
  }
537
+
514
538
  return yield* withWriteTransaction("append transaction")(
515
539
  Effect.gen(function* () {
516
540
  const recordIds = request.records.map((record) => record.recordId);
541
+
517
542
  if (new Set(recordIds).size !== recordIds.length) {
518
543
  return yield* SqliteAppendConflict.make({
519
544
  message: `Batch ${request.batchId} contains duplicate canonical record IDs.`,
@@ -531,6 +556,7 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
531
556
  FROM effect_agent_threads
532
557
  WHERE thread_id = ${request.threadId}
533
558
  `.pipe(Effect.mapError(storageError("read append tail")));
559
+
534
560
  const thread = yield* decodeSingleRow(
535
561
  Schema.Array(ThreadRow),
536
562
  "effect_agent_threads",
@@ -559,6 +585,7 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
559
585
  WHERE thread_id = ${request.threadId}
560
586
  AND batch_id = ${request.batchId}
561
587
  `.pipe(Effect.mapError(storageError("read idempotent batch")));
588
+
562
589
  const batches = yield* decodeRows(
563
590
  Schema.Array(BatchRow),
564
591
  "effect_agent_canonical_batches",
@@ -575,12 +602,14 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
575
602
  }
576
603
  if (batches.length === 1) {
577
604
  const existing = batches[0];
605
+
578
606
  if (existing.batch_digest !== request.batchDigest) {
579
607
  return yield* SqliteAppendConflict.make({
580
608
  message: `Batch ${request.batchId} already exists with different canonical content.`,
581
609
  reason: "batch-digest",
582
610
  });
583
611
  }
612
+
584
613
  return RawAppendResult.make({
585
614
  firstSequence: existing.first_sequence,
586
615
  lastSequence: existing.last_sequence,
@@ -621,12 +650,14 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
621
650
  AND record_id IN ${sql.in(recordIds)}
622
651
  ORDER BY sequence
623
652
  `.pipe(Effect.mapError(storageError("check canonical record identities")));
653
+
624
654
  const existingRecords = yield* decodeRows(
625
655
  Schema.Array(RecordRow),
626
656
  "effect_agent_canonical_records",
627
657
  `${request.threadId}/record_ids`,
628
658
  existingRecordRows,
629
659
  );
660
+
630
661
  if (existingRecords.length > 0) {
631
662
  return yield* SqliteAppendConflict.make({
632
663
  message: `Canonical record ID ${existingRecords[0].record_id} already exists.`,
@@ -645,6 +676,7 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
645
676
  }),
646
677
  ),
647
678
  );
679
+
648
680
  const lastSequence = yield* Schema.decodeUnknownEffect(CanonicalSequence)(
649
681
  firstSequence + request.records.length - 1,
650
682
  ).pipe(
@@ -736,6 +768,7 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
736
768
  ORDER BY sequence
737
769
  LIMIT ${request.limit}
738
770
  `.pipe(Effect.mapError(storageError("read canonical records")));
771
+
739
772
  return yield* decodeRows(
740
773
  Schema.Array(RecordRow),
741
774
  "effect_agent_canonical_records",
@@ -757,13 +790,16 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
757
790
  FROM effect_agent_threads
758
791
  WHERE thread_id = ${threadId}
759
792
  `.pipe(Effect.mapError(storageError("export thread")));
793
+
760
794
  const thread = yield* decodeSingleRow(
761
795
  Schema.Array(ThreadRow),
762
796
  "effect_agent_threads",
763
797
  threadId,
764
798
  threadRows,
765
799
  );
800
+
766
801
  yield* failpoint("export:after-thread-read");
802
+
767
803
  const batchRows = yield* sql<Record<string, unknown>>`
768
804
  SELECT
769
805
  thread_id,
@@ -777,6 +813,7 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
777
813
  WHERE thread_id = ${threadId}
778
814
  ORDER BY first_sequence
779
815
  `.pipe(Effect.mapError(storageError("export canonical batches")));
816
+
780
817
  const recordRows = yield* sql<Record<string, unknown>>`
781
818
  SELECT
782
819
  thread_id,
@@ -788,6 +825,7 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
788
825
  WHERE thread_id = ${threadId}
789
826
  ORDER BY sequence
790
827
  `.pipe(Effect.mapError(storageError("export canonical records")));
828
+
791
829
  const checkpointRows = yield* sql<Record<string, unknown>>`
792
830
  SELECT
793
831
  thread_id,
@@ -848,12 +886,14 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
848
886
  FROM effect_agent_threads
849
887
  WHERE thread_id = ${checkpoint.threadId}
850
888
  `.pipe(Effect.mapError(storageError("read checkpoint tail")));
889
+
851
890
  const thread = yield* decodeSingleRow(
852
891
  Schema.Array(ThreadRow),
853
892
  "effect_agent_threads",
854
893
  checkpoint.threadId,
855
894
  threadRows,
856
895
  );
896
+
857
897
  if (checkpoint.throughSequence > thread.tail_sequence) {
858
898
  return yield* SqliteCheckpointConflict.make({
859
899
  message:
@@ -872,12 +912,14 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
872
912
  WHERE thread_id = ${checkpoint.threadId}
873
913
  AND through_sequence = ${checkpoint.throughSequence}
874
914
  `.pipe(Effect.mapError(storageError("read idempotent checkpoint")));
915
+
875
916
  const existing = yield* decodeRows(
876
917
  Schema.Array(CheckpointRow),
877
918
  "effect_agent_checkpoints",
878
919
  `${checkpoint.threadId}/${checkpoint.throughSequence}`,
879
920
  checkpointRows,
880
921
  );
922
+
881
923
  if (existing.length > 1) {
882
924
  return yield* SqliteStorageCorruptionError.make({
883
925
  table: "effect_agent_checkpoints",
@@ -894,6 +936,7 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
894
936
  message: "A different checkpoint already exists at this canonical sequence.",
895
937
  });
896
938
  }
939
+
897
940
  return;
898
941
  }
899
942
 
@@ -930,6 +973,7 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
930
973
  ORDER BY through_sequence DESC
931
974
  LIMIT 1
932
975
  `.pipe(Effect.mapError(storageError("load checkpoint")));
976
+
933
977
  return yield* decodeRows(
934
978
  Schema.Array(CheckpointRow),
935
979
  "effect_agent_checkpoints",
@@ -944,12 +988,14 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
944
988
  ) {
945
989
  if (sequence === 0) {
946
990
  const threads = yield* getThread(threadId);
991
+
947
992
  return threads.length === 0
948
993
  ? []
949
994
  : [threads[0].tail_sequence === 0 ? threads[0].tail_digest : undefined].filter(
950
995
  (value): value is string => value !== undefined,
951
996
  );
952
997
  }
998
+
953
999
  const rows = yield* sql<Record<string, unknown>>`
954
1000
  SELECT
955
1001
  thread_id,
@@ -963,12 +1009,14 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
963
1009
  WHERE thread_id = ${threadId}
964
1010
  AND last_sequence = ${sequence}
965
1011
  `.pipe(Effect.mapError(storageError("read canonical digest at sequence")));
1012
+
966
1013
  const batches = yield* decodeRows(
967
1014
  Schema.Array(BatchRow),
968
1015
  "effect_agent_canonical_batches",
969
1016
  `${threadId}/${sequence}`,
970
1017
  rows,
971
1018
  );
1019
+
972
1020
  return batches.map((batch) => batch.tail_digest);
973
1021
  });
974
1022
 
@@ -985,6 +1033,7 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
985
1033
  FROM effect_agent_threads
986
1034
  ORDER BY thread_id
987
1035
  `.pipe(Effect.mapError(storageError("scan threads")));
1036
+
988
1037
  const batches = yield* sql<Record<string, unknown>>`
989
1038
  SELECT
990
1039
  thread_id,
@@ -997,6 +1046,7 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
997
1046
  FROM effect_agent_canonical_batches
998
1047
  ORDER BY thread_id, first_sequence
999
1048
  `.pipe(Effect.mapError(storageError("scan canonical batches")));
1049
+
1000
1050
  const records = yield* sql<Record<string, unknown>>`
1001
1051
  SELECT
1002
1052
  thread_id,
@@ -1007,6 +1057,7 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
1007
1057
  FROM effect_agent_canonical_records
1008
1058
  ORDER BY thread_id, sequence
1009
1059
  `.pipe(Effect.mapError(storageError("scan canonical records")));
1060
+
1010
1061
  const checkpoints = yield* sql<Record<string, unknown>>`
1011
1062
  SELECT
1012
1063
  thread_id,
@@ -1016,6 +1067,7 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
1016
1067
  FROM effect_agent_checkpoints
1017
1068
  ORDER BY thread_id, through_sequence
1018
1069
  `.pipe(Effect.mapError(storageError("scan checkpoints")));
1070
+
1019
1071
  return {
1020
1072
  threads: yield* decodeRows(
1021
1073
  Schema.Array(ThreadRow),