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