@orkestrel/database 0.0.17 → 0.0.19

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.
@@ -4,7 +4,73 @@ let _orkestrel_contract = require("@orkestrel/contract");
4
4
  let node_fs_promises = require("node:fs/promises");
5
5
  let node_path = require("node:path");
6
6
  let _orkestrel_sqlite = require("@orkestrel/sqlite");
7
- //#region src/server/constants.ts
7
+ // src/server/SQLiteTransaction.ts
8
+ /**
9
+ * Owns the candidate schema and table identities of one SQLite transaction.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * const transaction = new SQLiteTransaction(new Map(), new Map())
14
+ * transaction.schema.size // 0
15
+ * ```
16
+ */
17
+ var SQLiteTransaction = class {
18
+ #schema;
19
+ #identities;
20
+ constructor(schema, identities) {
21
+ this.#schema = new Map(schema);
22
+ this.#identities = new Map(identities);
23
+ }
24
+ get schema() {
25
+ return this.#schema;
26
+ }
27
+ get identities() {
28
+ return this.#identities;
29
+ }
30
+ project(schema, identities) {
31
+ this.#schema = new Map(schema.map((table) => [table.name, table]));
32
+ this.#identities = identities;
33
+ }
34
+ };
35
+ // src/server/JSONTransaction.ts
36
+ /**
37
+ * Owns the isolated memory store and schema of one JSON transaction.
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * const transaction = new JSONTransaction(new MemoryDriver(), [], new Map())
42
+ * transaction.stop()
43
+ * // transaction.memory throws CONFLICT after settlement.
44
+ * ```
45
+ */
46
+ var JSONTransaction = class {
47
+ #memory;
48
+ #schema;
49
+ #identities;
50
+ constructor(memory, schema, identities) {
51
+ this.#memory = memory;
52
+ this.#schema = schema;
53
+ this.#identities = new Map(identities);
54
+ }
55
+ get memory() {
56
+ if (this.#memory === void 0) throw new _src_core.DatabaseError("CONFLICT", "Transaction scope has settled");
57
+ return this.#memory;
58
+ }
59
+ get schema() {
60
+ return this.#schema;
61
+ }
62
+ get identities() {
63
+ return this.#identities;
64
+ }
65
+ project(schema, identities) {
66
+ this.#schema = schema;
67
+ this.#identities = identities;
68
+ }
69
+ stop() {
70
+ this.#memory = void 0;
71
+ }
72
+ };
73
+ // src/server/constants.ts
8
74
  /**
9
75
  * Lists the declared {@link ColumnStorage}s whose SQL equality comparisons (`equals` /
10
76
  * `not` / `any` / `none`) and `starts` / `ends` compiles are provably
@@ -53,8 +119,7 @@ var EXACT_RANGE_COLUMN_STORAGE = Object.freeze([
53
119
  * reservation — the caller's concern to avoid, documented on the driver class.
54
120
  */
55
121
  var METADATA_TABLE = "_metadata";
56
- //#endregion
57
- //#region src/server/helpers.ts
122
+ // src/server/helpers.ts
58
123
  /**
59
124
  * Reports whether a caught filesystem error says that nothing is there to read.
60
125
  *
@@ -236,12 +301,7 @@ function matchesAggregateExactly(operation, column, schema) {
236
301
  function matchesSQLiteAffinity(declared, storage) {
237
302
  if (!(0, _orkestrel_contract.isString)(declared)) return false;
238
303
  const type = declared.toUpperCase();
239
- let affinity;
240
- if (type.includes("INT")) affinity = "INTEGER";
241
- else if (type.includes("CHAR") || type.includes("CLOB") || type.includes("TEXT")) affinity = "TEXT";
242
- else if (type === "" || type.includes("BLOB")) affinity = "BLOB";
243
- else if (type.includes("REAL") || type.includes("FLOA") || type.includes("DOUB")) affinity = "REAL";
244
- else affinity = "NUMERIC";
304
+ const affinity = type.includes("INT") ? "INTEGER" : type.includes("CHAR") || type.includes("CLOB") || type.includes("TEXT") ? "TEXT" : type === "" || type.includes("BLOB") ? "BLOB" : type.includes("REAL") || type.includes("FLOA") || type.includes("DOUB") ? "REAL" : "NUMERIC";
245
305
  if (storage === "integer" || storage === "boolean") return affinity === "INTEGER";
246
306
  if (storage === "text" || storage === "json") return affinity === "TEXT";
247
307
  if (storage === "blob") return affinity === "BLOB";
@@ -454,8 +514,7 @@ function decodeRow(row, schema) {
454
514
  function deriveSQLiteIndexName(table, columns) {
455
515
  return "idx_" + [table, ...columns].map((part) => String(part.length) + "_" + part).join("_");
456
516
  }
457
- //#endregion
458
- //#region src/server/inferers.ts
517
+ // src/server/inferers.ts
459
518
  /**
460
519
  * Reads the storage type a nested (`json_extract`) operand encodes as from its
461
520
  * runtime value, never as `json`.
@@ -484,8 +543,7 @@ function inferValueStorage(value) {
484
543
  if ((0, _orkestrel_contract.isObject)(value)) return "json";
485
544
  return "text";
486
545
  }
487
- //#endregion
488
- //#region src/server/compilers.ts
546
+ // src/server/compilers.ts
489
547
  /**
490
548
  * Maps a portable {@link ColumnStorage} to its SQLite column type.
491
549
  *
@@ -633,17 +691,13 @@ function compileConditionSQL(condition, schema) {
633
691
  const second = condition.values[1];
634
692
  const nullOperand = first === null || first === void 0;
635
693
  const jsonType = !(0, _orkestrel_contract.isString)(condition.column) ? compileJSONTypeSQL(condition.column) : "";
636
- let sql;
637
- let values;
638
694
  switch (condition.operator) {
639
695
  case "equals":
640
696
  if (nullOperand && nested) return {
641
697
  sql: jsonType + " = 'null'",
642
698
  parameters: []
643
699
  };
644
- sql = column + " = ?";
645
- values = [first];
646
- break;
700
+ return compileBoundSQL(column + " = ?", [first], nested, declared);
647
701
  case "not":
648
702
  if (nullOperand) {
649
703
  if (nested) return {
@@ -655,37 +709,14 @@ function compileConditionSQL(condition, schema) {
655
709
  parameters: []
656
710
  };
657
711
  }
658
- sql = "(" + column + " != ? OR " + column + " IS NULL)";
659
- values = [first];
660
- break;
661
- case "above":
662
- sql = column + " > ?";
663
- values = [first];
664
- break;
665
- case "below":
666
- sql = "(" + column + " < ? OR " + column + " IS NULL)";
667
- values = [first];
668
- break;
669
- case "from":
670
- sql = column + " >= ?";
671
- values = [first];
672
- break;
673
- case "to":
674
- sql = "(" + column + " <= ? OR " + column + " IS NULL)";
675
- values = [first];
676
- break;
677
- case "between":
678
- sql = column + " BETWEEN ? AND ?";
679
- values = [first, second];
680
- break;
681
- case "like":
682
- sql = column + " LIKE ?";
683
- values = [first];
684
- break;
685
- case "glob":
686
- sql = column + " GLOB ?";
687
- values = [first];
688
- break;
712
+ return compileBoundSQL("(" + column + " != ? OR " + column + " IS NULL)", [first], nested, declared);
713
+ case "above": return compileBoundSQL(column + " > ?", [first], nested, declared);
714
+ case "below": return compileBoundSQL("(" + column + " < ? OR " + column + " IS NULL)", [first], nested, declared);
715
+ case "from": return compileBoundSQL(column + " >= ?", [first], nested, declared);
716
+ case "to": return compileBoundSQL("(" + column + " <= ? OR " + column + " IS NULL)", [first], nested, declared);
717
+ case "between": return compileBoundSQL(column + " BETWEEN ? AND ?", [first, second], nested, declared);
718
+ case "like": return compileBoundSQL(column + " LIKE ?", [first], nested, declared);
719
+ case "glob": return compileBoundSQL(column + " GLOB ?", [first], nested, declared);
689
720
  case "starts": {
690
721
  const text = (0, _orkestrel_contract.isString)(first) ? first : "";
691
722
  if (text === "") return {
@@ -693,9 +724,7 @@ function compileConditionSQL(condition, schema) {
693
724
  parameters: []
694
725
  };
695
726
  const length = Array.from(text).length;
696
- sql = "(typeof(" + column + ") = 'text' AND substr(" + column + ", 1, " + length + ") = ?)";
697
- values = [first];
698
- break;
727
+ return compileBoundSQL("(typeof(" + column + ") = 'text' AND substr(" + column + ", 1, " + length + ") = ?)", [first], nested, declared);
699
728
  }
700
729
  case "ends": {
701
730
  const text = (0, _orkestrel_contract.isString)(first) ? first : "";
@@ -704,26 +733,26 @@ function compileConditionSQL(condition, schema) {
704
733
  parameters: []
705
734
  };
706
735
  const length = Array.from(text).length;
707
- sql = "(typeof(" + column + ") = 'text' AND substr(" + column + ", -" + length + ") = ?)";
708
- values = [first];
709
- break;
736
+ return compileBoundSQL("(typeof(" + column + ") = 'text' AND substr(" + column + ", -" + length + ") = ?)", [first], nested, declared);
710
737
  }
711
- case "any":
738
+ case "any": {
712
739
  if (condition.values.length === 0) return {
713
740
  sql: "0",
714
741
  parameters: []
715
742
  };
716
- sql = column + " IN (" + condition.values.map(() => "?").join(", ") + ")";
717
- values = condition.values;
718
- break;
719
- case "none":
743
+ const sql = column + " IN (" + condition.values.map(() => "?").join(", ") + ")";
744
+ const values = condition.values;
745
+ return compileBoundSQL(sql, values, nested, declared);
746
+ }
747
+ case "none": {
720
748
  if (condition.values.length === 0) return {
721
749
  sql: "1",
722
750
  parameters: []
723
751
  };
724
- sql = "(" + column + " NOT IN (" + condition.values.map(() => "?").join(", ") + ") OR " + column + " IS NULL)";
725
- values = condition.values;
726
- break;
752
+ const sql = "(" + column + " NOT IN (" + condition.values.map(() => "?").join(", ") + ") OR " + column + " IS NULL)";
753
+ const values = condition.values;
754
+ return compileBoundSQL(sql, values, nested, declared);
755
+ }
727
756
  case "absent": return {
728
757
  sql: column + " IS NULL",
729
758
  parameters: []
@@ -733,6 +762,17 @@ function compileConditionSQL(condition, schema) {
733
762
  parameters: []
734
763
  };
735
764
  }
765
+ }
766
+ /**
767
+ * Encodes condition operands for a SQL fragment using their declared or inferred storage.
768
+ *
769
+ * @example
770
+ * ```ts
771
+ * compileBoundSQL('"age" > ?', [18], false, undefined)
772
+ * // { sql: '"age" > ?', parameters: ['18'] }
773
+ * ```
774
+ */
775
+ function compileBoundSQL(sql, values, nested, declared) {
736
776
  return {
737
777
  sql,
738
778
  parameters: values.map((value) => {
@@ -774,17 +814,18 @@ function compileWhereSQL(conditions, schema) {
774
814
  parameters: []
775
815
  };
776
816
  const head = compileConditionSQL(first, schema);
777
- let clause = head.sql;
778
- const parameters = [...head.parameters];
779
- for (const condition of remaining) {
817
+ const fragments = remaining.map((condition) => {
780
818
  const next = compileConditionSQL(condition, schema);
781
- const operator = condition.connector === "or" ? "OR" : "AND";
782
- clause = "(" + clause + " " + operator + " " + next.sql + ")";
783
- parameters.push(...next.parameters);
784
- }
819
+ return {
820
+ sql: " " + (condition.connector === "or" ? "OR" : "AND") + " " + next.sql + ")",
821
+ parameters: next.parameters
822
+ };
823
+ });
824
+ const opening = "(".repeat(fragments.length);
825
+ const tail = fragments.map((part) => part.sql).join("");
785
826
  return {
786
- sql: "WHERE " + clause,
787
- parameters
827
+ sql: "WHERE " + opening + head.sql + tail,
828
+ parameters: [...head.parameters, ...fragments.flatMap((part) => part.parameters)]
788
829
  };
789
830
  }
790
831
  /**
@@ -944,8 +985,7 @@ function stepToSQL(step) {
944
985
  case "index.remove": return ["DROP INDEX IF EXISTS " + quoteIdentifier(deriveSQLiteIndexName(step.table, step.index))];
945
986
  }
946
987
  }
947
- //#endregion
948
- //#region src/server/drivers/JSONDriver.ts
988
+ // src/server/drivers/JSONDriver.ts
949
989
  /**
950
990
  * Implements a persistent {@link DriverInterface} backed by a single JSON file — the
951
991
  * reference {@link MemoryDriver} plus file load / flush.
@@ -990,10 +1030,7 @@ var JSONDriver = class {
990
1030
  #metadata;
991
1031
  #flushCount = 0;
992
1032
  #chain = Promise.resolve();
993
- #transaction;
994
- #candidate;
995
- #candidateIdentities;
996
- #candidateSchema;
1033
+ #transactions = /* @__PURE__ */ new Set();
997
1034
  constructor(path) {
998
1035
  this.#path = path;
999
1036
  }
@@ -1137,33 +1174,7 @@ var JSONDriver = class {
1137
1174
  }
1138
1175
  async #open(declared) {
1139
1176
  const parsed = await this.#document();
1140
- let stored;
1141
- let tables;
1142
- if (parsed === void 0) {
1143
- const fresh = {};
1144
- for (const table of declared) fresh[table.name] = [];
1145
- tables = fresh;
1146
- } else {
1147
- if (!(0, _orkestrel_contract.isRecord)(parsed) || !Object.hasOwn(parsed, "tables") || Object.keys(parsed).some((key) => key !== "tables" && key !== "metadata")) throw new _src_core.DatabaseError("DRIVER", "Stored JSON database document is invalid", {
1148
- path: this.#path,
1149
- aspect: "document"
1150
- });
1151
- if (!(0, _orkestrel_contract.isRecord)(parsed.tables)) throw new _src_core.DatabaseError("DRIVER", "Stored JSON tables are invalid", {
1152
- path: this.#path,
1153
- aspect: "tables"
1154
- });
1155
- tables = parsed.tables;
1156
- if (Object.hasOwn(parsed, "metadata")) try {
1157
- stored = (0, _src_core.cloneDriverMetadata)(parsed.metadata);
1158
- } catch {
1159
- const cause = new _src_core.DatabaseError("VALIDATION", "Stored JSON metadata failed validation", { path: "metadata" });
1160
- throw new _src_core.DatabaseError("DRIVER", "Stored JSON metadata is invalid", {
1161
- path: this.#path,
1162
- aspect: "metadata",
1163
- cause
1164
- });
1165
- }
1166
- }
1177
+ const [stored, tables] = this.#decodeDocument(parsed, declared);
1167
1178
  const schema = (0, _src_core.normalizeDriverSchema)(stored?.schema ?? declared);
1168
1179
  const names = new Set(schema.map((table) => table.name));
1169
1180
  for (const table of schema) if (!Object.hasOwn(tables, table.name)) throw new _src_core.DatabaseError("DRIVER", "Stored JSON table set is invalid", {
@@ -1210,33 +1221,23 @@ var JSONDriver = class {
1210
1221
  async #transact(scope) {
1211
1222
  this.#root();
1212
1223
  const candidate = await this.#clone();
1213
- const token = {};
1214
- this.#transaction = token;
1215
- this.#candidate = candidate;
1216
- this.#candidateIdentities = new Map(this.#identities);
1217
- this.#candidateSchema = this.#schema;
1224
+ const token = new JSONTransaction(candidate, this.#schema, this.#identities);
1225
+ this.#transactions.add(token);
1218
1226
  try {
1219
1227
  const value = await scope(this.#capability(token));
1220
- const schema = this.#candidateSchema;
1221
- const identities = this.#candidateIdentities;
1222
- if (schema === void 0 || identities === void 0) throw new _src_core.DatabaseError("CONFLICT", "Transaction scope has settled");
1223
- this.#candidate = void 0;
1224
- this.#candidateIdentities = void 0;
1225
- this.#candidateSchema = void 0;
1228
+ const schema = token.schema;
1229
+ const identities = token.identities;
1230
+ token.stop();
1226
1231
  const metadata = await candidate.metadata();
1227
1232
  await this.#serialize(void 0, candidate, schema, metadata);
1228
1233
  this.#memory = candidate;
1229
- this.#identities = identities;
1234
+ this.#identities = new Map(identities);
1230
1235
  this.#schema = schema;
1231
1236
  this.#metadata = metadata === void 0 ? void 0 : (0, _src_core.cloneDriverMetadata)(metadata);
1232
1237
  return value;
1233
1238
  } finally {
1234
- if (this.#transaction === token) {
1235
- this.#transaction = void 0;
1236
- this.#candidate = void 0;
1237
- this.#candidateIdentities = void 0;
1238
- this.#candidateSchema = void 0;
1239
- }
1239
+ token.stop();
1240
+ this.#transactions.delete(token);
1240
1241
  }
1241
1242
  }
1242
1243
  async #clone() {
@@ -1336,13 +1337,11 @@ var JSONDriver = class {
1336
1337
  }
1337
1338
  async #migrateCandidate(token, input) {
1338
1339
  const memory = this.#requireCandidate(token);
1339
- const schema = this.#candidateSchema;
1340
- const identities = this.#candidateIdentities;
1341
- if (schema === void 0 || identities === void 0) throw new _src_core.DatabaseError("CONFLICT", "Transaction scope has settled");
1340
+ const schema = token.schema;
1341
+ const identities = token.identities;
1342
1342
  const owned = (0, _src_core.cloneMigrationInput)(input);
1343
1343
  const projected = this.#projectIdentities(identities, owned.plan.steps);
1344
- this.#candidateSchema = await this.#apply(memory, schema, owned);
1345
- this.#candidateIdentities = projected;
1344
+ token.project(await this.#apply(memory, schema, owned), projected);
1346
1345
  }
1347
1346
  async #metadataCandidate(token) {
1348
1347
  const metadata = await this.#requireCandidate(token).metadata();
@@ -1354,8 +1353,8 @@ var JSONDriver = class {
1354
1353
  await memory.stamp(owned);
1355
1354
  }
1356
1355
  #requireCandidate(token) {
1357
- if (this.#transaction !== token || this.#candidate === void 0) throw new _src_core.DatabaseError("CONFLICT", "Transaction scope has settled");
1358
- return this.#candidate;
1356
+ if (!this.#transactions.has(token)) throw new _src_core.DatabaseError("CONFLICT", "Transaction scope has settled");
1357
+ return token.memory;
1359
1358
  }
1360
1359
  #alignIdentities(schema) {
1361
1360
  const aligned = /* @__PURE__ */ new Map();
@@ -1371,19 +1370,34 @@ var JSONDriver = class {
1371
1370
  return projected;
1372
1371
  }
1373
1372
  #root() {
1374
- if (this.#transaction !== void 0) throw new _src_core.DatabaseError("CONFLICT", "A transaction is active on this driver");
1373
+ if (this.#transactions.size !== 0) throw new _src_core.DatabaseError("CONFLICT", "A transaction is active on this driver");
1375
1374
  }
1376
- async #document() {
1377
- let raw;
1378
- try {
1379
- raw = await (0, node_fs_promises.readFile)(this.#path, "utf-8");
1380
- } catch (error) {
1381
- if (matchesAbsentPath(error)) return void 0;
1382
- throw new _src_core.DatabaseError("DRIVER", "Failed to read the JSON database file", {
1375
+ #decodeDocument(parsed, declared) {
1376
+ if (parsed === void 0) return [void 0, Object.fromEntries(Iterator.from(declared).map((table) => [table.name, []]))];
1377
+ if (!(0, _orkestrel_contract.isRecord)(parsed) || !Object.hasOwn(parsed, "tables") || Object.keys(parsed).some((key) => key !== "tables" && key !== "metadata")) throw new _src_core.DatabaseError("DRIVER", "Stored JSON database document is invalid", {
1378
+ path: this.#path,
1379
+ aspect: "document"
1380
+ });
1381
+ if (!(0, _orkestrel_contract.isRecord)(parsed.tables)) throw new _src_core.DatabaseError("DRIVER", "Stored JSON tables are invalid", {
1382
+ path: this.#path,
1383
+ aspect: "tables"
1384
+ });
1385
+ const tables = parsed.tables;
1386
+ if (Object.hasOwn(parsed, "metadata")) try {
1387
+ return [(0, _src_core.cloneDriverMetadata)(parsed.metadata), tables];
1388
+ } catch {
1389
+ const cause = new _src_core.DatabaseError("VALIDATION", "Stored JSON metadata failed validation", { path: "metadata" });
1390
+ throw new _src_core.DatabaseError("DRIVER", "Stored JSON metadata is invalid", {
1383
1391
  path: this.#path,
1384
- cause: error
1392
+ aspect: "metadata",
1393
+ cause
1385
1394
  });
1386
1395
  }
1396
+ return [void 0, tables];
1397
+ }
1398
+ async #document() {
1399
+ const raw = await this.#readDocument();
1400
+ if (raw === void 0) return void 0;
1387
1401
  try {
1388
1402
  return JSON.parse(raw);
1389
1403
  } catch {
@@ -1393,6 +1407,17 @@ var JSONDriver = class {
1393
1407
  });
1394
1408
  }
1395
1409
  }
1410
+ async #readDocument() {
1411
+ try {
1412
+ return await (0, node_fs_promises.readFile)(this.#path, "utf-8");
1413
+ } catch (error) {
1414
+ if (matchesAbsentPath(error)) return void 0;
1415
+ throw new _src_core.DatabaseError("DRIVER", "Failed to read the JSON database file", {
1416
+ path: this.#path,
1417
+ cause: error
1418
+ });
1419
+ }
1420
+ }
1396
1421
  async #hydrate(memory, schema, tables) {
1397
1422
  for (const table of schema) {
1398
1423
  const rows = tables[table.name];
@@ -1429,18 +1454,16 @@ var JSONDriver = class {
1429
1454
  }
1430
1455
  async #enqueue(operation, signal) {
1431
1456
  (0, _src_core.checkAbort)(signal);
1432
- let started = false;
1457
+ const cleanup = new AbortController();
1433
1458
  const next = this.#chain.then(async () => {
1434
- started = true;
1459
+ cleanup.abort();
1435
1460
  (0, _src_core.checkAbort)(signal);
1436
1461
  return operation();
1437
1462
  });
1438
1463
  this.#chain = next.then(() => {}, () => {});
1439
1464
  if (signal === void 0) return next;
1440
- const cleanup = new AbortController();
1441
1465
  return new Promise((resolve, reject) => {
1442
1466
  signal.addEventListener("abort", () => {
1443
- if (started) return;
1444
1467
  try {
1445
1468
  (0, _src_core.checkAbort)(signal);
1446
1469
  } catch (error) {
@@ -1533,7 +1556,6 @@ var JSONDriver = class {
1533
1556
  metadata: owned,
1534
1557
  tables
1535
1558
  };
1536
- let dispatched = false;
1537
1559
  try {
1538
1560
  await (0, node_fs_promises.mkdir)((0, node_path.dirname)(this.#path), { recursive: true });
1539
1561
  const serialized = JSON.stringify(payload, null, 2);
@@ -1544,35 +1566,35 @@ var JSONDriver = class {
1544
1566
  signal
1545
1567
  });
1546
1568
  (0, _src_core.checkAbort)(signal);
1547
- dispatched = true;
1569
+ } catch (error) {
1570
+ const aborted = (0, _orkestrel_contract.attempt)(() => (0, _src_core.checkAbort)(signal));
1571
+ await this.#persistFailure(aborted.success ? error : aborted.error, temp);
1572
+ }
1573
+ try {
1548
1574
  await (0, node_fs_promises.rename)(temp, this.#path);
1549
1575
  } catch (error) {
1550
- let cause = error;
1551
- if (!dispatched) try {
1552
- (0, _src_core.checkAbort)(signal);
1553
- } catch (abort) {
1554
- cause = abort;
1555
- }
1556
- try {
1557
- await (0, node_fs_promises.rm)(temp, { force: true });
1558
- } catch (cleanup) {
1559
- if (!matchesAbsentPath(cleanup)) throw new _src_core.DatabaseError("DRIVER", "Failed to persist and clean the database file", {
1560
- path: this.#path,
1561
- temp,
1562
- cause,
1563
- cleanup
1564
- });
1565
- }
1566
- if ((0, _src_core.isDatabaseError)(cause) && cause.code === "ABORTED") throw cause;
1567
- throw new _src_core.DatabaseError("DRIVER", "Failed to persist the database file", {
1576
+ await this.#persistFailure(error, temp);
1577
+ }
1578
+ }
1579
+ async #persistFailure(cause, temp) {
1580
+ try {
1581
+ await (0, node_fs_promises.rm)(temp, { force: true });
1582
+ } catch (cleanup) {
1583
+ if (!matchesAbsentPath(cleanup)) throw new _src_core.DatabaseError("DRIVER", "Failed to persist and clean the database file", {
1568
1584
  path: this.#path,
1569
- cause
1585
+ temp,
1586
+ cause,
1587
+ cleanup
1570
1588
  });
1571
1589
  }
1590
+ if ((0, _src_core.isDatabaseError)(cause) && cause.code === "ABORTED") throw cause;
1591
+ throw new _src_core.DatabaseError("DRIVER", "Failed to persist the database file", {
1592
+ path: this.#path,
1593
+ cause
1594
+ });
1572
1595
  }
1573
1596
  };
1574
- //#endregion
1575
- //#region src/server/drivers/SQLiteDriver.ts
1597
+ // src/server/drivers/SQLiteDriver.ts
1576
1598
  /**
1577
1599
  * Implements the {@link DriverInterface} over SQLite — the server-native, trusted-mode
1578
1600
  * backend built on the published `@orkestrel/sqlite` synchronous wrapper.
@@ -1627,9 +1649,7 @@ var SQLiteDriver = class {
1627
1649
  #database;
1628
1650
  #schema = /* @__PURE__ */ new Map();
1629
1651
  #identities = /* @__PURE__ */ new Map();
1630
- #transaction;
1631
- #candidateSchema;
1632
- #candidateIdentities;
1652
+ #transactions = /* @__PURE__ */ new Set();
1633
1653
  constructor(options = {}) {
1634
1654
  this.#path = options.path ?? ":memory:";
1635
1655
  this.#options = options;
@@ -1777,36 +1797,22 @@ var SQLiteDriver = class {
1777
1797
  this.#root();
1778
1798
  const database = this.#require();
1779
1799
  this.#guard(() => database.begin());
1780
- const token = {};
1781
- this.#transaction = token;
1782
- this.#candidateSchema = new Map(this.#schema);
1783
- this.#candidateIdentities = new Map(this.#identities);
1800
+ const token = new SQLiteTransaction(this.#schema, this.#identities);
1801
+ this.#transactions.add(token);
1784
1802
  try {
1785
- let value;
1786
- try {
1787
- value = await scope(this.#capability(token));
1788
- } catch (error) {
1789
- this.#guard(() => database.rollback());
1790
- throw error;
1791
- }
1803
+ const value = await this.#invokeTransaction(scope, token, database);
1792
1804
  try {
1793
1805
  this.#guard(() => database.commit());
1794
1806
  } catch (error) {
1795
1807
  if (database.transacting) this.#guard(() => database.rollback());
1796
1808
  throw error;
1797
1809
  }
1798
- const schema = this.#candidateSchema;
1799
- const identities = this.#candidateIdentities;
1800
- if (schema === void 0 || identities === void 0) throw new _src_core.DatabaseError("CONFLICT", "Transaction scope has settled");
1801
- this.#schema = schema;
1802
- this.#identities = identities;
1810
+ this.#requireTransaction(token);
1811
+ this.#schema = new Map(token.schema);
1812
+ this.#identities = new Map(token.identities);
1803
1813
  return value;
1804
1814
  } finally {
1805
- if (this.#transaction === token) {
1806
- this.#transaction = void 0;
1807
- this.#candidateSchema = void 0;
1808
- this.#candidateIdentities = void 0;
1809
- }
1815
+ this.#transactions.delete(token);
1810
1816
  }
1811
1817
  }
1812
1818
  /**
@@ -1997,17 +2003,12 @@ var SQLiteDriver = class {
1997
2003
  }
1998
2004
  const offset = input.offset ?? 0;
1999
2005
  const limit = input.limit;
2000
- let skipped = 0;
2001
- let yielded = 0;
2006
+ const page = new _src_core.StreamPage(offset);
2002
2007
  for await (const row of this.#scan(table)) {
2003
- if (limit !== void 0 && yielded >= limit) return;
2008
+ if (limit !== void 0 && page.count >= limit) return;
2004
2009
  if (conditions.length > 0 && !(0, _src_core.matchesQuery)(row, conditions)) continue;
2005
- if (skipped < offset) {
2006
- skipped += 1;
2007
- continue;
2008
- }
2010
+ if (!page.next()) continue;
2009
2011
  yield row;
2010
- yielded += 1;
2011
2012
  }
2012
2013
  }
2013
2014
  async *#iterate(schema, sql, parameters = []) {
@@ -2132,16 +2133,13 @@ var SQLiteDriver = class {
2132
2133
  table: METADATA_TABLE,
2133
2134
  aspect: "metadata"
2134
2135
  });
2135
- let parsed;
2136
- try {
2137
- parsed = JSON.parse(text);
2138
- } catch (error) {
2139
- throw new _src_core.DatabaseError("DRIVER", "Stored SQLite metadata JSON is invalid", {
2140
- table: METADATA_TABLE,
2141
- aspect: "metadata",
2142
- cause: error
2143
- });
2144
- }
2136
+ const decoded = (0, _orkestrel_contract.attempt)(() => JSON.parse(text));
2137
+ if (!decoded.success) throw new _src_core.DatabaseError("DRIVER", "Stored SQLite metadata JSON is invalid", {
2138
+ table: METADATA_TABLE,
2139
+ aspect: "metadata",
2140
+ cause: decoded.error
2141
+ });
2142
+ const parsed = decoded.value;
2145
2143
  if (!(0, _orkestrel_contract.isArray)(parsed)) throw new _src_core.DatabaseError("DRIVER", "Stored SQLite metadata schema is invalid", {
2146
2144
  table: METADATA_TABLE,
2147
2145
  aspect: "metadata"
@@ -2169,6 +2167,14 @@ var SQLiteDriver = class {
2169
2167
  #writeMetadata(database, metadata) {
2170
2168
  database.prepare("INSERT OR REPLACE INTO " + quoteIdentifier(METADATA_TABLE) + " (\"id\", \"version\", \"schema\") VALUES (1, ?, ?)").execute([metadata.version, JSON.stringify(metadata.schema)]);
2171
2169
  }
2170
+ async #invokeTransaction(scope, token, database) {
2171
+ try {
2172
+ return await scope(this.#capability(token));
2173
+ } catch (error) {
2174
+ this.#guard(() => database.rollback());
2175
+ throw error;
2176
+ }
2177
+ }
2172
2178
  #capability(token) {
2173
2179
  return {
2174
2180
  read: this.#readTransaction.bind(this, token),
@@ -2216,9 +2222,8 @@ var SQLiteDriver = class {
2216
2222
  this.#requireTransaction(token);
2217
2223
  const database = this.#require();
2218
2224
  const owned = (0, _src_core.cloneMigrationInput)(input);
2219
- const candidate = this.#candidateSchema;
2220
- const identities = this.#candidateIdentities;
2221
- if (candidate === void 0 || identities === void 0) throw new _src_core.DatabaseError("CONFLICT", "Transaction scope has settled");
2225
+ const candidate = token.schema;
2226
+ const identities = token.identities;
2222
2227
  const projected = (0, _src_core.projectMigrationSchema)([...candidate.values()], owned.plan.steps);
2223
2228
  const projectedIdentities = this.#projectIdentities(identities, owned.plan.steps);
2224
2229
  if (owned.metadata !== void 0 && !(0, _src_core.equalsValue)((0, _src_core.normalizeDriverSchema)(owned.metadata.schema), projected)) throw new _src_core.DatabaseError("MIGRATION", "Migration metadata schema does not match the plan", {
@@ -2240,8 +2245,7 @@ var SQLiteDriver = class {
2240
2245
  throw error;
2241
2246
  }
2242
2247
  });
2243
- this.#candidateSchema = new Map(projected.map((table) => [table.name, table]));
2244
- this.#candidateIdentities = projectedIdentities;
2248
+ token.project(projected, projectedIdentities);
2245
2249
  }
2246
2250
  async #metadataTransaction(token) {
2247
2251
  this.#requireTransaction(token);
@@ -2252,10 +2256,10 @@ var SQLiteDriver = class {
2252
2256
  await this.#stamp(metadata);
2253
2257
  }
2254
2258
  #requireTransaction(token) {
2255
- if (this.#transaction !== token) throw new _src_core.DatabaseError("CONFLICT", "Transaction scope has settled");
2259
+ if (!this.#transactions.has(token)) throw new _src_core.DatabaseError("CONFLICT", "Transaction scope has settled");
2256
2260
  }
2257
2261
  #root() {
2258
- if (this.#transaction !== void 0) throw new _src_core.DatabaseError("CONFLICT", "A transaction is active on this driver");
2262
+ if (this.#transactions.size !== 0) throw new _src_core.DatabaseError("CONFLICT", "A transaction is active on this driver");
2259
2263
  }
2260
2264
  #guard(operation) {
2261
2265
  try {
@@ -2298,7 +2302,8 @@ var SQLiteDriver = class {
2298
2302
  }
2299
2303
  #table(name) {
2300
2304
  this.#require();
2301
- const schema = (this.#candidateSchema ?? this.#schema).get(name);
2305
+ const [transaction] = this.#transactions;
2306
+ const schema = (transaction?.schema ?? this.#schema).get(name);
2302
2307
  if (schema === void 0) throw new _src_core.DatabaseError("NOT_FOUND", `Table '${name}' is not in the schema`, { table: name });
2303
2308
  return schema;
2304
2309
  }
@@ -2314,8 +2319,7 @@ var SQLiteDriver = class {
2314
2319
  for (const step of input.plan.steps) for (const sql of stepToSQL(step)) database.execute(sql);
2315
2320
  }
2316
2321
  };
2317
- //#endregion
2318
- //#region src/server/factories.ts
2322
+ // src/server/factories.ts
2319
2323
  /**
2320
2324
  * Creates a persistent JSON-file {@link DriverInterface} for a given path.
2321
2325
  *
@@ -2389,13 +2393,15 @@ function createJSONDriver(path) {
2389
2393
  function createSQLiteDriver(options) {
2390
2394
  return new SQLiteDriver(options);
2391
2395
  }
2392
- //#endregion
2393
2396
  exports.EXACT_COLUMN_STORAGE = EXACT_COLUMN_STORAGE;
2394
2397
  exports.EXACT_RANGE_COLUMN_STORAGE = EXACT_RANGE_COLUMN_STORAGE;
2395
2398
  exports.JSONDriver = JSONDriver;
2399
+ exports.JSONTransaction = JSONTransaction;
2396
2400
  exports.METADATA_TABLE = METADATA_TABLE;
2397
2401
  exports.SQLiteDriver = SQLiteDriver;
2402
+ exports.SQLiteTransaction = SQLiteTransaction;
2398
2403
  exports.compileAggregateSQL = compileAggregateSQL;
2404
+ exports.compileBoundSQL = compileBoundSQL;
2399
2405
  exports.compileColumnSQL = compileColumnSQL;
2400
2406
  exports.compileConditionSQL = compileConditionSQL;
2401
2407
  exports.compileFieldSQL = compileFieldSQL;