@orkestrel/database 0.0.6 → 0.0.8

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