@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,116 +1,155 @@
1
- import { AggregateFunction } from '../core/index.ts';
2
- import { AggregateFunction as AggregateFunction_2 } from '../../core/index.ts';
3
- import { ColumnType } from '../core/index.ts';
1
+ import { AggregateOperation } from '../core/index.ts';
2
+ import { AggregateOperation as AggregateOperation_2 } from '../../core/index.ts';
3
+ import { ColumnSchema } from '../core/index.ts';
4
+ import { ColumnStorage } from '../core/index.ts';
4
5
  import { Condition } from '../core/index.ts';
5
- import { Criteria } from '../core/index.ts';
6
- import { Criteria as Criteria_2 } from '../../core/index.ts';
7
6
  import { DriverInterface } from '../core/index.ts';
8
7
  import { DriverInterface as DriverInterface_2 } from '../../core/index.ts';
9
- import { DriverMeta } from '../../core/index.ts';
8
+ import { DriverMetadata } from '../../core/index.ts';
10
9
  import { FieldPath } from '@orkestrel/contract';
11
10
  import { Key } from '../../core/index.ts';
12
- import { Migration } from '../../core/index.ts';
11
+ import { MigrationInput } from '../../core/index.ts';
13
12
  import { MigrationStep } from '../core/index.ts';
13
+ import { OperationOptions } from '../../core/index.ts';
14
14
  import { Order } from '../core/index.ts';
15
+ import { QueryInput } from '../core/index.ts';
16
+ import { QueryInput as QueryInput_2 } from '../../core/index.ts';
15
17
  import { Row } from '../core/index.ts';
16
18
  import { Row as Row_2 } from '../../core/index.ts';
19
+ import { SQLiteRow } from '@orkestrel/sqlite';
20
+ import { SQLiteValue } from '@orkestrel/sqlite';
21
+ import { StorageInterface } from '../../core/index.ts';
17
22
  import { TableSchema } from '../core/index.ts';
18
23
  import { TableSchema as TableSchema_2 } from '../../core/index.ts';
19
- import { TransactionInterface } from '../../core/index.ts';
20
24
 
21
25
  /**
22
- * Compile an {@link AggregateFunction} over a {@link FieldPath} to its SQL
23
- * aggregate expression — the SELECT body the SQLite driver's native `aggregate`
24
- * runs.
25
- *
26
- * @remarks
27
- * `count` → `COUNT(*)` (counting all matched ROWS, not non-null column values —
28
- * mirroring the engine's `computeAggregate('count')`, which is `rows.length`); the
29
- * numeric aggregates wrap the column's read expression (a flat column, or a nested
30
- * `json_extract` path) in `SUM` / `AVG` / `MIN` / `MAX`. Over zero matched rows
31
- * `COUNT(*)` is `0` and the numeric aggregates are SQL `NULL` (→ `undefined`),
32
- * matching the engine.
26
+ * Compile an {@link AggregateOperation} over a {@link FieldPath}.
33
27
  *
34
28
  * @param operation - The aggregate to compute
35
- * @param column - The column (or nested path) to aggregate
29
+ * @param column - The column or nested path to aggregate
36
30
  * @returns The SQL aggregate expression
37
- *
38
- * @example
39
- * ```ts
40
- * aggregateSQL('count', 'age') // 'COUNT(*)'
41
- * aggregateSQL('sum', 'age') // 'SUM("age")'
42
- * aggregateSQL('average', ['payload', 'score']) // 'AVG(json_extract("payload", \'$.score\'))'
43
- * ```
44
31
  */
45
- export declare function aggregateSQL(operation: AggregateFunction, column: FieldPath): string;
32
+ export declare function compileAggregateSQL(operation: AggregateOperation, column: FieldPath): string;
46
33
 
47
34
  /**
48
- * Map a portable {@link ColumnType} to its SQLite column type.
49
- *
50
- * @remarks
51
- * `text` / `json` → `TEXT` (JSON is stored as text and read back with
52
- * `json_extract` for nested-field queries); `integer` / `boolean` → `INTEGER`
53
- * (a boolean stores `1` / `0`); `real` → `REAL`; `blob` → `BLOB`. No `NOT NULL`
54
- * is ever emitted — the contract validates required-ness; the database is just
55
- * storage (AGENTS §14, the typed layer above imposes the shape).
35
+ * Map a portable {@link ColumnStorage} to its SQLite column type.
56
36
  *
57
- * @param type - The portable column type
37
+ * @param storage - The portable column type
58
38
  * @returns The SQLite column type keyword
59
- *
60
- * @example
61
- * ```ts
62
- * columnSQL('integer') // 'INTEGER'
63
- * columnSQL('json') // 'TEXT'
64
- * ```
65
39
  */
66
- export declare function columnSQL(type: ColumnType): string;
40
+ export declare function compileColumnSQL(storage: ColumnStorage): string;
67
41
 
68
42
  /**
69
- * Compile a {@link Criteria} into the SQL clause that follows a table name, with
70
- * its bound parameters in clause order.
43
+ * Compile one condition to its `<column> <operator>` SQL fragment and the parameters
44
+ * it binds engine-exact under SQL's three-valued NULL logic.
71
45
  *
72
46
  * @remarks
73
- * The driver's native `records` / `count` path: it assembles
74
- * `[where, orderBy, limitOffset]` (each possibly empty) into one clause so a
75
- * `SELECT * FROM <table> <clause>` runs the whole read in the engine instead of
76
- * over a JS `scan`. The WHERE fold is parenthesized **left-to-right** to mirror
77
- * the core engine's `matchesCriteria` (not SQL's native AND-over-OR precedence),
78
- * so a native and an engine read return identical rows. Each operand is encoded
79
- * via `encodeValue`: a flat column uses its declared schema type, while a nested
80
- * `FieldPath` (a `json_extract` read) encodes each operand as the native scalar
81
- * the extract returns — derived from the operand's runtime type — so it compares.
82
- * The 15 operators map per the databases guide's operator table, with
83
- * `starts` / `ends` using `LIKE … ESCAPE '\'` and an empty `any` / `none` list
84
- * collapsing to a constant. A `undefined` criteria (or one with no parts)
85
- * compiles to an empty clause.
47
+ * Every operand is run through `encodeValue`, so a bound value matches the SQL
48
+ * the column side compiles to. A flat column encodes operands with its DECLARED
49
+ * schema type (a flat `json` column `JSON.stringify`); a nested `FieldPath`
50
+ * encodes each operand as the NATIVE scalar `json_extract` returns, derived from
51
+ * the operand's runtime type (per-operand, since `between` / `any` / `none` can
52
+ * mix types). `any` / `none` collapse an empty list to a constant (`0` matches
53
+ * nothing, `1` matches all) with no parameters.
86
54
  *
87
- * @param criteria - The read specification, or `undefined` for all rows
88
- * @param schema - The table's schema (column types for operand encoding)
89
- * @returns The SQL tail and its bound parameters
55
+ * The core engine's total order ranks `undefined` (rank 0) BELOW `null`
56
+ * (rank 1) (see `compareValues`), so a MISSING/`NULL` column MATCHES
57
+ * `below` / `to` / a scalar `not` / `none` — the opposite of raw SQL, where a
58
+ * comparison against `NULL` is `NULL` (excluded). This fragment replicates the
59
+ * engine exactly. Truth table (`value` = the engine's decoded field read; a
60
+ * FLAT column's stored `NULL` decodes to `undefined` per `decodeRow`, so a
61
+ * flat `value` is NEVER a present `null` — only a NESTED path can be
62
+ * present-but-`null`):
63
+ *
64
+ * ```text
65
+ * operator | value=undefined (absent) | value=null (nested only) | value=scalar
66
+ * --------------------|--------------------------------|---------------------------|-------------
67
+ * equals, first=null | no match | MATCH | no match
68
+ * equals, first=X | no match | no match | value===X
69
+ * not, first=null | MATCH (flat: unconditionally; | no match | MATCH
70
+ * | nested: absent still matches)| |
71
+ * not, first=X | MATCH | MATCH | value!==X
72
+ * below/to, first=X | MATCH (rank 0 < rank(X)) | MATCH (rank 1 < rank(X)) | rank compare
73
+ * none, list=[…] | MATCH (no scalar rank-equal) | MATCH | not-in-list
74
+ * any, list=[…] | no match | no match | in-list
75
+ * above/from/between | no match | no match | rank compare
76
+ * like/glob/starts/… | no match (not a string) | no match | string test
77
+ * present | false | false | true
78
+ * absent | true | true | false
79
+ * ```
80
+ *
81
+ * A flat SQL `NULL` represents absence or explicit null according to its
82
+ * {@link ColumnSchema}; optional-and-nullable columns use a storage-class
83
+ * sentinel to distinguish the two. The native exactness gate therefore
84
+ * refines every optional or nullable scalar comparison through the core engine.
85
+ * This compiler still emits a total SQL fragment for direct consumers.
86
+ *
87
+ * A NESTED path can be present-but-`null` (a stored JSON `null`), which
88
+ * `json_extract` reads back as SQL `NULL` — indistinguishable from an ABSENT
89
+ * path. `json_type(col, path)` disambiguates them (`'null'` for present-null,
90
+ * SQL `NULL` for absent), so nested `equals` / `not` against a `null` operand
91
+ * compile through `json_type` instead of `IS NULL` / `IS NOT NULL`.
92
+ *
93
+ * Every other MATCH-on-null-or-absent row is expressed uniformly (flat and
94
+ * nested alike) as `(<column> <op> ? OR <column> IS NULL)` — for a nested
95
+ * path, `json_extract` already collapses BOTH absent and present-null to SQL
96
+ * `NULL`, so `IS NULL` catches both in one clause; for a flat column there is
97
+ * only the absent case to catch.
98
+ *
99
+ * @param condition - The condition to compile
100
+ * @param schema - The table's schema (for declared column types)
101
+ * @returns The SQL fragment and its bound parameters
90
102
  *
91
103
  * @example
92
104
  * ```ts
93
- * compileCriteria({ conditions: [{ column: 'age', operator: 'from', values: [18], connector: 'and' }] }, schema)
94
- * // { sql: 'WHERE "age" >= ? ORDER BY "id"', params: [18] }
105
+ * compileConditionSQL({ column: 'age', operator: 'above', values: [18], connector: 'and' }, schema)
106
+ * // { sql: '"age" > ?', parameters: [18] }
107
+ * compileConditionSQL({ column: 'age', operator: 'below', values: [18], connector: 'and' }, schema)
108
+ * // { sql: '("age" < ? OR "age" IS NULL)', parameters: [18] }
95
109
  * ```
96
110
  */
97
- export declare function compileCriteria(criteria: Criteria | undefined, schema: TableSchema): CompiledSQL;
111
+ export declare function compileConditionSQL(condition: Condition, schema: TableSchema): CompiledSQL;
98
112
 
99
113
  /**
100
114
  * A parameterized SQL fragment or statement plus its bind values.
101
115
  *
102
116
  * @remarks
103
117
  * Produced by the pure SQL compilers (`compilers.ts`) that turn a core
104
- * `Criteria` (or a table definition) into SQL text with `?` placeholders, and
105
- * consumed by the SQLite driver, which runs `sql` with `params` bound in
106
- * order — no further assembly. Keeping `sql` and `params` together prevents
118
+ * `QueryInput` (or a table definition) into SQL text with `?` placeholders, and
119
+ * consumed by the SQLite driver, which runs `sql` with `parameters` bound in
120
+ * order — no further assembly. Keeping `sql` and `parameters` together prevents
107
121
  * the two from drifting apart across compile and execute.
108
122
  */
109
123
  export declare interface CompiledSQL {
110
124
  readonly sql: string;
111
- readonly params: readonly SQLiteValue[];
125
+ readonly parameters: readonly SQLiteValue[];
112
126
  }
113
127
 
128
+ /**
129
+ * Compile a {@link FieldPath} to the SQL expression that reads it.
130
+ *
131
+ * @param path - The field path
132
+ * @returns The SQL expression selecting the value
133
+ */
134
+ export declare function compileFieldSQL(path: FieldPath): string;
135
+
136
+ /**
137
+ * Compile a NESTED {@link FieldPath} to the `json_type(<col>, <path>)` SQL
138
+ * expression — the {@link compileFieldSQL} `json_extract` sibling used to tell a
139
+ * PRESENT JSON `null` apart from an ABSENT path (both read back as SQL `NULL`
140
+ * through `json_extract`, but `json_type` reports `'null'` for the former and
141
+ * SQL `NULL` for the latter).
142
+ *
143
+ * @param path - The nested field path (a column plus its JSON keys)
144
+ * @returns The SQL expression reading the value's JSON type
145
+ *
146
+ * @example
147
+ * ```ts
148
+ * compileJSONTypeSQL(['payload', 'user', 'id']) // "json_type(\"payload\", '$.user.id')"
149
+ * ```
150
+ */
151
+ export declare function compileJSONTypeSQL(path: readonly string[]): string;
152
+
114
153
  /**
115
154
  * Compile the ORDER BY clause from the order terms, always ending with the
116
155
  * primary key as the final determinant.
@@ -152,20 +191,51 @@ export declare function compileOrder(order: readonly Order[] | undefined, schema
152
191
  *
153
192
  * @example
154
193
  * ```ts
155
- * compilePage(undefined, 5) // { sql: 'LIMIT -1 OFFSET ?', params: [5] }
194
+ * compilePage(undefined, 5) // { sql: 'LIMIT -1 OFFSET ?', parameters: [5] }
156
195
  * ```
157
196
  */
158
197
  export declare function compilePage(limit: number | undefined, offset: number | undefined): CompiledSQL;
159
198
 
199
+ /**
200
+ * Compile a {@link QueryInput} into the SQL clause that follows a table name, with
201
+ * its bound parameters in clause order.
202
+ *
203
+ * @remarks
204
+ * The driver's native `records` / `count` path: it assembles
205
+ * `[where, orderBy, limitOffset]` (each possibly empty) into one clause so a
206
+ * `SELECT * FROM <table> <clause>` runs the whole read in the engine instead of
207
+ * over a JS `scan`. The WHERE fold is parenthesized **left-to-right** to mirror
208
+ * the core engine's `matchesQuery` (not SQL's native AND-over-OR precedence),
209
+ * so a native and an engine read return identical rows. Each operand is encoded
210
+ * via `encodeValue`: a flat column uses its declared schema type, while a nested
211
+ * `FieldPath` (a `json_extract` read) encodes each operand as the native scalar
212
+ * the extract returns — derived from the operand's runtime type — so it compares.
213
+ * The 15 operators map per the databases guide's operator table, with
214
+ * `starts` / `ends` using `LIKE … ESCAPE '\'` and an empty `any` / `none` list
215
+ * collapsing to a constant. An `undefined` input (or one with no parts)
216
+ * compiles to an empty clause.
217
+ *
218
+ * @param input - The read specification, or `undefined` for all rows
219
+ * @param schema - The table's schema (column types for operand encoding)
220
+ * @returns The SQL tail and its bound parameters
221
+ *
222
+ * @example
223
+ * ```ts
224
+ * compileQuerySQL({ conditions: [{ column: 'age', operator: 'from', values: [18], connector: 'and' }] }, schema)
225
+ * // { sql: 'WHERE "age" >= ? ORDER BY "id"', parameters: [18] }
226
+ * ```
227
+ */
228
+ export declare function compileQuerySQL(input: QueryInput | undefined, schema: TableSchema): CompiledSQL;
229
+
160
230
  /**
161
231
  * Fold the conditions into one WHERE clause, parenthesizing progressively
162
- * left-to-right so the grouping matches the engine's `matchesCriteria` fold.
232
+ * left-to-right so the grouping matches the engine's `matchesQuery` fold.
163
233
  *
164
234
  * @remarks
165
235
  * The first condition's connector is ignored, per the {@link Condition} types.
166
- * Every fragment (see {@link fragment}'s truth table) replicates the core
236
+ * Every fragment (see {@link compileConditionSQL}'s truth table) replicates the core
167
237
  * engine's total order EXACTLY under SQL's three-valued NULL logic, so this
168
- * clause matches `applyCriteria` row-for-row over the same table — a native
238
+ * clause matches `applyQuery` row-for-row over the same table — a native
169
239
  * `records` / `count` read never disagrees with a scan-and-filter fallback.
170
240
  *
171
241
  * @param conditions - The conditions to fold
@@ -175,7 +245,7 @@ export declare function compilePage(limit: number | undefined, offset: number |
175
245
  * @example
176
246
  * ```ts
177
247
  * compileWhere([{ column: 'age', operator: 'from', values: [18], connector: 'and' }], schema)
178
- * // { sql: 'WHERE "age" >= ?', params: [18] }
248
+ * // { sql: 'WHERE "age" >= ?', parameters: [18] }
179
249
  * ```
180
250
  */
181
251
  export declare function compileWhere(conditions: readonly Condition[], schema: TableSchema): CompiledSQL;
@@ -184,9 +254,9 @@ export declare function compileWhere(conditions: readonly Condition[], schema: T
184
254
  * Create a persistent JSON-file {@link DriverInterface} for the core database layer.
185
255
  *
186
256
  * @remarks
187
- * Pass it to `createDatabase` from `@orkestrel/database` to run the whole typed database +
188
- * relations stack against a single JSON file instead of memory — the `Database` /
189
- * `Table` / `Query` / relations API is unchanged; only where the bytes live changes.
257
+ * Pass it to `createDatabase` from `@orkestrel/database` to run the typed
258
+ * database against a single JSON file instead of memory — the `Database` /
259
+ * `Table` / `Query` API is unchanged; only where the bytes live changes.
190
260
  * The driver is the reference `MemoryDriver` plus JSON-file persistence: `open` loads
191
261
  * the file, every mutation flushes the whole store back, and querying runs through
192
262
  * the core engine over `scan` (it is scan-only — no native `records` / `count` /
@@ -215,19 +285,20 @@ export declare function createJSONDriver(path: string): DriverInterface;
215
285
  * Create a trusted-mode SQLite {@link DriverInterface} for the core database layer.
216
286
  *
217
287
  * @remarks
218
- * Pass it to `createDatabase` from `@orkestrel/database` to run the whole typed database +
219
- * relations stack against a real SQLite database — the `Database` / `Table` /
220
- * `Query` / relations API is unchanged; only where the bytes live changes. Built
288
+ * Pass it to `createDatabase` from `@orkestrel/database` to run the typed
289
+ * database against a real SQLite database — the `Database` / `Table` /
290
+ * `Query` API is unchanged; only where the bytes live changes. Built
221
291
  * on the published `@orkestrel/sqlite` synchronous wrapper: `open` issues real
222
292
  * typed `CREATE TABLE` / `CREATE INDEX` statements (reopen-safe) plus a reserved
223
- * `_meta` table for `meta()` / `stamp()` — avoid naming a table `_meta`.
293
+ * `_metadata` table for `metadata()` / `stamp()` — avoid naming a table `_metadata`.
224
294
  * Querying, paging, and aggregation run natively (`records` / `count` /
225
295
  * `aggregate` / `stream`); `transaction` and `migrate` use real `BEGIN` /
226
296
  * `COMMIT` / `ROLLBACK`, so `migrate` is atomic even mid-plan.
227
297
  *
228
- * @param options - A bare database file path (`':memory:'` by default, for
229
- * back-compat), or a full {@link SQLiteDriverOptions} bag (`path`,
230
- * `readonly`, `timeout`, `foreignKeys`, `pragmas`)
298
+ * @param options - The {@link SQLiteDriverOptions} bag (`path`, `readonly`,
299
+ * `timeout`, `references`, `pragmas`); `references` directly enables or
300
+ * disables foreign-key enforcement, and omission retains the upstream
301
+ * default; omit the whole bag for an in-memory database
231
302
  * @returns A {@link DriverInterface} backed by SQLite
232
303
  *
233
304
  * @example
@@ -237,30 +308,16 @@ export declare function createJSONDriver(path: string): DriverInterface;
237
308
  * import { createSQLiteDriver } from '@orkestrel/database/server'
238
309
  *
239
310
  * const db = createDatabase({
240
- * driver: createSQLiteDriver('data/app.sqlite'),
311
+ * driver: createSQLiteDriver({ path: 'data/app.sqlite' }),
241
312
  * tables: { users: { id: stringShape(), name: stringShape() } },
242
313
  * })
243
314
  * await db.table('users').set({ id: 'u1', name: 'Ada' }) // persisted to app.sqlite
244
315
  *
245
- * // Or with options:
316
+ * // Or with additional options:
246
317
  * createSQLiteDriver({ path: 'data/app.sqlite', pragmas: { journal_mode: 'WAL' } })
247
318
  * ```
248
319
  */
249
- export declare function createSQLiteDriver(options?: string | SQLiteDriverOptions): DriverInterface;
250
-
251
- /**
252
- * The declared storage type of a flat (string) column, read from the schema.
253
- *
254
- * @param column - The column name
255
- * @param schema - The table's schema
256
- * @returns The column's {@link ColumnType}, or `undefined` if the schema does not carry it
257
- *
258
- * @example
259
- * ```ts
260
- * declaredType('age', schema) // 'integer'
261
- * ```
262
- */
263
- export declare function declaredType(column: string, schema: TableSchema): ColumnType | undefined;
320
+ export declare function createSQLiteDriver(options?: SQLiteDriverOptions): DriverInterface;
264
321
 
265
322
  /**
266
323
  * Decode a stored {@link SQLiteRow} back to a {@link Row} by its table's schema.
@@ -269,9 +326,8 @@ export declare function declaredType(column: string, schema: TableSchema): Colum
269
326
  * Decodes each declared column with {@link decodeValue} and **omits** any column
270
327
  * whose decoded value is `undefined` — so an absent / `NULL` optional column does
271
328
  * not surface as `{ bio: undefined }`, matching how the contract's optional
272
- * columns expect absence. A known, documented edge: a non-optional `nullableShape`
273
- * column storing `null` round-trips to absent (a `null` cell decodes to
274
- * `undefined`, and an `undefined` value is omitted).
329
+ * columns expect absence. Nullable-only SQL `NULL` cells remain explicit
330
+ * `null`; optional-and-nullable columns use their storage-class sentinel.
275
331
  *
276
332
  * @param row - The stored SQLite row
277
333
  * @param schema - The table's schema
@@ -285,27 +341,51 @@ export declare function declaredType(column: string, schema: TableSchema): Colum
285
341
  export declare function decodeRow(row: SQLiteRow, schema: TableSchema): Row;
286
342
 
287
343
  /**
288
- * Decode a stored {@link SQLiteValue} back to its JS value for a column's type
344
+ * Decode a stored {@link SQLiteValue} back to its JS value for a declared column —
289
345
  * the exact inverse of {@link encodeValue}.
290
346
  *
291
347
  * @remarks
292
- * A `boolean` column reads `1` / `0` back to `true` / `false` (a stored `null`
293
- * `undefined`); a `json` column `JSON.parse`s a string (anything else →
294
- * `undefined`); every other type passes the value through, mapping a stored
295
- * `NULL` to `undefined`. NULL decodes to `undefined` so {@link decodeRow} can
296
- * omit absent columns.
348
+ * Stored values must use the declared SQLite storage class. SQL `NULL` decodes
349
+ * to explicit `null` only for nullable-only columns and otherwise to absence.
350
+ * Optional-and-nullable sentinels decode to explicit `null`; malformed values
351
+ * decode to `undefined` so {@link decodeRow} omits them.
297
352
  *
298
353
  * @param value - The stored SQLite value
299
- * @param type - The column's portable storage type
300
- * @returns The decoded JS value (`undefined` for a stored `NULL`)
354
+ * @param column - The declared storage and absence/null contract
355
+ * @returns The decoded JS value, or `undefined` for absence/malformed storage
301
356
  *
302
357
  * @example
303
358
  * ```ts
304
- * decodeValue(1, 'boolean') // true
305
- * decodeValue('{"a":1}', 'json') // { a: 1 }
359
+ * decodeValue(1, booleanColumn) // true
360
+ * decodeValue('{"a":1}', jsonColumn) // { a: 1 }
306
361
  * ```
307
362
  */
308
- export declare function decodeValue(value: SQLiteValue, type: ColumnType): unknown;
363
+ export declare function decodeValue(value: SQLiteValue, column: ColumnSchema): unknown;
364
+
365
+ /**
366
+ * Build a collision-free SQL index name for a table + column-group index —
367
+ * shared by the compiler module's `schemaToIndexes` and `stepToSQL`,
368
+ * so a plan-built index name always matches one `open` would have created.
369
+ *
370
+ * @remarks
371
+ * A naive `idx_<table>_<cols joined by _>` is AMBIGUOUS: table `'a_b'` with
372
+ * column `'c'` and table `'a'` with columns `['b', 'c']` both produce
373
+ * `idx_a_b_c`. This encodes each part (the table name, then each column name)
374
+ * length-prefixed (`<len>_<part>`) so the boundary between parts is always
375
+ * unambiguous, however the names themselves are punctuated.
376
+ *
377
+ * @param table - The table name
378
+ * @param columns - The index's column names, in order
379
+ * @returns The deterministic, collision-free index identifier (unquoted)
380
+ *
381
+ * @example
382
+ * ```ts
383
+ * deriveSQLiteIndexName('users', ['name']) // 'idx_5_users_4_name'
384
+ * deriveSQLiteIndexName('a_b', ['c']) // 'idx_3_a_b_1_c'
385
+ * deriveSQLiteIndexName('a', ['b', 'c']) // 'idx_1_a_1_b_1_c'
386
+ * ```
387
+ */
388
+ export declare function deriveSQLiteIndexName(table: string, columns: readonly string[]): string;
309
389
 
310
390
  /**
311
391
  * Encode a whole {@link Row} to a {@link SQLiteRow} by its table's schema.
@@ -327,28 +407,25 @@ export declare function decodeValue(value: SQLiteValue, type: ColumnType): unkno
327
407
  export declare function encodeRow(row: Row, schema: TableSchema): SQLiteRow;
328
408
 
329
409
  /**
330
- * Encode a JS value to its stored {@link SQLiteValue} for a column's type.
410
+ * Encode a JS value to its stored {@link SQLiteValue} for a declared column.
331
411
  *
332
412
  * @remarks
333
- * The forward half of the bridge, total (AGENTS §14): a value that does not fit
334
- * its column's storage type encodes to `null` rather than throwing. A `boolean`
335
- * column stores `1` / `0` (and `null` / `undefined` `null`); a `json` column
336
- * stores `JSON.stringify` (or `null` for `null` / `undefined`); `integer` /
337
- * `real` keep a `number` / `bigint`, else `null`; `text` keeps a `string`, else
338
- * `null`; `blob` keeps a `Uint8Array`, else `null`. Narrowed with `typeof` /
339
- * `instanceof`, never `as`.
413
+ * The codec is total: a malformed value encodes to SQL `NULL`. Absence always
414
+ * uses SQL `NULL`. A nullable-only column also uses SQL `NULL` for explicit
415
+ * `null`; an optional-and-nullable column uses a storage-class sentinel so
416
+ * absence and explicit `null` remain distinct.
340
417
  *
341
418
  * @param value - The JS value to store
342
- * @param type - The column's portable storage type
419
+ * @param column - The declared storage and absence/null contract
343
420
  * @returns The value SQLite stores
344
421
  *
345
422
  * @example
346
423
  * ```ts
347
- * encodeValue(true, 'boolean') // 1
348
- * encodeValue({ a: 1 }, 'json') // '{"a":1}'
424
+ * encodeValue(true, booleanColumn) // 1
425
+ * encodeValue({ a: 1 }, jsonColumn) // '{"a":1}'
349
426
  * ```
350
427
  */
351
- export declare function encodeValue(value: unknown, type: ColumnType): SQLiteValue;
428
+ export declare function encodeValue(value: unknown, column: ColumnSchema): SQLiteValue;
352
429
 
353
430
  /**
354
431
  * Escape `\`, `%`, and `_` (each with a leading `\`) so a `starts` / `ends`
@@ -365,7 +442,7 @@ export declare function encodeValue(value: unknown, type: ColumnType): SQLiteVal
365
442
  export declare function escapeLike(text: string): string;
366
443
 
367
444
  /**
368
- * The declared {@link ColumnType}s whose SQL EQUALITY comparisons (`equals` /
445
+ * The declared {@link ColumnStorage}s whose SQL EQUALITY comparisons (`equals` /
369
446
  * `not` / `any` / `none`) and `starts` / `ends` compiles are provably
370
447
  * engine-exact under declared-type trust — `text` / `integer` / `real` /
371
448
  * `boolean`; a `json` or `blob` column always refines instead.
@@ -380,23 +457,20 @@ export declare function escapeLike(text: string): string;
380
457
  * compares UTF-16 CODE-UNIT order. The two orders diverge for supplementary-
381
458
  * plane characters (code points ≥ U+10000, e.g. many emoji): a lead surrogate
382
459
  * (`\uD800`–`\uDBFF`) sorts BELOW ``–`￿` in code-unit order, while
383
- * its code point sorts ABOVE them. So `isExactCondition`'s range family and
384
- * `isExactOrder` exclude `text`, refining through the core engine instead. A
385
- * future opt-in "trusted collation" mode (the caller vouches the column's
386
- * values are BMP-only, or a custom SQLite collation matching `compareValues`
387
- * is registered) could restore native text ranges/ordering.
460
+ * its code point sorts ABOVE them. So `matchesConditionExactly`'s range family and
461
+ * `matchesOrderExactly` exclude `text`, refining through the core engine instead.
388
462
  */
389
- export declare const EXACT_COLUMN_TYPES: readonly ColumnType[];
463
+ export declare const EXACT_COLUMN_STORAGE: readonly ColumnStorage[];
390
464
 
391
465
  /**
392
- * The declared {@link ColumnType}s whose SQL RANGE comparisons
466
+ * The declared {@link ColumnStorage}s whose SQL RANGE comparisons
393
467
  * (`above` / `below` / `from` / `to` / `between`) and `ORDER BY` compiles are
394
468
  * provably engine-exact — `integer` / `real` / `boolean` only. `text` is
395
- * excluded: see {@link EXACT_COLUMN_TYPES}'s remarks for the BINARY-collation
469
+ * excluded: see {@link EXACT_COLUMN_STORAGE}'s remarks for the BINARY-collation
396
470
  * (code-point) vs. JS `<` (code-unit) divergence on supplementary-plane
397
471
  * characters.
398
472
  */
399
- export declare const EXACT_RANGE_COLUMN_TYPES: readonly ColumnType[];
473
+ export declare const EXACT_RANGE_COLUMN_STORAGE: readonly ColumnStorage[];
400
474
 
401
475
  /**
402
476
  * Extract a stored row's values in a declared positional order.
@@ -420,239 +494,77 @@ export declare const EXACT_RANGE_COLUMN_TYPES: readonly ColumnType[];
420
494
  export declare function extractValues(row: SQLiteRow, names: readonly string[], table: string): readonly SQLiteValue[];
421
495
 
422
496
  /**
423
- * Compile a {@link FieldPath} to the SQL expression that reads it.
424
- *
425
- * @remarks
426
- * A single string is ONE column — `quote(path)`. An array descends a JSON column:
427
- * the first element is the (quoted) column, the rest a `json_extract` path
428
- * (`json_extract("payload", '$.user.id')`), matching the guide's nested-field
429
- * examples (simple identifier keys). The string's value is never split on `.`
430
- * (AGENTS — `FieldPath` semantics): a dotted string is one column literally.
431
- *
432
- * @param path - The field path (a column, or a column + nested keys)
433
- * @returns The SQL expression selecting the value
434
- *
435
- * @example
436
- * ```ts
437
- * fieldColumn('payload') // '"payload"'
438
- * fieldColumn(['payload', 'user', 'id']) // 'json_extract("payload", \'$.user.id\')'
439
- * ```
440
- */
441
- export declare function fieldColumn(path: FieldPath): string;
442
-
443
- /**
444
- * Compile one condition to its `<column> <operator>` SQL fragment and the params
445
- * it binds — engine-exact under SQL's three-valued NULL logic.
446
- *
447
- * @remarks
448
- * Every operand is run through `encodeValue`, so a bound value matches the SQL
449
- * the column side compiles to. A flat column encodes operands with its DECLARED
450
- * schema type (a flat `json` column → `JSON.stringify`); a nested `FieldPath`
451
- * encodes each operand as the NATIVE scalar `json_extract` returns, derived from
452
- * the operand's runtime type (per-operand, since `between` / `any` / `none` can
453
- * mix types). `any` / `none` collapse an empty list to a constant (`0` matches
454
- * nothing, `1` matches all) with no params.
455
- *
456
- * The core engine's total order ranks `undefined` (rank 0) BELOW `null`
457
- * (rank 1) (see `compareValues`), so a MISSING/`NULL` column MATCHES
458
- * `below` / `to` / a scalar `not` / `none` — the opposite of raw SQL, where a
459
- * comparison against `NULL` is `NULL` (excluded). This fragment replicates the
460
- * engine exactly. Truth table (`value` = the engine's decoded field read; a
461
- * FLAT column's stored `NULL` decodes to `undefined` per `decodeRow`, so a
462
- * flat `value` is NEVER a present `null` — only a NESTED path can be
463
- * present-but-`null`):
464
- *
465
- * ```text
466
- * operator | value=undefined (absent) | value=null (nested only) | value=scalar
467
- * --------------------|--------------------------------|---------------------------|-------------
468
- * equals, first=null | no match | MATCH | no match
469
- * equals, first=X | no match | no match | value===X
470
- * not, first=null | MATCH (flat: unconditionally; | no match | MATCH
471
- * | nested: absent still matches)| |
472
- * not, first=X | MATCH | MATCH | value!==X
473
- * below/to, first=X | MATCH (rank 0 < rank(X)) | MATCH (rank 1 < rank(X)) | rank compare
474
- * none, list=[…] | MATCH (no scalar rank-equal) | MATCH | not-in-list
475
- * any, list=[…] | no match | no match | in-list
476
- * above/from/between | no match | no match | rank compare
477
- * like/glob/starts/… | no match (not a string) | no match | string test
478
- * present | false | false | true
479
- * absent | true | true | false
480
- * ```
481
- *
482
- * Because a flat column's `NULL` always decodes to `undefined`, `equals`
483
- * against a `null` operand needs no special flat compilation (`col = ?`
484
- * binding a `NULL` param is already always-false in SQL, matching "no match"
485
- * above) — but flat `not` against `null` must match EVERY row (both the
486
- * absent and the scalar rows), which `col != ? OR col IS NULL` cannot express
487
- * (it only catches the `IS NULL` row), so a flat `not`-with-`null`-operand
488
- * compiles to the constant `1`.
489
- *
490
- * A NESTED path can be present-but-`null` (a stored JSON `null`), which
491
- * `json_extract` reads back as SQL `NULL` — indistinguishable from an ABSENT
492
- * path. `json_type(col, path)` disambiguates them (`'null'` for present-null,
493
- * SQL `NULL` for absent), so nested `equals` / `not` against a `null` operand
494
- * compile through `json_type` instead of `IS NULL` / `IS NOT NULL`.
495
- *
496
- * Every other MATCH-on-null-or-absent row is expressed uniformly (flat and
497
- * nested alike) as `(<column> <op> ? OR <column> IS NULL)` — for a nested
498
- * path, `json_extract` already collapses BOTH absent and present-null to SQL
499
- * `NULL`, so `IS NULL` catches both in one clause; for a flat column there is
500
- * only the absent case to catch.
501
- *
502
- * @param condition - The condition to compile
503
- * @param schema - The table's schema (for declared column types)
504
- * @returns The SQL fragment and its bound parameters
505
- *
506
- * @example
507
- * ```ts
508
- * fragment({ column: 'age', operator: 'above', values: [18], connector: 'and' }, schema)
509
- * // { sql: '"age" > ?', params: [18] }
510
- * fragment({ column: 'age', operator: 'below', values: [18], connector: 'and' }, schema)
511
- * // { sql: '("age" < ? OR "age" IS NULL)', params: [18] }
512
- * ```
513
- */
514
- export declare function fragment(condition: Condition, schema: TableSchema): CompiledSQL;
515
-
516
- /**
517
- * Generate a fresh unique key — a v4 UUID string, backed by `node:crypto`.
518
- *
519
- * @remarks
520
- * Supply this as {@link import('@orkestrel/database').DatabaseOptions.key} so a table mints
521
- * a key when a written row lacks its primary-key value. Strings work as keys on
522
- * every backend; supply your own key values directly to use numeric keys instead.
497
+ * The declared storage type of a flat (string) column, read from the schema.
523
498
  *
524
- * @returns A new UUID string
499
+ * @param column - The column name
500
+ * @param schema - The table's schema
501
+ * @returns The column's {@link ColumnStorage}, or `undefined` if the schema does not carry it
525
502
  *
526
503
  * @example
527
504
  * ```ts
528
- * const db = createDatabase({ driver, tables, key: generateKey })
505
+ * findColumnStorage('age', schema) // 'integer'
529
506
  * ```
530
507
  */
531
- export declare function generateKey(): string;
508
+ export declare function findColumnStorage(column: string, schema: TableSchema): ColumnStorage | undefined;
532
509
 
533
510
  /**
534
- * Build a collision-free SQL index name for a table + column-group index —
535
- * shared by {@link schemaToIndexes} (an `open`-time `CREATE INDEX`) and
536
- * {@link stepToSQL}'s `index.add` / `index.remove` (a migration-time DDL),
537
- * so a plan-built index name always matches one `open` would have created.
511
+ * The storage type a nested (`json_extract`) operand encodes as, derived from its
512
+ * RUNTIME value NOT `json`.
538
513
  *
539
514
  * @remarks
540
- * A naive `idx_<table>_<cols joined by _>` is AMBIGUOUS: table `'a_b'` with
541
- * column `'c'` and table `'a'` with columns `['b', 'c']` both produce
542
- * `idx_a_b_c`. This encodes each part (the table name, then each column name)
543
- * length-prefixed (`<len>_<part>`) so the boundary between parts is always
544
- * unambiguous, however the names themselves are punctuated.
515
+ * `json_extract` returns the unquoted, natively-typed scalar (a JSON boolean as
516
+ * `1` / `0`, a number as-is, a string as-is), so the operand must encode to that
517
+ * same scalar to compare. A boolean → `'boolean'` ( `1` / `0`); a number
518
+ * `'integer'` / `'real'`; a bigint `'integer'`; a string → `'text'`; `null` /
519
+ * `undefined` `'text'` (encodes to `null`); an object / array → `'json'` (the
520
+ * edge of comparing against a json subtree).
545
521
  *
546
- * @param table - The table name
547
- * @param columns - The index's column names, in order
548
- * @returns The deterministic, collision-free index identifier (unquoted)
522
+ * @param value - The runtime operand value
523
+ * @returns The {@link ColumnStorage} to encode it as
549
524
  *
550
525
  * @example
551
526
  * ```ts
552
- * indexName('users', ['name']) // 'idx_5_users_4_name'
553
- * indexName('a_b', ['c']) // 'idx_3_a_b_1_c'
554
- * indexName('a', ['b', 'c']) // 'idx_1_a_1_b_1_c'
527
+ * inferValueStorage(true) // 'boolean'
528
+ * inferValueStorage(9) // 'integer'
555
529
  * ```
556
530
  */
557
- export declare function indexName(table: string, columns: readonly string[]): string;
558
-
559
- /**
560
- * Whether one {@link Condition} compiles to SQL that is PROVABLY identical to
561
- * the core engine's `matchesCondition` for every value its column's declared
562
- * type can store.
563
- *
564
- * @remarks
565
- * `false` for a nested `FieldPath` (an array), a column absent from `schema`,
566
- * or a column whose declared type is not `text` / `integer` / `real` /
567
- * `boolean` (a `json` / `blob` column) — EXCEPT `absent` / `present`, which
568
- * compile to `IS NULL` / `IS NOT NULL` and match `decodeRow`'s "a stored NULL
569
- * decodes to `undefined`" rule for every column type, so they are exact
570
- * regardless of declared type. `equals` / `not` require a operand matching the
571
- * column's declared type (a `null` / `undefined` operand is never exact here —
572
- * `encodeRow` stores both an explicit `null` and an absent field as SQL NULL,
573
- * so native `IS NULL` semantics cannot match the engine's `deepEqual`-over-
574
- * decoded-rows truth). `above` / `below` / `from` / `to` / `between` are exact
575
- * ONLY for a declared type in {@link EXACT_RANGE_COLUMN_TYPES} (`integer` /
576
- * `real` / `boolean`) — a `text` column's range conditions REFINE, because
577
- * SQLite's default BINARY collation orders TEXT by Unicode CODE POINT while
578
- * the core engine's `compareValues` orders JS strings by UTF-16 CODE UNIT,
579
- * and the two diverge for supplementary-plane characters (see
580
- * {@link EXACT_COLUMN_TYPES}'s remarks for the full rationale).
581
- * `any` / `none` require a NON-EMPTY list where every element matches (an empty
582
- * list is exact under neither: the engine's `any([])` matches nothing while
583
- * `none([])` matches everything, and SQL `IN ()` is a syntax error) — these
584
- * stay exact on `text` (byte equality is collation-independent and engine-
585
- * identical). `starts` / `ends` are exact only on a `text` column with a
586
- * string operand (case-sensitive `substr` compile, see {@link fragment}) —
587
- * likewise collation-independent. `like` / `glob` are NEVER exact — SQLite
588
- * `LIKE` folds case ASCII-only against the engine's Unicode fold, and `GLOB`
589
- * has character classes the engine treats literally.
590
- *
591
- * @param condition - The condition to test
592
- * @param schema - The table's schema
593
- * @returns Whether `condition` is exact
594
- */
595
- export declare function isExactCondition(condition: Condition, schema: TableSchema): boolean;
596
-
597
- /**
598
- * Whether a whole {@link Criteria} is exact — every condition and every order
599
- * term is exact. `limit` / `offset` never affect exactness (SQL `LIMIT` /
600
- * `OFFSET` are always engine-identical).
601
- *
602
- * @param criteria - The criteria to test
603
- * @param schema - The table's schema
604
- * @returns Whether every part of `criteria` is exact
605
- */
606
- export declare function isExactCriteria(criteria: Criteria, schema: TableSchema): boolean;
607
-
608
- /**
609
- * Whether one {@link Order} term's column compiles to an `ORDER BY` that
610
- * matches the engine's {@link import('@src/core').sortRows} exactly.
611
- *
612
- * @remarks
613
- * `false` for a nested `FieldPath`, a column absent from `schema`, or a
614
- * declared type outside {@link EXACT_RANGE_COLUMN_TYPES} (`integer` / `real` /
615
- * `boolean`). `text` is NOT exact here: SQLite's default BINARY collation
616
- * orders TEXT by Unicode code point while the core engine's `compareValues`
617
- * orders JS strings by UTF-16 code unit, and the two diverge for
618
- * supplementary-plane characters (see {@link EXACT_COLUMN_TYPES}'s remarks) —
619
- * a `text` order term REFINES through the core engine instead.
620
- *
621
- * @param order - The order term to test
622
- * @param schema - The table's schema
623
- * @returns Whether `order` is exact
624
- */
625
- export declare function isExactOrder(order: Order, schema: TableSchema): boolean;
531
+ export declare function inferValueStorage(value: unknown): ColumnStorage;
626
532
 
627
533
  /**
628
534
  * A persistent {@link DriverInterface} backed by a single JSON file — the
629
535
  * reference {@link MemoryDriver} plus file load / flush.
630
536
  *
631
537
  * @remarks
632
- * A decorator, not a reimplementation: every primitive delegates to an inner
633
- * {@link MemoryDriver}, so querying, key-order `scan` / `keys`, and capture-replay
634
- * `snapshot` are inherited unchanged this layer adds only persistence. `open`
538
+ * A decorator, not a reimplementation: storage primitives delegate to an inner
539
+ * {@link MemoryDriver}, while this layer owns persistence, writer ordering,
540
+ * isolated transactions, and queued row-snapshot restoration. `open`
635
541
  * loads the file into memory; every mutation (`write` / `delete` / `clear`) flushes
636
- * the whole store back. The file is one JSON object, `{ meta?: DriverMeta, tables: {
637
- * [name]: rows } }` — `meta` is present only once the store has been `stamp`ed
638
- * (an unstamped store serializes the old `{ tables }` shape, preserving
639
- * backward compatibility); a per-table array of rows, each row carrying its own
542
+ * the whole store back. The file is one JSON object, `{ metadata?: DriverMetadata, tables: {
543
+ * [name]: rows } }` — `metadata` is present only once the store has been `stamp`ed
544
+ * (an unstamped store omits `metadata`); a per-table array of rows, each row carrying its own
640
545
  * primary (the table contract), so the key is recovered on load with
641
546
  * {@link extractKey} and the file need not store it. The parsed JSON crosses the
642
547
  * boundary as `unknown` and is narrowed with {@link isRecord} / {@link extractKey},
643
- * never asserted (AGENTS §14): a missing, corrupt, or wrong-shaped file starts
644
- * empty rather than throwing, and a malformed row (or malformed `meta`) is
645
- * skipped/dropped rather than thrown on. It is scan-only it implements none of
646
- * the optional native `records` / `count` / `aggregate` hooks, so the core engine
548
+ * never asserted (AGENTS §14). A read that reports no document there starts empty —
549
+ * `ENOENT` for a plain absence, and `ENOTDIR` for a path whose parent is not a
550
+ * directory, which no later write could find either; every other read failure or
551
+ * invalid existing document fails closed without publication, mutation, or
552
+ * automatic repair. It is scan-only — it implements none of
553
+ * the optional native `records` / `aggregate` hooks, so the core engine
647
554
  * over `scan` answers every query. For development, small datasets, and portable /
648
555
  * inspectable data; for large or concurrent workloads reach for a SQLite-backed
649
556
  * driver.
650
557
  *
651
- * A failure in the write path ({@link JSONDriver.#serialize} `mkdir` /
652
- * `writeFile` / `rename`) is wrapped and rethrown as `DatabaseError` `DRIVER`,
653
- * carrying the target `path` in its context; the read path ({@link
654
- * JSONDriver.#load}) tolerance above is a separate, deliberate contract and is
655
- * never touched by this wrapping.
558
+ * Metadata crosses {@link cloneDriverMetadata} at parsed-file ingress, public and
559
+ * scoped write ingress, candidate/root publication, serialization, and copy-out.
560
+ * Callers therefore cannot mutate queued metadata, and `metadata()` always returns a
561
+ * distinct deeply frozen snapshot. A failure in the write path ({@link
562
+ * JSONDriver.#serialize} `mkdir` / `writeFile` / `rename`) is wrapped and
563
+ * rethrown as `DatabaseError` `DRIVER`, carrying the target `path` and native
564
+ * `cause` in its context. If temporary-file cleanup also fails, the top-level
565
+ * `DRIVER` context additionally carries `temp` and `cleanup`; a precommit abort
566
+ * remains an `ABORTED` `DatabaseError` in `context.cause`. The fail-closed read path
567
+ * ({@link JSONDriver.#document}) remains separate from this write-error contract.
656
568
  */
657
569
  export declare class JSONDriver implements DriverInterface_2 {
658
570
  #private;
@@ -660,92 +572,153 @@ export declare class JSONDriver implements DriverInterface_2 {
660
572
  open(schema: readonly TableSchema_2[]): Promise<void>;
661
573
  close(): Promise<void>;
662
574
  read(table: string, key: Key): Promise<Row_2 | undefined>;
663
- write(table: string, key: Key, row: Row_2): Promise<void>;
664
- delete(table: string, key: Key): Promise<boolean>;
575
+ write(table: string, key: Key, row: Row_2, options?: OperationOptions): Promise<void>;
576
+ insert(table: string, key: Key, row: Row_2, options?: OperationOptions): Promise<void>;
577
+ delete(table: string, key: Key, options?: OperationOptions): Promise<boolean>;
665
578
  keys(table: string): Promise<readonly Key[]>;
666
579
  scan(table: string): AsyncIterable<Row_2>;
667
580
  /**
668
581
  * Natively filtered lazy iteration — delegates to the inner {@link MemoryDriver}.
669
582
  *
670
583
  * @remarks
671
- * Semantics are the memory driver's own: `criteria.conditions` filters, `offset`
672
- * / `limit` page lazily, and `criteria.order` is ignored (streaming yields key
584
+ * Semantics are the memory driver's own: `input.conditions` filters, `offset`
585
+ * / `limit` page lazily, and `input.order` is ignored (streaming yields key
673
586
  * order; sorted output is `records()`'s job).
674
587
  *
675
588
  * @param table - The table to stream
676
- * @param criteria - The filter / offset / limit to apply lazily
589
+ * @param input - The filter / offset / limit to apply lazily
677
590
  */
678
- stream(table: string, criteria: Criteria_2): AsyncIterable<Row_2>;
591
+ stream(table: string, input: QueryInput_2): AsyncIterable<Row_2>;
679
592
  clear(table: string): Promise<void>;
680
593
  /**
681
- * Begin a native transaction flush-coalescing over the inner {@link MemoryDriver}.
594
+ * Run an isolated native transaction callback over a candidate memory store.
682
595
  *
683
596
  * @remarks
684
- * Single-writer: throws `DatabaseError` `CONFLICT` if a transaction is already
685
- * active this driver does not support nesting. On begin, captures the inner
686
- * memory rollback thunk via `#memory.snapshot()` and suppresses per-mutation
687
- * `#flush` `write` / `delete` / `clear` still mutate memory but no longer
688
- * touch the file, so N mutations under the handle cost ONE file write instead
689
- * of N. `commit()` releases the suppression and performs that one atomic
690
- * `#flush()`, persisting the transaction's net state. `rollback()` restores
691
- * memory via the captured snapshot thunk, then `#flush()`s so the file reflects
692
- * the restored state. Outside a transaction, behavior is unchanged — every
693
- * mutation flushes on its own. Calling `commit` / `rollback` a second time (on
694
- * either method, in either order) throws `DatabaseError` `CONFLICT`.
597
+ * Single-writer: nesting and root operations while active throw `CONFLICT`.
598
+ * The callback receives a capability over cloned rows, schema, and metadata.
599
+ * Fulfillment atomically serializes that candidate and publishes it to root
600
+ * memory only after file replacement succeeds. Rejection or persistence
601
+ * failure discards the candidate, and every captured capability call after
602
+ * settlement throws `CONFLICT`.
695
603
  *
696
- * @returns A {@link TransactionInterface} handle to `commit` or `rollback`
604
+ * @returns The callback's resolved value
605
+ */
606
+ transaction<R>(scope: (storage: StorageInterface) => Promise<R>): Promise<R>;
607
+ /**
608
+ * Capture an owned row snapshot at an exact writer-queue position.
609
+ *
610
+ * @remarks
611
+ * Capture owns table names, schemas, rows, and one session-local identity per
612
+ * table. Rollback is repeatable: it clones the then-current root into a
613
+ * candidate, adapts captured rows to each surviving same-identity table
614
+ * through the portable migration engine, persists the candidate with current
615
+ * metadata, and publishes memory only after file replacement succeeds.
616
+ * Removed, replaced, uncaptured, and later-added tables remain untouched.
617
+ *
618
+ * @param tables - Existing tables to capture; omitted captures every current table
619
+ * @returns A repeatable rollback operation
697
620
  */
698
- transaction(): Promise<TransactionInterface>;
699
621
  snapshot(tables?: readonly string[]): Promise<() => Promise<void>>;
700
- meta(): Promise<DriverMeta | undefined>;
622
+ metadata(): Promise<DriverMetadata | undefined>;
701
623
  /**
702
- * Persist `meta` verbatim for a later `meta()` to return.
624
+ * Persist an owned metadata snapshot for a later `metadata()` to copy out.
703
625
  *
704
626
  * @remarks
705
- * Respects the same defer-flush suppression as `write` / `delete` / `clear`
706
- * (see {@link JSONDriver.transaction} @remarks) stamping inside an active
707
- * transaction updates memory but does not flush until the transaction settles.
627
+ * Root stamping conflicts while a transaction is active. The scoped
628
+ * {@link StorageInterface.stamp} updates candidate metadata and publishes
629
+ * with the candidate rows on callback fulfillment.
708
630
  *
709
- * @param meta - The {@link DriverMeta} to persist
631
+ * @param metadata - The {@link DriverMetadata} to persist
710
632
  */
711
- stamp(meta: DriverMeta): Promise<void>;
633
+ stamp(metadata: DriverMetadata): Promise<void>;
712
634
  /**
713
- * Apply a {@link Migration} plan by delegating to the inner {@link MemoryDriver},
714
- * then persist the migrated state.
635
+ * Apply one atomic {@link MigrationInput} through an isolated candidate.
715
636
  *
716
637
  * @remarks
717
- * The inner `MemoryDriver.migrate` applies each step (adding/removing tables,
718
- * adding/removing columns from stored rows, no-op index steps) and throws
719
- * `DatabaseError` `MIGRATION` for a step referencing an unknown table — that
720
- * error propagates untouched. `table.add` / `table.remove` steps also update
721
- * this driver's own declared `#schema`, mirroring the bookkeeping `open` does,
722
- * so a subsequent `#flush` / `#load` round-trip includes (or drops) the table.
723
- * A successful migration ends with one atomic `#flush()` so the new state
724
- * survives a close and reopen. A multi-step plan applies its steps
725
- * sequentially and is NOT atomic — a failure partway through a plan leaves
726
- * the earlier steps already applied.
638
+ * The candidate receives every plan step plus optional metadata. Its complete
639
+ * rows, derived schema, and metadata serialize through one atomic file
640
+ * replacement before root memory changes. Any migration or persistence failure
641
+ * therefore leaves root state and the prior file exact.
727
642
  *
728
- * @param plan - The migration plan to apply
643
+ * @param input - The plan and optional metadata to settle together
729
644
  */
730
- migrate(plan: Migration): Promise<void>;
645
+ migrate(input: MigrationInput): Promise<void>;
731
646
  }
732
647
 
733
648
  /**
734
- * Compile a NESTED {@link FieldPath} to the `json_type(<col>, <path>)` SQL
735
- * expression — the {@link fieldColumn} `json_extract` sibling used to tell a
736
- * PRESENT JSON `null` apart from an ABSENT path (both read back as SQL `NULL`
737
- * through `json_extract`, but `json_type` reports `'null'` for the former and
738
- * SQL `NULL` for the latter).
649
+ * Whether a caught filesystem error reports that nothing is there to read.
739
650
  *
740
- * @param path - The nested field path (a column plus its JSON keys)
741
- * @returns The SQL expression reading the value's JSON type
651
+ * @remarks
652
+ * Two codes carry that meaning: `ENOENT` is a plain absence, and `ENOTDIR` is a
653
+ * path whose parent is not a directory — an absence in the stronger sense, since
654
+ * no file can exist at that name and no later write could find one. Hosts
655
+ * disagree about which of the two they report for the second shape, so a driver
656
+ * that reads only `ENOENT` opens on one host and fails closed on another over
657
+ * the same tree.
658
+ *
659
+ * Every other code — a permission refusal, a symlink loop, an unreadable
660
+ * existing file — is a real failure and stays one.
661
+ *
662
+ * A caught value that is not a coded `Error` is not a report about a path, so it
663
+ * answers `false` rather than being read for a `code` any object could carry.
664
+ *
665
+ * @param error - The caught value to classify; any runtime is accepted
666
+ * @returns `true` when the error reports that the path holds nothing
742
667
  *
743
668
  * @example
744
669
  * ```ts
745
- * jsonTypeColumn(['payload', 'user', 'id']) // "json_type(\"payload\", '$.user.id')"
670
+ * matchesAbsentPath(Object.assign(new Error('gone'), { code: 'ENOENT' })) // true
671
+ * matchesAbsentPath(Object.assign(new Error('denied'), { code: 'EACCES' })) // false
672
+ * matchesAbsentPath('ENOENT') // false
746
673
  * ```
747
674
  */
748
- export declare function jsonTypeColumn(path: readonly string[]): string;
675
+ export declare function matchesAbsentPath(error: unknown): boolean;
676
+
677
+ /**
678
+ * Determine whether SQLite can execute an aggregate exactly like the core engine.
679
+ *
680
+ * @param operation - Aggregate operation
681
+ * @param column - Aggregate field
682
+ * @param schema - Current table schema
683
+ * @returns Whether native aggregation is exact
684
+ */
685
+ export declare function matchesAggregateExactly(operation: AggregateOperation, column: FieldPath, schema: TableSchema): boolean;
686
+
687
+ /**
688
+ * Whether one {@link Condition} compiles to SQL that is PROVABLY identical to
689
+ * the core engine's `matchesCondition` for every value its column's declared
690
+ * type can store.
691
+ *
692
+ * @remarks
693
+ * `false` for a nested `FieldPath` (an array) or a column absent from `schema`.
694
+ * `absent` / `present` are exact unless a column is both optional and nullable.
695
+ * In that combined case the storage sentinel for explicit `null` is not SQL
696
+ * `NULL`, while the core treats both absence and explicit `null` as absent.
697
+ * Every scalar operator refines when the column is optional
698
+ * OR nullable, because SQL null semantics and the core total order differ.
699
+ * Required non-null `equals` / `not` require an operand matching the declared
700
+ * storage and exclude `json` / `blob`. `above` / `below` / `from` / `to` /
701
+ * `between` are exact only for {@link EXACT_RANGE_COLUMN_STORAGE} (`integer` /
702
+ * `real` / `boolean`) — a `text` column's range conditions REFINE, because
703
+ * SQLite's default BINARY collation orders TEXT by Unicode CODE POINT while
704
+ * the core engine's `compareValues` orders JS strings by UTF-16 CODE UNIT,
705
+ * and the two diverge for supplementary-plane characters (see
706
+ * {@link EXACT_COLUMN_STORAGE}'s remarks for the full rationale).
707
+ * `any` / `none` require a NON-EMPTY list where every element matches (an empty
708
+ * list is exact under neither: the engine's `any([])` matches nothing while
709
+ * `none([])` matches everything, and SQL `IN ()` is a syntax error) — these
710
+ * stay exact on `text` (byte equality is collation-independent and engine-
711
+ * identical). `starts` / `ends` are exact only on a `text` column with a
712
+ * string operand (case-sensitive `substr` compile, see {@link compileConditionSQL}) —
713
+ * likewise collation-independent. `like` / `glob` are NEVER exact — SQLite
714
+ * `LIKE` folds case ASCII-only against the engine's Unicode fold, and `GLOB`
715
+ * has character classes the engine treats literally.
716
+ *
717
+ * @param condition - The condition to test
718
+ * @param schema - The table's schema
719
+ * @returns Whether `condition` is exact
720
+ */
721
+ export declare function matchesConditionExactly(condition: Condition, schema: TableSchema): boolean;
749
722
 
750
723
  /**
751
724
  * Whether a value's runtime type matches a column's declared exact type —
@@ -753,7 +726,7 @@ export declare function jsonTypeColumn(path: readonly string[]): string;
753
726
  *
754
727
  * @remarks
755
728
  * `text` ↔ string, `integer` / `real` ↔ FINITE number (`NaN` / `±Infinity`
756
- * fail), `boolean` ↔ boolean. Backs {@link isExactCondition}'s operand checks.
729
+ * fail), `boolean` ↔ boolean. Backs {@link matchesConditionExactly}'s operand checks.
757
730
  *
758
731
  * @param value - The condition operand to test
759
732
  * @param type - The column's declared portable type
@@ -765,18 +738,57 @@ export declare function jsonTypeColumn(path: readonly string[]): string;
765
738
  * matchesDeclaredType(Number.NaN, 'integer') // false — only finite numbers
766
739
  * ```
767
740
  */
768
- export declare function matchesDeclaredType(value: unknown, type: ColumnType): boolean;
741
+ export declare function matchesDeclaredStorage(value: unknown, storage: ColumnStorage): boolean;
742
+
743
+ /**
744
+ * Whether one {@link Order} term's column compiles to an `ORDER BY` that
745
+ * matches the engine's {@link import('@src/core').sortRows} exactly.
746
+ *
747
+ * @remarks
748
+ * `false` for a nested `FieldPath`, a column absent from `schema`, or a
749
+ * declared type outside {@link EXACT_RANGE_COLUMN_STORAGE} (`integer` / `real` /
750
+ * `boolean`). `text` is NOT exact here: SQLite's default BINARY collation
751
+ * orders TEXT by Unicode code point while the core engine's `compareValues`
752
+ * orders JS strings by UTF-16 code unit, and the two diverge for
753
+ * supplementary-plane characters (see {@link EXACT_COLUMN_STORAGE}'s remarks) —
754
+ * a `text` order term REFINES through the core engine instead.
755
+ *
756
+ * @param order - The order term to test
757
+ * @param schema - The table's schema
758
+ * @returns Whether `order` is exact
759
+ */
760
+ export declare function matchesOrderExactly(order: Order, schema: TableSchema): boolean;
761
+
762
+ /**
763
+ * Whether a whole {@link QueryInput} is exact — every condition and every order
764
+ * term is exact. `limit` / `offset` never affect exactness (SQL `LIMIT` /
765
+ * `OFFSET` are always engine-identical).
766
+ *
767
+ * @param input - The query input to test
768
+ * @param schema - The table's schema
769
+ * @returns Whether every part of `input` is exact
770
+ */
771
+ export declare function matchesQueryExactly(input: QueryInput, schema: TableSchema): boolean;
772
+
773
+ /**
774
+ * Test a declared SQLite type against a portable storage affinity.
775
+ *
776
+ * @param declared - Native declared type
777
+ * @param storage - Portable column storage
778
+ * @returns Whether SQLite's official affinity rules yield the expected affinity
779
+ */
780
+ export declare function matchesSQLiteAffinity(declared: unknown, storage: ColumnStorage): boolean;
769
781
 
770
782
  /**
771
783
  * The reserved metadata table the {@link SQLiteDriver} creates on `open` to
772
- * persist its stamped `DriverMeta` (`version` + declared schema JSON) — the
773
- * SQLite realization of the `meta` / `stamp` driver hooks.
784
+ * persist its stamped `DriverMetadata` (`version` + declared schema JSON) — the
785
+ * SQLite realization of the `metadata` / `stamp` driver hooks.
774
786
  *
775
787
  * @remarks
776
- * A single-row table (`id = 1`). A user table named `_meta` collides with the
788
+ * A single-row table (`id = 1`). A user table named `_metadata` collides with the
777
789
  * reservation — the caller's concern to avoid, documented on the driver class.
778
790
  */
779
- export declare const META_TABLE = "_meta";
791
+ export declare const METADATA_TABLE = "_metadata";
780
792
 
781
793
  /**
782
794
  * Quote a SQL identifier (a table or column name) so any characters are literal.
@@ -791,48 +803,24 @@ export declare const META_TABLE = "_meta";
791
803
  *
792
804
  * @example
793
805
  * ```ts
794
- * quote('order') // '"order"'
806
+ * quoteIdentifier('order') // '"order"'
795
807
  * ```
796
808
  */
797
- export declare function quote(identifier: string): string;
809
+ export declare function quoteIdentifier(identifier: string): string;
798
810
 
799
811
  /**
800
- * Project a {@link TableSchema} to the `CREATE INDEX IF NOT EXISTS` statements a
801
- * SQLite driver's `open` issues for its declared indexes.
812
+ * Project a {@link TableSchema} to its declared SQLite indexes.
802
813
  *
803
- * @remarks
804
- * One statement per index group; the index name is built by {@link indexName}
805
- * (collision-free and deterministic), matching the driver's naming so a
806
- * repeated `open` is idempotent.
807
- *
808
- * @param schema - The table's schema
809
- * @returns One `CREATE INDEX IF NOT EXISTS …` statement per declared index
810
- *
811
- * @example
812
- * ```ts
813
- * schemaToIndexes(schema)
814
- * // ['CREATE INDEX IF NOT EXISTS "idx_5_users_4_name" ON "users" ("name")']
815
- * ```
814
+ * @param schema - The table schema
815
+ * @returns One statement per declared index
816
816
  */
817
817
  export declare function schemaToIndexes(schema: TableSchema): readonly string[];
818
818
 
819
819
  /**
820
- * Project a {@link TableSchema} to the `CREATE TABLE IF NOT EXISTS` statement a
821
- * SQLite driver's `open` issues for it.
822
- *
823
- * @remarks
824
- * Each column compiles to `<quoted name> <columnSQL(type)>`; the statement ends
825
- * with `PRIMARY KEY (<quoted primary>)`. No `NOT NULL` is emitted — the contract
826
- * validates required-ness, the database is just storage (AGENTS §14).
820
+ * Project a {@link TableSchema} to its `CREATE TABLE IF NOT EXISTS` statement.
827
821
  *
828
- * @param schema - The table's schema
829
- * @returns The `CREATE TABLE IF NOT EXISTS …` statement
830
- *
831
- * @example
832
- * ```ts
833
- * schemaToTable(schema)
834
- * // 'CREATE TABLE IF NOT EXISTS "users" ("id" TEXT, "age" INTEGER, PRIMARY KEY ("id"))'
835
- * ```
822
+ * @param schema - The table schema
823
+ * @returns The complete table declaration
836
824
  */
837
825
  export declare function schemaToTable(schema: TableSchema): string;
838
826
 
@@ -846,21 +834,23 @@ export declare function schemaToTable(schema: TableSchema): string;
846
834
  * raw `node:sqlite`. `open` issues `CREATE TABLE IF NOT EXISTS` with real typed
847
835
  * columns (mapped from each {@link TableSchema}'s portable column types) and a
848
836
  * `PRIMARY KEY`, plus a `CREATE INDEX IF NOT EXISTS` per declared index (both
849
- * reopen-safe), and readies a reserved `_meta` single-row table `meta()` /
850
- * `stamp()` read and write — **a user table named `_meta` collides with it**;
837
+ * reopen-safe), and readies a reserved `_metadata` single-row table `metadata()` /
838
+ * `stamp()` read and write — **a user table named `_metadata` collides with it**;
851
839
  * avoid the name. Rows cross the boundary through the codecs in `helpers.ts`
852
840
  * (`json` columns store / parse JSON text, a `boolean` stores `1` / `0`), so the
853
841
  * typed layer above imposes the exact shape (AGENTS §14). `write` is an
854
- * `INSERT OR REPLACE` upsert the `Table` layer detects a `CONFLICT` via a
855
- * prior `has`, so this never translates a constraint error; a backend
856
- * `SQLiteError` otherwise propagates unchanged. Querying, ordering, paging, and
857
- * aggregation are native: `records` / `count` / `stream` compile a `Criteria`
858
- * to SQL with `compileCriteria`, and `aggregate` runs a SQL
859
- * `COUNT`/`SUM`/`AVG`/`MIN`/`MAX` (via `aggregateSQL`) over the same compiled
860
- * WHERE. `transaction` wraps native `BEGIN` / `COMMIT` / `ROLLBACK` with
861
- * double-settle guards. `migrate` runs the plan's projected DDL
862
- * ({@link import('../helpers.js').stepToSQL}) inside whichever native
863
- * transaction is active: joined into an already-open `transaction()` handle
842
+ * `INSERT OR REPLACE` upsert, while `insert` uses a plain `INSERT` and maps its
843
+ * atomic primary-key constraint failure to `CONFLICT`; every other backend
844
+ * `SQLiteError` is contained by the same `DatabaseError` boundary described
845
+ * below. Querying, ordering, paging, and
846
+ * aggregation is native: `records` / `stream` compile a `QueryInput`
847
+ * to SQL with `compileQuerySQL`, and `aggregate` runs a SQL
848
+ * `COUNT`/`SUM`/`AVG`/`MIN`/`MAX` (via `compileAggregateSQL`) over the same compiled
849
+ * WHERE. `transaction` runs a callback inside native `BEGIN` / `COMMIT` /
850
+ * `ROLLBACK`, passing a scoped storage capability that becomes invalid after
851
+ * settlement. `migrate` runs the plan's projected DDL
852
+ * ({@link import('../compilers.js').stepToSQL}) inside whichever native
853
+ * transaction is active: joined into the active transaction callback
864
854
  * when one exists (the core's versioned reconcile path wraps migrate + stamp
865
855
  * in one native `BEGIN`, and node:sqlite rejects a nested `BEGIN`), or inside
866
856
  * its own `database.transaction` otherwise — a mid-plan failure rolls back
@@ -885,67 +875,72 @@ export declare function schemaToTable(schema: TableSchema): string;
885
875
  */
886
876
  export declare class SQLiteDriver implements DriverInterface_2 {
887
877
  #private;
888
- constructor(path: string, options?: SQLiteDriverOptions);
878
+ constructor(options?: SQLiteDriverOptions);
889
879
  open(schema: readonly TableSchema_2[]): Promise<void>;
890
880
  close(): Promise<void>;
891
881
  read(table: string, key: Key): Promise<Row_2 | undefined>;
892
- write(table: string, key: Key, row: Row_2): Promise<void>;
893
- delete(table: string, key: Key): Promise<boolean>;
882
+ write(table: string, key: Key, row: Row_2, options?: OperationOptions): Promise<void>;
883
+ insert(table: string, key: Key, row: Row_2, options?: OperationOptions): Promise<void>;
884
+ delete(table: string, key: Key, options?: OperationOptions): Promise<boolean>;
894
885
  keys(table: string): Promise<readonly Key[]>;
895
886
  scan(table: string): AsyncIterable<Row_2>;
896
887
  clear(table: string): Promise<void>;
897
- records(table: string, criteria: Criteria_2): Promise<readonly Row_2[]>;
898
- count(table: string, criteria: Criteria_2): Promise<number>;
899
- aggregate(table: string, operation: AggregateFunction_2, column: FieldPath, criteria: Criteria_2): Promise<number | undefined>;
900
- stream(table: string, criteria: Criteria_2): AsyncIterable<Row_2>;
888
+ records(table: string, input: QueryInput_2): Promise<readonly Row_2[]>;
889
+ aggregate(table: string, operation: AggregateOperation_2, column: FieldPath, input: QueryInput_2): Promise<number | undefined>;
890
+ stream(table: string, input: QueryInput_2): AsyncIterable<Row_2>;
901
891
  /**
902
892
  * Begin a native transaction — real `BEGIN`, `COMMIT`, `ROLLBACK`.
903
893
  *
904
894
  * @remarks
905
- * Calling `commit` or `rollback` a second time (on either method, in either
906
- * order) throws `DatabaseError` `CONFLICT`.
895
+ * The callback receives a scoped {@link StorageInterface}. Fulfillment
896
+ * commits and returns its value; rejection rolls back and preserves the
897
+ * original error. Root operations and nesting conflict while active, and a
898
+ * captured capability conflicts after settlement.
907
899
  *
908
- * @returns A {@link TransactionInterface} handle to `commit` or `rollback`
900
+ * @returns The callback's resolved value
909
901
  */
910
- transaction(): Promise<TransactionInterface>;
902
+ transaction<R>(scope: (storage: StorageInterface) => Promise<R>): Promise<R>;
911
903
  /**
912
904
  * Apply a {@link Migration} plan by executing each step's projected DDL
913
- * ({@link import('../helpers.js').stepToSQL}).
905
+ * ({@link import('../compilers.js').stepToSQL}).
914
906
  *
915
907
  * @remarks
916
908
  * Atomicity is provided by whichever native transaction is active: when
917
- * this driver's own `transaction()` hook already has a handle open (the
909
+ * this driver's own `transaction()` callback is active (the
918
910
  * core's versioned reconcile / migrate path joins migrate + stamp under
919
911
  * one native `BEGIN`), the plan's DDL runs directly inside that enclosing
920
- * transaction — a mid-plan failure propagates out and the CALLER's
921
- * `commit`/`rollback` provides atomicity. node:sqlite (and SQLite
912
+ * transaction — a mid-plan failure rejects the callback and the driver
913
+ * rolls it back. node:sqlite (and SQLite
922
914
  * generally) rejects a nested `BEGIN`, so this driver must never open a
923
915
  * second native transaction while one is already open. Otherwise (no
924
916
  * enclosing transaction), `migrate` wraps the plan in its own native
925
917
  * `database.transaction` — atomic on its own: a mid-plan failure rolls
926
- * back every DDL statement already applied by the plan. A step
927
- * referencing a table not in this driver's declared schema (and that is
928
- * not itself a `table.add`) throws `DatabaseError` `MIGRATION` before any
929
- * DDL for that step runs, propagating out of whichever transaction is
930
- * active (which rolls back on a throw).
918
+ * back every DDL statement already applied by the plan. A scoped migration
919
+ * uses one fixed internal savepoint literal because the published SQLite
920
+ * wrapper intentionally exposes raw `exec` but no savepoint manager. That
921
+ * savepoint contains a caught inner migration so the outer callback
922
+ * transaction remains active and may continue safely. A step referencing a
923
+ * table not in this driver's declared schema (and that is not itself a
924
+ * `table.add`) throws `DatabaseError` `MIGRATION` before any DDL for that
925
+ * step runs.
931
926
  *
932
- * @param plan - The migration plan to apply
927
+ * @param input - The migration plan and optional metadata stamp to apply atomically
933
928
  */
934
- migrate(plan: Migration): Promise<void>;
929
+ migrate(input: MigrationInput): Promise<void>;
935
930
  /**
936
- * Read the persisted {@link DriverMeta} from the reserved `_meta` table.
931
+ * Read the persisted {@link DriverMetadata} from the reserved `_metadata` table.
937
932
  *
938
- * @returns The last-stamped `DriverMeta`, or `undefined` when never stamped
933
+ * @returns The last-stamped `DriverMetadata`, or `undefined` when never stamped
939
934
  * (or the stored row is malformed)
940
935
  */
941
- meta(): Promise<DriverMeta | undefined>;
936
+ metadata(): Promise<DriverMetadata | undefined>;
942
937
  /**
943
- * Persist `meta` verbatim (as JSON) into the reserved `_meta` table's single
944
- * row.
938
+ * Persist an owned metadata snapshot into the reserved `_metadata` table's
939
+ * single row.
945
940
  *
946
- * @param meta - The {@link DriverMeta} to persist
941
+ * @param metadata - The {@link DriverMetadata} to persist
947
942
  */
948
- stamp(meta: DriverMeta): Promise<void>;
943
+ stamp(metadata: DriverMetadata): Promise<void>;
949
944
  snapshot(tables?: readonly string[]): Promise<() => Promise<void>>;
950
945
  }
951
946
 
@@ -957,8 +952,9 @@ export declare class SQLiteDriver implements DriverInterface_2 {
957
952
  * `path` is the database file path (`':memory:'` when omitted); `readonly`
958
953
  * opens the connection read-only (a write then fails as a typed `DRIVER`
959
954
  * {@link DatabaseError}); `timeout` is the busy-timeout in milliseconds before
960
- * a locked database fails `BUSY`; `foreignKeys` enables foreign-key
961
- * constraint enforcement. `pragmas` is an ordered record of PRAGMA name to
955
+ * a locked database fails `BUSY`; `references` enables or disables foreign-key
956
+ * constraint enforcement, while omission retains the upstream default.
957
+ * `pragmas` is an ordered record of PRAGMA name to
962
958
  * value, applied via the wrapper's `pragma()` right after `connect()`, in
963
959
  * insertion order (e.g. `{ journal_mode: 'WAL' }`). Core rows are
964
960
  * number-typed — this driver never surfaces a `bigint`, so a stored integer
@@ -969,107 +965,16 @@ export declare interface SQLiteDriverOptions {
969
965
  readonly path?: string;
970
966
  readonly readonly?: boolean;
971
967
  readonly timeout?: number;
972
- readonly foreignKeys?: boolean;
968
+ readonly references?: boolean;
973
969
  readonly pragmas?: Readonly<Record<string, string | number>>;
974
970
  }
975
971
 
976
972
  /**
977
- * One row as a SQLite binding returns it — a plain object keyed by column name.
978
- *
979
- * @remarks
980
- * Every column value is a {@link SQLiteValue}. The SQLite driver decodes each
981
- * raw row into this shape before handing it to the core query engine; nothing
982
- * above the driver ever sees SQLite's native row representation directly.
983
- */
984
- export declare type SQLiteRow = Record<string, SQLiteValue>;
985
-
986
- /**
987
- * The value domain a SQLite binding accepts as a bound parameter and returns
988
- * from a row.
989
- *
990
- * @remarks
991
- * Mirrors SQLite's storage classes (`NULL`, `INTEGER`, `REAL`, `TEXT`, `BLOB`)
992
- * at the TypeScript boundary: `null`, `number` / `bigint` for integer and
993
- * floating-point values, `string` for text, and `Uint8Array` for blobs. This
994
- * type is pure — it names the shape a value must have to cross the binding,
995
- * independent of any concrete sqlite package (`node:sqlite`, `better-sqlite3`,
996
- * etc.), so a driver can encode/decode against it without importing one.
997
- */
998
- export declare type SQLiteValue = null | number | bigint | string | Uint8Array;
999
-
1000
- /**
1001
- * Project one {@link MigrationStep} onto its table's declared {@link TableSchema}
1002
- * — the bookkeeping counterpart to {@link stepToSQL} (which projects the DDL a
1003
- * driver's `migrate` runs against the live database).
1004
- *
1005
- * @remarks
1006
- * `column.add` / `column.remove` add / filter the named column;
1007
- * `index.add` / `index.remove` add / filter the matching index group (an exact
1008
- * ordered match on `index`). `table.add` / `table.remove` act on a WHOLE
1009
- * schema map rather than one table's shape, so they are the caller's concern
1010
- * (a driver's `migrate` applies them directly against its table map) — passed
1011
- * here, they return `schema` unchanged.
1012
- *
1013
- * @param schema - The table's current declared schema
1014
- * @param step - The migration step to project onto it
1015
- * @returns The table's schema after the step
1016
- *
1017
- * @example
1018
- * ```ts
1019
- * stepToSchema(schema, { operation: 'column.remove', table: 'users', column: 'legacy' })
1020
- * // schema with the 'legacy' column dropped from `columns`
1021
- * ```
1022
- */
1023
- export declare function stepToSchema(schema: TableSchema, step: MigrationStep): TableSchema;
1024
-
1025
- /**
1026
- * Project one {@link MigrationStep} to the DDL statement(s) a SQLite driver's
1027
- * `migrate` executes for it.
1028
- *
1029
- * @remarks
1030
- * `table.add` emits the `CREATE TABLE` plus one `CREATE INDEX` per declared
1031
- * index (via {@link schemaToTable} / {@link schemaToIndexes}); `table.remove`
1032
- * emits `DROP TABLE IF EXISTS`; `column.add` / `column.remove` emit `ALTER
1033
- * TABLE … ADD COLUMN` / `… DROP COLUMN`; `index.add` / `index.remove` emit
1034
- * `CREATE INDEX IF NOT EXISTS` / `DROP INDEX IF EXISTS`, naming the index the
1035
- * same way `schemaToIndexes` does (`idx_<table>_<columns joined by _>`) so a
1036
- * plan-built index matches one `open` would have created. Whether the named
1037
- * table actually exists is the caller's concern (a driver's `migrate` checks
1038
- * its own declared schema before running these statements) — this projection
1039
- * is pure and never inspects live state.
1040
- *
1041
- * @param step - The migration step to project
1042
- * @returns The DDL statement(s) that apply the step
973
+ * Project one {@link MigrationStep} to SQLite DDL.
1043
974
  *
1044
- * @example
1045
- * ```ts
1046
- * stepToSQL({ operation: 'column.remove', table: 'users', column: 'legacy' })
1047
- * // ['ALTER TABLE "users" DROP COLUMN "legacy"']
1048
- * ```
975
+ * @param step - The migration step
976
+ * @returns The statements that apply the step
1049
977
  */
1050
978
  export declare function stepToSQL(step: MigrationStep): readonly string[];
1051
979
 
1052
- /**
1053
- * The storage type a nested (`json_extract`) operand encodes as, derived from its
1054
- * RUNTIME value — NOT `json`.
1055
- *
1056
- * @remarks
1057
- * `json_extract` returns the unquoted, natively-typed scalar (a JSON boolean as
1058
- * `1` / `0`, a number as-is, a string as-is), so the operand must encode to that
1059
- * same scalar to compare. A boolean → `'boolean'` (→ `1` / `0`); a number →
1060
- * `'integer'` / `'real'`; a bigint → `'integer'`; a string → `'text'`; `null` /
1061
- * `undefined` → `'text'` (encodes to `null`); an object / array → `'json'` (the
1062
- * edge of comparing against a json subtree).
1063
- *
1064
- * @param value - The runtime operand value
1065
- * @returns The {@link ColumnType} to encode it as
1066
- *
1067
- * @example
1068
- * ```ts
1069
- * valueType(true) // 'boolean'
1070
- * valueType(9) // 'integer'
1071
- * ```
1072
- */
1073
- export declare function valueType(value: unknown): ColumnType;
1074
-
1075
980
  export { }