@orkestrel/database 0.0.6 → 0.0.7
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/README.md +13 -35
- package/dist/src/browser/index.d.ts +92 -79
- package/dist/src/browser/index.js +357 -220
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +1796 -803
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +545 -706
- package/dist/src/core/index.d.ts +545 -706
- package/dist/src/core/index.js +1775 -790
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +1508 -757
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +443 -569
- package/dist/src/server/index.d.ts +443 -569
- package/dist/src/server/index.js +1492 -741
- package/dist/src/server/index.js.map +1 -1
- package/package.json +9 -10
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
let _src_core = require("../core/index.cjs");
|
|
3
3
|
let _orkestrel_contract = require("@orkestrel/contract");
|
|
4
|
-
let node_crypto = require("node:crypto");
|
|
5
4
|
let node_fs_promises = require("node:fs/promises");
|
|
6
5
|
let node_path = require("node:path");
|
|
7
6
|
let _orkestrel_sqlite = require("@orkestrel/sqlite");
|
|
8
7
|
//#region src/server/constants.ts
|
|
9
8
|
/**
|
|
10
|
-
* The declared {@link
|
|
9
|
+
* The declared {@link ColumnStorage}s whose SQL EQUALITY comparisons (`equals` /
|
|
11
10
|
* `not` / `any` / `none`) and `starts` / `ends` compiles are provably
|
|
12
11
|
* engine-exact under declared-type trust — `text` / `integer` / `real` /
|
|
13
12
|
* `boolean`; a `json` or `blob` column always refines instead.
|
|
@@ -22,68 +21,47 @@ let _orkestrel_sqlite = require("@orkestrel/sqlite");
|
|
|
22
21
|
* compares UTF-16 CODE-UNIT order. The two orders diverge for supplementary-
|
|
23
22
|
* plane characters (code points ≥ U+10000, e.g. many emoji): a lead surrogate
|
|
24
23
|
* (`\uD800`–`\uDBFF`) sorts BELOW ``–`` in code-unit order, while
|
|
25
|
-
* its code point sorts ABOVE them. So `
|
|
26
|
-
* `
|
|
27
|
-
* future opt-in "trusted collation" mode (the caller vouches the column's
|
|
28
|
-
* values are BMP-only, or a custom SQLite collation matching `compareValues`
|
|
29
|
-
* is registered) could restore native text ranges/ordering.
|
|
24
|
+
* its code point sorts ABOVE them. So `matchesConditionExactly`'s range family and
|
|
25
|
+
* `matchesOrderExactly` exclude `text`, refining through the core engine instead.
|
|
30
26
|
*/
|
|
31
|
-
var
|
|
27
|
+
var EXACT_COLUMN_STORAGE = Object.freeze([
|
|
32
28
|
"text",
|
|
33
29
|
"integer",
|
|
34
30
|
"real",
|
|
35
31
|
"boolean"
|
|
36
32
|
]);
|
|
37
33
|
/**
|
|
38
|
-
* The declared {@link
|
|
34
|
+
* The declared {@link ColumnStorage}s whose SQL RANGE comparisons
|
|
39
35
|
* (`above` / `below` / `from` / `to` / `between`) and `ORDER BY` compiles are
|
|
40
36
|
* provably engine-exact — `integer` / `real` / `boolean` only. `text` is
|
|
41
|
-
* excluded: see {@link
|
|
37
|
+
* excluded: see {@link EXACT_COLUMN_STORAGE}'s remarks for the BINARY-collation
|
|
42
38
|
* (code-point) vs. JS `<` (code-unit) divergence on supplementary-plane
|
|
43
39
|
* characters.
|
|
44
40
|
*/
|
|
45
|
-
var
|
|
41
|
+
var EXACT_RANGE_COLUMN_STORAGE = Object.freeze([
|
|
46
42
|
"integer",
|
|
47
43
|
"real",
|
|
48
44
|
"boolean"
|
|
49
45
|
]);
|
|
50
46
|
/**
|
|
51
47
|
* The reserved metadata table the {@link SQLiteDriver} creates on `open` to
|
|
52
|
-
* persist its stamped `
|
|
53
|
-
* SQLite realization of the `
|
|
48
|
+
* persist its stamped `DriverMetadata` (`version` + declared schema JSON) — the
|
|
49
|
+
* SQLite realization of the `metadata` / `stamp` driver hooks.
|
|
54
50
|
*
|
|
55
51
|
* @remarks
|
|
56
|
-
* A single-row table (`id = 1`). A user table named `
|
|
52
|
+
* A single-row table (`id = 1`). A user table named `_metadata` collides with the
|
|
57
53
|
* reservation — the caller's concern to avoid, documented on the driver class.
|
|
58
54
|
*/
|
|
59
|
-
var
|
|
55
|
+
var METADATA_TABLE = "_metadata";
|
|
60
56
|
//#endregion
|
|
61
57
|
//#region src/server/helpers.ts
|
|
62
58
|
/**
|
|
63
|
-
* Generate a fresh unique key — a v4 UUID string, backed by `node:crypto`.
|
|
64
|
-
*
|
|
65
|
-
* @remarks
|
|
66
|
-
* Supply this as {@link import('@orkestrel/database').DatabaseOptions.key} so a table mints
|
|
67
|
-
* a key when a written row lacks its primary-key value. Strings work as keys on
|
|
68
|
-
* every backend; supply your own key values directly to use numeric keys instead.
|
|
69
|
-
*
|
|
70
|
-
* @returns A new UUID string
|
|
71
|
-
*
|
|
72
|
-
* @example
|
|
73
|
-
* ```ts
|
|
74
|
-
* const db = createDatabase({ driver, tables, key: generateKey })
|
|
75
|
-
* ```
|
|
76
|
-
*/
|
|
77
|
-
function generateKey() {
|
|
78
|
-
return (0, node_crypto.randomUUID)();
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
59
|
* Whether a value's runtime type matches a column's declared exact type —
|
|
82
60
|
* the operand side of the declared-type-trust proof.
|
|
83
61
|
*
|
|
84
62
|
* @remarks
|
|
85
63
|
* `text` ↔ string, `integer` / `real` ↔ FINITE number (`NaN` / `±Infinity`
|
|
86
|
-
* fail), `boolean` ↔ boolean. Backs {@link
|
|
64
|
+
* fail), `boolean` ↔ boolean. Backs {@link matchesConditionExactly}'s operand checks.
|
|
87
65
|
*
|
|
88
66
|
* @param value - The condition operand to test
|
|
89
67
|
* @param type - The column's declared portable type
|
|
@@ -95,9 +73,9 @@ function generateKey() {
|
|
|
95
73
|
* matchesDeclaredType(Number.NaN, 'integer') // false — only finite numbers
|
|
96
74
|
* ```
|
|
97
75
|
*/
|
|
98
|
-
function
|
|
99
|
-
if (
|
|
100
|
-
if (
|
|
76
|
+
function matchesDeclaredStorage(value, storage) {
|
|
77
|
+
if (storage === "text") return (0, _orkestrel_contract.isString)(value);
|
|
78
|
+
if (storage === "boolean") return (0, _orkestrel_contract.isBoolean)(value);
|
|
101
79
|
return (0, _orkestrel_contract.isFiniteNumber)(value);
|
|
102
80
|
}
|
|
103
81
|
/**
|
|
@@ -106,28 +84,26 @@ function matchesDeclaredType(value, type) {
|
|
|
106
84
|
* type can store.
|
|
107
85
|
*
|
|
108
86
|
* @remarks
|
|
109
|
-
* `false` for a nested `FieldPath` (an array)
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
* `
|
|
117
|
-
*
|
|
118
|
-
* decoded-rows truth). `above` / `below` / `from` / `to` / `between` are exact
|
|
119
|
-
* ONLY for a declared type in {@link EXACT_RANGE_COLUMN_TYPES} (`integer` /
|
|
87
|
+
* `false` for a nested `FieldPath` (an array) or a column absent from `schema`.
|
|
88
|
+
* `absent` / `present` are exact unless a column is both optional and nullable.
|
|
89
|
+
* In that combined case the storage sentinel for explicit `null` is not SQL
|
|
90
|
+
* `NULL`, while the core treats both absence and explicit `null` as absent.
|
|
91
|
+
* Every scalar operator refines when the column is optional
|
|
92
|
+
* OR nullable, because SQL null semantics and the core total order differ.
|
|
93
|
+
* Required non-null `equals` / `not` require an operand matching the declared
|
|
94
|
+
* storage and exclude `json` / `blob`. `above` / `below` / `from` / `to` /
|
|
95
|
+
* `between` are exact only for {@link EXACT_RANGE_COLUMN_STORAGE} (`integer` /
|
|
120
96
|
* `real` / `boolean`) — a `text` column's range conditions REFINE, because
|
|
121
97
|
* SQLite's default BINARY collation orders TEXT by Unicode CODE POINT while
|
|
122
98
|
* the core engine's `compareValues` orders JS strings by UTF-16 CODE UNIT,
|
|
123
99
|
* and the two diverge for supplementary-plane characters (see
|
|
124
|
-
* {@link
|
|
100
|
+
* {@link EXACT_COLUMN_STORAGE}'s remarks for the full rationale).
|
|
125
101
|
* `any` / `none` require a NON-EMPTY list where every element matches (an empty
|
|
126
102
|
* list is exact under neither: the engine's `any([])` matches nothing while
|
|
127
103
|
* `none([])` matches everything, and SQL `IN ()` is a syntax error) — these
|
|
128
104
|
* stay exact on `text` (byte equality is collation-independent and engine-
|
|
129
105
|
* identical). `starts` / `ends` are exact only on a `text` column with a
|
|
130
|
-
* string operand (case-sensitive `substr` compile, see {@link
|
|
106
|
+
* string operand (case-sensitive `substr` compile, see {@link compileConditionSQL}) —
|
|
131
107
|
* likewise collation-independent. `like` / `glob` are NEVER exact — SQLite
|
|
132
108
|
* `LIKE` folds case ASCII-only against the engine's Unicode fold, and `GLOB`
|
|
133
109
|
* has character classes the engine treats literally.
|
|
@@ -136,26 +112,27 @@ function matchesDeclaredType(value, type) {
|
|
|
136
112
|
* @param schema - The table's schema
|
|
137
113
|
* @returns Whether `condition` is exact
|
|
138
114
|
*/
|
|
139
|
-
function
|
|
115
|
+
function matchesConditionExactly(condition, schema) {
|
|
140
116
|
if (!(0, _orkestrel_contract.isString)(condition.column)) return false;
|
|
141
117
|
const column = schema.columns.find((candidate) => candidate.name === condition.column);
|
|
142
118
|
if (column === void 0) return false;
|
|
143
|
-
if (condition.operator === "absent" || condition.operator === "present") return
|
|
144
|
-
if (
|
|
119
|
+
if (condition.operator === "absent" || condition.operator === "present") return !(column.optional && column.nullable);
|
|
120
|
+
if (column.optional || column.nullable) return false;
|
|
121
|
+
if (!EXACT_COLUMN_STORAGE.some((storage) => storage === column.storage)) return false;
|
|
145
122
|
const first = condition.values[0];
|
|
146
123
|
const second = condition.values[1];
|
|
147
124
|
switch (condition.operator) {
|
|
148
125
|
case "equals":
|
|
149
|
-
case "not": return
|
|
126
|
+
case "not": return matchesDeclaredStorage(first, column.storage);
|
|
150
127
|
case "above":
|
|
151
128
|
case "below":
|
|
152
129
|
case "from":
|
|
153
|
-
case "to": return
|
|
154
|
-
case "between": return
|
|
130
|
+
case "to": return EXACT_RANGE_COLUMN_STORAGE.some((storage) => storage === column.storage) && matchesDeclaredStorage(first, column.storage);
|
|
131
|
+
case "between": return EXACT_RANGE_COLUMN_STORAGE.some((storage) => storage === column.storage) && matchesDeclaredStorage(first, column.storage) && matchesDeclaredStorage(second, column.storage);
|
|
155
132
|
case "any":
|
|
156
|
-
case "none": return condition.values.length > 0 && condition.values.every((value) =>
|
|
133
|
+
case "none": return condition.values.length > 0 && condition.values.every((value) => matchesDeclaredStorage(value, column.storage));
|
|
157
134
|
case "starts":
|
|
158
|
-
case "ends": return column.
|
|
135
|
+
case "ends": return column.storage === "text" && (0, _orkestrel_contract.isString)(first);
|
|
159
136
|
case "like":
|
|
160
137
|
case "glob": return false;
|
|
161
138
|
}
|
|
@@ -166,65 +143,71 @@ function isExactCondition(condition, schema) {
|
|
|
166
143
|
*
|
|
167
144
|
* @remarks
|
|
168
145
|
* `false` for a nested `FieldPath`, a column absent from `schema`, or a
|
|
169
|
-
* declared type outside {@link
|
|
146
|
+
* declared type outside {@link EXACT_RANGE_COLUMN_STORAGE} (`integer` / `real` /
|
|
170
147
|
* `boolean`). `text` is NOT exact here: SQLite's default BINARY collation
|
|
171
148
|
* orders TEXT by Unicode code point while the core engine's `compareValues`
|
|
172
149
|
* orders JS strings by UTF-16 code unit, and the two diverge for
|
|
173
|
-
* supplementary-plane characters (see {@link
|
|
150
|
+
* supplementary-plane characters (see {@link EXACT_COLUMN_STORAGE}'s remarks) —
|
|
174
151
|
* a `text` order term REFINES through the core engine instead.
|
|
175
152
|
*
|
|
176
153
|
* @param order - The order term to test
|
|
177
154
|
* @param schema - The table's schema
|
|
178
155
|
* @returns Whether `order` is exact
|
|
179
156
|
*/
|
|
180
|
-
function
|
|
157
|
+
function matchesOrderExactly(order, schema) {
|
|
181
158
|
if (!(0, _orkestrel_contract.isString)(order.column)) return false;
|
|
182
159
|
const column = schema.columns.find((candidate) => candidate.name === order.column);
|
|
183
160
|
if (column === void 0) return false;
|
|
184
|
-
return
|
|
161
|
+
return !column.optional && !column.nullable && EXACT_RANGE_COLUMN_STORAGE.some((storage) => storage === column.storage);
|
|
185
162
|
}
|
|
186
163
|
/**
|
|
187
|
-
* Whether a whole {@link
|
|
164
|
+
* Whether a whole {@link QueryInput} is exact — every condition and every order
|
|
188
165
|
* term is exact. `limit` / `offset` never affect exactness (SQL `LIMIT` /
|
|
189
166
|
* `OFFSET` are always engine-identical).
|
|
190
167
|
*
|
|
191
|
-
* @param
|
|
168
|
+
* @param input - The query input to test
|
|
192
169
|
* @param schema - The table's schema
|
|
193
|
-
* @returns Whether every part of `
|
|
170
|
+
* @returns Whether every part of `input` is exact
|
|
194
171
|
*/
|
|
195
|
-
function
|
|
196
|
-
const conditions =
|
|
197
|
-
const order =
|
|
198
|
-
return conditions.every((condition) =>
|
|
172
|
+
function matchesQueryExactly(input, schema) {
|
|
173
|
+
const conditions = input.conditions ?? [];
|
|
174
|
+
const order = input.order ?? [];
|
|
175
|
+
return conditions.every((condition) => matchesConditionExactly(condition, schema)) && order.every((term) => matchesOrderExactly(term, schema));
|
|
199
176
|
}
|
|
200
177
|
/**
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
* @remarks
|
|
204
|
-
* `text` / `json` → `TEXT` (JSON is stored as text and read back with
|
|
205
|
-
* `json_extract` for nested-field queries); `integer` / `boolean` → `INTEGER`
|
|
206
|
-
* (a boolean stores `1` / `0`); `real` → `REAL`; `blob` → `BLOB`. No `NOT NULL`
|
|
207
|
-
* is ever emitted — the contract validates required-ness; the database is just
|
|
208
|
-
* storage (AGENTS §14, the typed layer above imposes the shape).
|
|
178
|
+
* Determine whether SQLite can execute an aggregate exactly like the core engine.
|
|
209
179
|
*
|
|
210
|
-
* @param
|
|
211
|
-
* @
|
|
180
|
+
* @param operation - Aggregate operation
|
|
181
|
+
* @param column - Aggregate field
|
|
182
|
+
* @param schema - Current table schema
|
|
183
|
+
* @returns Whether native aggregation is exact
|
|
184
|
+
*/
|
|
185
|
+
function matchesAggregateExactly(operation, column, schema) {
|
|
186
|
+
if (operation === "count") return true;
|
|
187
|
+
if (operation === "sum" || operation === "average" || !(0, _orkestrel_contract.isString)(column)) return false;
|
|
188
|
+
const declared = schema.columns.find((candidate) => candidate.name === column);
|
|
189
|
+
return declared !== void 0 && (declared.storage === "integer" || declared.storage === "real") && !(declared.optional && declared.nullable);
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Test a declared SQLite type against a portable storage affinity.
|
|
212
193
|
*
|
|
213
|
-
* @
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
* columnSQL('json') // 'TEXT'
|
|
217
|
-
* ```
|
|
194
|
+
* @param declared - Native declared type
|
|
195
|
+
* @param storage - Portable column storage
|
|
196
|
+
* @returns Whether SQLite's official affinity rules yield the expected affinity
|
|
218
197
|
*/
|
|
219
|
-
function
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
198
|
+
function matchesSQLiteAffinity(declared, storage) {
|
|
199
|
+
if (!(0, _orkestrel_contract.isString)(declared)) return false;
|
|
200
|
+
const type = declared.toUpperCase();
|
|
201
|
+
let affinity;
|
|
202
|
+
if (type.includes("INT")) affinity = "INTEGER";
|
|
203
|
+
else if (type.includes("CHAR") || type.includes("CLOB") || type.includes("TEXT")) affinity = "TEXT";
|
|
204
|
+
else if (type === "" || type.includes("BLOB")) affinity = "BLOB";
|
|
205
|
+
else if (type.includes("REAL") || type.includes("FLOA") || type.includes("DOUB")) affinity = "REAL";
|
|
206
|
+
else affinity = "NUMERIC";
|
|
207
|
+
if (storage === "integer" || storage === "boolean") return affinity === "INTEGER";
|
|
208
|
+
if (storage === "text" || storage === "json") return affinity === "TEXT";
|
|
209
|
+
if (storage === "blob") return affinity === "BLOB";
|
|
210
|
+
return affinity === "REAL";
|
|
228
211
|
}
|
|
229
212
|
/**
|
|
230
213
|
* Quote a SQL identifier (a table or column name) so any characters are literal.
|
|
@@ -239,129 +222,90 @@ function columnSQL(type) {
|
|
|
239
222
|
*
|
|
240
223
|
* @example
|
|
241
224
|
* ```ts
|
|
242
|
-
*
|
|
225
|
+
* quoteIdentifier('order') // '"order"'
|
|
243
226
|
* ```
|
|
244
227
|
*/
|
|
245
|
-
function
|
|
228
|
+
function quoteIdentifier(identifier) {
|
|
246
229
|
return "\"" + identifier.replaceAll("\"", "\"\"") + "\"";
|
|
247
230
|
}
|
|
248
231
|
/**
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
* @remarks
|
|
252
|
-
* A single string is ONE column — `quote(path)`. An array descends a JSON column:
|
|
253
|
-
* the first element is the (quoted) column, the rest a `json_extract` path
|
|
254
|
-
* (`json_extract("payload", '$.user.id')`), matching the guide's nested-field
|
|
255
|
-
* examples (simple identifier keys). The string's value is never split on `.`
|
|
256
|
-
* (AGENTS — `FieldPath` semantics): a dotted string is one column literally.
|
|
257
|
-
*
|
|
258
|
-
* @param path - The field path (a column, or a column + nested keys)
|
|
259
|
-
* @returns The SQL expression selecting the value
|
|
260
|
-
*
|
|
261
|
-
* @example
|
|
262
|
-
* ```ts
|
|
263
|
-
* fieldColumn('payload') // '"payload"'
|
|
264
|
-
* fieldColumn(['payload', 'user', 'id']) // 'json_extract("payload", \'$.user.id\')'
|
|
265
|
-
* ```
|
|
266
|
-
*/
|
|
267
|
-
function fieldColumn(path) {
|
|
268
|
-
if ((0, _orkestrel_contract.isString)(path)) return quote(path);
|
|
269
|
-
const [column, ...nested] = path;
|
|
270
|
-
if (column === void 0) throw new _src_core.DatabaseError("VALIDATION", "A field path must contain at least one column");
|
|
271
|
-
const rest = nested.map((key) => "." + key.replaceAll("'", "''")).join("");
|
|
272
|
-
return "json_extract(" + quote(column) + ", '$" + rest + "')";
|
|
273
|
-
}
|
|
274
|
-
/**
|
|
275
|
-
* Compile an {@link AggregateFunction} over a {@link FieldPath} to its SQL
|
|
276
|
-
* aggregate expression — the SELECT body the SQLite driver's native `aggregate`
|
|
277
|
-
* runs.
|
|
232
|
+
* Encode a JS value to its stored {@link SQLiteValue} for a declared column.
|
|
278
233
|
*
|
|
279
234
|
* @remarks
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
* `COUNT(*)` is `0` and the numeric aggregates are SQL `NULL` (→ `undefined`),
|
|
285
|
-
* matching the engine.
|
|
286
|
-
*
|
|
287
|
-
* @param operation - The aggregate to compute
|
|
288
|
-
* @param column - The column (or nested path) to aggregate
|
|
289
|
-
* @returns The SQL aggregate expression
|
|
290
|
-
*
|
|
291
|
-
* @example
|
|
292
|
-
* ```ts
|
|
293
|
-
* aggregateSQL('count', 'age') // 'COUNT(*)'
|
|
294
|
-
* aggregateSQL('sum', 'age') // 'SUM("age")'
|
|
295
|
-
* aggregateSQL('average', ['payload', 'score']) // 'AVG(json_extract("payload", \'$.score\'))'
|
|
296
|
-
* ```
|
|
297
|
-
*/
|
|
298
|
-
function aggregateSQL(operation, column) {
|
|
299
|
-
switch (operation) {
|
|
300
|
-
case "count": return "COUNT(*)";
|
|
301
|
-
case "sum": return "SUM(" + fieldColumn(column) + ")";
|
|
302
|
-
case "average": return "AVG(" + fieldColumn(column) + ")";
|
|
303
|
-
case "minimum": return "MIN(" + fieldColumn(column) + ")";
|
|
304
|
-
case "maximum": return "MAX(" + fieldColumn(column) + ")";
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
/**
|
|
308
|
-
* Encode a JS value to its stored {@link SQLiteValue} for a column's type.
|
|
309
|
-
*
|
|
310
|
-
* @remarks
|
|
311
|
-
* The forward half of the bridge, total (AGENTS §14): a value that does not fit
|
|
312
|
-
* its column's storage type encodes to `null` rather than throwing. A `boolean`
|
|
313
|
-
* column stores `1` / `0` (and `null` / `undefined` → `null`); a `json` column
|
|
314
|
-
* stores `JSON.stringify` (or `null` for `null` / `undefined`); `integer` /
|
|
315
|
-
* `real` keep a `number` / `bigint`, else `null`; `text` keeps a `string`, else
|
|
316
|
-
* `null`; `blob` keeps a `Uint8Array`, else `null`. Narrowed with `typeof` /
|
|
317
|
-
* `instanceof`, never `as`.
|
|
235
|
+
* The codec is total: a malformed value encodes to SQL `NULL`. Absence always
|
|
236
|
+
* uses SQL `NULL`. A nullable-only column also uses SQL `NULL` for explicit
|
|
237
|
+
* `null`; an optional-and-nullable column uses a storage-class sentinel so
|
|
238
|
+
* absence and explicit `null` remain distinct.
|
|
318
239
|
*
|
|
319
240
|
* @param value - The JS value to store
|
|
320
|
-
* @param
|
|
241
|
+
* @param column - The declared storage and absence/null contract
|
|
321
242
|
* @returns The value SQLite stores
|
|
322
243
|
*
|
|
323
244
|
* @example
|
|
324
245
|
* ```ts
|
|
325
|
-
* encodeValue(true,
|
|
326
|
-
* encodeValue({ a: 1 },
|
|
246
|
+
* encodeValue(true, booleanColumn) // 1
|
|
247
|
+
* encodeValue({ a: 1 }, jsonColumn) // '{"a":1}'
|
|
327
248
|
* ```
|
|
328
249
|
*/
|
|
329
|
-
function encodeValue(value,
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
250
|
+
function encodeValue(value, column) {
|
|
251
|
+
if (value === void 0) return null;
|
|
252
|
+
if (value === null) {
|
|
253
|
+
if (!column.nullable) return null;
|
|
254
|
+
if (!column.optional) return null;
|
|
255
|
+
return column.storage === "text" || column.storage === "json" ? /* @__PURE__ */ new Uint8Array() : String(null);
|
|
256
|
+
}
|
|
257
|
+
switch (column.storage) {
|
|
258
|
+
case "boolean": return typeof value === "boolean" ? value ? 1 : 0 : null;
|
|
259
|
+
case "json": try {
|
|
260
|
+
return JSON.stringify((0, _orkestrel_contract.cloneJSONValue)(value));
|
|
261
|
+
} catch {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
case "integer": return typeof value === "bigint" || typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) ? value : null;
|
|
265
|
+
case "real": return typeof value === "bigint" || typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
335
266
|
case "text": return typeof value === "string" ? value : null;
|
|
336
267
|
case "blob": return value instanceof Uint8Array ? value : null;
|
|
337
268
|
}
|
|
338
269
|
}
|
|
339
270
|
/**
|
|
340
|
-
* Decode a stored {@link SQLiteValue} back to its JS value for a column
|
|
271
|
+
* Decode a stored {@link SQLiteValue} back to its JS value for a declared column —
|
|
341
272
|
* the exact inverse of {@link encodeValue}.
|
|
342
273
|
*
|
|
343
274
|
* @remarks
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
* omit absent columns.
|
|
275
|
+
* Stored values must use the declared SQLite storage class. SQL `NULL` decodes
|
|
276
|
+
* to explicit `null` only for nullable-only columns and otherwise to absence.
|
|
277
|
+
* Optional-and-nullable sentinels decode to explicit `null`; malformed values
|
|
278
|
+
* decode to `undefined` so {@link decodeRow} omits them.
|
|
349
279
|
*
|
|
350
280
|
* @param value - The stored SQLite value
|
|
351
|
-
* @param
|
|
352
|
-
* @returns The decoded JS value
|
|
281
|
+
* @param column - The declared storage and absence/null contract
|
|
282
|
+
* @returns The decoded JS value, or `undefined` for absence/malformed storage
|
|
353
283
|
*
|
|
354
284
|
* @example
|
|
355
285
|
* ```ts
|
|
356
|
-
* decodeValue(1,
|
|
357
|
-
* decodeValue('{"a":1}',
|
|
286
|
+
* decodeValue(1, booleanColumn) // true
|
|
287
|
+
* decodeValue('{"a":1}', jsonColumn) // { a: 1 }
|
|
358
288
|
* ```
|
|
359
289
|
*/
|
|
360
|
-
function decodeValue(value,
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
290
|
+
function decodeValue(value, column) {
|
|
291
|
+
if (value === null) return column.nullable && !column.optional ? null : void 0;
|
|
292
|
+
if (column.optional && column.nullable && (column.storage === "text" || column.storage === "json" ? value instanceof Uint8Array && value.byteLength === 0 : value === String(null))) return null;
|
|
293
|
+
switch (column.storage) {
|
|
294
|
+
case "boolean":
|
|
295
|
+
if (value === 0 || value === 0n) return false;
|
|
296
|
+
if (value === 1 || value === 1n) return true;
|
|
297
|
+
return;
|
|
298
|
+
case "json":
|
|
299
|
+
if (typeof value !== "string") return void 0;
|
|
300
|
+
try {
|
|
301
|
+
return structuredClone((0, _orkestrel_contract.cloneJSONValue)(JSON.parse(value)));
|
|
302
|
+
} catch {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
case "integer": return typeof value === "bigint" || typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) ? value : void 0;
|
|
306
|
+
case "real": return typeof value === "bigint" || typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
307
|
+
case "text": return typeof value === "string" ? value : void 0;
|
|
308
|
+
case "blob": return value instanceof Uint8Array ? value : void 0;
|
|
365
309
|
}
|
|
366
310
|
}
|
|
367
311
|
/**
|
|
@@ -383,7 +327,7 @@ function decodeValue(value, type) {
|
|
|
383
327
|
*/
|
|
384
328
|
function encodeRow(row, schema) {
|
|
385
329
|
const result = {};
|
|
386
|
-
for (const column of schema.columns) result[column.name] = encodeValue(row[column.name], column
|
|
330
|
+
for (const column of schema.columns) result[column.name] = encodeValue(row[column.name], column);
|
|
387
331
|
return result;
|
|
388
332
|
}
|
|
389
333
|
/**
|
|
@@ -424,9 +368,8 @@ function extractValues(row, names, table) {
|
|
|
424
368
|
* Decodes each declared column with {@link decodeValue} and **omits** any column
|
|
425
369
|
* whose decoded value is `undefined` — so an absent / `NULL` optional column does
|
|
426
370
|
* not surface as `{ bio: undefined }`, matching how the contract's optional
|
|
427
|
-
* columns expect absence.
|
|
428
|
-
*
|
|
429
|
-
* `undefined`, and an `undefined` value is omitted).
|
|
371
|
+
* columns expect absence. Nullable-only SQL `NULL` cells remain explicit
|
|
372
|
+
* `null`; optional-and-nullable columns use their storage-class sentinel.
|
|
430
373
|
*
|
|
431
374
|
* @param row - The stored SQLite row
|
|
432
375
|
* @param schema - The table's schema
|
|
@@ -442,37 +385,14 @@ function decodeRow(row, schema) {
|
|
|
442
385
|
for (const column of schema.columns) {
|
|
443
386
|
const value = row[column.name];
|
|
444
387
|
if (value === void 0) continue;
|
|
445
|
-
const decoded = decodeValue(value, column
|
|
388
|
+
const decoded = decodeValue(value, column);
|
|
446
389
|
if (decoded !== void 0) result[column.name] = decoded;
|
|
447
390
|
}
|
|
448
391
|
return result;
|
|
449
392
|
}
|
|
450
393
|
/**
|
|
451
|
-
* Project a {@link TableSchema} to the `CREATE TABLE IF NOT EXISTS` statement a
|
|
452
|
-
* SQLite driver's `open` issues for it.
|
|
453
|
-
*
|
|
454
|
-
* @remarks
|
|
455
|
-
* Each column compiles to `<quoted name> <columnSQL(type)>`; the statement ends
|
|
456
|
-
* with `PRIMARY KEY (<quoted primary>)`. No `NOT NULL` is emitted — the contract
|
|
457
|
-
* validates required-ness, the database is just storage (AGENTS §14).
|
|
458
|
-
*
|
|
459
|
-
* @param schema - The table's schema
|
|
460
|
-
* @returns The `CREATE TABLE IF NOT EXISTS …` statement
|
|
461
|
-
*
|
|
462
|
-
* @example
|
|
463
|
-
* ```ts
|
|
464
|
-
* schemaToTable(schema)
|
|
465
|
-
* // 'CREATE TABLE IF NOT EXISTS "users" ("id" TEXT, "age" INTEGER, PRIMARY KEY ("id"))'
|
|
466
|
-
* ```
|
|
467
|
-
*/
|
|
468
|
-
function schemaToTable(schema) {
|
|
469
|
-
const columns = schema.columns.map((column) => quote(column.name) + " " + columnSQL(column.type));
|
|
470
|
-
return "CREATE TABLE IF NOT EXISTS " + quote(schema.name) + " (" + columns.join(", ") + ", PRIMARY KEY (" + quote(schema.primary) + "))";
|
|
471
|
-
}
|
|
472
|
-
/**
|
|
473
394
|
* Build a collision-free SQL index name for a table + column-group index —
|
|
474
|
-
* shared by
|
|
475
|
-
* {@link stepToSQL}'s `index.add` / `index.remove` (a migration-time DDL),
|
|
395
|
+
* shared by the compiler module's `schemaToIndexes` and `stepToSQL`,
|
|
476
396
|
* so a plan-built index name always matches one `open` would have created.
|
|
477
397
|
*
|
|
478
398
|
* @remarks
|
|
@@ -488,120 +408,64 @@ function schemaToTable(schema) {
|
|
|
488
408
|
*
|
|
489
409
|
* @example
|
|
490
410
|
* ```ts
|
|
491
|
-
*
|
|
492
|
-
*
|
|
493
|
-
*
|
|
411
|
+
* deriveSQLiteIndexName('users', ['name']) // 'idx_5_users_4_name'
|
|
412
|
+
* deriveSQLiteIndexName('a_b', ['c']) // 'idx_3_a_b_1_c'
|
|
413
|
+
* deriveSQLiteIndexName('a', ['b', 'c']) // 'idx_1_a_1_b_1_c'
|
|
494
414
|
* ```
|
|
495
415
|
*/
|
|
496
|
-
function
|
|
416
|
+
function deriveSQLiteIndexName(table, columns) {
|
|
497
417
|
return "idx_" + [table, ...columns].map((part) => String(part.length) + "_" + part).join("_");
|
|
498
418
|
}
|
|
419
|
+
//#endregion
|
|
420
|
+
//#region src/server/compilers.ts
|
|
499
421
|
/**
|
|
500
|
-
*
|
|
501
|
-
* SQLite driver's `open` issues for its declared indexes.
|
|
502
|
-
*
|
|
503
|
-
* @remarks
|
|
504
|
-
* One statement per index group; the index name is built by {@link indexName}
|
|
505
|
-
* (collision-free and deterministic), matching the driver's naming so a
|
|
506
|
-
* repeated `open` is idempotent.
|
|
422
|
+
* Map a portable {@link ColumnStorage} to its SQLite column type.
|
|
507
423
|
*
|
|
508
|
-
* @param
|
|
509
|
-
* @returns
|
|
510
|
-
*
|
|
511
|
-
* @example
|
|
512
|
-
* ```ts
|
|
513
|
-
* schemaToIndexes(schema)
|
|
514
|
-
* // ['CREATE INDEX IF NOT EXISTS "idx_5_users_4_name" ON "users" ("name")']
|
|
515
|
-
* ```
|
|
424
|
+
* @param storage - The portable column type
|
|
425
|
+
* @returns The SQLite column type keyword
|
|
516
426
|
*/
|
|
517
|
-
function
|
|
518
|
-
|
|
427
|
+
function compileColumnSQL(storage) {
|
|
428
|
+
switch (storage) {
|
|
429
|
+
case "text":
|
|
430
|
+
case "json": return "TEXT";
|
|
431
|
+
case "integer":
|
|
432
|
+
case "boolean": return "INTEGER";
|
|
433
|
+
case "real": return "REAL";
|
|
434
|
+
case "blob": return "BLOB";
|
|
435
|
+
}
|
|
519
436
|
}
|
|
520
437
|
/**
|
|
521
|
-
*
|
|
522
|
-
* `migrate` executes for it.
|
|
523
|
-
*
|
|
524
|
-
* @remarks
|
|
525
|
-
* `table.add` emits the `CREATE TABLE` plus one `CREATE INDEX` per declared
|
|
526
|
-
* index (via {@link schemaToTable} / {@link schemaToIndexes}); `table.remove`
|
|
527
|
-
* emits `DROP TABLE IF EXISTS`; `column.add` / `column.remove` emit `ALTER
|
|
528
|
-
* TABLE … ADD COLUMN` / `… DROP COLUMN`; `index.add` / `index.remove` emit
|
|
529
|
-
* `CREATE INDEX IF NOT EXISTS` / `DROP INDEX IF EXISTS`, naming the index the
|
|
530
|
-
* same way `schemaToIndexes` does (`idx_<table>_<columns joined by _>`) so a
|
|
531
|
-
* plan-built index matches one `open` would have created. Whether the named
|
|
532
|
-
* table actually exists is the caller's concern (a driver's `migrate` checks
|
|
533
|
-
* its own declared schema before running these statements) — this projection
|
|
534
|
-
* is pure and never inspects live state.
|
|
535
|
-
*
|
|
536
|
-
* @param step - The migration step to project
|
|
537
|
-
* @returns The DDL statement(s) that apply the step
|
|
438
|
+
* Compile a {@link FieldPath} to the SQL expression that reads it.
|
|
538
439
|
*
|
|
539
|
-
* @
|
|
540
|
-
*
|
|
541
|
-
* stepToSQL({ operation: 'column.remove', table: 'users', column: 'legacy' })
|
|
542
|
-
* // ['ALTER TABLE "users" DROP COLUMN "legacy"']
|
|
543
|
-
* ```
|
|
440
|
+
* @param path - The field path
|
|
441
|
+
* @returns The SQL expression selecting the value
|
|
544
442
|
*/
|
|
545
|
-
function
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
case "index.add": return ["CREATE INDEX IF NOT EXISTS " + quote(indexName(step.table, step.index)) + " ON " + quote(step.table) + " (" + step.index.map(quote).join(", ") + ")"];
|
|
552
|
-
case "index.remove": return ["DROP INDEX IF EXISTS " + quote(indexName(step.table, step.index))];
|
|
553
|
-
}
|
|
443
|
+
function compileFieldSQL(path) {
|
|
444
|
+
if ((0, _orkestrel_contract.isString)(path)) return quoteIdentifier(path);
|
|
445
|
+
const [column, ...nested] = path;
|
|
446
|
+
if (column === void 0) throw new _src_core.DatabaseError("VALIDATION", "A field path must contain at least one column");
|
|
447
|
+
const rest = nested.map((key) => "." + key.replaceAll("'", "''")).join("");
|
|
448
|
+
return "json_extract(" + quoteIdentifier(column) + ", '$" + rest + "')";
|
|
554
449
|
}
|
|
555
450
|
/**
|
|
556
|
-
*
|
|
557
|
-
* — the bookkeeping counterpart to {@link stepToSQL} (which projects the DDL a
|
|
558
|
-
* driver's `migrate` runs against the live database).
|
|
559
|
-
*
|
|
560
|
-
* @remarks
|
|
561
|
-
* `column.add` / `column.remove` add / filter the named column;
|
|
562
|
-
* `index.add` / `index.remove` add / filter the matching index group (an exact
|
|
563
|
-
* ordered match on `index`). `table.add` / `table.remove` act on a WHOLE
|
|
564
|
-
* schema map rather than one table's shape, so they are the caller's concern
|
|
565
|
-
* (a driver's `migrate` applies them directly against its table map) — passed
|
|
566
|
-
* here, they return `schema` unchanged.
|
|
567
|
-
*
|
|
568
|
-
* @param schema - The table's current declared schema
|
|
569
|
-
* @param step - The migration step to project onto it
|
|
570
|
-
* @returns The table's schema after the step
|
|
451
|
+
* Compile an {@link AggregateOperation} over a {@link FieldPath}.
|
|
571
452
|
*
|
|
572
|
-
* @
|
|
573
|
-
*
|
|
574
|
-
*
|
|
575
|
-
* // schema with the 'legacy' column dropped from `columns`
|
|
576
|
-
* ```
|
|
453
|
+
* @param operation - The aggregate to compute
|
|
454
|
+
* @param column - The column or nested path to aggregate
|
|
455
|
+
* @returns The SQL aggregate expression
|
|
577
456
|
*/
|
|
578
|
-
function
|
|
579
|
-
switch (
|
|
580
|
-
case "
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
case "
|
|
585
|
-
...schema,
|
|
586
|
-
columns: schema.columns.filter((column) => column.name !== step.column)
|
|
587
|
-
};
|
|
588
|
-
case "index.add": return {
|
|
589
|
-
...schema,
|
|
590
|
-
indexes: [...schema.indexes, step.index]
|
|
591
|
-
};
|
|
592
|
-
case "index.remove": return {
|
|
593
|
-
...schema,
|
|
594
|
-
indexes: schema.indexes.filter((group) => !(group.length === step.index.length && group.every((name, position) => name === step.index[position])))
|
|
595
|
-
};
|
|
596
|
-
case "table.add":
|
|
597
|
-
case "table.remove": return schema;
|
|
457
|
+
function compileAggregateSQL(operation, column) {
|
|
458
|
+
switch (operation) {
|
|
459
|
+
case "count": return "COUNT(*)";
|
|
460
|
+
case "sum": return "SUM(" + compileFieldSQL(column) + ")";
|
|
461
|
+
case "average": return "AVG(" + compileFieldSQL(column) + ")";
|
|
462
|
+
case "minimum": return "MIN(" + compileFieldSQL(column) + ")";
|
|
463
|
+
case "maximum": return "MAX(" + compileFieldSQL(column) + ")";
|
|
598
464
|
}
|
|
599
465
|
}
|
|
600
|
-
//#endregion
|
|
601
|
-
//#region src/server/compilers.ts
|
|
602
466
|
/**
|
|
603
467
|
* Compile a NESTED {@link FieldPath} to the `json_type(<col>, <path>)` SQL
|
|
604
|
-
* expression — the {@link
|
|
468
|
+
* expression — the {@link compileFieldSQL} `json_extract` sibling used to tell a
|
|
605
469
|
* PRESENT JSON `null` apart from an ABSENT path (both read back as SQL `NULL`
|
|
606
470
|
* through `json_extract`, but `json_type` reports `'null'` for the former and
|
|
607
471
|
* SQL `NULL` for the latter).
|
|
@@ -611,14 +475,14 @@ function stepToSchema(schema, step) {
|
|
|
611
475
|
*
|
|
612
476
|
* @example
|
|
613
477
|
* ```ts
|
|
614
|
-
*
|
|
478
|
+
* compileJSONTypeSQL(['payload', 'user', 'id']) // "json_type(\"payload\", '$.user.id')"
|
|
615
479
|
* ```
|
|
616
480
|
*/
|
|
617
|
-
function
|
|
481
|
+
function compileJSONTypeSQL(path) {
|
|
618
482
|
const [column, ...nested] = path;
|
|
619
483
|
if (column === void 0) throw new _src_core.DatabaseError("VALIDATION", "A field path must contain at least one column");
|
|
620
484
|
const rest = nested.map((key) => "." + key.replaceAll("'", "''")).join("");
|
|
621
|
-
return "json_type(" +
|
|
485
|
+
return "json_type(" + quoteIdentifier(column) + ", '$" + rest + "')";
|
|
622
486
|
}
|
|
623
487
|
/**
|
|
624
488
|
* Escape `\`, `%`, and `_` (each with a leading `\`) so a `starts` / `ends`
|
|
@@ -640,15 +504,15 @@ function escapeLike(text) {
|
|
|
640
504
|
*
|
|
641
505
|
* @param column - The column name
|
|
642
506
|
* @param schema - The table's schema
|
|
643
|
-
* @returns The column's {@link
|
|
507
|
+
* @returns The column's {@link ColumnStorage}, or `undefined` if the schema does not carry it
|
|
644
508
|
*
|
|
645
509
|
* @example
|
|
646
510
|
* ```ts
|
|
647
|
-
*
|
|
511
|
+
* findColumnStorage('age', schema) // 'integer'
|
|
648
512
|
* ```
|
|
649
513
|
*/
|
|
650
|
-
function
|
|
651
|
-
return schema.columns.find((candidate) => candidate.name === column)?.
|
|
514
|
+
function findColumnStorage(column, schema) {
|
|
515
|
+
return schema.columns.find((candidate) => candidate.name === column)?.storage;
|
|
652
516
|
}
|
|
653
517
|
/**
|
|
654
518
|
* The storage type a nested (`json_extract`) operand encodes as, derived from its
|
|
@@ -663,15 +527,15 @@ function declaredType(column, schema) {
|
|
|
663
527
|
* edge of comparing against a json subtree).
|
|
664
528
|
*
|
|
665
529
|
* @param value - The runtime operand value
|
|
666
|
-
* @returns The {@link
|
|
530
|
+
* @returns The {@link ColumnStorage} to encode it as
|
|
667
531
|
*
|
|
668
532
|
* @example
|
|
669
533
|
* ```ts
|
|
670
|
-
*
|
|
671
|
-
*
|
|
534
|
+
* inferValueStorage(true) // 'boolean'
|
|
535
|
+
* inferValueStorage(9) // 'integer'
|
|
672
536
|
* ```
|
|
673
537
|
*/
|
|
674
|
-
function
|
|
538
|
+
function inferValueStorage(value) {
|
|
675
539
|
if (typeof value === "boolean") return "boolean";
|
|
676
540
|
if (typeof value === "number") return Number.isInteger(value) ? "integer" : "real";
|
|
677
541
|
if (typeof value === "bigint") return "integer";
|
|
@@ -679,7 +543,7 @@ function valueType(value) {
|
|
|
679
543
|
return "text";
|
|
680
544
|
}
|
|
681
545
|
/**
|
|
682
|
-
* Compile one condition to its `<column> <operator>` SQL fragment and the
|
|
546
|
+
* Compile one condition to its `<column> <operator>` SQL fragment and the parameters
|
|
683
547
|
* it binds — engine-exact under SQL's three-valued NULL logic.
|
|
684
548
|
*
|
|
685
549
|
* @remarks
|
|
@@ -689,7 +553,7 @@ function valueType(value) {
|
|
|
689
553
|
* encodes each operand as the NATIVE scalar `json_extract` returns, derived from
|
|
690
554
|
* the operand's runtime type (per-operand, since `between` / `any` / `none` can
|
|
691
555
|
* mix types). `any` / `none` collapse an empty list to a constant (`0` matches
|
|
692
|
-
* nothing, `1` matches all) with no
|
|
556
|
+
* nothing, `1` matches all) with no parameters.
|
|
693
557
|
*
|
|
694
558
|
* The core engine's total order ranks `undefined` (rank 0) BELOW `null`
|
|
695
559
|
* (rank 1) (see `compareValues`), so a MISSING/`NULL` column MATCHES
|
|
@@ -717,13 +581,11 @@ function valueType(value) {
|
|
|
717
581
|
* absent | true | true | false
|
|
718
582
|
* ```
|
|
719
583
|
*
|
|
720
|
-
*
|
|
721
|
-
*
|
|
722
|
-
*
|
|
723
|
-
*
|
|
724
|
-
*
|
|
725
|
-
* (it only catches the `IS NULL` row), so a flat `not`-with-`null`-operand
|
|
726
|
-
* compiles to the constant `1`.
|
|
584
|
+
* A flat SQL `NULL` represents absence or explicit null according to its
|
|
585
|
+
* {@link ColumnSchema}; optional-and-nullable columns use a storage-class
|
|
586
|
+
* sentinel to distinguish the two. The native exactness gate therefore
|
|
587
|
+
* refines every optional or nullable scalar comparison through the core engine.
|
|
588
|
+
* This compiler still emits a total SQL fragment for direct consumers.
|
|
727
589
|
*
|
|
728
590
|
* A NESTED path can be present-but-`null` (a stored JSON `null`), which
|
|
729
591
|
* `json_extract` reads back as SQL `NULL` — indistinguishable from an ABSENT
|
|
@@ -743,27 +605,27 @@ function valueType(value) {
|
|
|
743
605
|
*
|
|
744
606
|
* @example
|
|
745
607
|
* ```ts
|
|
746
|
-
*
|
|
747
|
-
* // { sql: '"age" > ?',
|
|
748
|
-
*
|
|
749
|
-
* // { sql: '("age" < ? OR "age" IS NULL)',
|
|
608
|
+
* compileConditionSQL({ column: 'age', operator: 'above', values: [18], connector: 'and' }, schema)
|
|
609
|
+
* // { sql: '"age" > ?', parameters: [18] }
|
|
610
|
+
* compileConditionSQL({ column: 'age', operator: 'below', values: [18], connector: 'and' }, schema)
|
|
611
|
+
* // { sql: '("age" < ? OR "age" IS NULL)', parameters: [18] }
|
|
750
612
|
* ```
|
|
751
613
|
*/
|
|
752
|
-
function
|
|
753
|
-
const column =
|
|
614
|
+
function compileConditionSQL(condition, schema) {
|
|
615
|
+
const column = compileFieldSQL(condition.column);
|
|
754
616
|
const nested = !(0, _orkestrel_contract.isString)(condition.column);
|
|
755
|
-
const declared = (0, _orkestrel_contract.isString)(condition.column) ?
|
|
617
|
+
const declared = (0, _orkestrel_contract.isString)(condition.column) ? schema.columns.find((candidate) => candidate.name === condition.column) : void 0;
|
|
756
618
|
const first = condition.values[0];
|
|
757
619
|
const second = condition.values[1];
|
|
758
620
|
const nullOperand = first === null || first === void 0;
|
|
759
|
-
const jsonType = !(0, _orkestrel_contract.isString)(condition.column) ?
|
|
621
|
+
const jsonType = !(0, _orkestrel_contract.isString)(condition.column) ? compileJSONTypeSQL(condition.column) : "";
|
|
760
622
|
let sql;
|
|
761
623
|
let values;
|
|
762
624
|
switch (condition.operator) {
|
|
763
625
|
case "equals":
|
|
764
626
|
if (nullOperand && nested) return {
|
|
765
627
|
sql: jsonType + " = 'null'",
|
|
766
|
-
|
|
628
|
+
parameters: []
|
|
767
629
|
};
|
|
768
630
|
sql = column + " = ?";
|
|
769
631
|
values = [first];
|
|
@@ -772,11 +634,11 @@ function fragment(condition, schema) {
|
|
|
772
634
|
if (nullOperand) {
|
|
773
635
|
if (nested) return {
|
|
774
636
|
sql: "(" + jsonType + " IS NULL OR " + jsonType + " != 'null')",
|
|
775
|
-
|
|
637
|
+
parameters: []
|
|
776
638
|
};
|
|
777
639
|
return {
|
|
778
640
|
sql: "1",
|
|
779
|
-
|
|
641
|
+
parameters: []
|
|
780
642
|
};
|
|
781
643
|
}
|
|
782
644
|
sql = "(" + column + " != ? OR " + column + " IS NULL)";
|
|
@@ -814,7 +676,7 @@ function fragment(condition, schema) {
|
|
|
814
676
|
const text = (0, _orkestrel_contract.isString)(first) ? first : "";
|
|
815
677
|
if (text === "") return {
|
|
816
678
|
sql: "typeof(" + column + ") = 'text'",
|
|
817
|
-
|
|
679
|
+
parameters: []
|
|
818
680
|
};
|
|
819
681
|
const length = Array.from(text).length;
|
|
820
682
|
sql = "(typeof(" + column + ") = 'text' AND substr(" + column + ", 1, " + length + ") = ?)";
|
|
@@ -825,7 +687,7 @@ function fragment(condition, schema) {
|
|
|
825
687
|
const text = (0, _orkestrel_contract.isString)(first) ? first : "";
|
|
826
688
|
if (text === "") return {
|
|
827
689
|
sql: "typeof(" + column + ") = 'text'",
|
|
828
|
-
|
|
690
|
+
parameters: []
|
|
829
691
|
};
|
|
830
692
|
const length = Array.from(text).length;
|
|
831
693
|
sql = "(typeof(" + column + ") = 'text' AND substr(" + column + ", -" + length + ") = ?)";
|
|
@@ -835,7 +697,7 @@ function fragment(condition, schema) {
|
|
|
835
697
|
case "any":
|
|
836
698
|
if (condition.values.length === 0) return {
|
|
837
699
|
sql: "0",
|
|
838
|
-
|
|
700
|
+
parameters: []
|
|
839
701
|
};
|
|
840
702
|
sql = column + " IN (" + condition.values.map(() => "?").join(", ") + ")";
|
|
841
703
|
values = condition.values;
|
|
@@ -843,34 +705,42 @@ function fragment(condition, schema) {
|
|
|
843
705
|
case "none":
|
|
844
706
|
if (condition.values.length === 0) return {
|
|
845
707
|
sql: "1",
|
|
846
|
-
|
|
708
|
+
parameters: []
|
|
847
709
|
};
|
|
848
710
|
sql = "(" + column + " NOT IN (" + condition.values.map(() => "?").join(", ") + ") OR " + column + " IS NULL)";
|
|
849
711
|
values = condition.values;
|
|
850
712
|
break;
|
|
851
713
|
case "absent": return {
|
|
852
714
|
sql: column + " IS NULL",
|
|
853
|
-
|
|
715
|
+
parameters: []
|
|
854
716
|
};
|
|
855
717
|
case "present": return {
|
|
856
718
|
sql: column + " IS NOT NULL",
|
|
857
|
-
|
|
719
|
+
parameters: []
|
|
858
720
|
};
|
|
859
721
|
}
|
|
860
722
|
return {
|
|
861
723
|
sql,
|
|
862
|
-
|
|
724
|
+
parameters: values.map((value) => {
|
|
725
|
+
const storage = nested ? inferValueStorage(value) : declared?.storage ?? "json";
|
|
726
|
+
return encodeValue(value, declared ?? {
|
|
727
|
+
name: "",
|
|
728
|
+
storage,
|
|
729
|
+
optional: false,
|
|
730
|
+
nullable: false
|
|
731
|
+
});
|
|
732
|
+
})
|
|
863
733
|
};
|
|
864
734
|
}
|
|
865
735
|
/**
|
|
866
736
|
* Fold the conditions into one WHERE clause, parenthesizing progressively
|
|
867
|
-
* left-to-right so the grouping matches the engine's `
|
|
737
|
+
* left-to-right so the grouping matches the engine's `matchesQuery` fold.
|
|
868
738
|
*
|
|
869
739
|
* @remarks
|
|
870
740
|
* The first condition's connector is ignored, per the {@link Condition} types.
|
|
871
|
-
* Every fragment (see {@link
|
|
741
|
+
* Every fragment (see {@link compileConditionSQL}'s truth table) replicates the core
|
|
872
742
|
* engine's total order EXACTLY under SQL's three-valued NULL logic, so this
|
|
873
|
-
* clause matches `
|
|
743
|
+
* clause matches `applyQuery` row-for-row over the same table — a native
|
|
874
744
|
* `records` / `count` read never disagrees with a scan-and-filter fallback.
|
|
875
745
|
*
|
|
876
746
|
* @param conditions - The conditions to fold
|
|
@@ -880,27 +750,27 @@ function fragment(condition, schema) {
|
|
|
880
750
|
* @example
|
|
881
751
|
* ```ts
|
|
882
752
|
* compileWhere([{ column: 'age', operator: 'from', values: [18], connector: 'and' }], schema)
|
|
883
|
-
* // { sql: 'WHERE "age" >= ?',
|
|
753
|
+
* // { sql: 'WHERE "age" >= ?', parameters: [18] }
|
|
884
754
|
* ```
|
|
885
755
|
*/
|
|
886
756
|
function compileWhere(conditions, schema) {
|
|
887
757
|
const [first, ...remaining] = conditions;
|
|
888
758
|
if (first === void 0) return {
|
|
889
759
|
sql: "",
|
|
890
|
-
|
|
760
|
+
parameters: []
|
|
891
761
|
};
|
|
892
|
-
const head =
|
|
762
|
+
const head = compileConditionSQL(first, schema);
|
|
893
763
|
let clause = head.sql;
|
|
894
|
-
const
|
|
764
|
+
const parameters = [...head.parameters];
|
|
895
765
|
for (const condition of remaining) {
|
|
896
|
-
const next =
|
|
766
|
+
const next = compileConditionSQL(condition, schema);
|
|
897
767
|
const operator = condition.connector === "or" ? "OR" : "AND";
|
|
898
768
|
clause = "(" + clause + " " + operator + " " + next.sql + ")";
|
|
899
|
-
|
|
769
|
+
parameters.push(...next.parameters);
|
|
900
770
|
}
|
|
901
771
|
return {
|
|
902
772
|
sql: "WHERE " + clause,
|
|
903
|
-
|
|
773
|
+
parameters
|
|
904
774
|
};
|
|
905
775
|
}
|
|
906
776
|
/**
|
|
@@ -930,8 +800,8 @@ function compileWhere(conditions, schema) {
|
|
|
930
800
|
* ```
|
|
931
801
|
*/
|
|
932
802
|
function compileOrder(order, schema) {
|
|
933
|
-
const terms = (order ?? []).map((term) =>
|
|
934
|
-
if (!(order ?? []).some((term) => (0, _orkestrel_contract.isString)(term.column) && term.column === schema.primary)) terms.push(
|
|
803
|
+
const terms = (order ?? []).map((term) => compileFieldSQL(term.column) + (term.direction === "descending" ? " DESC" : " ASC"));
|
|
804
|
+
if (!(order ?? []).some((term) => (0, _orkestrel_contract.isString)(term.column) && term.column === schema.primary)) terms.push(quoteIdentifier(schema.primary));
|
|
935
805
|
return terms.length === 0 ? "" : "ORDER BY " + terms.join(", ");
|
|
936
806
|
}
|
|
937
807
|
/**
|
|
@@ -947,29 +817,33 @@ function compileOrder(order, schema) {
|
|
|
947
817
|
*
|
|
948
818
|
* @example
|
|
949
819
|
* ```ts
|
|
950
|
-
* compilePage(undefined, 5) // { sql: 'LIMIT -1 OFFSET ?',
|
|
820
|
+
* compilePage(undefined, 5) // { sql: 'LIMIT -1 OFFSET ?', parameters: [5] }
|
|
951
821
|
* ```
|
|
952
822
|
*/
|
|
953
823
|
function compilePage(limit, offset) {
|
|
824
|
+
(0, _src_core.validatePage)({
|
|
825
|
+
...limit === void 0 ? {} : { limit },
|
|
826
|
+
...offset === void 0 ? {} : { offset }
|
|
827
|
+
});
|
|
954
828
|
if (limit !== void 0 && offset !== void 0) return {
|
|
955
829
|
sql: "LIMIT ? OFFSET ?",
|
|
956
|
-
|
|
830
|
+
parameters: [limit, offset]
|
|
957
831
|
};
|
|
958
832
|
if (limit !== void 0) return {
|
|
959
833
|
sql: "LIMIT ?",
|
|
960
|
-
|
|
834
|
+
parameters: [limit]
|
|
961
835
|
};
|
|
962
836
|
if (offset !== void 0) return {
|
|
963
837
|
sql: "LIMIT -1 OFFSET ?",
|
|
964
|
-
|
|
838
|
+
parameters: [offset]
|
|
965
839
|
};
|
|
966
840
|
return {
|
|
967
841
|
sql: "",
|
|
968
|
-
|
|
842
|
+
parameters: []
|
|
969
843
|
};
|
|
970
844
|
}
|
|
971
845
|
/**
|
|
972
|
-
* Compile a {@link
|
|
846
|
+
* Compile a {@link QueryInput} into the SQL clause that follows a table name, with
|
|
973
847
|
* its bound parameters in clause order.
|
|
974
848
|
*
|
|
975
849
|
* @remarks
|
|
@@ -977,39 +851,163 @@ function compilePage(limit, offset) {
|
|
|
977
851
|
* `[where, orderBy, limitOffset]` (each possibly empty) into one clause so a
|
|
978
852
|
* `SELECT * FROM <table> <clause>` runs the whole read in the engine instead of
|
|
979
853
|
* over a JS `scan`. The WHERE fold is parenthesized **left-to-right** to mirror
|
|
980
|
-
* the core engine's `
|
|
854
|
+
* the core engine's `matchesQuery` (not SQL's native AND-over-OR precedence),
|
|
981
855
|
* so a native and an engine read return identical rows. Each operand is encoded
|
|
982
856
|
* via `encodeValue`: a flat column uses its declared schema type, while a nested
|
|
983
857
|
* `FieldPath` (a `json_extract` read) encodes each operand as the native scalar
|
|
984
858
|
* the extract returns — derived from the operand's runtime type — so it compares.
|
|
985
859
|
* The 15 operators map per the databases guide's operator table, with
|
|
986
860
|
* `starts` / `ends` using `LIKE … ESCAPE '\'` and an empty `any` / `none` list
|
|
987
|
-
* collapsing to a constant.
|
|
861
|
+
* collapsing to a constant. An `undefined` input (or one with no parts)
|
|
988
862
|
* compiles to an empty clause.
|
|
989
863
|
*
|
|
990
|
-
* @param
|
|
864
|
+
* @param input - The read specification, or `undefined` for all rows
|
|
991
865
|
* @param schema - The table's schema (column types for operand encoding)
|
|
992
866
|
* @returns The SQL tail and its bound parameters
|
|
993
867
|
*
|
|
994
868
|
* @example
|
|
995
869
|
* ```ts
|
|
996
|
-
*
|
|
997
|
-
* // { sql: 'WHERE "age" >= ? ORDER BY "id"',
|
|
870
|
+
* compileQuerySQL({ conditions: [{ column: 'age', operator: 'from', values: [18], connector: 'and' }] }, schema)
|
|
871
|
+
* // { sql: 'WHERE "age" >= ? ORDER BY "id"', parameters: [18] }
|
|
998
872
|
* ```
|
|
999
873
|
*/
|
|
1000
|
-
function
|
|
1001
|
-
|
|
1002
|
-
const
|
|
1003
|
-
const
|
|
874
|
+
function compileQuerySQL(input, schema) {
|
|
875
|
+
(0, _src_core.validatePage)(input);
|
|
876
|
+
const where = compileWhere(input?.conditions ?? [], schema);
|
|
877
|
+
const orderBy = compileOrder(input?.order, schema);
|
|
878
|
+
const page = compilePage(input?.limit, input?.offset);
|
|
1004
879
|
return {
|
|
1005
880
|
sql: [
|
|
1006
881
|
where.sql,
|
|
1007
882
|
orderBy,
|
|
1008
883
|
page.sql
|
|
1009
884
|
].filter((part) => part !== "").join(" "),
|
|
1010
|
-
|
|
885
|
+
parameters: [...where.parameters, ...page.parameters]
|
|
1011
886
|
};
|
|
1012
887
|
}
|
|
888
|
+
/**
|
|
889
|
+
* Project a {@link TableSchema} to its `CREATE TABLE IF NOT EXISTS` statement.
|
|
890
|
+
*
|
|
891
|
+
* @param schema - The table schema
|
|
892
|
+
* @returns The complete table declaration
|
|
893
|
+
*/
|
|
894
|
+
function schemaToTable(schema) {
|
|
895
|
+
const columns = schema.columns.map((column) => quoteIdentifier(column.name) + " " + compileColumnSQL(column.storage) + (column.optional || column.nullable ? "" : " NOT NULL"));
|
|
896
|
+
return "CREATE TABLE IF NOT EXISTS " + quoteIdentifier(schema.name) + " (" + columns.join(", ") + ", PRIMARY KEY (" + quoteIdentifier(schema.primary) + "))";
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* Project a {@link TableSchema} to its declared SQLite indexes.
|
|
900
|
+
*
|
|
901
|
+
* @param schema - The table schema
|
|
902
|
+
* @returns One statement per declared index
|
|
903
|
+
*/
|
|
904
|
+
function schemaToIndexes(schema) {
|
|
905
|
+
return schema.indexes.map((group) => "CREATE INDEX IF NOT EXISTS " + quoteIdentifier(deriveSQLiteIndexName(schema.name, group)) + " ON " + quoteIdentifier(schema.name) + " (" + group.map(quoteIdentifier).join(", ") + ")");
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Project one {@link MigrationStep} to SQLite DDL.
|
|
909
|
+
*
|
|
910
|
+
* @param step - The migration step
|
|
911
|
+
* @returns The statements that apply the step
|
|
912
|
+
*/
|
|
913
|
+
function stepToSQL(step) {
|
|
914
|
+
switch (step.operation) {
|
|
915
|
+
case "table.add": return [schemaToTable(step.table), ...schemaToIndexes(step.table)];
|
|
916
|
+
case "table.remove": return ["DROP TABLE IF EXISTS " + quoteIdentifier(step.table)];
|
|
917
|
+
case "column.add": return ["ALTER TABLE " + quoteIdentifier(step.table) + " ADD COLUMN " + quoteIdentifier(step.column.name) + " " + compileColumnSQL(step.column.storage) + (step.column.optional || step.column.nullable ? "" : " NOT NULL")];
|
|
918
|
+
case "column.remove": return ["ALTER TABLE " + quoteIdentifier(step.table) + " DROP COLUMN " + quoteIdentifier(step.column)];
|
|
919
|
+
case "index.add": return ["CREATE INDEX IF NOT EXISTS " + quoteIdentifier(deriveSQLiteIndexName(step.table, step.index)) + " ON " + quoteIdentifier(step.table) + " (" + step.index.map(quoteIdentifier).join(", ") + ")"];
|
|
920
|
+
case "index.remove": return ["DROP INDEX IF EXISTS " + quoteIdentifier(deriveSQLiteIndexName(step.table, step.index))];
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
//#endregion
|
|
924
|
+
//#region src/core/DriverIterator.ts
|
|
925
|
+
/**
|
|
926
|
+
* The internal continuation boundary for a root driver async iterator.
|
|
927
|
+
*
|
|
928
|
+
* @remarks
|
|
929
|
+
* A driver transaction can begin while a caller holds an idle root iterator.
|
|
930
|
+
* Every `next` therefore checks the driver's root-state guard immediately
|
|
931
|
+
* before and after advancing the source. A failed continuation terminalizes the
|
|
932
|
+
* iterator, discards any row produced before the post-advance guard failed, and
|
|
933
|
+
* attempts source cleanup exactly once.
|
|
934
|
+
*/
|
|
935
|
+
var DriverIterator = class {
|
|
936
|
+
#source;
|
|
937
|
+
#guard;
|
|
938
|
+
#terminal = false;
|
|
939
|
+
#cleaned = false;
|
|
940
|
+
constructor(source, guard) {
|
|
941
|
+
this.#source = source;
|
|
942
|
+
this.#guard = guard;
|
|
943
|
+
}
|
|
944
|
+
[Symbol.asyncIterator]() {
|
|
945
|
+
return this;
|
|
946
|
+
}
|
|
947
|
+
async next() {
|
|
948
|
+
if (this.#terminal) return {
|
|
949
|
+
done: true,
|
|
950
|
+
value: void 0
|
|
951
|
+
};
|
|
952
|
+
try {
|
|
953
|
+
this.#guard();
|
|
954
|
+
const result = await this.#source.next();
|
|
955
|
+
this.#guard();
|
|
956
|
+
if (result.done === true) {
|
|
957
|
+
this.#terminal = true;
|
|
958
|
+
this.#cleaned = true;
|
|
959
|
+
}
|
|
960
|
+
return result;
|
|
961
|
+
} catch (error) {
|
|
962
|
+
this.#terminal = true;
|
|
963
|
+
await this.#discard();
|
|
964
|
+
throw error;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
async return() {
|
|
968
|
+
if (this.#terminal) return {
|
|
969
|
+
done: true,
|
|
970
|
+
value: void 0
|
|
971
|
+
};
|
|
972
|
+
this.#terminal = true;
|
|
973
|
+
if (this.#cleaned || this.#source.return === void 0) {
|
|
974
|
+
this.#cleaned = true;
|
|
975
|
+
return {
|
|
976
|
+
done: true,
|
|
977
|
+
value: void 0
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
this.#cleaned = true;
|
|
981
|
+
return this.#source.return();
|
|
982
|
+
}
|
|
983
|
+
async throw(error) {
|
|
984
|
+
if (this.#terminal) throw error;
|
|
985
|
+
if (this.#source.throw === void 0) {
|
|
986
|
+
this.#terminal = true;
|
|
987
|
+
await this.#discard();
|
|
988
|
+
throw error;
|
|
989
|
+
}
|
|
990
|
+
try {
|
|
991
|
+
const result = await this.#source.throw(error);
|
|
992
|
+
if (result.done === true) {
|
|
993
|
+
this.#terminal = true;
|
|
994
|
+
this.#cleaned = true;
|
|
995
|
+
}
|
|
996
|
+
return result;
|
|
997
|
+
} catch (cause) {
|
|
998
|
+
this.#terminal = true;
|
|
999
|
+
await this.#discard();
|
|
1000
|
+
throw cause;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
async #discard() {
|
|
1004
|
+
if (this.#cleaned) return;
|
|
1005
|
+
this.#cleaned = true;
|
|
1006
|
+
try {
|
|
1007
|
+
await this.#source.return?.();
|
|
1008
|
+
} catch {}
|
|
1009
|
+
}
|
|
1010
|
+
};
|
|
1013
1011
|
//#endregion
|
|
1014
1012
|
//#region src/server/drivers/JSONDriver.ts
|
|
1015
1013
|
/**
|
|
@@ -1017,233 +1015,619 @@ function compileCriteria(criteria, schema) {
|
|
|
1017
1015
|
* reference {@link MemoryDriver} plus file load / flush.
|
|
1018
1016
|
*
|
|
1019
1017
|
* @remarks
|
|
1020
|
-
* A decorator, not a reimplementation:
|
|
1021
|
-
* {@link MemoryDriver},
|
|
1022
|
-
*
|
|
1018
|
+
* A decorator, not a reimplementation: storage primitives delegate to an inner
|
|
1019
|
+
* {@link MemoryDriver}, while this layer owns persistence, writer ordering,
|
|
1020
|
+
* isolated transactions, and queued row-snapshot restoration. `open`
|
|
1023
1021
|
* loads the file into memory; every mutation (`write` / `delete` / `clear`) flushes
|
|
1024
|
-
* the whole store back. The file is one JSON object, `{
|
|
1025
|
-
* [name]: rows } }` — `
|
|
1026
|
-
* (an unstamped store
|
|
1027
|
-
* backward compatibility); a per-table array of rows, each row carrying its own
|
|
1022
|
+
* the whole store back. The file is one JSON object, `{ metadata?: DriverMetadata, tables: {
|
|
1023
|
+
* [name]: rows } }` — `metadata` is present only once the store has been `stamp`ed
|
|
1024
|
+
* (an unstamped store omits `metadata`); a per-table array of rows, each row carrying its own
|
|
1028
1025
|
* primary (the table contract), so the key is recovered on load with
|
|
1029
1026
|
* {@link extractKey} and the file need not store it. The parsed JSON crosses the
|
|
1030
1027
|
* boundary as `unknown` and is narrowed with {@link isRecord} / {@link extractKey},
|
|
1031
|
-
* never asserted (AGENTS §14)
|
|
1032
|
-
*
|
|
1033
|
-
*
|
|
1034
|
-
* the optional native `records` / `
|
|
1028
|
+
* never asserted (AGENTS §14). Only an `ENOENT` read starts empty; every other
|
|
1029
|
+
* read failure or invalid existing document fails closed without publication,
|
|
1030
|
+
* mutation, or automatic repair. It is scan-only — it implements none of
|
|
1031
|
+
* the optional native `records` / `aggregate` hooks, so the core engine
|
|
1035
1032
|
* over `scan` answers every query. For development, small datasets, and portable /
|
|
1036
1033
|
* inspectable data; for large or concurrent workloads reach for a SQLite-backed
|
|
1037
1034
|
* driver.
|
|
1038
1035
|
*
|
|
1039
|
-
*
|
|
1040
|
-
*
|
|
1041
|
-
*
|
|
1042
|
-
*
|
|
1043
|
-
*
|
|
1036
|
+
* Metadata crosses {@link cloneDriverMetadata} at parsed-file ingress, public and
|
|
1037
|
+
* scoped write ingress, candidate/root publication, serialization, and copy-out.
|
|
1038
|
+
* Callers therefore cannot mutate queued metadata, and `metadata()` always returns a
|
|
1039
|
+
* distinct deeply frozen snapshot. A failure in the write path ({@link
|
|
1040
|
+
* JSONDriver.#serialize} — `mkdir` / `writeFile` / `rename`) is wrapped and
|
|
1041
|
+
* rethrown as `DatabaseError` `DRIVER`, carrying the target `path` and native
|
|
1042
|
+
* `cause` in its context. If temporary-file cleanup also fails, the top-level
|
|
1043
|
+
* `DRIVER` context additionally carries `temp` and `cleanup`; a precommit abort
|
|
1044
|
+
* remains an `ABORTED` `DatabaseError` in `context.cause`. The fail-closed read path
|
|
1045
|
+
* ({@link JSONDriver.#document}) remains separate from this write-error contract.
|
|
1044
1046
|
*/
|
|
1045
1047
|
var JSONDriver = class {
|
|
1046
1048
|
#path;
|
|
1047
1049
|
#memory = new _src_core.MemoryDriver();
|
|
1050
|
+
#identities = /* @__PURE__ */ new Map();
|
|
1048
1051
|
#schema = [];
|
|
1049
|
-
#
|
|
1052
|
+
#metadata;
|
|
1050
1053
|
#flushCount = 0;
|
|
1051
1054
|
#chain = Promise.resolve();
|
|
1052
|
-
#deferring = false;
|
|
1053
1055
|
#transaction;
|
|
1056
|
+
#candidate;
|
|
1057
|
+
#candidateIdentities;
|
|
1058
|
+
#candidateSchema;
|
|
1054
1059
|
constructor(path) {
|
|
1055
1060
|
this.#path = path;
|
|
1056
1061
|
}
|
|
1057
1062
|
async open(schema) {
|
|
1058
|
-
this.#
|
|
1059
|
-
|
|
1060
|
-
await this.#
|
|
1063
|
+
this.#root();
|
|
1064
|
+
const owned = (0, _src_core.normalizeDriverSchema)(schema);
|
|
1065
|
+
await this.#enqueue(() => this.#open(owned));
|
|
1061
1066
|
}
|
|
1062
1067
|
async close() {
|
|
1068
|
+
this.#root();
|
|
1069
|
+
await this.#chain;
|
|
1063
1070
|
await this.#memory.close();
|
|
1064
1071
|
}
|
|
1065
1072
|
async read(table, key) {
|
|
1073
|
+
this.#root();
|
|
1074
|
+
await this.#chain;
|
|
1066
1075
|
return this.#memory.read(table, key);
|
|
1067
1076
|
}
|
|
1068
|
-
async write(table, key, row) {
|
|
1069
|
-
|
|
1070
|
-
|
|
1077
|
+
async write(table, key, row, options) {
|
|
1078
|
+
this.#root();
|
|
1079
|
+
await this.#enqueue(() => this.#write(table, key, row, options), options?.signal);
|
|
1080
|
+
}
|
|
1081
|
+
async insert(table, key, row, options) {
|
|
1082
|
+
this.#root();
|
|
1083
|
+
await this.#enqueue(() => this.#insert(table, key, row, options), options?.signal);
|
|
1071
1084
|
}
|
|
1072
|
-
async delete(table, key) {
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
return removed;
|
|
1085
|
+
async delete(table, key, options) {
|
|
1086
|
+
this.#root();
|
|
1087
|
+
return this.#enqueue(() => this.#delete(table, key, options), options?.signal);
|
|
1076
1088
|
}
|
|
1077
|
-
keys(table) {
|
|
1089
|
+
async keys(table) {
|
|
1090
|
+
this.#root();
|
|
1091
|
+
await this.#chain;
|
|
1078
1092
|
return this.#memory.keys(table);
|
|
1079
1093
|
}
|
|
1080
1094
|
scan(table) {
|
|
1081
|
-
return this.#
|
|
1095
|
+
return new DriverIterator(this.#scan(table)[Symbol.asyncIterator](), () => this.#root());
|
|
1082
1096
|
}
|
|
1083
1097
|
/**
|
|
1084
1098
|
* Natively filtered lazy iteration — delegates to the inner {@link MemoryDriver}.
|
|
1085
1099
|
*
|
|
1086
1100
|
* @remarks
|
|
1087
|
-
* Semantics are the memory driver's own: `
|
|
1088
|
-
* / `limit` page lazily, and `
|
|
1101
|
+
* Semantics are the memory driver's own: `input.conditions` filters, `offset`
|
|
1102
|
+
* / `limit` page lazily, and `input.order` is ignored (streaming yields key
|
|
1089
1103
|
* order; sorted output is `records()`'s job).
|
|
1090
1104
|
*
|
|
1091
1105
|
* @param table - The table to stream
|
|
1092
|
-
* @param
|
|
1106
|
+
* @param input - The filter / offset / limit to apply lazily
|
|
1093
1107
|
*/
|
|
1094
|
-
stream(table,
|
|
1095
|
-
|
|
1108
|
+
stream(table, input) {
|
|
1109
|
+
(0, _src_core.validatePage)(input);
|
|
1110
|
+
return new DriverIterator(this.#stream(table, input)[Symbol.asyncIterator](), () => this.#root());
|
|
1096
1111
|
}
|
|
1097
1112
|
async clear(table) {
|
|
1098
|
-
|
|
1099
|
-
|
|
1113
|
+
this.#root();
|
|
1114
|
+
await this.#enqueue(() => this.#clear(table));
|
|
1100
1115
|
}
|
|
1101
1116
|
/**
|
|
1102
|
-
*
|
|
1117
|
+
* Run an isolated native transaction callback over a candidate memory store.
|
|
1103
1118
|
*
|
|
1104
1119
|
* @remarks
|
|
1105
|
-
* Single-writer:
|
|
1106
|
-
*
|
|
1107
|
-
*
|
|
1108
|
-
*
|
|
1109
|
-
*
|
|
1110
|
-
*
|
|
1111
|
-
* `#flush()`, persisting the transaction's net state. `rollback()` restores
|
|
1112
|
-
* memory via the captured snapshot thunk, then `#flush()`s so the file reflects
|
|
1113
|
-
* the restored state. Outside a transaction, behavior is unchanged — every
|
|
1114
|
-
* mutation flushes on its own. Calling `commit` / `rollback` a second time (on
|
|
1115
|
-
* either method, in either order) throws `DatabaseError` `CONFLICT`.
|
|
1120
|
+
* Single-writer: nesting and root operations while active throw `CONFLICT`.
|
|
1121
|
+
* The callback receives a capability over cloned rows, schema, and metadata.
|
|
1122
|
+
* Fulfillment atomically serializes that candidate and publishes it to root
|
|
1123
|
+
* memory only after file replacement succeeds. Rejection or persistence
|
|
1124
|
+
* failure discards the candidate, and every captured capability call after
|
|
1125
|
+
* settlement throws `CONFLICT`.
|
|
1116
1126
|
*
|
|
1117
|
-
* @returns
|
|
1127
|
+
* @returns The callback's resolved value
|
|
1118
1128
|
*/
|
|
1119
|
-
async transaction() {
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
const token = {};
|
|
1123
|
-
this.#deferring = true;
|
|
1124
|
-
this.#transaction = token;
|
|
1125
|
-
return {
|
|
1126
|
-
commit: this.#commit.bind(this, token),
|
|
1127
|
-
rollback: this.#rollback.bind(this, token, rollback)
|
|
1128
|
-
};
|
|
1129
|
+
async transaction(scope) {
|
|
1130
|
+
this.#root();
|
|
1131
|
+
return this.#enqueue(() => this.#transact(scope));
|
|
1129
1132
|
}
|
|
1133
|
+
/**
|
|
1134
|
+
* Capture an owned row snapshot at an exact writer-queue position.
|
|
1135
|
+
*
|
|
1136
|
+
* @remarks
|
|
1137
|
+
* Capture owns table names, schemas, rows, and one session-local identity per
|
|
1138
|
+
* table. Rollback is repeatable: it clones the then-current root into a
|
|
1139
|
+
* candidate, adapts captured rows to each surviving same-identity table
|
|
1140
|
+
* through the portable migration engine, persists the candidate with current
|
|
1141
|
+
* metadata, and publishes memory only after file replacement succeeds.
|
|
1142
|
+
* Removed, replaced, uncaptured, and later-added tables remain untouched.
|
|
1143
|
+
*
|
|
1144
|
+
* @param tables - Existing tables to capture; omitted captures every current table
|
|
1145
|
+
* @returns A repeatable rollback operation
|
|
1146
|
+
*/
|
|
1130
1147
|
async snapshot(tables) {
|
|
1131
|
-
|
|
1148
|
+
this.#root();
|
|
1149
|
+
const names = tables === void 0 ? void 0 : [...tables];
|
|
1150
|
+
const captured = await this.#enqueue(() => this.#capture(names));
|
|
1132
1151
|
return async () => {
|
|
1133
|
-
|
|
1134
|
-
await this.#
|
|
1152
|
+
this.#root();
|
|
1153
|
+
await this.#enqueue(() => this.#restore(captured));
|
|
1135
1154
|
};
|
|
1136
1155
|
}
|
|
1137
|
-
async
|
|
1138
|
-
|
|
1156
|
+
async metadata() {
|
|
1157
|
+
this.#root();
|
|
1158
|
+
await this.#chain;
|
|
1159
|
+
return this.#metadata === void 0 ? void 0 : (0, _src_core.cloneDriverMetadata)(this.#metadata);
|
|
1139
1160
|
}
|
|
1140
1161
|
/**
|
|
1141
|
-
* Persist
|
|
1162
|
+
* Persist an owned metadata snapshot for a later `metadata()` to copy out.
|
|
1142
1163
|
*
|
|
1143
1164
|
* @remarks
|
|
1144
|
-
*
|
|
1145
|
-
*
|
|
1146
|
-
*
|
|
1165
|
+
* Root stamping conflicts while a transaction is active. The scoped
|
|
1166
|
+
* {@link StorageInterface.stamp} updates candidate metadata and publishes
|
|
1167
|
+
* with the candidate rows on callback fulfillment.
|
|
1147
1168
|
*
|
|
1148
|
-
* @param
|
|
1169
|
+
* @param metadata - The {@link DriverMetadata} to persist
|
|
1149
1170
|
*/
|
|
1150
|
-
async stamp(
|
|
1151
|
-
this.#
|
|
1152
|
-
|
|
1171
|
+
async stamp(metadata) {
|
|
1172
|
+
this.#root();
|
|
1173
|
+
const owned = (0, _src_core.cloneDriverMetadata)(metadata);
|
|
1174
|
+
await this.#enqueue(() => this.#stamp(owned));
|
|
1153
1175
|
}
|
|
1154
1176
|
/**
|
|
1155
|
-
* Apply
|
|
1156
|
-
* then persist the migrated state.
|
|
1177
|
+
* Apply one atomic {@link MigrationInput} through an isolated candidate.
|
|
1157
1178
|
*
|
|
1158
1179
|
* @remarks
|
|
1159
|
-
* The
|
|
1160
|
-
*
|
|
1161
|
-
*
|
|
1162
|
-
*
|
|
1163
|
-
* this driver's own declared `#schema`, mirroring the bookkeeping `open` does,
|
|
1164
|
-
* so a subsequent `#flush` / `#load` round-trip includes (or drops) the table.
|
|
1165
|
-
* A successful migration ends with one atomic `#flush()` so the new state
|
|
1166
|
-
* survives a close and reopen. A multi-step plan applies its steps
|
|
1167
|
-
* sequentially and is NOT atomic — a failure partway through a plan leaves
|
|
1168
|
-
* the earlier steps already applied.
|
|
1180
|
+
* The candidate receives every plan step plus optional metadata. Its complete
|
|
1181
|
+
* rows, derived schema, and metadata serialize through one atomic file
|
|
1182
|
+
* replacement before root memory changes. Any migration or persistence failure
|
|
1183
|
+
* therefore leaves root state and the prior file exact.
|
|
1169
1184
|
*
|
|
1170
|
-
* @param
|
|
1185
|
+
* @param input - The plan and optional metadata to settle together
|
|
1171
1186
|
*/
|
|
1172
|
-
async migrate(
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1187
|
+
async migrate(input) {
|
|
1188
|
+
this.#root();
|
|
1189
|
+
const owned = (0, _src_core.cloneMigrationInput)(input);
|
|
1190
|
+
await this.#enqueue(() => this.#migrate(owned));
|
|
1191
|
+
}
|
|
1192
|
+
async *#scan(table) {
|
|
1193
|
+
await this.#chain;
|
|
1194
|
+
for await (const row of this.#memory.scan(table)) yield row;
|
|
1195
|
+
}
|
|
1196
|
+
async *#stream(table, input) {
|
|
1197
|
+
await this.#chain;
|
|
1198
|
+
for await (const row of this.#memory.stream(table, input)) yield row;
|
|
1199
|
+
}
|
|
1200
|
+
async #open(declared) {
|
|
1201
|
+
const parsed = await this.#document();
|
|
1202
|
+
let stored;
|
|
1203
|
+
let tables;
|
|
1204
|
+
if (parsed === void 0) {
|
|
1205
|
+
const fresh = {};
|
|
1206
|
+
for (const table of declared) fresh[table.name] = [];
|
|
1207
|
+
tables = fresh;
|
|
1208
|
+
} else {
|
|
1209
|
+
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", {
|
|
1210
|
+
path: this.#path,
|
|
1211
|
+
aspect: "document"
|
|
1212
|
+
});
|
|
1213
|
+
if (!(0, _orkestrel_contract.isRecord)(parsed.tables)) throw new _src_core.DatabaseError("DRIVER", "Stored JSON tables are invalid", {
|
|
1214
|
+
path: this.#path,
|
|
1215
|
+
aspect: "tables"
|
|
1216
|
+
});
|
|
1217
|
+
tables = parsed.tables;
|
|
1218
|
+
if (Object.hasOwn(parsed, "metadata")) try {
|
|
1219
|
+
stored = (0, _src_core.cloneDriverMetadata)(parsed.metadata);
|
|
1220
|
+
} catch {
|
|
1221
|
+
const cause = new _src_core.DatabaseError("VALIDATION", "Stored JSON metadata failed validation", { path: "metadata" });
|
|
1222
|
+
throw new _src_core.DatabaseError("DRIVER", "Stored JSON metadata is invalid", {
|
|
1223
|
+
path: this.#path,
|
|
1224
|
+
aspect: "metadata",
|
|
1225
|
+
cause
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
const schema = (0, _src_core.normalizeDriverSchema)(stored?.schema ?? declared);
|
|
1230
|
+
const names = new Set(schema.map((table) => table.name));
|
|
1231
|
+
for (const table of schema) if (!Object.hasOwn(tables, table.name)) throw new _src_core.DatabaseError("DRIVER", "Stored JSON table set is invalid", {
|
|
1232
|
+
path: this.#path,
|
|
1233
|
+
table: table.name,
|
|
1234
|
+
aspect: "missing"
|
|
1235
|
+
});
|
|
1236
|
+
const unknown = Object.keys(tables).filter((name) => !names.has(name)).length;
|
|
1237
|
+
if (unknown > 0) throw new _src_core.DatabaseError("DRIVER", "Stored JSON table set is invalid", {
|
|
1238
|
+
path: this.#path,
|
|
1239
|
+
aspect: "unknown",
|
|
1240
|
+
count: unknown
|
|
1241
|
+
});
|
|
1242
|
+
const memory = new _src_core.MemoryDriver();
|
|
1243
|
+
await memory.open(schema);
|
|
1244
|
+
await this.#hydrate(memory, schema, tables);
|
|
1245
|
+
if (stored !== void 0) await memory.stamp(stored);
|
|
1246
|
+
this.#memory = memory;
|
|
1247
|
+
this.#schema = schema;
|
|
1248
|
+
this.#identities = this.#alignIdentities(schema);
|
|
1249
|
+
const metadata = await memory.metadata();
|
|
1250
|
+
this.#metadata = metadata === void 0 ? void 0 : (0, _src_core.cloneDriverMetadata)(metadata);
|
|
1251
|
+
}
|
|
1252
|
+
async #migrate(input) {
|
|
1253
|
+
const candidate = await this.#clone();
|
|
1254
|
+
const identities = this.#projectIdentities(this.#identities, input.plan.steps);
|
|
1255
|
+
const schema = await this.#apply(candidate, this.#schema, input);
|
|
1256
|
+
const metadata = await candidate.metadata();
|
|
1257
|
+
await this.#serialize(void 0, candidate, schema, metadata);
|
|
1258
|
+
this.#memory = candidate;
|
|
1259
|
+
this.#identities = identities;
|
|
1177
1260
|
this.#schema = schema;
|
|
1178
|
-
|
|
1179
|
-
}
|
|
1180
|
-
async #
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
this.#
|
|
1191
|
-
await this.#
|
|
1192
|
-
|
|
1193
|
-
|
|
1261
|
+
this.#metadata = metadata === void 0 ? void 0 : (0, _src_core.cloneDriverMetadata)(metadata);
|
|
1262
|
+
}
|
|
1263
|
+
async #apply(memory, current, input) {
|
|
1264
|
+
const schema = (0, _src_core.projectMigrationSchema)(current, input.plan.steps);
|
|
1265
|
+
if (input.metadata !== void 0 && !(0, _src_core.equalsValue)((0, _src_core.normalizeDriverSchema)(input.metadata.schema), schema)) throw new _src_core.DatabaseError("MIGRATION", "Migration metadata schema does not match the plan", {
|
|
1266
|
+
projected: schema,
|
|
1267
|
+
metadata: input.metadata.schema
|
|
1268
|
+
});
|
|
1269
|
+
await memory.migrate(input);
|
|
1270
|
+
return schema;
|
|
1271
|
+
}
|
|
1272
|
+
async #transact(scope) {
|
|
1273
|
+
this.#root();
|
|
1274
|
+
const candidate = await this.#clone();
|
|
1275
|
+
const token = {};
|
|
1276
|
+
this.#transaction = token;
|
|
1277
|
+
this.#candidate = candidate;
|
|
1278
|
+
this.#candidateIdentities = new Map(this.#identities);
|
|
1279
|
+
this.#candidateSchema = this.#schema;
|
|
1280
|
+
try {
|
|
1281
|
+
const value = await scope(this.#capability(token));
|
|
1282
|
+
const schema = this.#candidateSchema;
|
|
1283
|
+
const identities = this.#candidateIdentities;
|
|
1284
|
+
if (schema === void 0 || identities === void 0) throw new _src_core.DatabaseError("CONFLICT", "Transaction scope has settled");
|
|
1285
|
+
this.#candidate = void 0;
|
|
1286
|
+
this.#candidateIdentities = void 0;
|
|
1287
|
+
this.#candidateSchema = void 0;
|
|
1288
|
+
const metadata = await candidate.metadata();
|
|
1289
|
+
await this.#serialize(void 0, candidate, schema, metadata);
|
|
1290
|
+
this.#memory = candidate;
|
|
1291
|
+
this.#identities = identities;
|
|
1292
|
+
this.#schema = schema;
|
|
1293
|
+
this.#metadata = metadata === void 0 ? void 0 : (0, _src_core.cloneDriverMetadata)(metadata);
|
|
1294
|
+
return value;
|
|
1295
|
+
} finally {
|
|
1296
|
+
if (this.#transaction === token) {
|
|
1297
|
+
this.#transaction = void 0;
|
|
1298
|
+
this.#candidate = void 0;
|
|
1299
|
+
this.#candidateIdentities = void 0;
|
|
1300
|
+
this.#candidateSchema = void 0;
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
async #clone() {
|
|
1305
|
+
const candidate = new _src_core.MemoryDriver();
|
|
1306
|
+
await candidate.open(this.#schema);
|
|
1307
|
+
for (const table of this.#schema) for await (const row of this.#memory.scan(table.name)) {
|
|
1308
|
+
const key = (0, _src_core.extractKey)(row, table.primary);
|
|
1309
|
+
if (key !== void 0) await candidate.write(table.name, key, row);
|
|
1310
|
+
}
|
|
1311
|
+
if (this.#metadata !== void 0) await candidate.stamp((0, _src_core.cloneDriverMetadata)(this.#metadata));
|
|
1312
|
+
return candidate;
|
|
1313
|
+
}
|
|
1314
|
+
async #capture(names) {
|
|
1315
|
+
const selected = names === void 0 ? void 0 : new Set(names);
|
|
1316
|
+
const captured = /* @__PURE__ */ new Map();
|
|
1317
|
+
for (const table of this.#schema) {
|
|
1318
|
+
if (selected !== void 0 && !selected.has(table.name)) continue;
|
|
1319
|
+
const identity = this.#identities.get(table.name);
|
|
1320
|
+
if (identity === void 0) continue;
|
|
1321
|
+
const rows = [];
|
|
1322
|
+
for await (const row of this.#memory.scan(table.name)) rows.push(row);
|
|
1323
|
+
captured.set(table.name, {
|
|
1324
|
+
identity,
|
|
1325
|
+
schema: table,
|
|
1326
|
+
rows
|
|
1327
|
+
});
|
|
1328
|
+
}
|
|
1329
|
+
return captured;
|
|
1330
|
+
}
|
|
1331
|
+
async #restore(captured) {
|
|
1332
|
+
const replacements = /* @__PURE__ */ new Map();
|
|
1333
|
+
for (const [name, capture] of captured) {
|
|
1334
|
+
const current = this.#schema.find((table) => table.name === name);
|
|
1335
|
+
if (current === void 0 || this.#identities.get(name) !== capture.identity) continue;
|
|
1336
|
+
const plan = (0, _src_core.planMigration)([capture.schema], [current]);
|
|
1337
|
+
const migrated = (0, _src_core.migrateRows)(capture.rows, plan.steps);
|
|
1338
|
+
if (migrated.length !== capture.rows.length) throw new _src_core.DatabaseError("MIGRATION", "Snapshot row count changed during migration", { table: name });
|
|
1339
|
+
const rows = /* @__PURE__ */ new Map();
|
|
1340
|
+
for (const [index, row] of migrated.entries()) {
|
|
1341
|
+
const key = (0, _src_core.extractKey)(row, current.primary);
|
|
1342
|
+
if (key === void 0) throw new _src_core.DatabaseError("MIGRATION", `migrate: captured row is missing primary column '${current.primary}'`, {
|
|
1343
|
+
table: name,
|
|
1344
|
+
column: current.primary,
|
|
1345
|
+
index
|
|
1346
|
+
});
|
|
1347
|
+
rows.set(key, (0, _src_core.bindRowKey)(row, current.primary, key));
|
|
1348
|
+
}
|
|
1349
|
+
replacements.set(name, rows);
|
|
1350
|
+
}
|
|
1351
|
+
const candidate = await this.#clone();
|
|
1352
|
+
for (const [name, rows] of replacements) {
|
|
1353
|
+
await candidate.clear(name);
|
|
1354
|
+
for (const [key, row] of rows) await candidate.write(name, key, row);
|
|
1355
|
+
}
|
|
1356
|
+
const metadata = await candidate.metadata();
|
|
1357
|
+
await this.#serialize(void 0, candidate, this.#schema, metadata);
|
|
1358
|
+
this.#memory = candidate;
|
|
1359
|
+
this.#metadata = metadata === void 0 ? void 0 : (0, _src_core.cloneDriverMetadata)(metadata);
|
|
1360
|
+
}
|
|
1361
|
+
#capability(token) {
|
|
1362
|
+
return {
|
|
1363
|
+
read: this.#readCandidate.bind(this, token),
|
|
1364
|
+
write: this.#writeCandidate.bind(this, token),
|
|
1365
|
+
insert: this.#insertCandidate.bind(this, token),
|
|
1366
|
+
delete: this.#deleteCandidate.bind(this, token),
|
|
1367
|
+
keys: this.#keysCandidate.bind(this, token),
|
|
1368
|
+
scan: this.#scanCandidate.bind(this, token),
|
|
1369
|
+
clear: this.#clearCandidate.bind(this, token),
|
|
1370
|
+
migrate: this.#migrateCandidate.bind(this, token),
|
|
1371
|
+
metadata: this.#metadataCandidate.bind(this, token),
|
|
1372
|
+
stamp: this.#stampCandidate.bind(this, token)
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
async #readCandidate(token, table, key) {
|
|
1376
|
+
return this.#requireCandidate(token).read(table, key);
|
|
1377
|
+
}
|
|
1378
|
+
async #writeCandidate(token, table, key, row, options) {
|
|
1379
|
+
await this.#requireCandidate(token).write(table, key, row, options);
|
|
1380
|
+
}
|
|
1381
|
+
async #insertCandidate(token, table, key, row, options) {
|
|
1382
|
+
await this.#requireCandidate(token).insert(table, key, row, options);
|
|
1383
|
+
}
|
|
1384
|
+
async #deleteCandidate(token, table, key, options) {
|
|
1385
|
+
return this.#requireCandidate(token).delete(table, key, options);
|
|
1386
|
+
}
|
|
1387
|
+
async #keysCandidate(token, table) {
|
|
1388
|
+
return this.#requireCandidate(token).keys(table);
|
|
1389
|
+
}
|
|
1390
|
+
#scanCandidate(token, table) {
|
|
1391
|
+
return new DriverIterator(this.#requireCandidate(token).scan(table)[Symbol.asyncIterator](), () => {
|
|
1392
|
+
this.#requireCandidate(token);
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
async #clearCandidate(token, table) {
|
|
1396
|
+
await this.#requireCandidate(token).clear(table);
|
|
1397
|
+
}
|
|
1398
|
+
async #migrateCandidate(token, input) {
|
|
1399
|
+
const memory = this.#requireCandidate(token);
|
|
1400
|
+
const schema = this.#candidateSchema;
|
|
1401
|
+
const identities = this.#candidateIdentities;
|
|
1402
|
+
if (schema === void 0 || identities === void 0) throw new _src_core.DatabaseError("CONFLICT", "Transaction scope has settled");
|
|
1403
|
+
const owned = (0, _src_core.cloneMigrationInput)(input);
|
|
1404
|
+
const projected = this.#projectIdentities(identities, owned.plan.steps);
|
|
1405
|
+
this.#candidateSchema = await this.#apply(memory, schema, owned);
|
|
1406
|
+
this.#candidateIdentities = projected;
|
|
1407
|
+
}
|
|
1408
|
+
async #metadataCandidate(token) {
|
|
1409
|
+
const metadata = await this.#requireCandidate(token).metadata();
|
|
1410
|
+
return metadata === void 0 ? void 0 : (0, _src_core.cloneDriverMetadata)(metadata);
|
|
1411
|
+
}
|
|
1412
|
+
async #stampCandidate(token, metadata) {
|
|
1413
|
+
const memory = this.#requireCandidate(token);
|
|
1414
|
+
const owned = (0, _src_core.cloneDriverMetadata)(metadata);
|
|
1415
|
+
await memory.stamp(owned);
|
|
1416
|
+
}
|
|
1417
|
+
#requireCandidate(token) {
|
|
1418
|
+
if (this.#transaction !== token || this.#candidate === void 0) throw new _src_core.DatabaseError("CONFLICT", "Transaction scope has settled");
|
|
1419
|
+
return this.#candidate;
|
|
1420
|
+
}
|
|
1421
|
+
#alignIdentities(schema) {
|
|
1422
|
+
const aligned = /* @__PURE__ */ new Map();
|
|
1423
|
+
for (const table of schema) aligned.set(table.name, this.#identities.get(table.name) ?? {});
|
|
1424
|
+
return aligned;
|
|
1425
|
+
}
|
|
1426
|
+
#projectIdentities(identities, steps) {
|
|
1427
|
+
const projected = new Map(identities);
|
|
1428
|
+
for (const step of steps) {
|
|
1429
|
+
if (step.operation === "table.add") projected.set(step.table.name, {});
|
|
1430
|
+
if (step.operation === "table.remove") projected.delete(step.table);
|
|
1431
|
+
}
|
|
1432
|
+
return projected;
|
|
1433
|
+
}
|
|
1434
|
+
#root() {
|
|
1435
|
+
if (this.#transaction !== void 0) throw new _src_core.DatabaseError("CONFLICT", "A transaction is active on this driver");
|
|
1436
|
+
}
|
|
1437
|
+
async #document() {
|
|
1194
1438
|
let raw;
|
|
1195
1439
|
try {
|
|
1196
1440
|
raw = await (0, node_fs_promises.readFile)(this.#path, "utf-8");
|
|
1197
|
-
} catch {
|
|
1198
|
-
return;
|
|
1441
|
+
} catch (error) {
|
|
1442
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") return;
|
|
1443
|
+
throw new _src_core.DatabaseError("DRIVER", "Failed to read the JSON database file", {
|
|
1444
|
+
path: this.#path,
|
|
1445
|
+
cause: error
|
|
1446
|
+
});
|
|
1199
1447
|
}
|
|
1200
|
-
let parsed;
|
|
1201
1448
|
try {
|
|
1202
|
-
|
|
1449
|
+
return JSON.parse(raw);
|
|
1203
1450
|
} catch {
|
|
1204
|
-
|
|
1451
|
+
throw new _src_core.DatabaseError("DRIVER", "Stored JSON database is invalid JSON", {
|
|
1452
|
+
path: this.#path,
|
|
1453
|
+
aspect: "syntax"
|
|
1454
|
+
});
|
|
1205
1455
|
}
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
for (const table of
|
|
1456
|
+
}
|
|
1457
|
+
async #hydrate(memory, schema, tables) {
|
|
1458
|
+
for (const table of schema) {
|
|
1209
1459
|
const rows = tables[table.name];
|
|
1210
|
-
if (!Array.isArray(rows))
|
|
1211
|
-
|
|
1212
|
-
|
|
1460
|
+
if (!Array.isArray(rows)) throw new _src_core.DatabaseError("DRIVER", "Stored JSON table is invalid", {
|
|
1461
|
+
path: this.#path,
|
|
1462
|
+
table: table.name,
|
|
1463
|
+
aspect: "container"
|
|
1464
|
+
});
|
|
1465
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1466
|
+
for (const [index, entry] of rows.entries()) {
|
|
1467
|
+
if (!(0, _orkestrel_contract.isRecord)(entry)) throw new _src_core.DatabaseError("DRIVER", "Stored JSON row is invalid", {
|
|
1468
|
+
path: this.#path,
|
|
1469
|
+
table: table.name,
|
|
1470
|
+
index,
|
|
1471
|
+
aspect: "record"
|
|
1472
|
+
});
|
|
1213
1473
|
const key = (0, _src_core.extractKey)(entry, table.primary);
|
|
1214
|
-
if (key === void 0)
|
|
1215
|
-
|
|
1474
|
+
if (key === void 0) throw new _src_core.DatabaseError("DRIVER", "Stored JSON row is invalid", {
|
|
1475
|
+
path: this.#path,
|
|
1476
|
+
table: table.name,
|
|
1477
|
+
index,
|
|
1478
|
+
aspect: "primary"
|
|
1479
|
+
});
|
|
1480
|
+
if (keys.has(key)) throw new _src_core.DatabaseError("DRIVER", "Stored JSON row is invalid", {
|
|
1481
|
+
path: this.#path,
|
|
1482
|
+
table: table.name,
|
|
1483
|
+
index,
|
|
1484
|
+
aspect: "duplicate"
|
|
1485
|
+
});
|
|
1486
|
+
keys.add(key);
|
|
1487
|
+
await memory.write(table.name, key, entry);
|
|
1216
1488
|
}
|
|
1217
1489
|
}
|
|
1218
|
-
if ((0, _src_core.isDriverMeta)(parsed.meta)) this.#meta = parsed.meta;
|
|
1219
1490
|
}
|
|
1220
|
-
async #
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1491
|
+
async #enqueue(operation, signal) {
|
|
1492
|
+
(0, _src_core.checkAbort)(signal);
|
|
1493
|
+
let started = false;
|
|
1494
|
+
const next = this.#chain.then(async () => {
|
|
1495
|
+
started = true;
|
|
1496
|
+
(0, _src_core.checkAbort)(signal);
|
|
1497
|
+
return operation();
|
|
1498
|
+
});
|
|
1499
|
+
this.#chain = next.then(() => {}, () => {});
|
|
1500
|
+
if (signal === void 0) return next;
|
|
1501
|
+
const cleanup = new AbortController();
|
|
1502
|
+
return new Promise((resolve, reject) => {
|
|
1503
|
+
signal.addEventListener("abort", () => {
|
|
1504
|
+
if (started) return;
|
|
1505
|
+
try {
|
|
1506
|
+
(0, _src_core.checkAbort)(signal);
|
|
1507
|
+
} catch (error) {
|
|
1508
|
+
reject(error);
|
|
1509
|
+
}
|
|
1510
|
+
}, {
|
|
1511
|
+
once: true,
|
|
1512
|
+
signal: cleanup.signal
|
|
1513
|
+
});
|
|
1514
|
+
next.then((result) => {
|
|
1515
|
+
cleanup.abort();
|
|
1516
|
+
resolve(result);
|
|
1517
|
+
}, (error) => {
|
|
1518
|
+
cleanup.abort();
|
|
1519
|
+
reject(error);
|
|
1520
|
+
});
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
async #write(table, key, row, options) {
|
|
1524
|
+
const previous = await this.#memory.read(table, key);
|
|
1525
|
+
await this.#memory.write(table, key, row, options);
|
|
1526
|
+
try {
|
|
1527
|
+
await this.#serialize(options?.signal);
|
|
1528
|
+
} catch (error) {
|
|
1529
|
+
if (previous === void 0) await this.#memory.delete(table, key);
|
|
1530
|
+
else await this.#memory.write(table, key, previous);
|
|
1531
|
+
throw error;
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
async #insert(table, key, row, options) {
|
|
1535
|
+
await this.#memory.insert(table, key, row, options);
|
|
1536
|
+
try {
|
|
1537
|
+
await this.#serialize(options?.signal);
|
|
1538
|
+
} catch (error) {
|
|
1539
|
+
await this.#memory.delete(table, key);
|
|
1540
|
+
throw error;
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
async #delete(table, key, options) {
|
|
1544
|
+
const previous = await this.#memory.read(table, key);
|
|
1545
|
+
if (previous === void 0) {
|
|
1546
|
+
(0, _src_core.checkAbort)(options?.signal);
|
|
1547
|
+
return false;
|
|
1548
|
+
}
|
|
1549
|
+
await this.#memory.delete(table, key, options);
|
|
1550
|
+
try {
|
|
1551
|
+
await this.#serialize(options?.signal);
|
|
1552
|
+
} catch (error) {
|
|
1553
|
+
await this.#memory.write(table, key, previous);
|
|
1554
|
+
throw error;
|
|
1555
|
+
}
|
|
1556
|
+
return true;
|
|
1557
|
+
}
|
|
1558
|
+
async #clear(table) {
|
|
1559
|
+
const rollback = await this.#memory.snapshot([table]);
|
|
1560
|
+
await this.#memory.clear(table);
|
|
1561
|
+
try {
|
|
1562
|
+
await this.#serialize();
|
|
1563
|
+
} catch (error) {
|
|
1564
|
+
await rollback();
|
|
1565
|
+
throw error;
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
async #stamp(metadata) {
|
|
1569
|
+
const previous = this.#metadata;
|
|
1570
|
+
this.#metadata = (0, _src_core.cloneDriverMetadata)(metadata);
|
|
1571
|
+
try {
|
|
1572
|
+
await this.#serialize();
|
|
1573
|
+
} catch (error) {
|
|
1574
|
+
this.#metadata = previous;
|
|
1575
|
+
throw error;
|
|
1576
|
+
}
|
|
1224
1577
|
}
|
|
1225
|
-
async #serialize() {
|
|
1578
|
+
async #serialize(signal, memory = this.#memory, schema = this.#schema, metadata = this.#metadata) {
|
|
1579
|
+
const owned = metadata === void 0 ? void 0 : (0, _src_core.cloneDriverMetadata)(metadata);
|
|
1226
1580
|
const tables = {};
|
|
1227
|
-
|
|
1581
|
+
(0, _src_core.checkAbort)(signal);
|
|
1582
|
+
for (const table of schema) {
|
|
1228
1583
|
const rows = [];
|
|
1229
|
-
for await (const row of
|
|
1584
|
+
for await (const row of memory.scan(table.name)) {
|
|
1585
|
+
(0, _src_core.checkAbort)(signal);
|
|
1586
|
+
rows.push(row);
|
|
1587
|
+
}
|
|
1230
1588
|
tables[table.name] = rows;
|
|
1231
1589
|
}
|
|
1590
|
+
(0, _src_core.checkAbort)(signal);
|
|
1232
1591
|
this.#flushCount += 1;
|
|
1233
1592
|
const temp = `${this.#path}.${process.pid}.${this.#flushCount}.tmp`;
|
|
1234
|
-
const payload =
|
|
1235
|
-
|
|
1593
|
+
const payload = owned === void 0 ? { tables } : {
|
|
1594
|
+
metadata: owned,
|
|
1236
1595
|
tables
|
|
1237
1596
|
};
|
|
1597
|
+
let dispatched = false;
|
|
1238
1598
|
try {
|
|
1239
1599
|
await (0, node_fs_promises.mkdir)((0, node_path.dirname)(this.#path), { recursive: true });
|
|
1240
|
-
|
|
1600
|
+
const serialized = JSON.stringify(payload, null, 2);
|
|
1601
|
+
(0, _src_core.checkAbort)(signal);
|
|
1602
|
+
await (0, node_fs_promises.writeFile)(temp, serialized, {
|
|
1603
|
+
encoding: "utf-8",
|
|
1604
|
+
flush: true,
|
|
1605
|
+
signal
|
|
1606
|
+
});
|
|
1607
|
+
(0, _src_core.checkAbort)(signal);
|
|
1608
|
+
dispatched = true;
|
|
1241
1609
|
await (0, node_fs_promises.rename)(temp, this.#path);
|
|
1242
1610
|
} catch (error) {
|
|
1243
|
-
|
|
1611
|
+
let cause = error;
|
|
1612
|
+
if (!dispatched) try {
|
|
1613
|
+
(0, _src_core.checkAbort)(signal);
|
|
1614
|
+
} catch (abort) {
|
|
1615
|
+
cause = abort;
|
|
1616
|
+
}
|
|
1617
|
+
try {
|
|
1618
|
+
await (0, node_fs_promises.rm)(temp, { force: true });
|
|
1619
|
+
} catch (cleanup) {
|
|
1620
|
+
throw new _src_core.DatabaseError("DRIVER", "Failed to persist and clean the database file", {
|
|
1621
|
+
path: this.#path,
|
|
1622
|
+
temp,
|
|
1623
|
+
cause,
|
|
1624
|
+
cleanup
|
|
1625
|
+
});
|
|
1626
|
+
}
|
|
1627
|
+
if ((0, _src_core.isDatabaseError)(cause) && cause.code === "ABORTED") throw cause;
|
|
1244
1628
|
throw new _src_core.DatabaseError("DRIVER", "Failed to persist the database file", {
|
|
1245
1629
|
path: this.#path,
|
|
1246
|
-
cause
|
|
1630
|
+
cause
|
|
1247
1631
|
});
|
|
1248
1632
|
}
|
|
1249
1633
|
}
|
|
@@ -1260,21 +1644,23 @@ var JSONDriver = class {
|
|
|
1260
1644
|
* raw `node:sqlite`. `open` issues `CREATE TABLE IF NOT EXISTS` with real typed
|
|
1261
1645
|
* columns (mapped from each {@link TableSchema}'s portable column types) and a
|
|
1262
1646
|
* `PRIMARY KEY`, plus a `CREATE INDEX IF NOT EXISTS` per declared index (both
|
|
1263
|
-
* reopen-safe), and readies a reserved `
|
|
1264
|
-
* `stamp()` read and write — **a user table named `
|
|
1647
|
+
* reopen-safe), and readies a reserved `_metadata` single-row table `metadata()` /
|
|
1648
|
+
* `stamp()` read and write — **a user table named `_metadata` collides with it**;
|
|
1265
1649
|
* avoid the name. Rows cross the boundary through the codecs in `helpers.ts`
|
|
1266
1650
|
* (`json` columns store / parse JSON text, a `boolean` stores `1` / `0`), so the
|
|
1267
1651
|
* typed layer above imposes the exact shape (AGENTS §14). `write` is an
|
|
1268
|
-
* `INSERT OR REPLACE` upsert
|
|
1269
|
-
*
|
|
1270
|
-
* `SQLiteError`
|
|
1271
|
-
*
|
|
1272
|
-
*
|
|
1273
|
-
* `
|
|
1274
|
-
*
|
|
1275
|
-
*
|
|
1276
|
-
*
|
|
1277
|
-
*
|
|
1652
|
+
* `INSERT OR REPLACE` upsert, while `insert` uses a plain `INSERT` and maps its
|
|
1653
|
+
* atomic primary-key constraint failure to `CONFLICT`; every other backend
|
|
1654
|
+
* `SQLiteError` is contained by the same `DatabaseError` boundary described
|
|
1655
|
+
* below. Querying, ordering, paging, and
|
|
1656
|
+
* aggregation is native: `records` / `stream` compile a `QueryInput`
|
|
1657
|
+
* to SQL with `compileQuerySQL`, and `aggregate` runs a SQL
|
|
1658
|
+
* `COUNT`/`SUM`/`AVG`/`MIN`/`MAX` (via `compileAggregateSQL`) over the same compiled
|
|
1659
|
+
* WHERE. `transaction` runs a callback inside native `BEGIN` / `COMMIT` /
|
|
1660
|
+
* `ROLLBACK`, passing a scoped storage capability that becomes invalid after
|
|
1661
|
+
* settlement. `migrate` runs the plan's projected DDL
|
|
1662
|
+
* ({@link import('../compilers.js').stepToSQL}) inside whichever native
|
|
1663
|
+
* transaction is active: joined into the active transaction callback
|
|
1278
1664
|
* when one exists (the core's versioned reconcile path wraps migrate + stamp
|
|
1279
1665
|
* in one native `BEGIN`, and node:sqlite rejects a nested `BEGIN`), or inside
|
|
1280
1666
|
* its own `database.transaction` otherwise — a mid-plan failure rolls back
|
|
@@ -1302,278 +1688,641 @@ var SQLiteDriver = class {
|
|
|
1302
1688
|
#options;
|
|
1303
1689
|
#database;
|
|
1304
1690
|
#schema = /* @__PURE__ */ new Map();
|
|
1691
|
+
#identities = /* @__PURE__ */ new Map();
|
|
1305
1692
|
#transaction;
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1693
|
+
#candidateSchema;
|
|
1694
|
+
#candidateIdentities;
|
|
1695
|
+
constructor(options = {}) {
|
|
1696
|
+
this.#path = options.path ?? ":memory:";
|
|
1697
|
+
this.#options = options;
|
|
1309
1698
|
}
|
|
1310
1699
|
async open(schema) {
|
|
1311
|
-
|
|
1700
|
+
this.#root();
|
|
1701
|
+
const owned = (0, _src_core.normalizeDriverSchema)(schema);
|
|
1702
|
+
if (owned.some((table) => table.name === "_metadata")) throw new _src_core.DatabaseError("VALIDATION", `A declared table cannot be named '${METADATA_TABLE}' — it is reserved for driver metadata`, { table: METADATA_TABLE });
|
|
1312
1703
|
this.#guard(() => {
|
|
1313
|
-
this.#database
|
|
1704
|
+
const current = this.#database;
|
|
1705
|
+
const previousIdentities = this.#identities;
|
|
1706
|
+
this.#database = void 0;
|
|
1707
|
+
this.#schema = /* @__PURE__ */ new Map();
|
|
1708
|
+
this.#identities = /* @__PURE__ */ new Map();
|
|
1709
|
+
current?.close();
|
|
1314
1710
|
const database = (0, _orkestrel_sqlite.createSQLiteDatabase)({
|
|
1315
1711
|
path: this.#path,
|
|
1316
1712
|
...this.#options.readonly !== void 0 ? { readonly: this.#options.readonly } : {},
|
|
1317
1713
|
...this.#options.timeout !== void 0 ? { timeout: this.#options.timeout } : {},
|
|
1318
|
-
...this.#options.
|
|
1714
|
+
...this.#options.references !== void 0 ? { foreignKeys: this.#options.references } : {}
|
|
1319
1715
|
});
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
database.
|
|
1326
|
-
|
|
1716
|
+
try {
|
|
1717
|
+
database.connect();
|
|
1718
|
+
for (const [name, value] of Object.entries(this.#options.pragmas ?? {})) database.pragma(name, value);
|
|
1719
|
+
const map = /* @__PURE__ */ new Map();
|
|
1720
|
+
const identities = /* @__PURE__ */ new Map();
|
|
1721
|
+
database.transaction(() => {
|
|
1722
|
+
this.#ensureMetadataTable(database);
|
|
1723
|
+
const stored = this.#readMetadata(database);
|
|
1724
|
+
const deployed = (0, _src_core.normalizeDriverSchema)(stored?.schema ?? owned);
|
|
1725
|
+
const missing = /* @__PURE__ */ new Map();
|
|
1726
|
+
for (const table of deployed) {
|
|
1727
|
+
map.set(table.name, table);
|
|
1728
|
+
missing.set(table.name, this.#validateTable(database, table));
|
|
1729
|
+
}
|
|
1730
|
+
if (stored !== void 0) {
|
|
1731
|
+
for (const table of stored.schema) if (missing.get(table.name) === void 0) throw new _src_core.DatabaseError("DRIVER", "Stored SQLite table is missing", {
|
|
1732
|
+
table: table.name,
|
|
1733
|
+
aspect: "missing"
|
|
1734
|
+
});
|
|
1735
|
+
}
|
|
1736
|
+
for (const table of deployed) {
|
|
1737
|
+
const absent = missing.get(table.name);
|
|
1738
|
+
if (absent === void 0) {
|
|
1739
|
+
database.exec(schemaToTable(table));
|
|
1740
|
+
for (const sql of schemaToIndexes(table)) database.exec(sql);
|
|
1741
|
+
} else for (const [index, group] of table.indexes.entries()) if (absent.includes(deriveSQLiteIndexName(table.name, group))) {
|
|
1742
|
+
const sql = schemaToIndexes(table)[index];
|
|
1743
|
+
if (sql !== void 0) database.exec(sql);
|
|
1744
|
+
}
|
|
1745
|
+
identities.set(table.name, previousIdentities.get(table.name) ?? {});
|
|
1746
|
+
}
|
|
1747
|
+
});
|
|
1748
|
+
this.#schema = map;
|
|
1749
|
+
this.#identities = identities;
|
|
1750
|
+
this.#database = database;
|
|
1751
|
+
} catch (error) {
|
|
1752
|
+
try {
|
|
1753
|
+
database.close();
|
|
1754
|
+
} catch {}
|
|
1755
|
+
this.#schema = /* @__PURE__ */ new Map();
|
|
1756
|
+
this.#identities = /* @__PURE__ */ new Map();
|
|
1757
|
+
throw error;
|
|
1327
1758
|
}
|
|
1328
|
-
database.exec("CREATE TABLE IF NOT EXISTS " + quote(META_TABLE) + " (\"id\" INTEGER, \"version\" INTEGER, \"schema\" TEXT, PRIMARY KEY (\"id\"))");
|
|
1329
|
-
this.#schema = map;
|
|
1330
|
-
this.#database = database;
|
|
1331
1759
|
});
|
|
1332
1760
|
}
|
|
1333
1761
|
async close() {
|
|
1334
|
-
this.#
|
|
1335
|
-
this.#
|
|
1762
|
+
this.#root();
|
|
1763
|
+
this.#guard(() => {
|
|
1764
|
+
this.#database?.close();
|
|
1765
|
+
this.#database = void 0;
|
|
1766
|
+
});
|
|
1336
1767
|
}
|
|
1337
1768
|
async read(table, key) {
|
|
1338
|
-
|
|
1339
|
-
return this.#
|
|
1340
|
-
const row = this.#require().prepare("SELECT * FROM " + quote(table) + " WHERE " + quote(schema.primary) + " = ?").get([this.#key(key, schema)]);
|
|
1341
|
-
return row === void 0 ? void 0 : decodeRow(row, schema);
|
|
1342
|
-
});
|
|
1769
|
+
this.#root();
|
|
1770
|
+
return this.#read(table, key);
|
|
1343
1771
|
}
|
|
1344
|
-
async write(table, key, row) {
|
|
1345
|
-
|
|
1346
|
-
this.#
|
|
1347
|
-
const encoded = encodeRow({
|
|
1348
|
-
...row,
|
|
1349
|
-
[schema.primary]: key
|
|
1350
|
-
}, schema);
|
|
1351
|
-
const names = schema.columns.map((column) => column.name);
|
|
1352
|
-
const values = extractValues(encoded, names, table);
|
|
1353
|
-
this.#require().prepare("INSERT OR REPLACE INTO " + quote(table) + " (" + names.map(quote).join(", ") + ") VALUES (" + names.map(() => "?").join(", ") + ")").run(values);
|
|
1354
|
-
});
|
|
1772
|
+
async write(table, key, row, options) {
|
|
1773
|
+
this.#root();
|
|
1774
|
+
await this.#write(table, key, row, options);
|
|
1355
1775
|
}
|
|
1356
|
-
async
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1776
|
+
async insert(table, key, row, options) {
|
|
1777
|
+
this.#root();
|
|
1778
|
+
await this.#insert(table, key, row, options);
|
|
1779
|
+
}
|
|
1780
|
+
async delete(table, key, options) {
|
|
1781
|
+
this.#root();
|
|
1782
|
+
return this.#delete(table, key, options);
|
|
1361
1783
|
}
|
|
1362
1784
|
async keys(table) {
|
|
1363
|
-
|
|
1364
|
-
return this.#
|
|
1365
|
-
const primary = quote(schema.primary);
|
|
1366
|
-
const rows = this.#require().prepare("SELECT " + primary + " FROM " + quote(table) + " ORDER BY " + primary).all();
|
|
1367
|
-
const keys = [];
|
|
1368
|
-
for (const row of rows) {
|
|
1369
|
-
const value = row[schema.primary];
|
|
1370
|
-
if (typeof value === "string" || typeof value === "number") keys.push(value);
|
|
1371
|
-
}
|
|
1372
|
-
return keys;
|
|
1373
|
-
});
|
|
1785
|
+
this.#root();
|
|
1786
|
+
return this.#keys(table);
|
|
1374
1787
|
}
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
const iterator = this.#guard(() => this.#require().prepare("SELECT * FROM " + quote(table) + " ORDER BY " + quote(schema.primary)).iterate())[Symbol.iterator]();
|
|
1378
|
-
while (true) {
|
|
1379
|
-
const step = this.#guard(() => iterator.next());
|
|
1380
|
-
if (step.done === true) return;
|
|
1381
|
-
yield this.#guard(() => decodeRow(step.value, schema));
|
|
1382
|
-
}
|
|
1788
|
+
scan(table) {
|
|
1789
|
+
return new DriverIterator(this.#scan(table)[Symbol.asyncIterator](), () => this.#root());
|
|
1383
1790
|
}
|
|
1384
1791
|
async clear(table) {
|
|
1385
|
-
this.#
|
|
1386
|
-
this.#
|
|
1387
|
-
this.#require().prepare("DELETE FROM " + quote(table)).run();
|
|
1388
|
-
});
|
|
1389
|
-
}
|
|
1390
|
-
async records(table, criteria) {
|
|
1391
|
-
const schema = this.#table(table);
|
|
1392
|
-
if (isExactCriteria(criteria, schema)) return this.#guard(() => {
|
|
1393
|
-
const { sql, params } = compileCriteria(criteria, schema);
|
|
1394
|
-
return this.#require().prepare("SELECT * FROM " + quote(table) + (sql === "" ? "" : " " + sql)).all(params).map((row) => decodeRow(row, schema));
|
|
1395
|
-
});
|
|
1396
|
-
const rows = [];
|
|
1397
|
-
for await (const row of this.scan(table)) rows.push(row);
|
|
1398
|
-
return (0, _src_core.applyCriteria)(rows, criteria);
|
|
1792
|
+
this.#root();
|
|
1793
|
+
await this.#clear(table);
|
|
1399
1794
|
}
|
|
1400
|
-
async
|
|
1795
|
+
async records(table, input) {
|
|
1796
|
+
(0, _src_core.validatePage)(input);
|
|
1797
|
+
this.#root();
|
|
1401
1798
|
const schema = this.#table(table);
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
const value = this.#require().prepare("SELECT COUNT(*) AS count FROM " + quote(table) + (sql === "" ? "" : " " + sql)).get(params)?.count;
|
|
1406
|
-
return typeof value === "number" || typeof value === "bigint" ? Number(value) : 0;
|
|
1799
|
+
if (matchesQueryExactly(input, schema)) return this.#guard(() => {
|
|
1800
|
+
const { sql, parameters } = compileQuerySQL(input, schema);
|
|
1801
|
+
return this.#require().prepare("SELECT * FROM " + quoteIdentifier(table) + (sql === "" ? "" : " " + sql)).all(parameters).map((row) => decodeRow(row, schema));
|
|
1407
1802
|
});
|
|
1408
1803
|
const rows = [];
|
|
1409
|
-
for await (const row of this
|
|
1410
|
-
return (0, _src_core.
|
|
1804
|
+
for await (const row of this.#scan(table)) rows.push(row);
|
|
1805
|
+
return (0, _src_core.applyQuery)(rows, input);
|
|
1411
1806
|
}
|
|
1412
|
-
async aggregate(table, operation, column,
|
|
1807
|
+
async aggregate(table, operation, column, input) {
|
|
1808
|
+
(0, _src_core.validatePage)(input);
|
|
1809
|
+
this.#root();
|
|
1413
1810
|
const schema = this.#table(table);
|
|
1414
|
-
const conditions =
|
|
1415
|
-
const conditionsExact = conditions.every((condition) =>
|
|
1416
|
-
const columnExact = operation
|
|
1811
|
+
const conditions = input.conditions ?? [];
|
|
1812
|
+
const conditionsExact = conditions.every((condition) => matchesConditionExactly(condition, schema));
|
|
1813
|
+
const columnExact = matchesAggregateExactly(operation, column, schema);
|
|
1417
1814
|
if (conditionsExact && columnExact) return this.#guard(() => {
|
|
1418
|
-
const { sql,
|
|
1419
|
-
const value = this.#require().prepare("SELECT " +
|
|
1815
|
+
const { sql, parameters } = compileWhere(conditions, schema);
|
|
1816
|
+
const value = this.#require().prepare("SELECT " + compileAggregateSQL(operation, column) + " AS value FROM " + quoteIdentifier(table) + (sql === "" ? "" : " " + sql)).get(parameters)?.value;
|
|
1420
1817
|
return value === null || value === void 0 ? void 0 : Number(value);
|
|
1421
1818
|
});
|
|
1422
1819
|
const rows = [];
|
|
1423
|
-
for await (const row of this
|
|
1820
|
+
for await (const row of this.#scan(table)) rows.push(row);
|
|
1424
1821
|
return (0, _src_core.computeAggregate)((0, _src_core.filterRows)(rows, conditions), operation, column);
|
|
1425
1822
|
}
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
if (conditions.every((condition) => isExactCondition(condition, schema))) {
|
|
1430
|
-
const compiled = compileCriteria({
|
|
1431
|
-
conditions,
|
|
1432
|
-
...criteria.limit !== void 0 ? { limit: criteria.limit } : {},
|
|
1433
|
-
...criteria.offset !== void 0 ? { offset: criteria.offset } : {}
|
|
1434
|
-
}, schema);
|
|
1435
|
-
for (const row of this.#require().prepare("SELECT * FROM " + quote(table) + (compiled.sql === "" ? "" : " " + compiled.sql)).iterate(compiled.params)) yield decodeRow(row, schema);
|
|
1436
|
-
return;
|
|
1437
|
-
}
|
|
1438
|
-
const offset = criteria.offset ?? 0;
|
|
1439
|
-
const limit = criteria.limit;
|
|
1440
|
-
let skipped = 0;
|
|
1441
|
-
let yielded = 0;
|
|
1442
|
-
for await (const row of this.scan(table)) {
|
|
1443
|
-
if (limit !== void 0 && yielded >= limit) return;
|
|
1444
|
-
if (conditions.length > 0 && !(0, _src_core.matchesCriteria)(row, conditions)) continue;
|
|
1445
|
-
if (skipped < offset) {
|
|
1446
|
-
skipped += 1;
|
|
1447
|
-
continue;
|
|
1448
|
-
}
|
|
1449
|
-
yield row;
|
|
1450
|
-
yielded += 1;
|
|
1451
|
-
}
|
|
1823
|
+
stream(table, input) {
|
|
1824
|
+
(0, _src_core.validatePage)(input);
|
|
1825
|
+
return new DriverIterator(this.#stream(table, input)[Symbol.asyncIterator](), () => this.#root());
|
|
1452
1826
|
}
|
|
1453
1827
|
/**
|
|
1454
1828
|
* Begin a native transaction — real `BEGIN`, `COMMIT`, `ROLLBACK`.
|
|
1455
1829
|
*
|
|
1456
1830
|
* @remarks
|
|
1457
|
-
*
|
|
1458
|
-
*
|
|
1831
|
+
* The callback receives a scoped {@link StorageInterface}. Fulfillment
|
|
1832
|
+
* commits and returns its value; rejection rolls back and preserves the
|
|
1833
|
+
* original error. Root operations and nesting conflict while active, and a
|
|
1834
|
+
* captured capability conflicts after settlement.
|
|
1459
1835
|
*
|
|
1460
|
-
* @returns
|
|
1836
|
+
* @returns The callback's resolved value
|
|
1461
1837
|
*/
|
|
1462
|
-
async transaction() {
|
|
1838
|
+
async transaction(scope) {
|
|
1839
|
+
this.#root();
|
|
1463
1840
|
const database = this.#require();
|
|
1464
|
-
this.#guard(() => database.
|
|
1841
|
+
this.#guard(() => database.begin());
|
|
1465
1842
|
const token = {};
|
|
1466
1843
|
this.#transaction = token;
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1844
|
+
this.#candidateSchema = new Map(this.#schema);
|
|
1845
|
+
this.#candidateIdentities = new Map(this.#identities);
|
|
1846
|
+
try {
|
|
1847
|
+
let value;
|
|
1848
|
+
try {
|
|
1849
|
+
value = await scope(this.#capability(token));
|
|
1850
|
+
} catch (error) {
|
|
1851
|
+
this.#guard(() => database.rollback());
|
|
1852
|
+
throw error;
|
|
1853
|
+
}
|
|
1854
|
+
try {
|
|
1855
|
+
this.#guard(() => database.commit());
|
|
1856
|
+
} catch (error) {
|
|
1857
|
+
if (database.transacting) this.#guard(() => database.rollback());
|
|
1858
|
+
throw error;
|
|
1859
|
+
}
|
|
1860
|
+
const schema = this.#candidateSchema;
|
|
1861
|
+
const identities = this.#candidateIdentities;
|
|
1862
|
+
if (schema === void 0 || identities === void 0) throw new _src_core.DatabaseError("CONFLICT", "Transaction scope has settled");
|
|
1863
|
+
this.#schema = schema;
|
|
1864
|
+
this.#identities = identities;
|
|
1865
|
+
return value;
|
|
1866
|
+
} finally {
|
|
1867
|
+
if (this.#transaction === token) {
|
|
1868
|
+
this.#transaction = void 0;
|
|
1869
|
+
this.#candidateSchema = void 0;
|
|
1870
|
+
this.#candidateIdentities = void 0;
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1471
1873
|
}
|
|
1472
1874
|
/**
|
|
1473
1875
|
* Apply a {@link Migration} plan by executing each step's projected DDL
|
|
1474
|
-
* ({@link import('../
|
|
1876
|
+
* ({@link import('../compilers.js').stepToSQL}).
|
|
1475
1877
|
*
|
|
1476
1878
|
* @remarks
|
|
1477
1879
|
* Atomicity is provided by whichever native transaction is active: when
|
|
1478
|
-
* this driver's own `transaction()`
|
|
1880
|
+
* this driver's own `transaction()` callback is active (the
|
|
1479
1881
|
* core's versioned reconcile / migrate path joins migrate + stamp under
|
|
1480
1882
|
* one native `BEGIN`), the plan's DDL runs directly inside that enclosing
|
|
1481
|
-
* transaction — a mid-plan failure
|
|
1482
|
-
*
|
|
1883
|
+
* transaction — a mid-plan failure rejects the callback and the driver
|
|
1884
|
+
* rolls it back. node:sqlite (and SQLite
|
|
1483
1885
|
* generally) rejects a nested `BEGIN`, so this driver must never open a
|
|
1484
1886
|
* second native transaction while one is already open. Otherwise (no
|
|
1485
1887
|
* enclosing transaction), `migrate` wraps the plan in its own native
|
|
1486
1888
|
* `database.transaction` — atomic on its own: a mid-plan failure rolls
|
|
1487
|
-
* back every DDL statement already applied by the plan. A
|
|
1488
|
-
*
|
|
1489
|
-
*
|
|
1490
|
-
*
|
|
1491
|
-
* active
|
|
1889
|
+
* back every DDL statement already applied by the plan. A scoped migration
|
|
1890
|
+
* uses one fixed internal savepoint literal because the published SQLite
|
|
1891
|
+
* wrapper intentionally exposes raw `exec` but no savepoint manager. That
|
|
1892
|
+
* savepoint contains a caught inner migration so the outer callback
|
|
1893
|
+
* transaction remains active and may continue safely. A step referencing a
|
|
1894
|
+
* table not in this driver's declared schema (and that is not itself a
|
|
1895
|
+
* `table.add`) throws `DatabaseError` `MIGRATION` before any DDL for that
|
|
1896
|
+
* step runs.
|
|
1492
1897
|
*
|
|
1493
|
-
* @param
|
|
1898
|
+
* @param input - The migration plan and optional metadata stamp to apply atomically
|
|
1494
1899
|
*/
|
|
1495
|
-
async migrate(
|
|
1900
|
+
async migrate(input) {
|
|
1901
|
+
this.#root();
|
|
1496
1902
|
const database = this.#require();
|
|
1497
|
-
const
|
|
1903
|
+
const owned = (0, _src_core.cloneMigrationInput)(input);
|
|
1904
|
+
const projected = (0, _src_core.projectMigrationSchema)([...this.#schema.values()], owned.plan.steps);
|
|
1905
|
+
const identities = this.#projectIdentities(this.#identities, owned.plan.steps);
|
|
1906
|
+
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", {
|
|
1907
|
+
projected,
|
|
1908
|
+
metadata: owned.metadata.schema
|
|
1909
|
+
});
|
|
1498
1910
|
this.#guard(() => {
|
|
1499
|
-
|
|
1500
|
-
|
|
1911
|
+
database.transaction(() => {
|
|
1912
|
+
this.#applyPlan(database, owned);
|
|
1913
|
+
if (owned.metadata !== void 0) this.#writeMetadata(database, owned.metadata);
|
|
1914
|
+
});
|
|
1501
1915
|
});
|
|
1502
|
-
this.#schema =
|
|
1916
|
+
this.#schema = new Map(projected.map((table) => [table.name, table]));
|
|
1917
|
+
this.#identities = identities;
|
|
1503
1918
|
}
|
|
1504
1919
|
/**
|
|
1505
|
-
* Read the persisted {@link
|
|
1920
|
+
* Read the persisted {@link DriverMetadata} from the reserved `_metadata` table.
|
|
1506
1921
|
*
|
|
1507
|
-
* @returns The last-stamped `
|
|
1922
|
+
* @returns The last-stamped `DriverMetadata`, or `undefined` when never stamped
|
|
1508
1923
|
* (or the stored row is malformed)
|
|
1509
1924
|
*/
|
|
1510
|
-
async
|
|
1511
|
-
|
|
1925
|
+
async metadata() {
|
|
1926
|
+
this.#root();
|
|
1927
|
+
return this.#metadata();
|
|
1928
|
+
}
|
|
1929
|
+
/**
|
|
1930
|
+
* Persist an owned metadata snapshot into the reserved `_metadata` table's
|
|
1931
|
+
* single row.
|
|
1932
|
+
*
|
|
1933
|
+
* @param metadata - The {@link DriverMetadata} to persist
|
|
1934
|
+
*/
|
|
1935
|
+
async stamp(metadata) {
|
|
1936
|
+
this.#root();
|
|
1937
|
+
await this.#stamp(metadata);
|
|
1938
|
+
}
|
|
1939
|
+
async snapshot(tables) {
|
|
1940
|
+
this.#root();
|
|
1941
|
+
const captured = this.#guard(() => {
|
|
1942
|
+
const database = this.#require();
|
|
1943
|
+
const names = tables === void 0 ? [...this.#schema.keys()] : [...new Set(tables)];
|
|
1944
|
+
const snapshots = /* @__PURE__ */ new Map();
|
|
1945
|
+
for (const name of names) {
|
|
1946
|
+
const schema = this.#schema.get(name);
|
|
1947
|
+
const identity = this.#identities.get(name);
|
|
1948
|
+
if (schema === void 0 || identity === void 0) continue;
|
|
1949
|
+
snapshots.set(name, {
|
|
1950
|
+
identity,
|
|
1951
|
+
rows: database.prepare("SELECT * FROM " + quoteIdentifier(name)).all().map((row) => decodeRow(row, schema)),
|
|
1952
|
+
schema
|
|
1953
|
+
});
|
|
1954
|
+
}
|
|
1955
|
+
return snapshots;
|
|
1956
|
+
});
|
|
1957
|
+
return async () => {
|
|
1958
|
+
this.#root();
|
|
1959
|
+
const replacements = /* @__PURE__ */ new Map();
|
|
1960
|
+
for (const [name, capture] of captured) {
|
|
1961
|
+
const schema = this.#schema.get(name);
|
|
1962
|
+
if (schema === void 0 || this.#identities.get(name) !== capture.identity) continue;
|
|
1963
|
+
const plan = (0, _src_core.planMigration)([capture.schema], [schema]);
|
|
1964
|
+
const rows = (0, _src_core.migrateRows)(capture.rows, plan.steps);
|
|
1965
|
+
if (rows.length !== capture.rows.length) throw new _src_core.DatabaseError("MIGRATION", "Snapshot row count changed during migration", { table: name });
|
|
1966
|
+
const names = schema.columns.map((column) => column.name);
|
|
1967
|
+
const values = [];
|
|
1968
|
+
for (const [index, row] of rows.entries()) {
|
|
1969
|
+
const key = (0, _src_core.extractKey)(row, schema.primary);
|
|
1970
|
+
if (!(0, _src_core.isKey)(key)) throw new _src_core.DatabaseError("MIGRATION", "Snapshot row has no usable primary key", {
|
|
1971
|
+
table: name,
|
|
1972
|
+
column: schema.primary,
|
|
1973
|
+
index
|
|
1974
|
+
});
|
|
1975
|
+
const encoded = encodeRow((0, _src_core.bindRowKey)(row, schema.primary, key), schema);
|
|
1976
|
+
values.push(extractValues(encoded, names, name));
|
|
1977
|
+
}
|
|
1978
|
+
replacements.set(name, {
|
|
1979
|
+
names,
|
|
1980
|
+
values
|
|
1981
|
+
});
|
|
1982
|
+
}
|
|
1983
|
+
this.#guard(() => {
|
|
1984
|
+
const current = this.#require();
|
|
1985
|
+
current.transaction(() => {
|
|
1986
|
+
for (const [name, replacement] of replacements) {
|
|
1987
|
+
current.exec("DELETE FROM " + quoteIdentifier(name));
|
|
1988
|
+
const statement = current.prepare("INSERT OR REPLACE INTO " + quoteIdentifier(name) + " (" + replacement.names.map(quoteIdentifier).join(", ") + ") VALUES (" + replacement.names.map(() => "?").join(", ") + ")");
|
|
1989
|
+
for (const values of replacement.values) statement.run(values);
|
|
1990
|
+
}
|
|
1991
|
+
});
|
|
1992
|
+
});
|
|
1993
|
+
};
|
|
1994
|
+
}
|
|
1995
|
+
async #read(table, key) {
|
|
1996
|
+
const schema = this.#table(table);
|
|
1997
|
+
return this.#guard(() => {
|
|
1998
|
+
const row = this.#require().prepare("SELECT * FROM " + quoteIdentifier(table) + " WHERE " + quoteIdentifier(schema.primary) + " = ?").get([this.#key(key, schema)]);
|
|
1999
|
+
return row === void 0 ? void 0 : decodeRow(row, schema);
|
|
2000
|
+
});
|
|
2001
|
+
}
|
|
2002
|
+
async #write(table, key, row, options) {
|
|
2003
|
+
const schema = this.#table(table);
|
|
2004
|
+
this.#guard(() => {
|
|
2005
|
+
const encoded = encodeRow((0, _src_core.bindRowKey)(row, schema.primary, key), schema);
|
|
2006
|
+
const names = schema.columns.map((column) => column.name);
|
|
2007
|
+
const values = extractValues(encoded, names, table);
|
|
2008
|
+
const statement = this.#require().prepare("INSERT OR REPLACE INTO " + quoteIdentifier(table) + " (" + names.map(quoteIdentifier).join(", ") + ") VALUES (" + names.map(() => "?").join(", ") + ")");
|
|
2009
|
+
(0, _src_core.checkAbort)(options?.signal);
|
|
2010
|
+
statement.run(values);
|
|
2011
|
+
});
|
|
2012
|
+
}
|
|
2013
|
+
async #insert(table, key, row, options) {
|
|
2014
|
+
const schema = this.#table(table);
|
|
2015
|
+
this.#guard(() => {
|
|
2016
|
+
const encoded = encodeRow((0, _src_core.bindRowKey)(row, schema.primary, key), schema);
|
|
2017
|
+
const names = schema.columns.map((column) => column.name);
|
|
2018
|
+
const values = extractValues(encoded, names, table);
|
|
2019
|
+
const statement = this.#require().prepare("INSERT INTO " + quoteIdentifier(table) + " (" + names.map(quoteIdentifier).join(", ") + ") VALUES (" + names.map(() => "?").join(", ") + ")");
|
|
2020
|
+
(0, _src_core.checkAbort)(options?.signal);
|
|
2021
|
+
statement.run(values);
|
|
2022
|
+
});
|
|
2023
|
+
}
|
|
2024
|
+
async #delete(table, key, options) {
|
|
2025
|
+
const schema = this.#table(table);
|
|
2026
|
+
return this.#guard(() => {
|
|
2027
|
+
const statement = this.#require().prepare("DELETE FROM " + quoteIdentifier(table) + " WHERE " + quoteIdentifier(schema.primary) + " = ?");
|
|
2028
|
+
(0, _src_core.checkAbort)(options?.signal);
|
|
2029
|
+
return statement.run([this.#key(key, schema)]).changes > 0;
|
|
2030
|
+
});
|
|
2031
|
+
}
|
|
2032
|
+
async #keys(table) {
|
|
2033
|
+
const schema = this.#table(table);
|
|
2034
|
+
return this.#guard(() => {
|
|
2035
|
+
const primary = quoteIdentifier(schema.primary);
|
|
2036
|
+
const rows = this.#require().prepare("SELECT " + primary + " FROM " + quoteIdentifier(table) + " ORDER BY " + primary).all();
|
|
2037
|
+
const keys = [];
|
|
2038
|
+
for (const row of rows) {
|
|
2039
|
+
const value = row[schema.primary];
|
|
2040
|
+
if (typeof value === "string" || typeof value === "number") keys.push(value);
|
|
2041
|
+
}
|
|
2042
|
+
return keys;
|
|
2043
|
+
});
|
|
2044
|
+
}
|
|
2045
|
+
async *#scan(table) {
|
|
2046
|
+
const schema = this.#table(table);
|
|
2047
|
+
for await (const row of this.#iterate(schema, "SELECT * FROM " + quoteIdentifier(table) + " ORDER BY " + quoteIdentifier(schema.primary))) yield row;
|
|
2048
|
+
}
|
|
2049
|
+
async *#stream(table, input) {
|
|
2050
|
+
const schema = this.#table(table);
|
|
2051
|
+
const conditions = input.conditions ?? [];
|
|
2052
|
+
if (conditions.every((condition) => matchesConditionExactly(condition, schema))) {
|
|
2053
|
+
const compiled = compileQuerySQL({
|
|
2054
|
+
conditions,
|
|
2055
|
+
...input.limit !== void 0 ? { limit: input.limit } : {},
|
|
2056
|
+
...input.offset !== void 0 ? { offset: input.offset } : {}
|
|
2057
|
+
}, schema);
|
|
2058
|
+
for await (const row of this.#iterate(schema, "SELECT * FROM " + quoteIdentifier(table) + (compiled.sql === "" ? "" : " " + compiled.sql), compiled.parameters)) yield row;
|
|
2059
|
+
return;
|
|
2060
|
+
}
|
|
2061
|
+
const offset = input.offset ?? 0;
|
|
2062
|
+
const limit = input.limit;
|
|
2063
|
+
let skipped = 0;
|
|
2064
|
+
let yielded = 0;
|
|
2065
|
+
for await (const row of this.#scan(table)) {
|
|
2066
|
+
if (limit !== void 0 && yielded >= limit) return;
|
|
2067
|
+
if (conditions.length > 0 && !(0, _src_core.matchesQuery)(row, conditions)) continue;
|
|
2068
|
+
if (skipped < offset) {
|
|
2069
|
+
skipped += 1;
|
|
2070
|
+
continue;
|
|
2071
|
+
}
|
|
2072
|
+
yield row;
|
|
2073
|
+
yielded += 1;
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
async *#iterate(schema, sql, parameters = []) {
|
|
2077
|
+
const iterator = this.#guard(() => this.#require().prepare(sql).iterate(parameters)[Symbol.iterator]());
|
|
2078
|
+
try {
|
|
2079
|
+
while (true) {
|
|
2080
|
+
const step = this.#guard(() => iterator.next());
|
|
2081
|
+
if (step.done === true) return;
|
|
2082
|
+
yield this.#guard(() => decodeRow(step.value, schema));
|
|
2083
|
+
}
|
|
2084
|
+
} finally {
|
|
2085
|
+
if (iterator.return !== void 0) this.#guard(() => iterator.return?.());
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
async #clear(table) {
|
|
2089
|
+
this.#table(table);
|
|
2090
|
+
this.#guard(() => {
|
|
2091
|
+
this.#require().prepare("DELETE FROM " + quoteIdentifier(table)).run();
|
|
2092
|
+
});
|
|
2093
|
+
}
|
|
2094
|
+
async #metadata() {
|
|
2095
|
+
return this.#guard(() => this.#readMetadata(this.#require()));
|
|
2096
|
+
}
|
|
2097
|
+
#ensureMetadataTable(database) {
|
|
2098
|
+
database.exec("CREATE TABLE IF NOT EXISTS " + quoteIdentifier(METADATA_TABLE) + " (\"id\" INTEGER, \"version\" INTEGER, \"schema\" TEXT, PRIMARY KEY (\"id\"))");
|
|
2099
|
+
}
|
|
2100
|
+
#validateTable(database, schema) {
|
|
2101
|
+
const object = database.prepare("SELECT \"type\" AS \"category\" FROM \"sqlite_schema\" WHERE \"name\" = ?").get([schema.name]);
|
|
2102
|
+
if (object === void 0) return void 0;
|
|
2103
|
+
if (object.category !== "table") throw new _src_core.DatabaseError("DRIVER", "SQLite object is not a table", {
|
|
2104
|
+
table: schema.name,
|
|
2105
|
+
aspect: "object",
|
|
2106
|
+
actual: object.category
|
|
2107
|
+
});
|
|
2108
|
+
const trigger = database.prepare("SELECT \"name\" FROM \"sqlite_schema\" WHERE \"type\" = ? AND \"tbl_name\" = ? LIMIT 1").get(["trigger", schema.name]);
|
|
2109
|
+
if (trigger !== void 0) throw new _src_core.DatabaseError("DRIVER", "SQLite table has an undeclared trigger", {
|
|
2110
|
+
table: schema.name,
|
|
2111
|
+
aspect: "trigger",
|
|
2112
|
+
actual: trigger.name
|
|
2113
|
+
});
|
|
2114
|
+
const columns = database.prepare("SELECT * FROM pragma_table_xinfo(?)").all([schema.name]);
|
|
2115
|
+
if (columns.length !== schema.columns.length) throw new _src_core.DatabaseError("DRIVER", "SQLite table has different columns", {
|
|
2116
|
+
table: schema.name,
|
|
2117
|
+
aspect: "columns",
|
|
2118
|
+
expected: schema.columns.map((column) => column.name),
|
|
2119
|
+
actual: columns.map((column) => column.name)
|
|
2120
|
+
});
|
|
2121
|
+
for (const declared of schema.columns) {
|
|
2122
|
+
const column = columns.find((candidate) => candidate.name === declared.name);
|
|
2123
|
+
if (column === void 0) throw new _src_core.DatabaseError("DRIVER", "SQLite table is missing a declared column", {
|
|
2124
|
+
table: schema.name,
|
|
2125
|
+
aspect: "column",
|
|
2126
|
+
column: declared.name
|
|
2127
|
+
});
|
|
2128
|
+
const expectedRequired = !declared.optional && !declared.nullable;
|
|
2129
|
+
const required = column.notnull === 1 || column.notnull === 1n;
|
|
2130
|
+
const expectedPrimary = declared.name === schema.primary ? 1 : 0;
|
|
2131
|
+
const primary = column.pk === expectedPrimary || column.pk === BigInt(expectedPrimary);
|
|
2132
|
+
const hidden = column.hidden === 0 || column.hidden === 0n;
|
|
2133
|
+
if (!matchesSQLiteAffinity(column.type, declared.storage) || required !== expectedRequired || !primary || !hidden) throw new _src_core.DatabaseError("DRIVER", "SQLite column does not match its declaration", {
|
|
2134
|
+
table: schema.name,
|
|
2135
|
+
aspect: "column",
|
|
2136
|
+
column: declared.name,
|
|
2137
|
+
expected: declared,
|
|
2138
|
+
actual: column
|
|
2139
|
+
});
|
|
2140
|
+
}
|
|
2141
|
+
const indexes = database.prepare("SELECT * FROM pragma_index_list(?)").all([schema.name]);
|
|
2142
|
+
for (const index of indexes) if ((index.unique === 1 || index.unique === 1n) && index.origin !== "pk") throw new _src_core.DatabaseError("DRIVER", "SQLite table has an undeclared unique constraint", {
|
|
2143
|
+
table: schema.name,
|
|
2144
|
+
aspect: "index",
|
|
2145
|
+
actual: index.name
|
|
2146
|
+
});
|
|
2147
|
+
const missing = [];
|
|
2148
|
+
for (const group of schema.indexes) {
|
|
2149
|
+
const name = deriveSQLiteIndexName(schema.name, group);
|
|
2150
|
+
const index = indexes.find((candidate) => candidate.name === name);
|
|
2151
|
+
if (index === void 0) {
|
|
2152
|
+
missing.push(name);
|
|
2153
|
+
continue;
|
|
2154
|
+
}
|
|
2155
|
+
const ordinary = index.unique === 0 || index.unique === 0n;
|
|
2156
|
+
const complete = index.partial === 0 || index.partial === 0n;
|
|
2157
|
+
if (!ordinary || !complete || index.origin !== "c") throw new _src_core.DatabaseError("DRIVER", "SQLite index does not match its declaration", {
|
|
2158
|
+
table: schema.name,
|
|
2159
|
+
aspect: "index",
|
|
2160
|
+
index: name,
|
|
2161
|
+
actual: index
|
|
2162
|
+
});
|
|
2163
|
+
const entries = database.prepare("SELECT * FROM pragma_index_xinfo(?)").all([name]).filter((entry) => entry.key === 1 || entry.key === 1n);
|
|
2164
|
+
if (entries.length !== group.length) throw new _src_core.DatabaseError("DRIVER", "SQLite index has different columns", {
|
|
2165
|
+
table: schema.name,
|
|
2166
|
+
aspect: "index",
|
|
2167
|
+
index: name,
|
|
2168
|
+
expected: group,
|
|
2169
|
+
actual: entries.map((entry) => entry.name)
|
|
2170
|
+
});
|
|
2171
|
+
for (const [position, column] of group.entries()) {
|
|
2172
|
+
const entry = entries.find((candidate) => candidate.seqno === position || candidate.seqno === BigInt(position));
|
|
2173
|
+
const stored = entry !== void 0 && (typeof entry.cid === "number" && Number.isInteger(entry.cid) && entry.cid >= 0 || typeof entry.cid === "bigint" && entry.cid >= 0n);
|
|
2174
|
+
if (entry === void 0 || entry.name !== column || !stored || entry.desc !== 0 && entry.desc !== 0n || entry.coll !== "BINARY") throw new _src_core.DatabaseError("DRIVER", "SQLite index column does not match its declaration", {
|
|
2175
|
+
table: schema.name,
|
|
2176
|
+
aspect: "index",
|
|
2177
|
+
index: name,
|
|
2178
|
+
column,
|
|
2179
|
+
actual: entry
|
|
2180
|
+
});
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
return missing;
|
|
2184
|
+
}
|
|
2185
|
+
#readMetadata(database) {
|
|
2186
|
+
const row = database.prepare("SELECT \"version\", \"schema\" FROM " + quoteIdentifier(METADATA_TABLE) + " WHERE \"id\" = 1").get();
|
|
1512
2187
|
if (row === void 0) return void 0;
|
|
1513
2188
|
const version = row.version;
|
|
1514
2189
|
const text = row.schema;
|
|
1515
|
-
if (typeof text !== "string")
|
|
1516
|
-
|
|
2190
|
+
if (typeof text !== "string") throw new _src_core.DatabaseError("DRIVER", "Stored SQLite metadata schema is invalid", {
|
|
2191
|
+
table: METADATA_TABLE,
|
|
2192
|
+
aspect: "metadata"
|
|
2193
|
+
});
|
|
2194
|
+
if (typeof version !== "number" && typeof version !== "bigint") throw new _src_core.DatabaseError("DRIVER", "Stored SQLite metadata version is invalid", {
|
|
2195
|
+
table: METADATA_TABLE,
|
|
2196
|
+
aspect: "metadata"
|
|
2197
|
+
});
|
|
1517
2198
|
let parsed;
|
|
1518
2199
|
try {
|
|
1519
2200
|
parsed = JSON.parse(text);
|
|
1520
|
-
} catch {
|
|
1521
|
-
|
|
2201
|
+
} catch (error) {
|
|
2202
|
+
throw new _src_core.DatabaseError("DRIVER", "Stored SQLite metadata JSON is invalid", {
|
|
2203
|
+
table: METADATA_TABLE,
|
|
2204
|
+
aspect: "metadata",
|
|
2205
|
+
cause: error
|
|
2206
|
+
});
|
|
1522
2207
|
}
|
|
2208
|
+
if (!Array.isArray(parsed)) throw new _src_core.DatabaseError("DRIVER", "Stored SQLite metadata schema is invalid", {
|
|
2209
|
+
table: METADATA_TABLE,
|
|
2210
|
+
aspect: "metadata"
|
|
2211
|
+
});
|
|
1523
2212
|
const candidate = {
|
|
1524
2213
|
version: Number(version),
|
|
1525
2214
|
schema: parsed
|
|
1526
2215
|
};
|
|
1527
|
-
|
|
1528
|
-
|
|
2216
|
+
try {
|
|
2217
|
+
return (0, _src_core.cloneDriverMetadata)(candidate);
|
|
2218
|
+
} catch (error) {
|
|
2219
|
+
if ((0, _src_core.isDatabaseError)(error) && error.code === "VALIDATION") throw new _src_core.DatabaseError("DRIVER", "Stored SQLite metadata is invalid", {
|
|
2220
|
+
table: METADATA_TABLE,
|
|
2221
|
+
aspect: "metadata",
|
|
2222
|
+
cause: error
|
|
2223
|
+
});
|
|
2224
|
+
throw error;
|
|
2225
|
+
}
|
|
1529
2226
|
}
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
2227
|
+
async #stamp(metadata) {
|
|
2228
|
+
const database = this.#require();
|
|
2229
|
+
const owned = (0, _src_core.cloneDriverMetadata)(metadata);
|
|
2230
|
+
this.#guard(() => this.#writeMetadata(database, owned));
|
|
2231
|
+
}
|
|
2232
|
+
#writeMetadata(database, metadata) {
|
|
2233
|
+
database.prepare("INSERT OR REPLACE INTO " + quoteIdentifier(METADATA_TABLE) + " (\"id\", \"version\", \"schema\") VALUES (1, ?, ?)").run([metadata.version, JSON.stringify(metadata.schema)]);
|
|
2234
|
+
}
|
|
2235
|
+
#capability(token) {
|
|
2236
|
+
return {
|
|
2237
|
+
read: this.#readTransaction.bind(this, token),
|
|
2238
|
+
write: this.#writeTransaction.bind(this, token),
|
|
2239
|
+
insert: this.#insertTransaction.bind(this, token),
|
|
2240
|
+
delete: this.#deleteTransaction.bind(this, token),
|
|
2241
|
+
keys: this.#keysTransaction.bind(this, token),
|
|
2242
|
+
scan: this.#scanTransaction.bind(this, token),
|
|
2243
|
+
clear: this.#clearTransaction.bind(this, token),
|
|
2244
|
+
migrate: this.#migrateTransaction.bind(this, token),
|
|
2245
|
+
metadata: this.#metadataTransaction.bind(this, token),
|
|
2246
|
+
stamp: this.#stampTransaction.bind(this, token)
|
|
2247
|
+
};
|
|
2248
|
+
}
|
|
2249
|
+
async #readTransaction(token, table, key) {
|
|
2250
|
+
this.#requireTransaction(token);
|
|
2251
|
+
return this.#read(table, key);
|
|
2252
|
+
}
|
|
2253
|
+
async #writeTransaction(token, table, key, row, options) {
|
|
2254
|
+
this.#requireTransaction(token);
|
|
2255
|
+
await this.#write(table, key, row, options);
|
|
2256
|
+
}
|
|
2257
|
+
async #insertTransaction(token, table, key, row, options) {
|
|
2258
|
+
this.#requireTransaction(token);
|
|
2259
|
+
await this.#insert(table, key, row, options);
|
|
2260
|
+
}
|
|
2261
|
+
async #deleteTransaction(token, table, key, options) {
|
|
2262
|
+
this.#requireTransaction(token);
|
|
2263
|
+
return this.#delete(table, key, options);
|
|
2264
|
+
}
|
|
2265
|
+
async #keysTransaction(token, table) {
|
|
2266
|
+
this.#requireTransaction(token);
|
|
2267
|
+
return this.#keys(table);
|
|
2268
|
+
}
|
|
2269
|
+
#scanTransaction(token, table) {
|
|
2270
|
+
return new DriverIterator(this.#scan(table)[Symbol.asyncIterator](), () => {
|
|
2271
|
+
this.#requireTransaction(token);
|
|
1539
2272
|
});
|
|
1540
2273
|
}
|
|
1541
|
-
async
|
|
2274
|
+
async #clearTransaction(token, table) {
|
|
2275
|
+
this.#requireTransaction(token);
|
|
2276
|
+
await this.#clear(table);
|
|
2277
|
+
}
|
|
2278
|
+
async #migrateTransaction(token, input) {
|
|
2279
|
+
this.#requireTransaction(token);
|
|
1542
2280
|
const database = this.#require();
|
|
1543
|
-
const
|
|
1544
|
-
const
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
}
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
2281
|
+
const owned = (0, _src_core.cloneMigrationInput)(input);
|
|
2282
|
+
const candidate = this.#candidateSchema;
|
|
2283
|
+
const identities = this.#candidateIdentities;
|
|
2284
|
+
if (candidate === void 0 || identities === void 0) throw new _src_core.DatabaseError("CONFLICT", "Transaction scope has settled");
|
|
2285
|
+
const projected = (0, _src_core.projectMigrationSchema)([...candidate.values()], owned.plan.steps);
|
|
2286
|
+
const projectedIdentities = this.#projectIdentities(identities, owned.plan.steps);
|
|
2287
|
+
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", {
|
|
2288
|
+
projected,
|
|
2289
|
+
metadata: owned.metadata.schema
|
|
2290
|
+
});
|
|
2291
|
+
this.#guard(() => {
|
|
2292
|
+
database.exec("SAVEPOINT \"_orkestrel_migration\"");
|
|
2293
|
+
try {
|
|
2294
|
+
this.#applyPlan(database, owned);
|
|
2295
|
+
if (owned.metadata !== void 0) this.#writeMetadata(database, owned.metadata);
|
|
2296
|
+
database.exec("RELEASE SAVEPOINT \"_orkestrel_migration\"");
|
|
2297
|
+
} catch (error) {
|
|
2298
|
+
try {
|
|
2299
|
+
database.exec("ROLLBACK TO SAVEPOINT \"_orkestrel_migration\"");
|
|
2300
|
+
} finally {
|
|
2301
|
+
database.exec("RELEASE SAVEPOINT \"_orkestrel_migration\"");
|
|
1560
2302
|
}
|
|
1561
|
-
|
|
1562
|
-
|
|
2303
|
+
throw error;
|
|
2304
|
+
}
|
|
2305
|
+
});
|
|
2306
|
+
this.#candidateSchema = new Map(projected.map((table) => [table.name, table]));
|
|
2307
|
+
this.#candidateIdentities = projectedIdentities;
|
|
2308
|
+
}
|
|
2309
|
+
async #metadataTransaction(token) {
|
|
2310
|
+
this.#requireTransaction(token);
|
|
2311
|
+
return this.#metadata();
|
|
1563
2312
|
}
|
|
1564
|
-
async #
|
|
1565
|
-
|
|
1566
|
-
this.#
|
|
1567
|
-
this.#guard(() => database.exec("COMMIT"));
|
|
2313
|
+
async #stampTransaction(token, metadata) {
|
|
2314
|
+
this.#requireTransaction(token);
|
|
2315
|
+
await this.#stamp(metadata);
|
|
1568
2316
|
}
|
|
1569
|
-
|
|
1570
|
-
if (this.#transaction !== token) throw new _src_core.DatabaseError("CONFLICT", "Transaction
|
|
1571
|
-
this.#transaction = void 0;
|
|
1572
|
-
this.#guard(() => database.exec("ROLLBACK"));
|
|
2317
|
+
#requireTransaction(token) {
|
|
2318
|
+
if (this.#transaction !== token) throw new _src_core.DatabaseError("CONFLICT", "Transaction scope has settled");
|
|
1573
2319
|
}
|
|
1574
|
-
#
|
|
2320
|
+
#root() {
|
|
2321
|
+
if (this.#transaction !== void 0) throw new _src_core.DatabaseError("CONFLICT", "A transaction is active on this driver");
|
|
2322
|
+
}
|
|
2323
|
+
#guard(operation) {
|
|
1575
2324
|
try {
|
|
1576
|
-
return
|
|
2325
|
+
return operation();
|
|
1577
2326
|
} catch (error) {
|
|
1578
2327
|
if (error instanceof _src_core.DatabaseError) throw error;
|
|
1579
2328
|
if ((0, _orkestrel_sqlite.isSQLiteError)(error)) {
|
|
@@ -1602,28 +2351,30 @@ var SQLiteDriver = class {
|
|
|
1602
2351
|
if (this.#database === void 0) throw new _src_core.DatabaseError("CLOSED", `SQLite database '${this.#path}' is not open`, { path: this.#path });
|
|
1603
2352
|
return this.#database;
|
|
1604
2353
|
}
|
|
2354
|
+
#projectIdentities(identities, steps) {
|
|
2355
|
+
const projected = new Map(identities);
|
|
2356
|
+
for (const step of steps) {
|
|
2357
|
+
if (step.operation === "table.add") projected.set(step.table.name, {});
|
|
2358
|
+
if (step.operation === "table.remove") projected.delete(step.table);
|
|
2359
|
+
}
|
|
2360
|
+
return projected;
|
|
2361
|
+
}
|
|
1605
2362
|
#table(name) {
|
|
1606
2363
|
this.#require();
|
|
1607
|
-
const schema = this.#schema.get(name);
|
|
2364
|
+
const schema = (this.#candidateSchema ?? this.#schema).get(name);
|
|
1608
2365
|
if (schema === void 0) throw new _src_core.DatabaseError("NOT_FOUND", `Table '${name}' is not in the schema`, { table: name });
|
|
1609
2366
|
return schema;
|
|
1610
2367
|
}
|
|
1611
2368
|
#key(key, schema) {
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
else if (step.operation === "table.remove") schema.delete(step.table);
|
|
1622
|
-
else {
|
|
1623
|
-
const existing = schema.get(table);
|
|
1624
|
-
if (existing !== void 0) schema.set(table, stepToSchema(existing, step));
|
|
1625
|
-
}
|
|
1626
|
-
}
|
|
2369
|
+
return encodeValue(key, schema.columns.find((column) => column.name === schema.primary) ?? {
|
|
2370
|
+
name: schema.primary,
|
|
2371
|
+
storage: "text",
|
|
2372
|
+
optional: false,
|
|
2373
|
+
nullable: false
|
|
2374
|
+
});
|
|
2375
|
+
}
|
|
2376
|
+
#applyPlan(database, input) {
|
|
2377
|
+
for (const step of input.plan.steps) for (const sql of stepToSQL(step)) database.exec(sql);
|
|
1627
2378
|
}
|
|
1628
2379
|
};
|
|
1629
2380
|
//#endregion
|
|
@@ -1632,9 +2383,9 @@ var SQLiteDriver = class {
|
|
|
1632
2383
|
* Create a persistent JSON-file {@link DriverInterface} for the core database layer.
|
|
1633
2384
|
*
|
|
1634
2385
|
* @remarks
|
|
1635
|
-
* Pass it to `createDatabase` from `@orkestrel/database` to run the
|
|
1636
|
-
*
|
|
1637
|
-
* `Table` / `Query`
|
|
2386
|
+
* Pass it to `createDatabase` from `@orkestrel/database` to run the typed
|
|
2387
|
+
* database against a single JSON file instead of memory — the `Database` /
|
|
2388
|
+
* `Table` / `Query` API is unchanged; only where the bytes live changes.
|
|
1638
2389
|
* The driver is the reference `MemoryDriver` plus JSON-file persistence: `open` loads
|
|
1639
2390
|
* the file, every mutation flushes the whole store back, and querying runs through
|
|
1640
2391
|
* the core engine over `scan` (it is scan-only — no native `records` / `count` /
|
|
@@ -1664,19 +2415,20 @@ function createJSONDriver(path) {
|
|
|
1664
2415
|
* Create a trusted-mode SQLite {@link DriverInterface} for the core database layer.
|
|
1665
2416
|
*
|
|
1666
2417
|
* @remarks
|
|
1667
|
-
* Pass it to `createDatabase` from `@orkestrel/database` to run the
|
|
1668
|
-
*
|
|
1669
|
-
* `Query`
|
|
2418
|
+
* Pass it to `createDatabase` from `@orkestrel/database` to run the typed
|
|
2419
|
+
* database against a real SQLite database — the `Database` / `Table` /
|
|
2420
|
+
* `Query` API is unchanged; only where the bytes live changes. Built
|
|
1670
2421
|
* on the published `@orkestrel/sqlite` synchronous wrapper: `open` issues real
|
|
1671
2422
|
* typed `CREATE TABLE` / `CREATE INDEX` statements (reopen-safe) plus a reserved
|
|
1672
|
-
* `
|
|
2423
|
+
* `_metadata` table for `metadata()` / `stamp()` — avoid naming a table `_metadata`.
|
|
1673
2424
|
* Querying, paging, and aggregation run natively (`records` / `count` /
|
|
1674
2425
|
* `aggregate` / `stream`); `transaction` and `migrate` use real `BEGIN` /
|
|
1675
2426
|
* `COMMIT` / `ROLLBACK`, so `migrate` is atomic even mid-plan.
|
|
1676
2427
|
*
|
|
1677
|
-
* @param options -
|
|
1678
|
-
*
|
|
1679
|
-
*
|
|
2428
|
+
* @param options - The {@link SQLiteDriverOptions} bag (`path`, `readonly`,
|
|
2429
|
+
* `timeout`, `references`, `pragmas`); `references` directly enables or
|
|
2430
|
+
* disables foreign-key enforcement, and omission retains the upstream
|
|
2431
|
+
* default; omit the whole bag for an in-memory database
|
|
1680
2432
|
* @returns A {@link DriverInterface} backed by SQLite
|
|
1681
2433
|
*
|
|
1682
2434
|
* @example
|
|
@@ -1686,54 +2438,53 @@ function createJSONDriver(path) {
|
|
|
1686
2438
|
* import { createSQLiteDriver } from '@orkestrel/database/server'
|
|
1687
2439
|
*
|
|
1688
2440
|
* const db = createDatabase({
|
|
1689
|
-
* driver: createSQLiteDriver('data/app.sqlite'),
|
|
2441
|
+
* driver: createSQLiteDriver({ path: 'data/app.sqlite' }),
|
|
1690
2442
|
* tables: { users: { id: stringShape(), name: stringShape() } },
|
|
1691
2443
|
* })
|
|
1692
2444
|
* await db.table('users').set({ id: 'u1', name: 'Ada' }) // persisted to app.sqlite
|
|
1693
2445
|
*
|
|
1694
|
-
* // Or with options:
|
|
2446
|
+
* // Or with additional options:
|
|
1695
2447
|
* createSQLiteDriver({ path: 'data/app.sqlite', pragmas: { journal_mode: 'WAL' } })
|
|
1696
2448
|
* ```
|
|
1697
2449
|
*/
|
|
1698
|
-
function createSQLiteDriver(options
|
|
1699
|
-
|
|
1700
|
-
return new SQLiteDriver(resolved.path ?? ":memory:", resolved);
|
|
2450
|
+
function createSQLiteDriver(options) {
|
|
2451
|
+
return new SQLiteDriver(options);
|
|
1701
2452
|
}
|
|
1702
2453
|
//#endregion
|
|
1703
|
-
exports.
|
|
1704
|
-
exports.
|
|
2454
|
+
exports.EXACT_COLUMN_STORAGE = EXACT_COLUMN_STORAGE;
|
|
2455
|
+
exports.EXACT_RANGE_COLUMN_STORAGE = EXACT_RANGE_COLUMN_STORAGE;
|
|
1705
2456
|
exports.JSONDriver = JSONDriver;
|
|
1706
|
-
exports.
|
|
2457
|
+
exports.METADATA_TABLE = METADATA_TABLE;
|
|
1707
2458
|
exports.SQLiteDriver = SQLiteDriver;
|
|
1708
|
-
exports.
|
|
1709
|
-
exports.
|
|
1710
|
-
exports.
|
|
2459
|
+
exports.compileAggregateSQL = compileAggregateSQL;
|
|
2460
|
+
exports.compileColumnSQL = compileColumnSQL;
|
|
2461
|
+
exports.compileConditionSQL = compileConditionSQL;
|
|
2462
|
+
exports.compileFieldSQL = compileFieldSQL;
|
|
2463
|
+
exports.compileJSONTypeSQL = compileJSONTypeSQL;
|
|
1711
2464
|
exports.compileOrder = compileOrder;
|
|
1712
2465
|
exports.compilePage = compilePage;
|
|
2466
|
+
exports.compileQuerySQL = compileQuerySQL;
|
|
1713
2467
|
exports.compileWhere = compileWhere;
|
|
1714
2468
|
exports.createJSONDriver = createJSONDriver;
|
|
1715
2469
|
exports.createSQLiteDriver = createSQLiteDriver;
|
|
1716
|
-
exports.declaredType = declaredType;
|
|
1717
2470
|
exports.decodeRow = decodeRow;
|
|
1718
2471
|
exports.decodeValue = decodeValue;
|
|
2472
|
+
exports.deriveSQLiteIndexName = deriveSQLiteIndexName;
|
|
1719
2473
|
exports.encodeRow = encodeRow;
|
|
1720
2474
|
exports.encodeValue = encodeValue;
|
|
1721
2475
|
exports.escapeLike = escapeLike;
|
|
1722
2476
|
exports.extractValues = extractValues;
|
|
1723
|
-
exports.
|
|
1724
|
-
exports.
|
|
1725
|
-
exports.
|
|
1726
|
-
exports.
|
|
1727
|
-
exports.
|
|
1728
|
-
exports.
|
|
1729
|
-
exports.
|
|
1730
|
-
exports.
|
|
1731
|
-
exports.
|
|
1732
|
-
exports.quote = quote;
|
|
2477
|
+
exports.findColumnStorage = findColumnStorage;
|
|
2478
|
+
exports.inferValueStorage = inferValueStorage;
|
|
2479
|
+
exports.matchesAggregateExactly = matchesAggregateExactly;
|
|
2480
|
+
exports.matchesConditionExactly = matchesConditionExactly;
|
|
2481
|
+
exports.matchesDeclaredStorage = matchesDeclaredStorage;
|
|
2482
|
+
exports.matchesOrderExactly = matchesOrderExactly;
|
|
2483
|
+
exports.matchesQueryExactly = matchesQueryExactly;
|
|
2484
|
+
exports.matchesSQLiteAffinity = matchesSQLiteAffinity;
|
|
2485
|
+
exports.quoteIdentifier = quoteIdentifier;
|
|
1733
2486
|
exports.schemaToIndexes = schemaToIndexes;
|
|
1734
2487
|
exports.schemaToTable = schemaToTable;
|
|
1735
2488
|
exports.stepToSQL = stepToSQL;
|
|
1736
|
-
exports.stepToSchema = stepToSchema;
|
|
1737
|
-
exports.valueType = valueType;
|
|
1738
2489
|
|
|
1739
2490
|
//# sourceMappingURL=index.cjs.map
|