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