@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.
- 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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
case "
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
case "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
719
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
782
|
-
|
|
783
|
-
|
|
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 " +
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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.#
|
|
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 =
|
|
1221
|
-
const identities =
|
|
1222
|
-
|
|
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
|
-
|
|
1235
|
-
|
|
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 =
|
|
1340
|
-
const identities =
|
|
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
|
-
|
|
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.#
|
|
1358
|
-
return
|
|
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.#
|
|
1373
|
+
if (this.#transactions.size !== 0) throw new _src_core.DatabaseError("CONFLICT", "A transaction is active on this driver");
|
|
1375
1374
|
}
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1457
|
+
const cleanup = new AbortController();
|
|
1433
1458
|
const next = this.#chain.then(async () => {
|
|
1434
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
}
|
|
1556
|
-
|
|
1557
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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.#
|
|
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
|
-
|
|
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
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 &&
|
|
2008
|
+
if (limit !== void 0 && page.count >= limit) return;
|
|
2004
2009
|
if (conditions.length > 0 && !(0, _src_core.matchesQuery)(row, conditions)) continue;
|
|
2005
|
-
if (
|
|
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
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
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 =
|
|
2220
|
-
const identities =
|
|
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
|
-
|
|
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.#
|
|
2259
|
+
if (!this.#transactions.has(token)) throw new _src_core.DatabaseError("CONFLICT", "Transaction scope has settled");
|
|
2256
2260
|
}
|
|
2257
2261
|
#root() {
|
|
2258
|
-
if (this.#
|
|
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
|
|
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
|
-
|
|
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;
|