@orkestrel/database 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -10
- package/dist/src/browser/index.d.ts +342 -0
- package/dist/src/browser/index.js +686 -0
- package/dist/src/browser/index.js.map +1 -0
- package/dist/src/core/index.cjs +2494 -0
- package/dist/src/core/index.cjs.map +1 -0
- package/dist/src/core/index.d.cts +1766 -0
- package/dist/src/core/index.d.ts +1766 -11
- package/dist/src/core/index.js +237 -1142
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +836 -153
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +1054 -0
- package/dist/src/server/index.d.ts +1054 -5
- package/dist/src/server/index.js +1663 -0
- package/dist/src/server/index.js.map +1 -0
- package/package.json +40 -16
- package/dist/src/core/Clause.d.ts +0 -30
- package/dist/src/core/Cursor.d.ts +0 -21
- package/dist/src/core/Database.d.ts +0 -80
- package/dist/src/core/Query.d.ts +0 -47
- package/dist/src/core/Table.d.ts +0 -69
- package/dist/src/core/constants.d.ts +0 -22
- package/dist/src/core/drivers/MemoryDriver.d.ts +0 -94
- package/dist/src/core/errors.d.ts +0 -38
- package/dist/src/core/factories.d.ts +0 -43
- package/dist/src/core/helpers.d.ts +0 -383
- package/dist/src/core/types.d.ts +0 -739
- package/dist/src/server/compilers.d.ts +0 -169
- package/dist/src/server/drivers/JSONDriver.d.ts +0 -106
- package/dist/src/server/factories.d.ts +0 -31
- package/dist/src/server/helpers.d.ts +0 -222
- package/dist/src/server/types.d.ts +0 -36
|
@@ -1,5 +1,1054 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { AggregateFunction } from '../core/index.js';
|
|
2
|
+
import { AggregateFunction as AggregateFunction_2 } from '../core/index.js';
|
|
3
|
+
import { ColumnType } from '../core/index.js';
|
|
4
|
+
import { Condition } from '../core/index.js';
|
|
5
|
+
import { Criteria } from '../core/index.js';
|
|
6
|
+
import { Criteria as Criteria_2 } from '../core/index.js';
|
|
7
|
+
import { DriverInterface } from '../core/index.js';
|
|
8
|
+
import { DriverInterface as DriverInterface_2 } from '../core/index.js';
|
|
9
|
+
import { DriverMeta } from '../core/index.js';
|
|
10
|
+
import { FieldPath } from '@orkestrel/contract';
|
|
11
|
+
import { Key } from '../core/index.js';
|
|
12
|
+
import { Migration } from '../core/index.js';
|
|
13
|
+
import { MigrationStep } from '../core/index.js';
|
|
14
|
+
import { Order } from '../core/index.js';
|
|
15
|
+
import { Row } from '../core/index.js';
|
|
16
|
+
import { Row as Row_2 } from '../core/index.js';
|
|
17
|
+
import { TableSchema } from '../core/index.js';
|
|
18
|
+
import { TableSchema as TableSchema_2 } from '../core/index.js';
|
|
19
|
+
import { TransactionInterface } from '../core/index.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
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.
|
|
33
|
+
*
|
|
34
|
+
* @param operation - The aggregate to compute
|
|
35
|
+
* @param column - The column (or nested path) to aggregate
|
|
36
|
+
* @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
|
+
*/
|
|
45
|
+
export declare function aggregateSQL(operation: AggregateFunction, column: FieldPath): string;
|
|
46
|
+
|
|
47
|
+
/**
|
|
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).
|
|
56
|
+
*
|
|
57
|
+
* @param type - The portable column type
|
|
58
|
+
* @returns The SQLite column type keyword
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```ts
|
|
62
|
+
* columnSQL('integer') // 'INTEGER'
|
|
63
|
+
* columnSQL('json') // 'TEXT'
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export declare function columnSQL(type: ColumnType): string;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Compile a {@link Criteria} into the SQL clause that follows a table name, with
|
|
70
|
+
* its bound parameters in clause order.
|
|
71
|
+
*
|
|
72
|
+
* @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.
|
|
86
|
+
*
|
|
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
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```ts
|
|
93
|
+
* compileCriteria({ conditions: [{ column: 'age', operator: 'from', values: [18], connector: 'and' }] }, schema)
|
|
94
|
+
* // { sql: 'WHERE "age" >= ? ORDER BY "id"', params: [18] }
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
export declare function compileCriteria(criteria: Criteria | undefined, schema: TableSchema): CompiledSQL;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* A parameterized SQL fragment or statement plus its bind values.
|
|
101
|
+
*
|
|
102
|
+
* @remarks
|
|
103
|
+
* 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
|
|
107
|
+
* the two from drifting apart across compile and execute.
|
|
108
|
+
*/
|
|
109
|
+
export declare interface CompiledSQL {
|
|
110
|
+
readonly sql: string;
|
|
111
|
+
readonly params: readonly SQLiteValue[];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Compile the ORDER BY clause from the order terms, always ending with the
|
|
116
|
+
* primary key as the final determinant.
|
|
117
|
+
*
|
|
118
|
+
* @remarks
|
|
119
|
+
* The native `records` read then resolves ties in key order, matching a
|
|
120
|
+
* primary-key-ordered `scan` and the core engine's stable `sortRows` over a
|
|
121
|
+
* key-ordered scan (and IndexedDB's key-ordered reads), so a native read equals
|
|
122
|
+
* the scan path (AGENTS §21 / §22 native ↔ engine parity). SQLite without an
|
|
123
|
+
* `ORDER BY` returns rowid (insertion) order, and an explicit order alone breaks
|
|
124
|
+
* ties by rowid too — both diverge from every key-ordered backend. The
|
|
125
|
+
* tie-breaker is ASCENDING regardless of the explicit directions: the engine's
|
|
126
|
+
* stable sort runs over key-ascending input, so equal rows stay in
|
|
127
|
+
* ascending-key order whichever way the explicit terms point. Skipped when the
|
|
128
|
+
* primary is already an explicit order term (no double-append).
|
|
129
|
+
*
|
|
130
|
+
* @param order - The explicit order terms, or `undefined`
|
|
131
|
+
* @param schema - The table's schema (for the primary key)
|
|
132
|
+
* @returns The `ORDER BY …` clause, or an empty string when there is nothing to order by
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```ts
|
|
136
|
+
* compileOrder([{ column: 'age', direction: 'descending' }], schema)
|
|
137
|
+
* // 'ORDER BY "age" DESC, "id"'
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
export declare function compileOrder(order: readonly Order[] | undefined, schema: TableSchema): string;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Compile the LIMIT / OFFSET clause.
|
|
144
|
+
*
|
|
145
|
+
* @remarks
|
|
146
|
+
* An offset without a limit uses `LIMIT -1` (SQLite's "no limit") so OFFSET is
|
|
147
|
+
* still honored.
|
|
148
|
+
*
|
|
149
|
+
* @param limit - The maximum row count, or `undefined`
|
|
150
|
+
* @param offset - The row count to skip, or `undefined`
|
|
151
|
+
* @returns The `LIMIT …` clause and its bound parameters, or an empty clause when neither is set
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* ```ts
|
|
155
|
+
* compilePage(undefined, 5) // { sql: 'LIMIT -1 OFFSET ?', params: [5] }
|
|
156
|
+
* ```
|
|
157
|
+
*/
|
|
158
|
+
export declare function compilePage(limit: number | undefined, offset: number | undefined): CompiledSQL;
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Fold the conditions into one WHERE clause, parenthesizing progressively
|
|
162
|
+
* left-to-right so the grouping matches the engine's `matchesCriteria` fold.
|
|
163
|
+
*
|
|
164
|
+
* @remarks
|
|
165
|
+
* The first condition's connector is ignored, per the {@link Condition} types.
|
|
166
|
+
* Every fragment (see {@link fragment}'s truth table) replicates the core
|
|
167
|
+
* 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
|
|
169
|
+
* `records` / `count` read never disagrees with a scan-and-filter fallback.
|
|
170
|
+
*
|
|
171
|
+
* @param conditions - The conditions to fold
|
|
172
|
+
* @param schema - The table's schema
|
|
173
|
+
* @returns The `WHERE …` clause and its bound parameters, or an empty clause for zero conditions
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* ```ts
|
|
177
|
+
* compileWhere([{ column: 'age', operator: 'from', values: [18], connector: 'and' }], schema)
|
|
178
|
+
* // { sql: 'WHERE "age" >= ?', params: [18] }
|
|
179
|
+
* ```
|
|
180
|
+
*/
|
|
181
|
+
export declare function compileWhere(conditions: readonly Condition[], schema: TableSchema): CompiledSQL;
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Create a persistent JSON-file {@link DriverInterface} for the core database layer.
|
|
185
|
+
*
|
|
186
|
+
* @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.
|
|
190
|
+
* The driver is the reference `MemoryDriver` plus JSON-file persistence: `open` loads
|
|
191
|
+
* the file, every mutation flushes the whole store back, and querying runs through
|
|
192
|
+
* the core engine over `scan` (it is scan-only — no native `records` / `count` /
|
|
193
|
+
* `aggregate`). A missing, corrupt, or wrong-shaped file starts empty rather than
|
|
194
|
+
* throwing.
|
|
195
|
+
*
|
|
196
|
+
* @param path - The JSON file path data is loaded from and flushed to
|
|
197
|
+
* @returns A {@link DriverInterface} backed by a JSON file
|
|
198
|
+
*
|
|
199
|
+
* @example
|
|
200
|
+
* ```ts
|
|
201
|
+
* import { createDatabase } from '@orkestrel/database'
|
|
202
|
+
* import { stringShape } from '@orkestrel/contract'
|
|
203
|
+
* import { createJSONDriver } from '@orkestrel/database/server'
|
|
204
|
+
*
|
|
205
|
+
* const db = createDatabase({
|
|
206
|
+
* driver: createJSONDriver('data/app.json'),
|
|
207
|
+
* tables: { users: { id: stringShape(), name: stringShape() } },
|
|
208
|
+
* })
|
|
209
|
+
* await db.table('users').set({ id: 'u1', name: 'Ada' }) // persisted to app.json
|
|
210
|
+
* ```
|
|
211
|
+
*/
|
|
212
|
+
export declare function createJSONDriver(path: string): DriverInterface;
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Create a trusted-mode SQLite {@link DriverInterface} for the core database layer.
|
|
216
|
+
*
|
|
217
|
+
* @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
|
|
221
|
+
* on the published `@orkestrel/sqlite` synchronous wrapper: `open` issues real
|
|
222
|
+
* typed `CREATE TABLE` / `CREATE INDEX` statements (reopen-safe) plus a reserved
|
|
223
|
+
* `_meta` table for `meta()` / `stamp()` — avoid naming a table `_meta`.
|
|
224
|
+
* Querying, paging, and aggregation run natively (`records` / `count` /
|
|
225
|
+
* `aggregate` / `stream`); `transaction` and `migrate` use real `BEGIN` /
|
|
226
|
+
* `COMMIT` / `ROLLBACK`, so `migrate` is atomic even mid-plan.
|
|
227
|
+
*
|
|
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`)
|
|
231
|
+
* @returns A {@link DriverInterface} backed by SQLite
|
|
232
|
+
*
|
|
233
|
+
* @example
|
|
234
|
+
* ```ts
|
|
235
|
+
* import { createDatabase } from '@orkestrel/database'
|
|
236
|
+
* import { stringShape } from '@orkestrel/contract'
|
|
237
|
+
* import { createSQLiteDriver } from '@orkestrel/database/server'
|
|
238
|
+
*
|
|
239
|
+
* const db = createDatabase({
|
|
240
|
+
* driver: createSQLiteDriver('data/app.sqlite'),
|
|
241
|
+
* tables: { users: { id: stringShape(), name: stringShape() } },
|
|
242
|
+
* })
|
|
243
|
+
* await db.table('users').set({ id: 'u1', name: 'Ada' }) // persisted to app.sqlite
|
|
244
|
+
*
|
|
245
|
+
* // Or with options:
|
|
246
|
+
* createSQLiteDriver({ path: 'data/app.sqlite', pragmas: { journal_mode: 'WAL' } })
|
|
247
|
+
* ```
|
|
248
|
+
*/
|
|
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;
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Decode a stored {@link SQLiteRow} back to a {@link Row} by its table's schema.
|
|
267
|
+
*
|
|
268
|
+
* @remarks
|
|
269
|
+
* Decodes each declared column with {@link decodeValue} and **omits** any column
|
|
270
|
+
* whose decoded value is `undefined` — so an absent / `NULL` optional column does
|
|
271
|
+
* 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).
|
|
275
|
+
*
|
|
276
|
+
* @param row - The stored SQLite row
|
|
277
|
+
* @param schema - The table's schema
|
|
278
|
+
* @returns The decoded JS row (absent columns omitted)
|
|
279
|
+
*
|
|
280
|
+
* @example
|
|
281
|
+
* ```ts
|
|
282
|
+
* decodeRow({ id: 'u1', active: 1, bio: null }, schema) // { id: 'u1', active: true }
|
|
283
|
+
* ```
|
|
284
|
+
*/
|
|
285
|
+
export declare function decodeRow(row: SQLiteRow, schema: TableSchema): Row;
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Decode a stored {@link SQLiteValue} back to its JS value for a column's type —
|
|
289
|
+
* the exact inverse of {@link encodeValue}.
|
|
290
|
+
*
|
|
291
|
+
* @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.
|
|
297
|
+
*
|
|
298
|
+
* @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`)
|
|
301
|
+
*
|
|
302
|
+
* @example
|
|
303
|
+
* ```ts
|
|
304
|
+
* decodeValue(1, 'boolean') // true
|
|
305
|
+
* decodeValue('{"a":1}', 'json') // { a: 1 }
|
|
306
|
+
* ```
|
|
307
|
+
*/
|
|
308
|
+
export declare function decodeValue(value: SQLiteValue, type: ColumnType): unknown;
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Encode a whole {@link Row} to a {@link SQLiteRow} by its table's schema.
|
|
312
|
+
*
|
|
313
|
+
* @remarks
|
|
314
|
+
* Encodes each declared column's value with {@link encodeValue}; columns the row
|
|
315
|
+
* does not carry encode from `undefined` (so they store `null`). Only the
|
|
316
|
+
* schema's columns appear in the result — an extra row key is dropped.
|
|
317
|
+
*
|
|
318
|
+
* @param row - The JS row to store
|
|
319
|
+
* @param schema - The table's schema
|
|
320
|
+
* @returns The storable SQLite row
|
|
321
|
+
*
|
|
322
|
+
* @example
|
|
323
|
+
* ```ts
|
|
324
|
+
* encodeRow({ id: 'u1', active: true }, schema) // { id: 'u1', active: 1, ... }
|
|
325
|
+
* ```
|
|
326
|
+
*/
|
|
327
|
+
export declare function encodeRow(row: Row, schema: TableSchema): SQLiteRow;
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Encode a JS value to its stored {@link SQLiteValue} for a column's type.
|
|
331
|
+
*
|
|
332
|
+
* @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`.
|
|
340
|
+
*
|
|
341
|
+
* @param value - The JS value to store
|
|
342
|
+
* @param type - The column's portable storage type
|
|
343
|
+
* @returns The value SQLite stores
|
|
344
|
+
*
|
|
345
|
+
* @example
|
|
346
|
+
* ```ts
|
|
347
|
+
* encodeValue(true, 'boolean') // 1
|
|
348
|
+
* encodeValue({ a: 1 }, 'json') // '{"a":1}'
|
|
349
|
+
* ```
|
|
350
|
+
*/
|
|
351
|
+
export declare function encodeValue(value: unknown, type: ColumnType): SQLiteValue;
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Escape `\`, `%`, and `_` (each with a leading `\`) so a `starts` / `ends`
|
|
355
|
+
* operand is matched literally under the `LIKE … ESCAPE '\'` clause.
|
|
356
|
+
*
|
|
357
|
+
* @param text - The raw operand text
|
|
358
|
+
* @returns The text with LIKE metacharacters escaped
|
|
359
|
+
*
|
|
360
|
+
* @example
|
|
361
|
+
* ```ts
|
|
362
|
+
* escapeLike('50%_off') // '50\\%\\_off'
|
|
363
|
+
* ```
|
|
364
|
+
*/
|
|
365
|
+
export declare function escapeLike(text: string): string;
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* The declared {@link ColumnType}s whose SQL EQUALITY comparisons (`equals` /
|
|
369
|
+
* `not` / `any` / `none`) and `starts` / `ends` compiles are provably
|
|
370
|
+
* engine-exact under declared-type trust — `text` / `integer` / `real` /
|
|
371
|
+
* `boolean`; a `json` or `blob` column always refines instead.
|
|
372
|
+
*
|
|
373
|
+
* @remarks
|
|
374
|
+
* This set governs equality and prefix/suffix matching only. RANGE
|
|
375
|
+
* comparisons (`above` / `below` / `from` / `to` / `between`) and `ORDER BY`
|
|
376
|
+
* are exact for `integer` / `real` / `boolean` but NOT for `text`: compiled
|
|
377
|
+
* SQL orders/ranges under SQLite's default BINARY collation, which compares
|
|
378
|
+
* TEXT byte-for-byte as UTF-8 — equivalent to Unicode CODE-POINT order —
|
|
379
|
+
* while the core engine's `compareValues` orders JS strings with `<`, which
|
|
380
|
+
* compares UTF-16 CODE-UNIT order. The two orders diverge for supplementary-
|
|
381
|
+
* plane characters (code points ≥ U+10000, e.g. many emoji): a lead surrogate
|
|
382
|
+
* (`\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.
|
|
388
|
+
*/
|
|
389
|
+
export declare const EXACT_COLUMN_TYPES: readonly ColumnType[];
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* The declared {@link ColumnType}s whose SQL RANGE comparisons
|
|
393
|
+
* (`above` / `below` / `from` / `to` / `between`) and `ORDER BY` compiles are
|
|
394
|
+
* provably engine-exact — `integer` / `real` / `boolean` only. `text` is
|
|
395
|
+
* excluded: see {@link EXACT_COLUMN_TYPES}'s remarks for the BINARY-collation
|
|
396
|
+
* (code-point) vs. JS `<` (code-unit) divergence on supplementary-plane
|
|
397
|
+
* characters.
|
|
398
|
+
*/
|
|
399
|
+
export declare const EXACT_RANGE_COLUMN_TYPES: readonly ColumnType[];
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Compile a {@link FieldPath} to the SQL expression that reads it.
|
|
403
|
+
*
|
|
404
|
+
* @remarks
|
|
405
|
+
* A single string is ONE column — `quote(path)`. An array descends a JSON column:
|
|
406
|
+
* the first element is the (quoted) column, the rest a `json_extract` path
|
|
407
|
+
* (`json_extract("payload", '$.user.id')`), matching the guide's nested-field
|
|
408
|
+
* examples (simple identifier keys). The string's value is never split on `.`
|
|
409
|
+
* (AGENTS — `FieldPath` semantics): a dotted string is one column literally.
|
|
410
|
+
*
|
|
411
|
+
* @param path - The field path (a column, or a column + nested keys)
|
|
412
|
+
* @returns The SQL expression selecting the value
|
|
413
|
+
*
|
|
414
|
+
* @example
|
|
415
|
+
* ```ts
|
|
416
|
+
* fieldColumn('payload') // '"payload"'
|
|
417
|
+
* fieldColumn(['payload', 'user', 'id']) // 'json_extract("payload", \'$.user.id\')'
|
|
418
|
+
* ```
|
|
419
|
+
*/
|
|
420
|
+
export declare function fieldColumn(path: FieldPath): string;
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Compile one condition to its `<column> <operator>` SQL fragment and the params
|
|
424
|
+
* it binds — engine-exact under SQL's three-valued NULL logic.
|
|
425
|
+
*
|
|
426
|
+
* @remarks
|
|
427
|
+
* Every operand is run through `encodeValue`, so a bound value matches the SQL
|
|
428
|
+
* the column side compiles to. A flat column encodes operands with its DECLARED
|
|
429
|
+
* schema type (a flat `json` column → `JSON.stringify`); a nested `FieldPath`
|
|
430
|
+
* encodes each operand as the NATIVE scalar `json_extract` returns, derived from
|
|
431
|
+
* the operand's runtime type (per-operand, since `between` / `any` / `none` can
|
|
432
|
+
* mix types). `any` / `none` collapse an empty list to a constant (`0` matches
|
|
433
|
+
* nothing, `1` matches all) with no params.
|
|
434
|
+
*
|
|
435
|
+
* The core engine's total order ranks `undefined` (rank 0) BELOW `null`
|
|
436
|
+
* (rank 1) (see `compareValues`), so a MISSING/`NULL` column MATCHES
|
|
437
|
+
* `below` / `to` / a scalar `not` / `none` — the opposite of raw SQL, where a
|
|
438
|
+
* comparison against `NULL` is `NULL` (excluded). This fragment replicates the
|
|
439
|
+
* engine exactly. Truth table (`value` = the engine's decoded field read; a
|
|
440
|
+
* FLAT column's stored `NULL` decodes to `undefined` per `decodeRow`, so a
|
|
441
|
+
* flat `value` is NEVER a present `null` — only a NESTED path can be
|
|
442
|
+
* present-but-`null`):
|
|
443
|
+
*
|
|
444
|
+
* ```text
|
|
445
|
+
* operator | value=undefined (absent) | value=null (nested only) | value=scalar
|
|
446
|
+
* --------------------|--------------------------------|---------------------------|-------------
|
|
447
|
+
* equals, first=null | no match | MATCH | no match
|
|
448
|
+
* equals, first=X | no match | no match | value===X
|
|
449
|
+
* not, first=null | MATCH (flat: unconditionally; | no match | MATCH
|
|
450
|
+
* | nested: absent still matches)| |
|
|
451
|
+
* not, first=X | MATCH | MATCH | value!==X
|
|
452
|
+
* below/to, first=X | MATCH (rank 0 < rank(X)) | MATCH (rank 1 < rank(X)) | rank compare
|
|
453
|
+
* none, list=[…] | MATCH (no scalar rank-equal) | MATCH | not-in-list
|
|
454
|
+
* any, list=[…] | no match | no match | in-list
|
|
455
|
+
* above/from/between | no match | no match | rank compare
|
|
456
|
+
* like/glob/starts/… | no match (not a string) | no match | string test
|
|
457
|
+
* present | false | false | true
|
|
458
|
+
* absent | true | true | false
|
|
459
|
+
* ```
|
|
460
|
+
*
|
|
461
|
+
* Because a flat column's `NULL` always decodes to `undefined`, `equals`
|
|
462
|
+
* against a `null` operand needs no special flat compilation (`col = ?`
|
|
463
|
+
* binding a `NULL` param is already always-false in SQL, matching "no match"
|
|
464
|
+
* above) — but flat `not` against `null` must match EVERY row (both the
|
|
465
|
+
* absent and the scalar rows), which `col != ? OR col IS NULL` cannot express
|
|
466
|
+
* (it only catches the `IS NULL` row), so a flat `not`-with-`null`-operand
|
|
467
|
+
* compiles to the constant `1`.
|
|
468
|
+
*
|
|
469
|
+
* A NESTED path can be present-but-`null` (a stored JSON `null`), which
|
|
470
|
+
* `json_extract` reads back as SQL `NULL` — indistinguishable from an ABSENT
|
|
471
|
+
* path. `json_type(col, path)` disambiguates them (`'null'` for present-null,
|
|
472
|
+
* SQL `NULL` for absent), so nested `equals` / `not` against a `null` operand
|
|
473
|
+
* compile through `json_type` instead of `IS NULL` / `IS NOT NULL`.
|
|
474
|
+
*
|
|
475
|
+
* Every other MATCH-on-null-or-absent row is expressed uniformly (flat and
|
|
476
|
+
* nested alike) as `(<column> <op> ? OR <column> IS NULL)` — for a nested
|
|
477
|
+
* path, `json_extract` already collapses BOTH absent and present-null to SQL
|
|
478
|
+
* `NULL`, so `IS NULL` catches both in one clause; for a flat column there is
|
|
479
|
+
* only the absent case to catch.
|
|
480
|
+
*
|
|
481
|
+
* @param condition - The condition to compile
|
|
482
|
+
* @param schema - The table's schema (for declared column types)
|
|
483
|
+
* @returns The SQL fragment and its bound parameters
|
|
484
|
+
*
|
|
485
|
+
* @example
|
|
486
|
+
* ```ts
|
|
487
|
+
* fragment({ column: 'age', operator: 'above', values: [18], connector: 'and' }, schema)
|
|
488
|
+
* // { sql: '"age" > ?', params: [18] }
|
|
489
|
+
* fragment({ column: 'age', operator: 'below', values: [18], connector: 'and' }, schema)
|
|
490
|
+
* // { sql: '("age" < ? OR "age" IS NULL)', params: [18] }
|
|
491
|
+
* ```
|
|
492
|
+
*/
|
|
493
|
+
export declare function fragment(condition: Condition, schema: TableSchema): CompiledSQL;
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Generate a fresh unique key — a v4 UUID string, backed by `node:crypto`.
|
|
497
|
+
*
|
|
498
|
+
* @remarks
|
|
499
|
+
* Supply this as {@link import('@orkestrel/database').DatabaseOptions.key} so a table mints
|
|
500
|
+
* a key when a written row lacks its primary-key value. Strings work as keys on
|
|
501
|
+
* every backend; supply your own key values directly to use numeric keys instead.
|
|
502
|
+
*
|
|
503
|
+
* @returns A new UUID string
|
|
504
|
+
*
|
|
505
|
+
* @example
|
|
506
|
+
* ```ts
|
|
507
|
+
* const db = createDatabase({ driver, tables, key: generateKey })
|
|
508
|
+
* ```
|
|
509
|
+
*/
|
|
510
|
+
export declare function generateKey(): string;
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Build a collision-free SQL index name for a table + column-group index —
|
|
514
|
+
* shared by {@link schemaToIndexes} (an `open`-time `CREATE INDEX`) and
|
|
515
|
+
* {@link stepToSQL}'s `index.add` / `index.remove` (a migration-time DDL),
|
|
516
|
+
* so a plan-built index name always matches one `open` would have created.
|
|
517
|
+
*
|
|
518
|
+
* @remarks
|
|
519
|
+
* A naive `idx_<table>_<cols joined by _>` is AMBIGUOUS: table `'a_b'` with
|
|
520
|
+
* column `'c'` and table `'a'` with columns `['b', 'c']` both produce
|
|
521
|
+
* `idx_a_b_c`. This encodes each part (the table name, then each column name)
|
|
522
|
+
* length-prefixed (`<len>_<part>`) so the boundary between parts is always
|
|
523
|
+
* unambiguous, however the names themselves are punctuated.
|
|
524
|
+
*
|
|
525
|
+
* @param table - The table name
|
|
526
|
+
* @param columns - The index's column names, in order
|
|
527
|
+
* @returns The deterministic, collision-free index identifier (unquoted)
|
|
528
|
+
*
|
|
529
|
+
* @example
|
|
530
|
+
* ```ts
|
|
531
|
+
* indexName('users', ['name']) // 'idx_5_users_4_name'
|
|
532
|
+
* indexName('a_b', ['c']) // 'idx_3_a_b_1_c'
|
|
533
|
+
* indexName('a', ['b', 'c']) // 'idx_1_a_1_b_1_c'
|
|
534
|
+
* ```
|
|
535
|
+
*/
|
|
536
|
+
export declare function indexName(table: string, columns: readonly string[]): string;
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Whether one {@link Condition} compiles to SQL that is PROVABLY identical to
|
|
540
|
+
* the core engine's `matchesCondition` for every value its column's declared
|
|
541
|
+
* type can store.
|
|
542
|
+
*
|
|
543
|
+
* @remarks
|
|
544
|
+
* `false` for a nested `FieldPath` (an array), a column absent from `schema`,
|
|
545
|
+
* or a column whose declared type is not `text` / `integer` / `real` /
|
|
546
|
+
* `boolean` (a `json` / `blob` column) — EXCEPT `absent` / `present`, which
|
|
547
|
+
* compile to `IS NULL` / `IS NOT NULL` and match `decodeRow`'s "a stored NULL
|
|
548
|
+
* decodes to `undefined`" rule for every column type, so they are exact
|
|
549
|
+
* regardless of declared type. `equals` / `not` require a operand matching the
|
|
550
|
+
* column's declared type (a `null` / `undefined` operand is never exact here —
|
|
551
|
+
* `encodeRow` stores both an explicit `null` and an absent field as SQL NULL,
|
|
552
|
+
* so native `IS NULL` semantics cannot match the engine's `deepEqual`-over-
|
|
553
|
+
* decoded-rows truth). `above` / `below` / `from` / `to` / `between` are exact
|
|
554
|
+
* ONLY for a declared type in {@link EXACT_RANGE_COLUMN_TYPES} (`integer` /
|
|
555
|
+
* `real` / `boolean`) — a `text` column's range conditions REFINE, because
|
|
556
|
+
* SQLite's default BINARY collation orders TEXT by Unicode CODE POINT while
|
|
557
|
+
* the core engine's `compareValues` orders JS strings by UTF-16 CODE UNIT,
|
|
558
|
+
* and the two diverge for supplementary-plane characters (see
|
|
559
|
+
* {@link EXACT_COLUMN_TYPES}'s remarks for the full rationale).
|
|
560
|
+
* `any` / `none` require a NON-EMPTY list where every element matches (an empty
|
|
561
|
+
* list is exact under neither: the engine's `any([])` matches nothing while
|
|
562
|
+
* `none([])` matches everything, and SQL `IN ()` is a syntax error) — these
|
|
563
|
+
* stay exact on `text` (byte equality is collation-independent and engine-
|
|
564
|
+
* identical). `starts` / `ends` are exact only on a `text` column with a
|
|
565
|
+
* string operand (case-sensitive `substr` compile, see {@link fragment}) —
|
|
566
|
+
* likewise collation-independent. `like` / `glob` are NEVER exact — SQLite
|
|
567
|
+
* `LIKE` folds case ASCII-only against the engine's Unicode fold, and `GLOB`
|
|
568
|
+
* has character classes the engine treats literally.
|
|
569
|
+
*
|
|
570
|
+
* @param condition - The condition to test
|
|
571
|
+
* @param schema - The table's schema
|
|
572
|
+
* @returns Whether `condition` is exact
|
|
573
|
+
*/
|
|
574
|
+
export declare function isExactCondition(condition: Condition, schema: TableSchema): boolean;
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Whether a whole {@link Criteria} is exact — every condition and every order
|
|
578
|
+
* term is exact. `limit` / `offset` never affect exactness (SQL `LIMIT` /
|
|
579
|
+
* `OFFSET` are always engine-identical).
|
|
580
|
+
*
|
|
581
|
+
* @param criteria - The criteria to test
|
|
582
|
+
* @param schema - The table's schema
|
|
583
|
+
* @returns Whether every part of `criteria` is exact
|
|
584
|
+
*/
|
|
585
|
+
export declare function isExactCriteria(criteria: Criteria, schema: TableSchema): boolean;
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Whether one {@link Order} term's column compiles to an `ORDER BY` that
|
|
589
|
+
* matches the engine's {@link import('../core/index.js').sortRows} exactly.
|
|
590
|
+
*
|
|
591
|
+
* @remarks
|
|
592
|
+
* `false` for a nested `FieldPath`, a column absent from `schema`, or a
|
|
593
|
+
* declared type outside {@link EXACT_RANGE_COLUMN_TYPES} (`integer` / `real` /
|
|
594
|
+
* `boolean`). `text` is NOT exact here: SQLite's default BINARY collation
|
|
595
|
+
* orders TEXT by Unicode code point while the core engine's `compareValues`
|
|
596
|
+
* orders JS strings by UTF-16 code unit, and the two diverge for
|
|
597
|
+
* supplementary-plane characters (see {@link EXACT_COLUMN_TYPES}'s remarks) —
|
|
598
|
+
* a `text` order term REFINES through the core engine instead.
|
|
599
|
+
*
|
|
600
|
+
* @param order - The order term to test
|
|
601
|
+
* @param schema - The table's schema
|
|
602
|
+
* @returns Whether `order` is exact
|
|
603
|
+
*/
|
|
604
|
+
export declare function isExactOrder(order: Order, schema: TableSchema): boolean;
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* A persistent {@link DriverInterface} backed by a single JSON file — the
|
|
608
|
+
* reference {@link MemoryDriver} plus file load / flush.
|
|
609
|
+
*
|
|
610
|
+
* @remarks
|
|
611
|
+
* A decorator, not a reimplementation: every primitive delegates to an inner
|
|
612
|
+
* {@link MemoryDriver}, so querying, key-order `scan` / `keys`, and capture-replay
|
|
613
|
+
* `snapshot` are inherited unchanged — this layer adds only persistence. `open`
|
|
614
|
+
* loads the file into memory; every mutation (`write` / `delete` / `clear`) flushes
|
|
615
|
+
* the whole store back. The file is one JSON object, `{ meta?: DriverMeta, tables: {
|
|
616
|
+
* [name]: rows } }` — `meta` is present only once the store has been `stamp`ed
|
|
617
|
+
* (an unstamped store serializes the old `{ tables }` shape, preserving
|
|
618
|
+
* backward compatibility); a per-table array of rows, each row carrying its own
|
|
619
|
+
* primary (the table contract), so the key is recovered on load with
|
|
620
|
+
* {@link extractKey} and the file need not store it. The parsed JSON crosses the
|
|
621
|
+
* boundary as `unknown` and is narrowed with {@link isRecord} / {@link extractKey},
|
|
622
|
+
* never asserted (AGENTS §14): a missing, corrupt, or wrong-shaped file starts
|
|
623
|
+
* empty rather than throwing, and a malformed row (or malformed `meta`) is
|
|
624
|
+
* skipped/dropped rather than thrown on. It is scan-only — it implements none of
|
|
625
|
+
* the optional native `records` / `count` / `aggregate` hooks, so the core engine
|
|
626
|
+
* over `scan` answers every query. For development, small datasets, and portable /
|
|
627
|
+
* inspectable data; for large or concurrent workloads reach for a SQLite-backed
|
|
628
|
+
* driver.
|
|
629
|
+
*
|
|
630
|
+
* A failure in the write path ({@link JSONDriver.#serialize} — `mkdir` /
|
|
631
|
+
* `writeFile` / `rename`) is wrapped and rethrown as `DatabaseError` `DRIVER`,
|
|
632
|
+
* carrying the target `path` in its context; the read path ({@link
|
|
633
|
+
* JSONDriver.#load}) tolerance above is a separate, deliberate contract and is
|
|
634
|
+
* never touched by this wrapping.
|
|
635
|
+
*/
|
|
636
|
+
export declare class JSONDriver implements DriverInterface_2 {
|
|
637
|
+
#private;
|
|
638
|
+
constructor(path: string);
|
|
639
|
+
open(schema: readonly TableSchema_2[]): Promise<void>;
|
|
640
|
+
close(): Promise<void>;
|
|
641
|
+
read(table: string, key: Key): Promise<Row_2 | undefined>;
|
|
642
|
+
write(table: string, key: Key, row: Row_2): Promise<void>;
|
|
643
|
+
delete(table: string, key: Key): Promise<boolean>;
|
|
644
|
+
keys(table: string): Promise<readonly Key[]>;
|
|
645
|
+
scan(table: string): AsyncIterable<Row_2>;
|
|
646
|
+
/**
|
|
647
|
+
* Natively filtered lazy iteration — delegates to the inner {@link MemoryDriver}.
|
|
648
|
+
*
|
|
649
|
+
* @remarks
|
|
650
|
+
* Semantics are the memory driver's own: `criteria.conditions` filters, `offset`
|
|
651
|
+
* / `limit` page lazily, and `criteria.order` is ignored (streaming yields key
|
|
652
|
+
* order; sorted output is `records()`'s job).
|
|
653
|
+
*
|
|
654
|
+
* @param table - The table to stream
|
|
655
|
+
* @param criteria - The filter / offset / limit to apply lazily
|
|
656
|
+
*/
|
|
657
|
+
stream(table: string, criteria: Criteria_2): AsyncIterable<Row_2>;
|
|
658
|
+
clear(table: string): Promise<void>;
|
|
659
|
+
/**
|
|
660
|
+
* Begin a native transaction — flush-coalescing over the inner {@link MemoryDriver}.
|
|
661
|
+
*
|
|
662
|
+
* @remarks
|
|
663
|
+
* Single-writer: throws `DatabaseError` `CONFLICT` if a transaction is already
|
|
664
|
+
* active — this driver does not support nesting. On begin, captures the inner
|
|
665
|
+
* memory rollback thunk via `#memory.snapshot()` and suppresses per-mutation
|
|
666
|
+
* `#flush` — `write` / `delete` / `clear` still mutate memory but no longer
|
|
667
|
+
* touch the file, so N mutations under the handle cost ONE file write instead
|
|
668
|
+
* of N. `commit()` releases the suppression and performs that one atomic
|
|
669
|
+
* `#flush()`, persisting the transaction's net state. `rollback()` restores
|
|
670
|
+
* memory via the captured snapshot thunk, then `#flush()`s so the file reflects
|
|
671
|
+
* the restored state. Outside a transaction, behavior is unchanged — every
|
|
672
|
+
* mutation flushes on its own. Calling `commit` / `rollback` a second time (on
|
|
673
|
+
* either method, in either order) throws `DatabaseError` `CONFLICT`.
|
|
674
|
+
*
|
|
675
|
+
* @returns A {@link TransactionInterface} handle to `commit` or `rollback`
|
|
676
|
+
*/
|
|
677
|
+
transaction(): Promise<TransactionInterface>;
|
|
678
|
+
snapshot(tables?: readonly string[]): Promise<() => Promise<void>>;
|
|
679
|
+
meta(): Promise<DriverMeta | undefined>;
|
|
680
|
+
/**
|
|
681
|
+
* Persist `meta` verbatim for a later `meta()` to return.
|
|
682
|
+
*
|
|
683
|
+
* @remarks
|
|
684
|
+
* Respects the same defer-flush suppression as `write` / `delete` / `clear`
|
|
685
|
+
* (see {@link JSONDriver.transaction} @remarks) — stamping inside an active
|
|
686
|
+
* transaction updates memory but does not flush until the transaction settles.
|
|
687
|
+
*
|
|
688
|
+
* @param meta - The {@link DriverMeta} to persist
|
|
689
|
+
*/
|
|
690
|
+
stamp(meta: DriverMeta): Promise<void>;
|
|
691
|
+
/**
|
|
692
|
+
* Apply a {@link Migration} plan by delegating to the inner {@link MemoryDriver},
|
|
693
|
+
* then persist the migrated state.
|
|
694
|
+
*
|
|
695
|
+
* @remarks
|
|
696
|
+
* The inner `MemoryDriver.migrate` applies each step (adding/removing tables,
|
|
697
|
+
* adding/removing columns from stored rows, no-op index steps) and throws
|
|
698
|
+
* `DatabaseError` `MIGRATION` for a step referencing an unknown table — that
|
|
699
|
+
* error propagates untouched. `table.add` / `table.remove` steps also update
|
|
700
|
+
* this driver's own declared `#schema`, mirroring the bookkeeping `open` does,
|
|
701
|
+
* so a subsequent `#flush` / `#load` round-trip includes (or drops) the table.
|
|
702
|
+
* A successful migration ends with one atomic `#flush()` so the new state
|
|
703
|
+
* survives a close and reopen. A multi-step plan applies its steps
|
|
704
|
+
* sequentially and is NOT atomic — a failure partway through a plan leaves
|
|
705
|
+
* the earlier steps already applied.
|
|
706
|
+
*
|
|
707
|
+
* @param plan - The migration plan to apply
|
|
708
|
+
*/
|
|
709
|
+
migrate(plan: Migration): Promise<void>;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Compile a NESTED {@link FieldPath} to the `json_type(<col>, <path>)` SQL
|
|
714
|
+
* expression — the {@link fieldColumn} `json_extract` sibling used to tell a
|
|
715
|
+
* PRESENT JSON `null` apart from an ABSENT path (both read back as SQL `NULL`
|
|
716
|
+
* through `json_extract`, but `json_type` reports `'null'` for the former and
|
|
717
|
+
* SQL `NULL` for the latter).
|
|
718
|
+
*
|
|
719
|
+
* @param path - The nested field path (a column plus its JSON keys)
|
|
720
|
+
* @returns The SQL expression reading the value's JSON type
|
|
721
|
+
*
|
|
722
|
+
* @example
|
|
723
|
+
* ```ts
|
|
724
|
+
* jsonTypeColumn(['payload', 'user', 'id']) // "json_type(\"payload\", '$.user.id')"
|
|
725
|
+
* ```
|
|
726
|
+
*/
|
|
727
|
+
export declare function jsonTypeColumn(path: readonly string[]): string;
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Whether a value's runtime type matches a column's declared exact type —
|
|
731
|
+
* the operand side of the declared-type-trust proof.
|
|
732
|
+
*
|
|
733
|
+
* @remarks
|
|
734
|
+
* `text` ↔ string, `integer` / `real` ↔ FINITE number (`NaN` / `±Infinity`
|
|
735
|
+
* fail), `boolean` ↔ boolean. Backs {@link isExactCondition}'s operand checks.
|
|
736
|
+
*
|
|
737
|
+
* @param value - The condition operand to test
|
|
738
|
+
* @param type - The column's declared portable type
|
|
739
|
+
* @returns `true` when the operand's runtime type matches the declared type
|
|
740
|
+
*
|
|
741
|
+
* @example
|
|
742
|
+
* ```ts
|
|
743
|
+
* matchesDeclaredType('Ada', 'text') // true
|
|
744
|
+
* matchesDeclaredType(Number.NaN, 'integer') // false — only finite numbers
|
|
745
|
+
* ```
|
|
746
|
+
*/
|
|
747
|
+
export declare function matchesDeclaredType(value: unknown, type: ColumnType): boolean;
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* The reserved metadata table the {@link SQLiteDriver} creates on `open` to
|
|
751
|
+
* persist its stamped `DriverMeta` (`version` + declared schema JSON) — the
|
|
752
|
+
* SQLite realization of the `meta` / `stamp` driver hooks.
|
|
753
|
+
*
|
|
754
|
+
* @remarks
|
|
755
|
+
* A single-row table (`id = 1`). A user table named `_meta` collides with the
|
|
756
|
+
* reservation — the caller's concern to avoid, documented on the driver class.
|
|
757
|
+
*/
|
|
758
|
+
export declare const META_TABLE = "_meta";
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Quote a SQL identifier (a table or column name) so any characters are literal.
|
|
762
|
+
*
|
|
763
|
+
* @remarks
|
|
764
|
+
* Wraps the name in double quotes and doubles any embedded quote — the standard
|
|
765
|
+
* SQL identifier-quoting that lets a column named `order` or `from` be referenced
|
|
766
|
+
* safely. Identifiers cannot be bound as parameters, so they are quoted instead.
|
|
767
|
+
*
|
|
768
|
+
* @param identifier - The raw identifier
|
|
769
|
+
* @returns The double-quoted identifier
|
|
770
|
+
*
|
|
771
|
+
* @example
|
|
772
|
+
* ```ts
|
|
773
|
+
* quote('order') // '"order"'
|
|
774
|
+
* ```
|
|
775
|
+
*/
|
|
776
|
+
export declare function quote(identifier: string): string;
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* Project a {@link TableSchema} to the `CREATE INDEX IF NOT EXISTS` statements a
|
|
780
|
+
* SQLite driver's `open` issues for its declared indexes.
|
|
781
|
+
*
|
|
782
|
+
* @remarks
|
|
783
|
+
* One statement per index group; the index name is built by {@link indexName}
|
|
784
|
+
* (collision-free and deterministic), matching the driver's naming so a
|
|
785
|
+
* repeated `open` is idempotent.
|
|
786
|
+
*
|
|
787
|
+
* @param schema - The table's schema
|
|
788
|
+
* @returns One `CREATE INDEX IF NOT EXISTS …` statement per declared index
|
|
789
|
+
*
|
|
790
|
+
* @example
|
|
791
|
+
* ```ts
|
|
792
|
+
* schemaToIndexes(schema)
|
|
793
|
+
* // ['CREATE INDEX IF NOT EXISTS "idx_5_users_4_name" ON "users" ("name")']
|
|
794
|
+
* ```
|
|
795
|
+
*/
|
|
796
|
+
export declare function schemaToIndexes(schema: TableSchema): readonly string[];
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* Project a {@link TableSchema} to the `CREATE TABLE IF NOT EXISTS` statement a
|
|
800
|
+
* SQLite driver's `open` issues for it.
|
|
801
|
+
*
|
|
802
|
+
* @remarks
|
|
803
|
+
* Each column compiles to `<quoted name> <columnSQL(type)>`; the statement ends
|
|
804
|
+
* with `PRIMARY KEY (<quoted primary>)`. No `NOT NULL` is emitted — the contract
|
|
805
|
+
* validates required-ness, the database is just storage (AGENTS §14).
|
|
806
|
+
*
|
|
807
|
+
* @param schema - The table's schema
|
|
808
|
+
* @returns The `CREATE TABLE IF NOT EXISTS …` statement
|
|
809
|
+
*
|
|
810
|
+
* @example
|
|
811
|
+
* ```ts
|
|
812
|
+
* schemaToTable(schema)
|
|
813
|
+
* // 'CREATE TABLE IF NOT EXISTS "users" ("id" TEXT, "age" INTEGER, PRIMARY KEY ("id"))'
|
|
814
|
+
* ```
|
|
815
|
+
*/
|
|
816
|
+
export declare function schemaToTable(schema: TableSchema): string;
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* The SQLite {@link DriverInterface} — the server-native, trusted-mode backend
|
|
820
|
+
* built on the published `@orkestrel/sqlite` synchronous wrapper.
|
|
821
|
+
*
|
|
822
|
+
* @remarks
|
|
823
|
+
* A thin adapter: it implements the storage primitives the core database layer
|
|
824
|
+
* needs by delegating to the wrapper's prepared statements — it never touches
|
|
825
|
+
* raw `node:sqlite`. `open` issues `CREATE TABLE IF NOT EXISTS` with real typed
|
|
826
|
+
* columns (mapped from each {@link TableSchema}'s portable column types) and a
|
|
827
|
+
* `PRIMARY KEY`, plus a `CREATE INDEX IF NOT EXISTS` per declared index (both
|
|
828
|
+
* reopen-safe), and readies a reserved `_meta` single-row table `meta()` /
|
|
829
|
+
* `stamp()` read and write — **a user table named `_meta` collides with it**;
|
|
830
|
+
* avoid the name. Rows cross the boundary through the codecs in `helpers.ts`
|
|
831
|
+
* (`json` columns store / parse JSON text, a `boolean` stores `1` / `0`), so the
|
|
832
|
+
* typed layer above imposes the exact shape (AGENTS §14). `write` is an
|
|
833
|
+
* `INSERT OR REPLACE` upsert — the `Table` layer detects a `CONFLICT` via a
|
|
834
|
+
* prior `has`, so this never translates a constraint error; a backend
|
|
835
|
+
* `SQLiteError` otherwise propagates unchanged. Querying, ordering, paging, and
|
|
836
|
+
* aggregation are native: `records` / `count` / `stream` compile a `Criteria`
|
|
837
|
+
* to SQL with `compileCriteria`, and `aggregate` runs a SQL
|
|
838
|
+
* `COUNT`/`SUM`/`AVG`/`MIN`/`MAX` (via `aggregateSQL`) over the same compiled
|
|
839
|
+
* WHERE. `transaction` wraps native `BEGIN` / `COMMIT` / `ROLLBACK` with
|
|
840
|
+
* double-settle guards. `migrate` runs the plan's projected DDL
|
|
841
|
+
* ({@link import('../helpers.js').stepToSQL}) inside whichever native
|
|
842
|
+
* transaction is active: joined into an already-open `transaction()` handle
|
|
843
|
+
* when one exists (the core's versioned reconcile path wraps migrate + stamp
|
|
844
|
+
* in one native `BEGIN`, and node:sqlite rejects a nested `BEGIN`), or inside
|
|
845
|
+
* its own `database.transaction` otherwise — a mid-plan failure rolls back
|
|
846
|
+
* atomically either way, an improvement over the non-atomic `MemoryDriver` /
|
|
847
|
+
* `JSONDriver` migrate; a step referencing an undeclared table throws
|
|
848
|
+
* `DatabaseError` `MIGRATION` before any DDL for that step runs. `snapshot` is
|
|
849
|
+
* capture-replay (SELECT the
|
|
850
|
+
* named tables' rows, replay via DELETE + INSERT OR REPLACE inside a native
|
|
851
|
+
* transaction on rollback) rather than a SQL `SAVEPOINT`, since the core
|
|
852
|
+
* `transaction` calls the rollback thunk only on failure with no commit-on-
|
|
853
|
+
* success signal — a long-lived `SAVEPOINT` would leave the connection
|
|
854
|
+
* uncommitted (lost on close). Every backend interaction runs through `#guard`,
|
|
855
|
+
* which maps a thrown backend `SQLiteError` (or any unexpected non-`SQLiteError`
|
|
856
|
+
* throw) to a typed {@link DatabaseError} — never a raw backend error escapes
|
|
857
|
+
* `DriverInterface`: `CONSTRAINT` → `CONFLICT`, the wrapper's own `CLOSED` →
|
|
858
|
+
* `CLOSED`, `BUSY` (a locked database that outlasted the configured `timeout`)
|
|
859
|
+
* → a retryable `DRIVER` (`context.retryable` is `true`), and `UNKNOWN` / any
|
|
860
|
+
* other throw → `DRIVER`. The original error is preserved as `context.cause`.
|
|
861
|
+
* A `DatabaseError` this driver throws directly (`CLOSED` from the `#require`
|
|
862
|
+
* gate, `NOT_FOUND` from `#table`, `MIGRATION` from a migration-plan fault)
|
|
863
|
+
* passes through `#guard` unchanged, never re-wrapped.
|
|
864
|
+
*/
|
|
865
|
+
export declare class SQLiteDriver implements DriverInterface_2 {
|
|
866
|
+
#private;
|
|
867
|
+
constructor(path: string, options?: SQLiteDriverOptions);
|
|
868
|
+
open(schema: readonly TableSchema_2[]): Promise<void>;
|
|
869
|
+
close(): Promise<void>;
|
|
870
|
+
read(table: string, key: Key): Promise<Row_2 | undefined>;
|
|
871
|
+
write(table: string, key: Key, row: Row_2): Promise<void>;
|
|
872
|
+
delete(table: string, key: Key): Promise<boolean>;
|
|
873
|
+
keys(table: string): Promise<readonly Key[]>;
|
|
874
|
+
scan(table: string): AsyncIterable<Row_2>;
|
|
875
|
+
clear(table: string): Promise<void>;
|
|
876
|
+
records(table: string, criteria: Criteria_2): Promise<readonly Row_2[]>;
|
|
877
|
+
count(table: string, criteria: Criteria_2): Promise<number>;
|
|
878
|
+
aggregate(table: string, operation: AggregateFunction_2, column: FieldPath, criteria: Criteria_2): Promise<number | undefined>;
|
|
879
|
+
stream(table: string, criteria: Criteria_2): AsyncIterable<Row_2>;
|
|
880
|
+
/**
|
|
881
|
+
* Begin a native transaction — real `BEGIN`, `COMMIT`, `ROLLBACK`.
|
|
882
|
+
*
|
|
883
|
+
* @remarks
|
|
884
|
+
* Calling `commit` or `rollback` a second time (on either method, in either
|
|
885
|
+
* order) throws `DatabaseError` `CONFLICT`.
|
|
886
|
+
*
|
|
887
|
+
* @returns A {@link TransactionInterface} handle to `commit` or `rollback`
|
|
888
|
+
*/
|
|
889
|
+
transaction(): Promise<TransactionInterface>;
|
|
890
|
+
/**
|
|
891
|
+
* Apply a {@link Migration} plan by executing each step's projected DDL
|
|
892
|
+
* ({@link import('../helpers.js').stepToSQL}).
|
|
893
|
+
*
|
|
894
|
+
* @remarks
|
|
895
|
+
* Atomicity is provided by whichever native transaction is active: when
|
|
896
|
+
* this driver's own `transaction()` hook already has a handle open (the
|
|
897
|
+
* core's versioned reconcile / migrate path joins migrate + stamp under
|
|
898
|
+
* one native `BEGIN`), the plan's DDL runs directly inside that enclosing
|
|
899
|
+
* transaction — a mid-plan failure propagates out and the CALLER's
|
|
900
|
+
* `commit`/`rollback` provides atomicity. node:sqlite (and SQLite
|
|
901
|
+
* generally) rejects a nested `BEGIN`, so this driver must never open a
|
|
902
|
+
* second native transaction while one is already open. Otherwise (no
|
|
903
|
+
* enclosing transaction), `migrate` wraps the plan in its own native
|
|
904
|
+
* `database.transaction` — atomic on its own: a mid-plan failure rolls
|
|
905
|
+
* back every DDL statement already applied by the plan. A step
|
|
906
|
+
* referencing a table not in this driver's declared schema (and that is
|
|
907
|
+
* not itself a `table.add`) throws `DatabaseError` `MIGRATION` before any
|
|
908
|
+
* DDL for that step runs, propagating out of whichever transaction is
|
|
909
|
+
* active (which rolls back on a throw).
|
|
910
|
+
*
|
|
911
|
+
* @param plan - The migration plan to apply
|
|
912
|
+
*/
|
|
913
|
+
migrate(plan: Migration): Promise<void>;
|
|
914
|
+
/**
|
|
915
|
+
* Read the persisted {@link DriverMeta} from the reserved `_meta` table.
|
|
916
|
+
*
|
|
917
|
+
* @returns The last-stamped `DriverMeta`, or `undefined` when never stamped
|
|
918
|
+
* (or the stored row is malformed)
|
|
919
|
+
*/
|
|
920
|
+
meta(): Promise<DriverMeta | undefined>;
|
|
921
|
+
/**
|
|
922
|
+
* Persist `meta` verbatim (as JSON) into the reserved `_meta` table's single
|
|
923
|
+
* row.
|
|
924
|
+
*
|
|
925
|
+
* @param meta - The {@link DriverMeta} to persist
|
|
926
|
+
*/
|
|
927
|
+
stamp(meta: DriverMeta): Promise<void>;
|
|
928
|
+
snapshot(tables?: readonly string[]): Promise<() => Promise<void>>;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* Options for {@link import('./factories.js').createSQLiteDriver}.
|
|
933
|
+
*
|
|
934
|
+
* @remarks
|
|
935
|
+
* Threaded into the underlying `@orkestrel/sqlite` wrapper's connection.
|
|
936
|
+
* `path` is the database file path (`':memory:'` when omitted); `readonly`
|
|
937
|
+
* opens the connection read-only (a write then fails as a typed `DRIVER`
|
|
938
|
+
* {@link DatabaseError}); `timeout` is the busy-timeout in milliseconds before
|
|
939
|
+
* a locked database fails `BUSY`; `foreignKeys` enables foreign-key
|
|
940
|
+
* constraint enforcement. `pragmas` is an ordered record of PRAGMA name to
|
|
941
|
+
* value, applied via the wrapper's `pragma()` right after `connect()`, in
|
|
942
|
+
* insertion order (e.g. `{ journal_mode: 'WAL' }`). Core rows are
|
|
943
|
+
* number-typed — this driver never surfaces a `bigint`, so a stored integer
|
|
944
|
+
* beyond `Number.MAX_SAFE_INTEGER` reads back imprecisely (the wrapper's own
|
|
945
|
+
* `bigints` option is not exposed here).
|
|
946
|
+
*/
|
|
947
|
+
export declare interface SQLiteDriverOptions {
|
|
948
|
+
readonly path?: string;
|
|
949
|
+
readonly readonly?: boolean;
|
|
950
|
+
readonly timeout?: number;
|
|
951
|
+
readonly foreignKeys?: boolean;
|
|
952
|
+
readonly pragmas?: Readonly<Record<string, string | number>>;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
/**
|
|
956
|
+
* One row as a SQLite binding returns it — a plain object keyed by column name.
|
|
957
|
+
*
|
|
958
|
+
* @remarks
|
|
959
|
+
* Every column value is a {@link SQLiteValue}. The SQLite driver decodes each
|
|
960
|
+
* raw row into this shape before handing it to the core query engine; nothing
|
|
961
|
+
* above the driver ever sees SQLite's native row representation directly.
|
|
962
|
+
*/
|
|
963
|
+
export declare type SQLiteRow = Record<string, SQLiteValue>;
|
|
964
|
+
|
|
965
|
+
/**
|
|
966
|
+
* The value domain a SQLite binding accepts as a bound parameter and returns
|
|
967
|
+
* from a row.
|
|
968
|
+
*
|
|
969
|
+
* @remarks
|
|
970
|
+
* Mirrors SQLite's storage classes (`NULL`, `INTEGER`, `REAL`, `TEXT`, `BLOB`)
|
|
971
|
+
* at the TypeScript boundary: `null`, `number` / `bigint` for integer and
|
|
972
|
+
* floating-point values, `string` for text, and `Uint8Array` for blobs. This
|
|
973
|
+
* type is pure — it names the shape a value must have to cross the binding,
|
|
974
|
+
* independent of any concrete sqlite package (`node:sqlite`, `better-sqlite3`,
|
|
975
|
+
* etc.), so a driver can encode/decode against it without importing one.
|
|
976
|
+
*/
|
|
977
|
+
export declare type SQLiteValue = null | number | bigint | string | Uint8Array;
|
|
978
|
+
|
|
979
|
+
/**
|
|
980
|
+
* Project one {@link MigrationStep} onto its table's declared {@link TableSchema}
|
|
981
|
+
* — the bookkeeping counterpart to {@link stepToSQL} (which projects the DDL a
|
|
982
|
+
* driver's `migrate` runs against the live database).
|
|
983
|
+
*
|
|
984
|
+
* @remarks
|
|
985
|
+
* `column.add` / `column.remove` add / filter the named column;
|
|
986
|
+
* `index.add` / `index.remove` add / filter the matching index group (an exact
|
|
987
|
+
* ordered match on `index`). `table.add` / `table.remove` act on a WHOLE
|
|
988
|
+
* schema map rather than one table's shape, so they are the caller's concern
|
|
989
|
+
* (a driver's `migrate` applies them directly against its table map) — passed
|
|
990
|
+
* here, they return `schema` unchanged.
|
|
991
|
+
*
|
|
992
|
+
* @param schema - The table's current declared schema
|
|
993
|
+
* @param step - The migration step to project onto it
|
|
994
|
+
* @returns The table's schema after the step
|
|
995
|
+
*
|
|
996
|
+
* @example
|
|
997
|
+
* ```ts
|
|
998
|
+
* stepToSchema(schema, { operation: 'column.remove', table: 'users', column: 'legacy' })
|
|
999
|
+
* // schema with the 'legacy' column dropped from `columns`
|
|
1000
|
+
* ```
|
|
1001
|
+
*/
|
|
1002
|
+
export declare function stepToSchema(schema: TableSchema, step: MigrationStep): TableSchema;
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* Project one {@link MigrationStep} to the DDL statement(s) a SQLite driver's
|
|
1006
|
+
* `migrate` executes for it.
|
|
1007
|
+
*
|
|
1008
|
+
* @remarks
|
|
1009
|
+
* `table.add` emits the `CREATE TABLE` plus one `CREATE INDEX` per declared
|
|
1010
|
+
* index (via {@link schemaToTable} / {@link schemaToIndexes}); `table.remove`
|
|
1011
|
+
* emits `DROP TABLE IF EXISTS`; `column.add` / `column.remove` emit `ALTER
|
|
1012
|
+
* TABLE … ADD COLUMN` / `… DROP COLUMN`; `index.add` / `index.remove` emit
|
|
1013
|
+
* `CREATE INDEX IF NOT EXISTS` / `DROP INDEX IF EXISTS`, naming the index the
|
|
1014
|
+
* same way `schemaToIndexes` does (`idx_<table>_<columns joined by _>`) so a
|
|
1015
|
+
* plan-built index matches one `open` would have created. Whether the named
|
|
1016
|
+
* table actually exists is the caller's concern (a driver's `migrate` checks
|
|
1017
|
+
* its own declared schema before running these statements) — this projection
|
|
1018
|
+
* is pure and never inspects live state.
|
|
1019
|
+
*
|
|
1020
|
+
* @param step - The migration step to project
|
|
1021
|
+
* @returns The DDL statement(s) that apply the step
|
|
1022
|
+
*
|
|
1023
|
+
* @example
|
|
1024
|
+
* ```ts
|
|
1025
|
+
* stepToSQL({ operation: 'column.remove', table: 'users', column: 'legacy' })
|
|
1026
|
+
* // ['ALTER TABLE "users" DROP COLUMN "legacy"']
|
|
1027
|
+
* ```
|
|
1028
|
+
*/
|
|
1029
|
+
export declare function stepToSQL(step: MigrationStep): readonly string[];
|
|
1030
|
+
|
|
1031
|
+
/**
|
|
1032
|
+
* The storage type a nested (`json_extract`) operand encodes as, derived from its
|
|
1033
|
+
* RUNTIME value — NOT `json`.
|
|
1034
|
+
*
|
|
1035
|
+
* @remarks
|
|
1036
|
+
* `json_extract` returns the unquoted, natively-typed scalar (a JSON boolean as
|
|
1037
|
+
* `1` / `0`, a number as-is, a string as-is), so the operand must encode to that
|
|
1038
|
+
* same scalar to compare. A boolean → `'boolean'` (→ `1` / `0`); a number →
|
|
1039
|
+
* `'integer'` / `'real'`; a bigint → `'integer'`; a string → `'text'`; `null` /
|
|
1040
|
+
* `undefined` → `'text'` (encodes to `null`); an object / array → `'json'` (the
|
|
1041
|
+
* edge of comparing against a json subtree).
|
|
1042
|
+
*
|
|
1043
|
+
* @param value - The runtime operand value
|
|
1044
|
+
* @returns The {@link ColumnType} to encode it as
|
|
1045
|
+
*
|
|
1046
|
+
* @example
|
|
1047
|
+
* ```ts
|
|
1048
|
+
* valueType(true) // 'boolean'
|
|
1049
|
+
* valueType(9) // 'integer'
|
|
1050
|
+
* ```
|
|
1051
|
+
*/
|
|
1052
|
+
export declare function valueType(value: unknown): ColumnType;
|
|
1053
|
+
|
|
1054
|
+
export { }
|