@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.
- package/dist/src/browser/index.js +59 -45
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +219 -233
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +65 -11
- package/dist/src/core/index.d.ts +65 -11
- package/dist/src/core/index.js +218 -235
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +225 -219
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +49 -0
- package/dist/src/server/index.d.ts +49 -0
- package/dist/src/server/index.js +225 -222
- package/dist/src/server/index.js.map +1 -1
- package/package.json +20 -20
package/dist/src/server/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
case "
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
case "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
718
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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 " +
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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.#
|
|
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 =
|
|
1220
|
-
const identities =
|
|
1221
|
-
|
|
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
|
-
|
|
1234
|
-
|
|
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 =
|
|
1339
|
-
const identities =
|
|
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
|
-
|
|
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.#
|
|
1357
|
-
return
|
|
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.#
|
|
1372
|
+
if (this.#transactions.size !== 0) throw new DatabaseError("CONFLICT", "A transaction is active on this driver");
|
|
1374
1373
|
}
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1456
|
+
const cleanup = new AbortController();
|
|
1432
1457
|
const next = this.#chain.then(async () => {
|
|
1433
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
}
|
|
1555
|
-
|
|
1556
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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.#
|
|
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
|
-
|
|
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
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 &&
|
|
2007
|
+
if (limit !== void 0 && page.count >= limit) return;
|
|
2003
2008
|
if (conditions.length > 0 && !matchesQuery(row, conditions)) continue;
|
|
2004
|
-
if (
|
|
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
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
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 =
|
|
2219
|
-
const identities =
|
|
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
|
-
|
|
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.#
|
|
2258
|
+
if (!this.#transactions.has(token)) throw new DatabaseError("CONFLICT", "Transaction scope has settled");
|
|
2255
2259
|
}
|
|
2256
2260
|
#root() {
|
|
2257
|
-
if (this.#
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|