@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
package/dist/src/core/index.d.ts
CHANGED
|
@@ -1,11 +1,1766 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
import { ContractInterface } from '@orkestrel/contract';
|
|
2
|
+
import { ContractShape } from '@orkestrel/contract';
|
|
3
|
+
import { EmitterErrorHandler } from '@orkestrel/emitter';
|
|
4
|
+
import { EmitterHooks } from '@orkestrel/emitter';
|
|
5
|
+
import { EmitterInterface } from '@orkestrel/emitter';
|
|
6
|
+
import { FieldPath } from '@orkestrel/contract';
|
|
7
|
+
import { Infer } from '@orkestrel/contract';
|
|
8
|
+
import { JSONSchema } from '@orkestrel/contract';
|
|
9
|
+
import { RandomFunction } from '@orkestrel/contract';
|
|
10
|
+
|
|
11
|
+
/** An aggregate computed over a numeric column. */
|
|
12
|
+
export declare type AggregateFunction = 'count' | 'sum' | 'average' | 'minimum' | 'maximum';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Apply a {@link Criteria} to rows — filter, then sort, then page.
|
|
16
|
+
*
|
|
17
|
+
* @remarks
|
|
18
|
+
* The whole portable read pipeline in one place: conditions filter, `order`
|
|
19
|
+
* sorts, and `offset` / `limit` window the result. Each step is skipped when its
|
|
20
|
+
* part of the criteria is absent. The reference {@link DriverInterface} backends
|
|
21
|
+
* lean on this rather than each re-deriving it.
|
|
22
|
+
*
|
|
23
|
+
* @param rows - The rows to process (typically a table's full `scan`)
|
|
24
|
+
* @param criteria - The read specification, or `undefined` for all rows as-is
|
|
25
|
+
* @returns The filtered, sorted, paged rows
|
|
26
|
+
*/
|
|
27
|
+
export declare function applyCriteria(rows: readonly Row[], criteria?: Criteria): readonly Row[];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Run the FULL driver-conformance battery and collect every violation — the
|
|
31
|
+
* audit entry point for a driver author who wants a complete report rather
|
|
32
|
+
* than a single fail-fast throw.
|
|
33
|
+
*
|
|
34
|
+
* @remarks
|
|
35
|
+
* Drains {@link driverFindings} to completion: every phase runs regardless
|
|
36
|
+
* of earlier violations, so a driver breaking two independent invariants
|
|
37
|
+
* reports both. An empty array means the driver is fully conformant.
|
|
38
|
+
*
|
|
39
|
+
* @param factory - Mints a fresh, unopened driver instance (called once per phase)
|
|
40
|
+
* @returns Every violated invariant found, in phase order (empty when fully conformant)
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```ts
|
|
44
|
+
* import { auditDriver, createMemoryDriver } from '@orkestrel/database'
|
|
45
|
+
*
|
|
46
|
+
* const findings = await auditDriver(() => createMemoryDriver())
|
|
47
|
+
* for (const finding of findings) console.log(`${finding.check}: ${finding.message}`)
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export declare function auditDriver(factory: () => DriverInterface): Promise<readonly ConformanceFinding[]>;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Throw when an {@link ReadOptions.signal | AbortSignal} has fired — the shared
|
|
54
|
+
* cancellation gate checked at operation boundaries and between streamed rows.
|
|
55
|
+
*
|
|
56
|
+
* @remarks
|
|
57
|
+
* A no-op for `undefined` or a live signal, so callers thread `options?.signal`
|
|
58
|
+
* straight through. When the signal has aborted, throws an `ABORTED`
|
|
59
|
+
* {@link DatabaseError} carrying the signal's `reason` in its context — callers
|
|
60
|
+
* mint signals with whatever tool they like (`AbortSignal.timeout(ms)`,
|
|
61
|
+
* `new AbortController()`, `@orkestrel/abort`).
|
|
62
|
+
*
|
|
63
|
+
* @param signal - The signal to check, if any
|
|
64
|
+
* @returns Nothing — returns normally while the signal is live
|
|
65
|
+
* @throws An `ABORTED` {@link DatabaseError} when the signal has aborted
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```ts
|
|
69
|
+
* import { checkAbort } from '@orkestrel/database'
|
|
70
|
+
*
|
|
71
|
+
* const controller = new AbortController()
|
|
72
|
+
* checkAbort(controller.signal) // returns
|
|
73
|
+
* controller.abort('too slow')
|
|
74
|
+
* checkAbort(controller.signal) // throws DatabaseError('ABORTED', …)
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export declare function checkAbort(signal: AbortSignal | undefined): void;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A pending condition opened by a query's `where` / `and` / `or`.
|
|
81
|
+
*
|
|
82
|
+
* @remarks
|
|
83
|
+
* Holds the column, the connector that will join this condition to the ones
|
|
84
|
+
* before it, and a recorder the owning query supplies. Each operator builds the
|
|
85
|
+
* {@link Condition}, hands it to the recorder, and returns the query — so the
|
|
86
|
+
* fluent chain flows straight back into the builder without exposing a mutator.
|
|
87
|
+
*/
|
|
88
|
+
export declare class Clause<T = Record<string, unknown>> implements ClauseInterface<T> {
|
|
89
|
+
#private;
|
|
90
|
+
constructor(record: (condition: Condition) => QueryInterface<T>, column: FieldPath, connector: Connector);
|
|
91
|
+
equals(value: unknown): QueryInterface<T>;
|
|
92
|
+
not(value: unknown): QueryInterface<T>;
|
|
93
|
+
above(value: unknown): QueryInterface<T>;
|
|
94
|
+
below(value: unknown): QueryInterface<T>;
|
|
95
|
+
from(value: unknown): QueryInterface<T>;
|
|
96
|
+
to(value: unknown): QueryInterface<T>;
|
|
97
|
+
between(lower: unknown, upper: unknown): QueryInterface<T>;
|
|
98
|
+
like(pattern: string): QueryInterface<T>;
|
|
99
|
+
glob(pattern: string): QueryInterface<T>;
|
|
100
|
+
starts(prefix: string): QueryInterface<T>;
|
|
101
|
+
ends(suffix: string): QueryInterface<T>;
|
|
102
|
+
any(values: readonly unknown[]): QueryInterface<T>;
|
|
103
|
+
none(values: readonly unknown[]): QueryInterface<T>;
|
|
104
|
+
absent(): QueryInterface<T>;
|
|
105
|
+
present(): QueryInterface<T>;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* A pending condition opened by `where` / `and` / `or`.
|
|
110
|
+
*
|
|
111
|
+
* @remarks
|
|
112
|
+
* Each operator records its condition against the query and returns the query,
|
|
113
|
+
* so the chain continues fluently. `absent` / `present` take no operand;
|
|
114
|
+
* `between` takes two; `any` / `none` take a list.
|
|
115
|
+
*/
|
|
116
|
+
export declare interface ClauseInterface<T = Row> {
|
|
117
|
+
equals(value: unknown): QueryInterface<T>;
|
|
118
|
+
not(value: unknown): QueryInterface<T>;
|
|
119
|
+
above(value: unknown): QueryInterface<T>;
|
|
120
|
+
below(value: unknown): QueryInterface<T>;
|
|
121
|
+
from(value: unknown): QueryInterface<T>;
|
|
122
|
+
to(value: unknown): QueryInterface<T>;
|
|
123
|
+
between(lower: unknown, upper: unknown): QueryInterface<T>;
|
|
124
|
+
like(pattern: string): QueryInterface<T>;
|
|
125
|
+
glob(pattern: string): QueryInterface<T>;
|
|
126
|
+
starts(prefix: string): QueryInterface<T>;
|
|
127
|
+
ends(suffix: string): QueryInterface<T>;
|
|
128
|
+
any(values: readonly unknown[]): QueryInterface<T>;
|
|
129
|
+
none(values: readonly unknown[]): QueryInterface<T>;
|
|
130
|
+
absent(): QueryInterface<T>;
|
|
131
|
+
present(): QueryInterface<T>;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* One table's columns — a map of column name to its value {@link ContractShape}.
|
|
136
|
+
*
|
|
137
|
+
* @remarks
|
|
138
|
+
* This is exactly the property map an `objectShape` takes. A table row is always
|
|
139
|
+
* an object, so you write the columns directly (`{ id: stringShape(), … }`) and
|
|
140
|
+
* the database wraps them in an `objectShape` for you — no redundant `objectShape`
|
|
141
|
+
* at the table level. (Nested object *columns* still use `objectShape`, since a
|
|
142
|
+
* column is not always an object.)
|
|
143
|
+
*/
|
|
144
|
+
export declare type Columns = Readonly<Record<string, ContractShape>>;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* One column of a {@link TableSchema} — its name, portable {@link ColumnType}, and
|
|
148
|
+
* whether it is nullable (its shape is `optionalShape` / `nullableShape`).
|
|
149
|
+
*/
|
|
150
|
+
export declare interface ColumnSchema {
|
|
151
|
+
readonly name: string;
|
|
152
|
+
readonly type: ColumnType;
|
|
153
|
+
readonly nullable: boolean;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* A portable storage type for a column — the backend maps it to its native type
|
|
158
|
+
* (SQLite affinity, an IndexedDB value). Derived from a column's `ContractShape`
|
|
159
|
+
* by `shapeToColumnType`; `json` covers object/array/union/raw values a backend stores
|
|
160
|
+
* as JSON text and can `json_extract` for nested-field queries.
|
|
161
|
+
*/
|
|
162
|
+
export declare type ColumnType = 'text' | 'integer' | 'real' | 'boolean' | 'json' | 'blob';
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* A total ordering over arbitrary values — the comparator behind sorting and the
|
|
166
|
+
* range operators.
|
|
167
|
+
*
|
|
168
|
+
* @remarks
|
|
169
|
+
* Values of different types order by a fixed type rank (`undefined` < `null` <
|
|
170
|
+
* boolean < number < string < other); same-typed values compare naturally.
|
|
171
|
+
* `NaN` sorts after every other number and equal to itself, so the comparator
|
|
172
|
+
* is total and never returns `NaN`.
|
|
173
|
+
*
|
|
174
|
+
* @param left - The left value
|
|
175
|
+
* @param right - The right value
|
|
176
|
+
* @returns `-1`, `0`, or `1`
|
|
177
|
+
*/
|
|
178
|
+
export declare function compareValues(left: unknown, right: unknown): number;
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Compute an aggregate over a column across rows.
|
|
182
|
+
*
|
|
183
|
+
* @remarks
|
|
184
|
+
* `count` returns the row count. The numeric aggregates coerce each cell with
|
|
185
|
+
* the contracts `parseNumber` (so `'42'` counts) and ignore non-numeric cells;
|
|
186
|
+
* over zero numeric values they return `undefined` — the SQL `NULL` of an empty
|
|
187
|
+
* aggregate.
|
|
188
|
+
*
|
|
189
|
+
* @param rows - The rows to aggregate (non-record entries are ignored)
|
|
190
|
+
* @param operation - The aggregate to compute
|
|
191
|
+
* @param column - The column to aggregate
|
|
192
|
+
* @returns The aggregate value, or `undefined` when undefined for the inputs
|
|
193
|
+
*/
|
|
194
|
+
export declare function computeAggregate(rows: readonly unknown[], operation: AggregateFunction, column: FieldPath): number | undefined;
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* One compiled WHERE condition.
|
|
198
|
+
*
|
|
199
|
+
* @remarks
|
|
200
|
+
* `values` carries the operands the operator needs — none for `absent` /
|
|
201
|
+
* `present`, one for most, two for `between`, a list for `any` / `none`.
|
|
202
|
+
* `connector` folds this condition into the accumulated result left-to-right;
|
|
203
|
+
* the first condition's connector seeds the fold and is otherwise ignored.
|
|
204
|
+
* `column` is a {@link FieldPath}: a single string is ONE column (never split on
|
|
205
|
+
* `.`), an array descends into a nested (object/`json`) value.
|
|
206
|
+
*/
|
|
207
|
+
export declare interface Condition {
|
|
208
|
+
readonly column: FieldPath;
|
|
209
|
+
readonly operator: ConditionOperator;
|
|
210
|
+
readonly values: readonly unknown[];
|
|
211
|
+
readonly connector: Connector;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* A WHERE operator — the comparison a single {@link Condition} applies.
|
|
216
|
+
*
|
|
217
|
+
* @remarks
|
|
218
|
+
* Each maps to a SQL operator and an IndexedDB read strategy (a key range where
|
|
219
|
+
* the bounds allow it, a scanned predicate otherwise). The set is closed to
|
|
220
|
+
* comparisons expressible on both backends.
|
|
221
|
+
*/
|
|
222
|
+
export declare type ConditionOperator = 'equals' | 'not' | 'above' | 'below' | 'from' | 'to' | 'between' | 'like' | 'glob' | 'starts' | 'ends' | 'any' | 'none' | 'absent' | 'present';
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* One violated invariant from the driver-conformance battery.
|
|
226
|
+
*
|
|
227
|
+
* @remarks
|
|
228
|
+
* Mirrors the payload shape of a `DatabaseError` `CONFORMANCE` `context` —
|
|
229
|
+
* `check` names the invariant, `message` describes the violation, and
|
|
230
|
+
* `context` carries the offending table / key / value that failed it.
|
|
231
|
+
*/
|
|
232
|
+
export declare interface ConformanceFinding {
|
|
233
|
+
readonly check: string;
|
|
234
|
+
readonly message: string;
|
|
235
|
+
readonly context: Readonly<Record<string, unknown>>;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Run the driver-conformance battery, throwing on the first violated
|
|
240
|
+
* invariant — the fail-fast entry point most callers (test setup, CI smoke
|
|
241
|
+
* checks) want.
|
|
242
|
+
*
|
|
243
|
+
* @remarks
|
|
244
|
+
* A thin driver over {@link driverFindings}: because that generator is
|
|
245
|
+
* lazy, consuming only its first yielded value means every LATER phase
|
|
246
|
+
* never runs — true fail-fast, not merely "report only the first". The
|
|
247
|
+
* thrown error is byte-compatible with the historical shape: a
|
|
248
|
+
* `CONFORMANCE` {@link DatabaseError} whose `message` is the finding's
|
|
249
|
+
* `message` and whose `context` is `{ check, ...finding.context }`.
|
|
250
|
+
*
|
|
251
|
+
* @param factory - Mints a fresh, unopened driver instance (called once per phase)
|
|
252
|
+
* @returns Nothing — resolves once every phase has passed
|
|
253
|
+
* @throws A `CONFORMANCE` {@link DatabaseError} on the first violated invariant
|
|
254
|
+
*
|
|
255
|
+
* @example
|
|
256
|
+
* ```ts
|
|
257
|
+
* import { conformDriver, createMemoryDriver } from '@orkestrel/database'
|
|
258
|
+
*
|
|
259
|
+
* await conformDriver(() => createMemoryDriver()) // resolves when every invariant holds
|
|
260
|
+
* ```
|
|
261
|
+
*/
|
|
262
|
+
export declare function conformDriver(factory: () => DriverInterface): Promise<void>;
|
|
263
|
+
|
|
264
|
+
/** How a {@link Condition} joins to the running result of the conditions before it. */
|
|
265
|
+
export declare type Connector = 'and' | 'or';
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Create a database over a driver and a declared `tables` schema.
|
|
269
|
+
*
|
|
270
|
+
* @remarks
|
|
271
|
+
* `tables` maps each name to its columns (a `column → shape` map); the database
|
|
272
|
+
* wraps each in an `objectShape`, so you never write `objectShape` at the table
|
|
273
|
+
* level. The `const` type parameter captures the literal names and columns, so
|
|
274
|
+
* `db.table('users')` is checked against the schema and typed by `Infer` of its
|
|
275
|
+
* columns — no annotations. Name a non-`id` primary-key column per table via the
|
|
276
|
+
* optional `keys` map.
|
|
277
|
+
*
|
|
278
|
+
* @param options - The driver, the `tables` column map, optional `keys`, and an
|
|
279
|
+
* optional `name`
|
|
280
|
+
* @returns A typed {@link DatabaseInterface}
|
|
281
|
+
*
|
|
282
|
+
* @example
|
|
283
|
+
* ```ts
|
|
284
|
+
* import { createDatabase, createMemoryDriver } from '@orkestrel/database'
|
|
285
|
+
* import { integerShape, stringShape } from '@orkestrel/contract'
|
|
286
|
+
*
|
|
287
|
+
* const db = createDatabase({
|
|
288
|
+
* driver: createMemoryDriver(),
|
|
289
|
+
* tables: {
|
|
290
|
+
* users: { id: stringShape(), age: integerShape() },
|
|
291
|
+
* posts: { slug: stringShape(), title: stringShape() },
|
|
292
|
+
* },
|
|
293
|
+
* keys: { posts: 'slug' },
|
|
294
|
+
* })
|
|
295
|
+
* await db.table('users').set({ id: 'u1', age: 36 }) // typed; coerced + validated
|
|
296
|
+
* ```
|
|
297
|
+
*/
|
|
298
|
+
export declare function createDatabase<const T extends TablesShape>(options: DatabaseOptions<T>): DatabaseInterface<T>;
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Create the in-memory reference {@link DriverInterface}.
|
|
302
|
+
*
|
|
303
|
+
* @remarks
|
|
304
|
+
* Backed by nested maps with no I/O — the same driver runs in a browser or on a
|
|
305
|
+
* server, making it the natural choice for tests and ephemeral storage.
|
|
306
|
+
*
|
|
307
|
+
* @returns A fresh in-memory driver
|
|
308
|
+
*/
|
|
309
|
+
export declare function createMemoryDriver(): DriverInterface;
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* A serializable read specification — everything a backend needs to compile one
|
|
313
|
+
* read, free of JS callbacks so any backend can honor it.
|
|
314
|
+
*
|
|
315
|
+
* @remarks
|
|
316
|
+
* The post-fetch `filter` predicate lives on {@link QueryInterface}, never here,
|
|
317
|
+
* so `Criteria` stays portable across backends.
|
|
318
|
+
*/
|
|
319
|
+
export declare interface Criteria {
|
|
320
|
+
readonly conditions?: readonly Condition[];
|
|
321
|
+
readonly order?: readonly Order[];
|
|
322
|
+
readonly limit?: number;
|
|
323
|
+
readonly offset?: number;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* A forward row cursor for bulk in-place mutation.
|
|
328
|
+
*
|
|
329
|
+
* @remarks
|
|
330
|
+
* Iterates a snapshot of the table's keys captured when the cursor was opened,
|
|
331
|
+
* reading each row lazily through the owning table — so a mutation made during
|
|
332
|
+
* iteration cannot corrupt the walk, and a key removed mid-iteration is simply
|
|
333
|
+
* skipped. `update` and `remove` act on the row at the current position.
|
|
334
|
+
*/
|
|
335
|
+
export declare class Cursor<T = Record<string, unknown>> implements CursorInterface<T> {
|
|
336
|
+
#private;
|
|
337
|
+
constructor(table: TableInterface<T>, keys: readonly Key[]);
|
|
338
|
+
get value(): T | undefined;
|
|
339
|
+
get index(): number;
|
|
340
|
+
get done(): boolean;
|
|
341
|
+
next(): Promise<void>;
|
|
342
|
+
update(changes: Partial<T>): Promise<void>;
|
|
343
|
+
remove(): Promise<void>;
|
|
344
|
+
close(): void;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* A forward row cursor for bulk in-place mutation.
|
|
349
|
+
*
|
|
350
|
+
* @remarks
|
|
351
|
+
* Iterates a snapshot of the table's keys taken at creation; `update` and
|
|
352
|
+
* `remove` act on the row at the current position through the owning table.
|
|
353
|
+
* `done` is `true` once iteration has advanced past the last key.
|
|
354
|
+
*/
|
|
355
|
+
export declare interface CursorInterface<T = Row> {
|
|
356
|
+
readonly value: T | undefined;
|
|
357
|
+
readonly index: number;
|
|
358
|
+
readonly done: boolean;
|
|
359
|
+
next(): Promise<void>;
|
|
360
|
+
update(changes: Partial<T>): Promise<void>;
|
|
361
|
+
remove(): Promise<void>;
|
|
362
|
+
close(): void;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* A database — the ergonomic entry point over a {@link DriverInterface}.
|
|
367
|
+
*
|
|
368
|
+
* @remarks
|
|
369
|
+
* Owns the driver and a `tables` shape map, connecting the driver lazily on first
|
|
370
|
+
* use so a freshly created database is immediately usable. `table(name)` returns
|
|
371
|
+
* a table typed by that table's shape `Infer`. `import` registers more tables and
|
|
372
|
+
* returns a database re-typed with them over the **same** driver and storage;
|
|
373
|
+
* `export` emits a portable {@link TableExport} per table. `transaction` snapshots
|
|
374
|
+
* the driver, runs the scope, and rolls every table back if it throws — an
|
|
375
|
+
* optimistic model that works uniformly across backends rather than reconciling
|
|
376
|
+
* SQL's and IndexedDB's incompatible native transactions.
|
|
377
|
+
*
|
|
378
|
+
* @remarks
|
|
379
|
+
* - **Versioned (optional).** When {@link DatabaseOptions.version} is set and the driver
|
|
380
|
+
* implements both {@link DriverInterface.meta} and {@link DriverInterface.stamp},
|
|
381
|
+
* `open()` reconciles the driver's persisted {@link DriverMeta} against the declared
|
|
382
|
+
* version INSIDE the same lazy-connect chain, AFTER the `open` event fires — see
|
|
383
|
+
* {@link DatabaseOptions.version} for the full reconciliation contract.
|
|
384
|
+
* - **Observable (§13).** The owned {@link emitter} ({@link DatabaseEventMap}) carries the
|
|
385
|
+
* connection + transaction lifecycle — `open` / `close` / `transaction` / `commit` /
|
|
386
|
+
* `rollback` — for fire-and-forget observers, ALONGSIDE each table's per-row events. Every
|
|
387
|
+
* event is emitted directly, strictly AFTER the relevant transition: `commit` only after
|
|
388
|
+
* the scope succeeds, `rollback` only after every table is restored. The `rollback` emit
|
|
389
|
+
* OBSERVES the propagated error — it never swallows it (the original throw propagates
|
|
390
|
+
* exactly as before). The emitter isolates a listener throw and routes it to its `error`
|
|
391
|
+
* handler (the `error` option), so observation can never reorder, throw into, or corrupt
|
|
392
|
+
* the snapshot / commit / rollback flow.
|
|
393
|
+
*/
|
|
394
|
+
export declare class Database<T extends TablesShape = TablesShape> implements DatabaseInterface<T> {
|
|
395
|
+
#private;
|
|
396
|
+
constructor(options: DatabaseOptions<T>);
|
|
397
|
+
get emitter(): EmitterInterface<DatabaseEventMap>;
|
|
398
|
+
get name(): string;
|
|
399
|
+
get status(): DatabaseStatus;
|
|
400
|
+
table<K extends keyof T & string>(name: K): TableInterface<RowOf<T[K]>>;
|
|
401
|
+
import<U extends TablesShape>(tables: U, keys?: TableKeys): DatabaseInterface<U>;
|
|
402
|
+
export(): Readonly<Record<string, TableExport>>;
|
|
403
|
+
open(): Promise<void>;
|
|
404
|
+
close(): Promise<void>;
|
|
405
|
+
/**
|
|
406
|
+
* Run `scope` transactionally: commit its writes on success, roll every table
|
|
407
|
+
* back if it throws.
|
|
408
|
+
*
|
|
409
|
+
* @remarks
|
|
410
|
+
* When the driver implements the optional native {@link DriverInterface.transaction}
|
|
411
|
+
* hook, that native `commit` / `rollback` handle drives the transaction; otherwise
|
|
412
|
+
* the universal snapshot floor (`driver.snapshot()`) runs unchanged. Either path
|
|
413
|
+
* emits the same `transaction` / `commit` / `rollback` lifecycle (AGENTS §13).
|
|
414
|
+
* `options.signal` is checked ONCE at entry, before connecting or starting any
|
|
415
|
+
* transactional work — an already-aborted signal throws `ABORTED` and neither the
|
|
416
|
+
* native hook nor the snapshot floor is invoked. Nesting is unguarded and
|
|
417
|
+
* unsupported exactly as before: this is a single-writer model, not reentrant.
|
|
418
|
+
* On the native path, a `scope` throw rolls back via the native handle; a
|
|
419
|
+
* native `commit` failure propagates as-is with no rollback attempt — the
|
|
420
|
+
* engine owns transaction state after a failed COMMIT.
|
|
421
|
+
*
|
|
422
|
+
* @param scope - The transactional work to run
|
|
423
|
+
* @param options - `{ signal }` to abort before the transaction starts
|
|
424
|
+
* @returns The scope's resolved value
|
|
425
|
+
* @throws An `ABORTED` {@link DatabaseError} when `options.signal` has already fired
|
|
426
|
+
*/
|
|
427
|
+
transaction<R>(scope: () => Promise<R>, options?: ReadOptions): Promise<R>;
|
|
428
|
+
/**
|
|
429
|
+
* Diff `deployed` against this database's declared schema and apply the
|
|
430
|
+
* resulting plan through the driver's optional `migrate` hook.
|
|
431
|
+
*
|
|
432
|
+
* @param deployed - The schema currently deployed, as {@link TableSchema}s
|
|
433
|
+
* @param options - `{ signal }` to abort before the migration starts
|
|
434
|
+
* @returns The applied {@link Migration} plan
|
|
435
|
+
* @throws A `MIGRATION` {@link DatabaseError} when the driver does not
|
|
436
|
+
* implement `migrate`, or when a step references an unknown table
|
|
437
|
+
* (propagated from the driver)
|
|
438
|
+
* @throws An `ABORTED` {@link DatabaseError} when `options.signal` has
|
|
439
|
+
* already fired at entry
|
|
440
|
+
*/
|
|
441
|
+
migrate(deployed: readonly TableSchema[], options?: ReadOptions): Promise<Migration>;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* An error thrown by the database layer.
|
|
446
|
+
*
|
|
447
|
+
* @remarks
|
|
448
|
+
* Carries a {@link DatabaseErrorCode} and an optional `context` bag naming the
|
|
449
|
+
* offending table / key. Thrown for: operating on a closed database (`CLOSED`), a
|
|
450
|
+
* `resolve` miss (`NOT_FOUND`), an `add` onto an existing key (`CONFLICT`), a
|
|
451
|
+
* row that fails its table's contract (`VALIDATION`), a cancelled operation whose
|
|
452
|
+
* {@link ReadOptions.signal} aborted (`ABORTED`, carrying `signal.reason` in
|
|
453
|
+
* `context`), an inapplicable {@link Migration} plan (`MIGRATION`), a
|
|
454
|
+
* driver that violates a {@link DriverInterface} invariant, thrown by the
|
|
455
|
+
* `conformDriver` helper (`CONFORMANCE`), and an unexpected infrastructure
|
|
456
|
+
* fault surfaced by a driver seam — e.g. a filesystem failure while
|
|
457
|
+
* persisting (`DRIVER`) — as opposed to expected domain conditions, which
|
|
458
|
+
* keep their specific codes.
|
|
459
|
+
*/
|
|
460
|
+
export declare class DatabaseError extends Error {
|
|
461
|
+
readonly code: DatabaseErrorCode;
|
|
462
|
+
readonly context?: Readonly<Record<string, unknown>>;
|
|
463
|
+
constructor(code: DatabaseErrorCode, message: string, context?: Readonly<Record<string, unknown>>);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** A machine-readable {@link DatabaseError} code. */
|
|
467
|
+
export declare type DatabaseErrorCode = 'CLOSED' | 'NOT_FOUND' | 'CONFLICT' | 'VALIDATION' | 'ABORTED' | 'MIGRATION' | 'CONFORMANCE' | 'DRIVER';
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* The push observation surface of a {@link DatabaseInterface} (AGENTS §13) — the
|
|
471
|
+
* connection + transaction lifecycle a fire-and-forget observer (logging, metrics,
|
|
472
|
+
* tracing, cache invalidation) subscribes to.
|
|
473
|
+
*
|
|
474
|
+
* @remarks
|
|
475
|
+
* Pure signals carrying no row data — these are the database-level (not per-row)
|
|
476
|
+
* moments, so a non-generic map stays lean (per-row writes are {@link TableEventMap}).
|
|
477
|
+
* Listener isolation is the emitter's (AGENTS §13): every event is emitted directly and a
|
|
478
|
+
* listener throw is routed to the emitter's OWN `error` handler (the `error` option), never
|
|
479
|
+
* onto this domain map and never into the snapshot / commit / rollback flow — so a buggy
|
|
480
|
+
* observer can never reorder, throw into, or corrupt a transaction. Every emit sits AFTER the
|
|
481
|
+
* relevant transition: `commit` only after the scope succeeds, `rollback` only after every
|
|
482
|
+
* table has been restored (it OBSERVES the propagated error; the original throw still
|
|
483
|
+
* propagates exactly as before). Subscribe via `database.emitter.on(...)`.
|
|
484
|
+
*
|
|
485
|
+
* Declared as a `type` alias (not `interface extends EventMap`, §4.5 — `EventMap` is a
|
|
486
|
+
* `type` kind): a type-literal satisfies the `EventMap` constraint
|
|
487
|
+
* (`Record<string, readonly unknown[]>`) structurally, whereas an interface lacks the
|
|
488
|
+
* required index signature.
|
|
489
|
+
*/
|
|
490
|
+
export declare type DatabaseEventMap = {
|
|
491
|
+
/** The driver connected (`open`, or the lazy first-use connect completed). */
|
|
492
|
+
readonly open: readonly [];
|
|
493
|
+
/** The database was closed (the driver released). */
|
|
494
|
+
readonly close: readonly [];
|
|
495
|
+
/** A transaction scope began — the store was snapshotted, the scope is about to run. */
|
|
496
|
+
readonly transaction: readonly [];
|
|
497
|
+
/** A transaction scope completed successfully (no rollback). */
|
|
498
|
+
readonly commit: readonly [];
|
|
499
|
+
/** A transaction scope threw and every table was rolled back — the propagated error. */
|
|
500
|
+
readonly rollback: readonly [error: unknown];
|
|
501
|
+
/** A {@link Migration} plan was applied via `migrate` — the applied plan. */
|
|
502
|
+
readonly migrate: readonly [migration: Migration];
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* A database — the ergonomic entry point that owns the driver and its tables.
|
|
507
|
+
*
|
|
508
|
+
* @remarks
|
|
509
|
+
* A database is a typed view over a set of tables on one driver. Tables are
|
|
510
|
+
* declared up front in `createDatabase({ tables })` and reached, fully typed,
|
|
511
|
+
* with `table(name)`. The driver connects lazily on first use, so a freshly
|
|
512
|
+
* created database is immediately usable. `import` defines more than one table
|
|
513
|
+
* from a shape map and returns a new typed view of **those** tables over the
|
|
514
|
+
* same driver and storage (so views can be split by concern and still share
|
|
515
|
+
* data); `export` produces a portable {@link TableExport} per table for moving a
|
|
516
|
+
* schema between databases or environments. `transaction` snapshots the store,
|
|
517
|
+
* runs the scope, and rolls every table back if it throws — an optimistic model
|
|
518
|
+
* that works uniformly across backends rather than reconciling SQL's and
|
|
519
|
+
* IndexedDB's incompatible native transactions.
|
|
520
|
+
*/
|
|
521
|
+
export declare interface DatabaseInterface<T extends TablesShape = TablesShape> {
|
|
522
|
+
readonly emitter: EmitterInterface<DatabaseEventMap>;
|
|
523
|
+
readonly name: string;
|
|
524
|
+
readonly status: DatabaseStatus;
|
|
525
|
+
table<K extends keyof T & string>(name: K): TableInterface<RowOf<T[K]>>;
|
|
526
|
+
import<U extends TablesShape>(tables: U, keys?: TableKeys): DatabaseInterface<U>;
|
|
527
|
+
export(): Readonly<Record<string, TableExport>>;
|
|
528
|
+
open(): Promise<void>;
|
|
529
|
+
close(): Promise<void>;
|
|
530
|
+
transaction<R>(scope: () => Promise<R>, options?: ReadOptions): Promise<R>;
|
|
531
|
+
/**
|
|
532
|
+
* Diff a caller-supplied deployed schema against this database's declared
|
|
533
|
+
* schema (its `tables`, as configured) via `planMigration`, apply the
|
|
534
|
+
* resulting plan through the driver's optional `migrate` hook, and return
|
|
535
|
+
* the applied plan.
|
|
536
|
+
*
|
|
537
|
+
* @param deployed - The schema currently deployed, as {@link TableSchema}s
|
|
538
|
+
* @param options - Optional abort signal, checked at entry
|
|
539
|
+
* @returns The applied {@link Migration} plan
|
|
540
|
+
*
|
|
541
|
+
* @remarks
|
|
542
|
+
* Throws `DatabaseError` `MIGRATION` when the driver does not implement
|
|
543
|
+
* `migrate`, or when a step references an unknown table (propagated from
|
|
544
|
+
* the driver). Throws `ABORTED` when `options.signal` has already fired at
|
|
545
|
+
* entry. Emits the `migrate` event after a successful apply. Version
|
|
546
|
+
* TRACKING (persisting `from` / `to`) remains deferred to persistent
|
|
547
|
+
* backends — the caller owns knowing what was deployed.
|
|
548
|
+
*/
|
|
549
|
+
migrate(deployed: readonly TableSchema[], options?: ReadOptions): Promise<Migration>;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Options for `createDatabase`.
|
|
554
|
+
*
|
|
555
|
+
* @remarks
|
|
556
|
+
* `driver` is the storage backend; `tables` declares each table's columns;
|
|
557
|
+
* `keys` overrides the primary-key column per table ({@link DEFAULT_PRIMARY}
|
|
558
|
+
* otherwise); `indexes` declares secondary indexes per table (contracts don't
|
|
559
|
+
* express them) that flow into each derived {@link TableSchema}; `name` labels
|
|
560
|
+
* the database; `on` wires initial {@link DatabaseEventMap} listeners (§8); `error`
|
|
561
|
+
* is the emitter's listener-error handler (§13 — a listener throw routes here);
|
|
562
|
+
* `key` is the key factory a table uses when a written row lacks its primary
|
|
563
|
+
* key — without one, writing a keyless row is a `VALIDATION` error (the core
|
|
564
|
+
* mints no keys itself).
|
|
565
|
+
*/
|
|
566
|
+
export declare interface DatabaseOptions<T extends TablesShape = TablesShape> {
|
|
567
|
+
readonly on?: EmitterHooks<DatabaseEventMap>;
|
|
568
|
+
/** The emitter's listener-error handler (AGENTS §13) — a listener throw routes here, not to a domain event. */
|
|
569
|
+
readonly error?: EmitterErrorHandler;
|
|
570
|
+
readonly driver: DriverInterface;
|
|
571
|
+
readonly tables: T;
|
|
572
|
+
readonly keys?: TableKeys;
|
|
573
|
+
readonly indexes?: TableIndexes;
|
|
574
|
+
readonly name?: string;
|
|
575
|
+
readonly key?: KeyFunction;
|
|
576
|
+
/**
|
|
577
|
+
* The declared schema version.
|
|
578
|
+
*
|
|
579
|
+
* @remarks
|
|
580
|
+
* Only meaningful when the driver implements BOTH {@link DriverInterface.meta}
|
|
581
|
+
* and {@link DriverInterface.stamp} (a versioning driver); unset, or a
|
|
582
|
+
* non-versioning driver, leaves `open()` unchanged from today's behavior.
|
|
583
|
+
* When set and the driver versions, `open()` reconciles against the
|
|
584
|
+
* driver's persisted {@link DriverMeta}:
|
|
585
|
+
* - **Fresh store** (`meta()` returns `undefined`) — no migration is
|
|
586
|
+
* possible (there is nothing deployed to diff against), so `open()`
|
|
587
|
+
* simply `stamp`s `{ version, schema }` for next time.
|
|
588
|
+
* - **Stored version < `version`** — `planMigration(stored.schema, declared
|
|
589
|
+
* schema)` computes the upgrade plan, applied via the driver's optional
|
|
590
|
+
* `migrate` hook. If `migrate` is absent and the plan is non-empty,
|
|
591
|
+
* `open()` throws `DatabaseError` `MIGRATION`. On success, `open()`
|
|
592
|
+
* `stamp`s the new `{ version, schema }` and emits the `migrate` event.
|
|
593
|
+
* - **Stored version > `version`** — the store is newer than the declared
|
|
594
|
+
* schema; `open()` throws `DatabaseError` `MIGRATION`.
|
|
595
|
+
* - **Stored version === `version`** — no-op.
|
|
596
|
+
*
|
|
597
|
+
* When the driver ALSO implements {@link DriverInterface.transaction}, the
|
|
598
|
+
* `migrate` + `stamp` pair applies atomically through that native handle
|
|
599
|
+
* (all-or-nothing, rolled back cleanly on a mid-plan failure); otherwise the
|
|
600
|
+
* pair applies sequentially, with a small documented window in which a `stamp`
|
|
601
|
+
* failure after a successful `migrate` can leave new data under old meta.
|
|
602
|
+
*/
|
|
603
|
+
readonly version?: number;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/** The lifecycle state of a {@link DatabaseInterface}. */
|
|
607
|
+
export declare type DatabaseStatus = 'idle' | 'open' | 'closed';
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Structural equality by SameValueZero leaves — the comparator behind conformance
|
|
611
|
+
* checks and any test/fixture that needs "same data", not "same reference".
|
|
612
|
+
*
|
|
613
|
+
* @remarks
|
|
614
|
+
* Primitives compare by SameValueZero (`NaN` equals itself; `+0` equals `-0`).
|
|
615
|
+
* Arrays compare by index (same length, every element `deepEqual`). Plain
|
|
616
|
+
* records (via `isRecord`) compare by their OWN enumerable keys: same key
|
|
617
|
+
* COUNT and, for every key in `left`, `right` has that key (`Object.hasOwn`)
|
|
618
|
+
* with a `deepEqual` value — so a key present with value `undefined` is NOT
|
|
619
|
+
* equal to that key being absent (both differ in `Object.keys` membership).
|
|
620
|
+
* Anything else (functions, class instances, mismatched shapes) falls through
|
|
621
|
+
* to `false`. There is no cycle detection — a cyclic input recurses forever;
|
|
622
|
+
* callers pass acyclic data (rows, plans, config).
|
|
623
|
+
*
|
|
624
|
+
* @param left - The left value
|
|
625
|
+
* @param right - The right value
|
|
626
|
+
* @returns Whether `left` and `right` are structurally equal
|
|
627
|
+
*
|
|
628
|
+
* @example
|
|
629
|
+
* ```ts
|
|
630
|
+
* deepEqual(Number.NaN, Number.NaN) // true
|
|
631
|
+
* deepEqual({ a: [1, { b: 2 }] }, { a: [1, { b: 2 }] }) // true
|
|
632
|
+
* deepEqual({ a: undefined }, {}) // false — present-undefined ≠ absent
|
|
633
|
+
* ```
|
|
634
|
+
*/
|
|
635
|
+
export declare function deepEqual(left: unknown, right: unknown): boolean;
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* The primary-key column assumed when {@link TableKeys} does not name one.
|
|
639
|
+
*
|
|
640
|
+
* @remarks
|
|
641
|
+
* `id` is the convention IndexedDB (`keyPath: 'id'`) and SQL (`id` / rowid) both
|
|
642
|
+
* lean on, so a table that omits `key` keys its rows by `id`.
|
|
643
|
+
*/
|
|
644
|
+
export declare const DEFAULT_PRIMARY = "id";
|
|
645
|
+
|
|
646
|
+
/** A sort direction. */
|
|
647
|
+
export declare type Direction = 'ascending' | 'descending';
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Run the driver-conformance battery against a fresh {@link DriverInterface}
|
|
651
|
+
* per phase, yielding one {@link ConformanceFinding} per violated invariant —
|
|
652
|
+
* the shared invariant suite every backend (in-memory, SQLite, IndexedDB)
|
|
653
|
+
* must uphold to be a drop-in {@link DriverInterface}.
|
|
654
|
+
*
|
|
655
|
+
* @remarks
|
|
656
|
+
* Framework-agnostic: no test-runner or Node imports, only sibling core
|
|
657
|
+
* modules — so it runs equally from a unit test, a smoke script, or a new
|
|
658
|
+
* driver's own README. Opens a fixed two-table schema (`users` keyed by the
|
|
659
|
+
* default `id`, `posts` keyed by a non-id `slug`) and, calling `factory()`
|
|
660
|
+
* fresh for each phase so failures stay isolated, verifies: `open`/`close`;
|
|
661
|
+
* `read` of a missing key returns `undefined`; `write`/`read` round-trip with
|
|
662
|
+
* DEEP copy-in/copy-out isolation (mutating the caller's row — including a
|
|
663
|
+
* NESTED field — after `write`, or a row `read` returns, never perturbs
|
|
664
|
+
* stored state) and upsert-overwrite; `delete` returns `true` then `false`;
|
|
665
|
+
* `keys`/`scan` yield in ascending key order; `clear` empties only its target
|
|
666
|
+
* table; `snapshot`'s rollback thunk restores pre-snapshot state, including a
|
|
667
|
+
* NESTED field mutated in place on a read-back row between capture and
|
|
668
|
+
* restore; a scoped `snapshot(['users'])` rolls back only the named table,
|
|
669
|
+
* leaving a concurrent mutation to another table intact; a
|
|
670
|
+
* non-`id` primary key (`posts.slug`) round-trips; a nested-object row
|
|
671
|
+
* round-trips structurally (via {@link deepEqual}). The optional surface is
|
|
672
|
+
* presence-gated: when `migrate` exists, a `column.remove` plan strips the
|
|
673
|
+
* column from stored rows and a plan referencing an unknown table throws
|
|
674
|
+
* `DatabaseError` `MIGRATION`; when `stream` exists, it yields only
|
|
675
|
+
* condition-matching rows and honors `offset`/`limit`; when `transaction`
|
|
676
|
+
* exists, `commit` persists and `rollback` restores; when both `meta` and
|
|
677
|
+
* `stamp` exist, a fresh store's `meta()` is `undefined`, and after
|
|
678
|
+
* `stamp({ version, schema })`, `meta()` returns the exact stamped value.
|
|
679
|
+
*
|
|
680
|
+
* Each phase runs within a `try`/`catch`: an EXPECTED mismatch yields a
|
|
681
|
+
* finding built from the assertion, while an UNEXPECTED throw (a driver
|
|
682
|
+
* crash mid-phase) is caught and yielded as a finding too, naming the phase
|
|
683
|
+
* as `check` and carrying the caught error in `context.error` — a broken
|
|
684
|
+
* driver can never escape the battery as an unhandled rejection. Within a
|
|
685
|
+
* phase, the FIRST violated assertion yields and the phase stops (matching
|
|
686
|
+
* the historical fail-fast shape at phase granularity); the generator then
|
|
687
|
+
* moves on to the next phase regardless. Because this is a **generator**,
|
|
688
|
+
* consuming only the first yielded value reproduces true fail-fast (later
|
|
689
|
+
* phases never run) — that is exactly what {@link conformDriver} does.
|
|
690
|
+
*
|
|
691
|
+
* @param factory - Mints a fresh, unopened driver instance (called once per phase)
|
|
692
|
+
* @yields One {@link ConformanceFinding} per violated invariant, in phase order
|
|
693
|
+
*
|
|
694
|
+
* @example
|
|
695
|
+
* ```ts
|
|
696
|
+
* import { createMemoryDriver, driverFindings } from '@orkestrel/database'
|
|
697
|
+
*
|
|
698
|
+
* for await (const finding of driverFindings(() => createMemoryDriver())) {
|
|
699
|
+
* console.log(finding.check, finding.message)
|
|
700
|
+
* }
|
|
701
|
+
* ```
|
|
702
|
+
*/
|
|
703
|
+
export declare function driverFindings(factory: () => DriverInterface): AsyncIterable<ConformanceFinding>;
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* The storage primitive every backend implements — the whole of the bridge.
|
|
707
|
+
*
|
|
708
|
+
* @remarks
|
|
709
|
+
* The REQUIRED surface is deliberately minimal: keyed read / write / delete, an
|
|
710
|
+
* ordered `scan`, a key listing, and a `snapshot` that backs transactions — the
|
|
711
|
+
* irreducible primitive. There is **no** required query, count, or aggregate
|
|
712
|
+
* here: all of that is one query engine in the core (`helpers.ts`) running over
|
|
713
|
+
* `scan`, so a new backend implements a handful of tiny methods rather than
|
|
714
|
+
* re-deriving WHERE compilation. `open` now receives a derived
|
|
715
|
+
* {@link TableSchema}`[]` (columns, types, primary, indexes) so a native backend
|
|
716
|
+
* can build real tables and indexes; a scan-only backend reads only `name`. The
|
|
717
|
+
* optional `records?` / `count?` / `aggregate?` are native overrides the engine
|
|
718
|
+
* falls back from (AGENTS §21). The API is async (Promises) because IndexedDB is; synchronous
|
|
719
|
+
* backends resolve immediately. Lookups that may miss return `undefined` /
|
|
720
|
+
* `false` rather than throwing (AGENTS §12).
|
|
721
|
+
*/
|
|
722
|
+
export declare interface DriverInterface {
|
|
723
|
+
open(schema: readonly TableSchema[]): Promise<void>;
|
|
724
|
+
close(): Promise<void>;
|
|
725
|
+
read(table: string, key: Key): Promise<Row | undefined>;
|
|
726
|
+
write(table: string, key: Key, row: Row): Promise<void>;
|
|
727
|
+
delete(table: string, key: Key): Promise<boolean>;
|
|
728
|
+
keys(table: string): Promise<readonly Key[]>;
|
|
729
|
+
scan(table: string): AsyncIterable<Row>;
|
|
730
|
+
clear(table: string): Promise<void>;
|
|
731
|
+
/**
|
|
732
|
+
* Capture the current state and return a thunk that rolls every table back to
|
|
733
|
+
* it — the primitive transactions are built on (SQL `SAVEPOINT`, an IndexedDB
|
|
734
|
+
* key buffer, a cloned map).
|
|
735
|
+
*
|
|
736
|
+
* @remarks
|
|
737
|
+
* `tables` omitted captures/rolls back the WHOLE store (existing behavior).
|
|
738
|
+
* `tables` provided captures/restores ONLY the named tables — the returned
|
|
739
|
+
* rollback thunk leaves every other table untouched.
|
|
740
|
+
*/
|
|
741
|
+
snapshot(tables?: readonly string[]): Promise<() => Promise<void>>;
|
|
742
|
+
/**
|
|
743
|
+
* Optional native filtered read (AGENTS §21). A backend that can evaluate a
|
|
744
|
+
* {@link Criteria} natively (SQL `WHERE` + `ORDER`/`LIMIT`, an index range)
|
|
745
|
+
* implements this; `Table` prefers it and falls back to `applyCriteria` over
|
|
746
|
+
* `scan` when it is absent. Must honor the full criteria (filter, order, page).
|
|
747
|
+
*/
|
|
748
|
+
records?(table: string, criteria: Criteria): Promise<readonly Row[]>;
|
|
749
|
+
/**
|
|
750
|
+
* Optional native count (AGENTS §21). Counts rows matching the criteria's
|
|
751
|
+
* conditions (paging is irrelevant to a count); `Table` falls back to counting
|
|
752
|
+
* the engine-filtered `scan` when absent.
|
|
753
|
+
*/
|
|
754
|
+
count?(table: string, criteria: Criteria): Promise<number>;
|
|
755
|
+
/**
|
|
756
|
+
* Optional native aggregate (AGENTS §21). A backend that can compute an
|
|
757
|
+
* aggregate natively (SQL `COUNT`/`SUM`/`AVG`/`MIN`/`MAX`, an indexed count)
|
|
758
|
+
* implements this; `Table.aggregate` prefers it and otherwise falls back to
|
|
759
|
+
* `computeAggregate` over the native-filtered (or scanned) rows. Aggregates
|
|
760
|
+
* ignore paging, so `criteria` carries only conditions.
|
|
761
|
+
*/
|
|
762
|
+
aggregate?(table: string, operation: AggregateFunction, column: FieldPath, criteria: Criteria): Promise<number | undefined>;
|
|
763
|
+
/**
|
|
764
|
+
* Optional native transaction (BEGIN). When present, the engine uses the
|
|
765
|
+
* returned {@link TransactionInterface}'s `commit` / `rollback` instead of
|
|
766
|
+
* the snapshot-based rollback floor.
|
|
767
|
+
*/
|
|
768
|
+
transaction?(): Promise<TransactionInterface>;
|
|
769
|
+
/**
|
|
770
|
+
* Optional natively filtered lazy iteration — a {@link Criteria}-aware
|
|
771
|
+
* streaming read. Drivers without it are served by the core scan fallback
|
|
772
|
+
* (filtering `scan` lazily).
|
|
773
|
+
*/
|
|
774
|
+
stream?(table: string, criteria: Criteria): AsyncIterable<Row>;
|
|
775
|
+
/**
|
|
776
|
+
* Optional native migration — applies a {@link Migration} plan directly.
|
|
777
|
+
* Throws `DatabaseError` `MIGRATION` when a step references an unknown
|
|
778
|
+
* table.
|
|
779
|
+
*/
|
|
780
|
+
migrate?(plan: Migration): Promise<void>;
|
|
781
|
+
/**
|
|
782
|
+
* Optional persisted-metadata read (PAIRED with {@link stamp} — a driver
|
|
783
|
+
* implements both or neither). Returns the {@link DriverMeta} last stamped,
|
|
784
|
+
* or `undefined` when the store has never been stamped.
|
|
785
|
+
*/
|
|
786
|
+
meta?(): Promise<DriverMeta | undefined>;
|
|
787
|
+
/**
|
|
788
|
+
* Optional persisted-metadata write (PAIRED with {@link meta} — a driver
|
|
789
|
+
* implements both or neither). Persists `meta` verbatim for a later `meta()`
|
|
790
|
+
* to return.
|
|
791
|
+
*/
|
|
792
|
+
stamp?(meta: DriverMeta): Promise<void>;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* Persisted schema metadata a versioning driver stores verbatim and returns on
|
|
797
|
+
* demand.
|
|
798
|
+
*
|
|
799
|
+
* @remarks
|
|
800
|
+
* The driver never introspects this payload — it hands back exactly what was
|
|
801
|
+
* last stamped via {@link DriverInterface.stamp}. `meta()` returning `undefined`
|
|
802
|
+
* is how a fresh store is distinguished from an upgradable one: it means the
|
|
803
|
+
* store has never been stamped, not that it is at version zero.
|
|
804
|
+
*/
|
|
805
|
+
export declare interface DriverMeta {
|
|
806
|
+
readonly version: number;
|
|
807
|
+
readonly schema: readonly TableSchema[];
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* Read a row's primary key from a column, when it is a usable {@link Key}.
|
|
812
|
+
*
|
|
813
|
+
* @param row - The row to read
|
|
814
|
+
* @param column - The primary-key column name
|
|
815
|
+
* @returns The key (a string or finite number), or `undefined`
|
|
816
|
+
*/
|
|
817
|
+
export declare function extractKey(row: Row, column: string): Key | undefined;
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* Filter rows by a list of conditions — the shared basis for a table's count
|
|
821
|
+
* and aggregate paths (no sort/page, unlike {@link applyCriteria}).
|
|
822
|
+
*
|
|
823
|
+
* @remarks
|
|
824
|
+
* An empty condition list matches every row (returned as-is, no copy). Folds
|
|
825
|
+
* each row through {@link matchesCriteria}.
|
|
826
|
+
*
|
|
827
|
+
* @param rows - The rows to filter
|
|
828
|
+
* @param conditions - The conditions to apply (empty matches everything)
|
|
829
|
+
* @returns The matching rows
|
|
830
|
+
*
|
|
831
|
+
* @example
|
|
832
|
+
* ```ts
|
|
833
|
+
* filterRows(
|
|
834
|
+
* [{ age: 30 }, { age: 12 }],
|
|
835
|
+
* [{ column: 'age', operator: 'above', values: [18], connector: 'and' }],
|
|
836
|
+
* ) // => [{ age: 30 }]
|
|
837
|
+
* ```
|
|
838
|
+
*/
|
|
839
|
+
export declare function filterRows(rows: readonly Row[], conditions: readonly Condition[]): readonly Row[];
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* Generate an RFC 4122 version 4 UUID from a number source — no host crypto global.
|
|
843
|
+
*
|
|
844
|
+
* @remarks
|
|
845
|
+
* Draws exactly {@link UUID_BYTE_COUNT} values from `random`, one per byte, then
|
|
846
|
+
* forces the version (`4`) and variant (`10xx`) bits. The default source is
|
|
847
|
+
* `Math.random` — a pure-ECMAScript intrinsic, so generation works on every host;
|
|
848
|
+
* pass a seeded source (`seededRandom` from `@orkestrel/contract`) and reuse it
|
|
849
|
+
* across calls for reproducible sequences in tests and fixtures — production
|
|
850
|
+
* identifiers should keep the default source, whose engine entropy is far larger
|
|
851
|
+
* than a 32-bit seed. Each byte is floored and masked, so a source straying
|
|
852
|
+
* outside `[0, 1)` (negative, `>= 1`, `NaN`, `Infinity`) can never yield a
|
|
853
|
+
* malformed UUID. Suitable as a collision-resistant record identifier — not a
|
|
854
|
+
* cryptographic token; never use one as a secret.
|
|
855
|
+
*
|
|
856
|
+
* @param random - A number source returning values in the half-open range `[0, 1)` (defaults to `Math.random`)
|
|
857
|
+
* @returns A lowercase RFC 4122 version 4 UUID
|
|
858
|
+
*
|
|
859
|
+
* @example
|
|
860
|
+
* ```ts
|
|
861
|
+
* import { generateUUID } from '@orkestrel/database'
|
|
862
|
+
* import { seededRandom } from '@orkestrel/contract'
|
|
863
|
+
*
|
|
864
|
+
* generateUUID() // e.g. '9b2f7c1e-3d4a-4f6b-8e2d-5a1c0b9f8e7d'
|
|
865
|
+
* generateUUID(seededRandom(42)) // the same UUID on every run
|
|
866
|
+
* ```
|
|
867
|
+
*/
|
|
868
|
+
export declare function generateUUID(random?: RandomFunction): string;
|
|
869
|
+
|
|
870
|
+
export declare function globMatch(value: string, pattern: string): boolean;
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* Narrow an unknown caught value to a {@link DatabaseError}.
|
|
874
|
+
*
|
|
875
|
+
* @param value - The value to test (typically a `catch` binding)
|
|
876
|
+
* @returns `true` when `value` is a {@link DatabaseError}
|
|
877
|
+
*
|
|
878
|
+
* @example
|
|
879
|
+
* ```ts
|
|
880
|
+
* try {
|
|
881
|
+
* await users.add(row)
|
|
882
|
+
* } catch (error) {
|
|
883
|
+
* if (isDatabaseError(error) && error.code === 'CONFLICT') await users.set(row)
|
|
884
|
+
* }
|
|
885
|
+
* ```
|
|
886
|
+
*/
|
|
887
|
+
export declare function isDatabaseError(value: unknown): value is DatabaseError;
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* Whether a value is a well-formed {@link DriverMeta} — the boundary guard a
|
|
891
|
+
* versioning driver's `meta()` narrows a stored (structured-clone or
|
|
892
|
+
* `JSON.parse`d) value through before trusting it, replacing the per-driver
|
|
893
|
+
* duplicated narrowing every backend used to hand-roll (AGENTS §14: never `as`).
|
|
894
|
+
*
|
|
895
|
+
* @remarks
|
|
896
|
+
* Total and total-recursive over the whole shape: a finite `version`, and a
|
|
897
|
+
* `schema` array of well-formed {@link TableSchema} entries — each a `name` /
|
|
898
|
+
* `primary` string pair, a `columns` array of well-formed {@link ColumnSchema}
|
|
899
|
+
* entries (a `name` string, a {@link ColumnType} literal, a `nullable`
|
|
900
|
+
* boolean), and an `indexes` array of string arrays. Anything off-shape
|
|
901
|
+
* (including a non-record) returns `false` rather than throwing.
|
|
902
|
+
*
|
|
903
|
+
* @param value - The value to test
|
|
904
|
+
* @returns `true` when `value` is a well-formed `DriverMeta`
|
|
905
|
+
*
|
|
906
|
+
* @example
|
|
907
|
+
* ```ts
|
|
908
|
+
* isDriverMeta({ version: 1, schema: [] }) // true
|
|
909
|
+
* isDriverMeta({ version: 1, schema: [{ name: 'users' }] }) // false
|
|
910
|
+
* ```
|
|
911
|
+
*/
|
|
912
|
+
export declare function isDriverMeta(value: unknown): value is DriverMeta;
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* A primary key — the value identifying a row within its table.
|
|
916
|
+
*
|
|
917
|
+
* @remarks
|
|
918
|
+
* `string | number` is the intersection of what IndexedDB key ranges and SQL
|
|
919
|
+
* primary keys both express without coercion. Auto-generated keys are UUID
|
|
920
|
+
* strings; supply your own to use numeric keys.
|
|
921
|
+
*/
|
|
922
|
+
export declare type Key = string | number;
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* A caller-supplied key minting function.
|
|
926
|
+
*
|
|
927
|
+
* @remarks
|
|
928
|
+
* Environment surfaces provide implementations (the server's `node:crypto`-backed
|
|
929
|
+
* `generateKey`); the core mints no keys itself. Supplied via
|
|
930
|
+
* {@link DatabaseOptions.key} and used by a table when a written row lacks its
|
|
931
|
+
* primary key. Without one, writing a keyless row is a `VALIDATION` error.
|
|
932
|
+
*/
|
|
933
|
+
export declare type KeyFunction = () => Key;
|
|
934
|
+
|
|
935
|
+
export declare function likeMatch(value: string, pattern: string): boolean;
|
|
936
|
+
|
|
937
|
+
/**
|
|
938
|
+
* Evaluate one {@link Condition} against a row — the per-operator predicate.
|
|
939
|
+
*
|
|
940
|
+
* @remarks
|
|
941
|
+
* Reads the condition's column — a `FieldPath`, resolved with `resolveField` (a
|
|
942
|
+
* string is one column; an array descends a nested value) — and applies the
|
|
943
|
+
* operator. Range operators (`above` / `below` / `from` / `to` / `between`) use
|
|
944
|
+
* {@link compareValues}, the total order; the equality family (`equals` / `not`
|
|
945
|
+
* / `any` / `none`) uses {@link deepEqual} — STRUCTURAL equality, not the total
|
|
946
|
+
* order's rank-5-collapses-all-objects behavior, so `equals` on an object/array
|
|
947
|
+
* operand only matches a structurally-equal value, never every row holding any
|
|
948
|
+
* object. This is a semantics change from ranking: `deepEqual` is SameValueZero
|
|
949
|
+
* on leaves, so `NaN` now equals `NaN` under `equals` / `any` (it never matched
|
|
950
|
+
* anything under the old rank-based comparison). `like` / `glob` / `starts` /
|
|
951
|
+
* `ends` match only strings; `absent` / `present` test nullishness. Total — a
|
|
952
|
+
* type mismatch is simply a non-match.
|
|
953
|
+
*
|
|
954
|
+
* @param row - The row to test
|
|
955
|
+
* @param condition - The condition to apply
|
|
956
|
+
* @returns Whether the row satisfies the condition
|
|
957
|
+
*/
|
|
958
|
+
export declare function matchesCondition(row: Row, condition: Condition): boolean;
|
|
959
|
+
|
|
960
|
+
/**
|
|
961
|
+
* Fold a row through a list of conditions, joining each by its connector.
|
|
962
|
+
*
|
|
963
|
+
* @remarks
|
|
964
|
+
* Evaluated left-to-right: the first condition seeds the result, and each later
|
|
965
|
+
* condition combines with `&&` (`and`) or `||` (`or`). An empty list matches
|
|
966
|
+
* every row. There is no operator precedence — conditions combine in the order
|
|
967
|
+
* the query builder recorded them.
|
|
968
|
+
*
|
|
969
|
+
* @param row - The row to test
|
|
970
|
+
* @param conditions - The conditions to fold
|
|
971
|
+
* @returns Whether the row satisfies the combined conditions
|
|
972
|
+
*/
|
|
973
|
+
export declare function matchesCriteria(row: Row, conditions: readonly Condition[]): boolean;
|
|
974
|
+
|
|
975
|
+
/**
|
|
976
|
+
* The longest `LIKE` / `GLOB` pattern the wildcard matcher accepts before rejecting it.
|
|
977
|
+
*
|
|
978
|
+
* @remarks
|
|
979
|
+
* A ReDoS bound (AGENTS §6.5): the SA1–SA4 migration lets a model supply `list`
|
|
980
|
+
* criteria over the wire, so `likeMatch` / `globMatch` run attacker-controlled
|
|
981
|
+
* patterns. The matcher is the LINEAR greedy two-pointer wildcard match — never a
|
|
982
|
+
* backtracking regex (`.*`-segments-separated-by-literals against a long input is the
|
|
983
|
+
* catastrophic shape JS cannot bound without atomic groups), so it is O(value ×
|
|
984
|
+
* pattern). Capping the pattern length bounds that pattern factor, leaving a match
|
|
985
|
+
* linear in the value length whatever the pattern. A longer pattern throws a
|
|
986
|
+
* `VALIDATION` {@link DatabaseError}; the cap is generous for any legitimate search.
|
|
987
|
+
*/
|
|
988
|
+
export declare const MAX_PATTERN_LENGTH = 1024;
|
|
989
|
+
|
|
990
|
+
/**
|
|
991
|
+
* The reference {@link DriverInterface} — nested maps, no I/O.
|
|
992
|
+
*
|
|
993
|
+
* @remarks
|
|
994
|
+
* The in-between made concrete: it runs identically in a browser or on a server,
|
|
995
|
+
* so it is the storage behind tests, ephemeral caches, and any code that wants
|
|
996
|
+
* the database API without a persistent backend. Rows are DEEP-copied (via
|
|
997
|
+
* `structuredClone`) in and out — at `write`, `read`, `scan`, `stream`, and both
|
|
998
|
+
* snapshot capture and restore — so a caller mutating a nested field of an input
|
|
999
|
+
* row, a returned row, or a row mutated in place between snapshot and rollback
|
|
1000
|
+
* can never perturb stored state (AGENTS §11); a shallow `{ ...row }` spread
|
|
1001
|
+
* would still share nested object/array references. `snapshot`
|
|
1002
|
+
* clones every table to give transactions an exact rollback point. `scan` and
|
|
1003
|
+
* `keys` yield in key order — sorted by the core {@link compareValues} total
|
|
1004
|
+
* order, the same contract the SQLite (`ORDER BY`) and IndexedDB (key-ordered
|
|
1005
|
+
* reads) backends honor, so an unordered read agrees across every backend rather
|
|
1006
|
+
* than leaking Map insertion order. A persistent backend (IndexedDB, SQLite)
|
|
1007
|
+
* implements the same nine methods over real storage.
|
|
1008
|
+
*/
|
|
1009
|
+
export declare class MemoryDriver implements DriverInterface {
|
|
1010
|
+
#private;
|
|
1011
|
+
open(schema: readonly TableSchema[]): Promise<void>;
|
|
1012
|
+
close(): Promise<void>;
|
|
1013
|
+
read(table: string, key: Key): Promise<Row | undefined>;
|
|
1014
|
+
write(table: string, key: Key, row: Row): Promise<void>;
|
|
1015
|
+
delete(table: string, key: Key): Promise<boolean>;
|
|
1016
|
+
keys(table: string): Promise<readonly Key[]>;
|
|
1017
|
+
scan(table: string): AsyncIterable<Row>;
|
|
1018
|
+
/**
|
|
1019
|
+
* Natively filtered lazy iteration — the {@link DriverInterface.stream} hook.
|
|
1020
|
+
*
|
|
1021
|
+
* @remarks
|
|
1022
|
+
* Iterates the table's keys in the same key order `scan` and `keys` yield
|
|
1023
|
+
* (sorted by {@link compareValues}), testing each row against
|
|
1024
|
+
* `criteria.conditions` (via {@link matchesCriteria}) before counting it
|
|
1025
|
+
* toward `offset` / `limit`. Both are applied lazily as matches are found —
|
|
1026
|
+
* `offset` matches are skipped without being yielded, and iteration stops the
|
|
1027
|
+
* instant `limit` yields have been produced, so a large table is never fully
|
|
1028
|
+
* walked for a small page. `criteria.order` is IGNORED (the same contract as
|
|
1029
|
+
* `TableInterface.scan` and `QueryInterface.stream`): streaming yields key
|
|
1030
|
+
* order, sorted output is `records()`'s job. Rows yield copy-out (AGENTS
|
|
1031
|
+
* §11), and an unknown table mirrors `scan`'s empty-yield behavior.
|
|
1032
|
+
*
|
|
1033
|
+
* @param table - The table to stream
|
|
1034
|
+
* @param criteria - The filter / offset / limit to apply lazily
|
|
1035
|
+
*
|
|
1036
|
+
* @example
|
|
1037
|
+
* ```ts
|
|
1038
|
+
* for await (const row of driver.stream('users', { conditions, limit: 10 })) {
|
|
1039
|
+
* // one matched row at a time, in key order
|
|
1040
|
+
* }
|
|
1041
|
+
* ```
|
|
1042
|
+
*/
|
|
1043
|
+
stream(table: string, criteria: Criteria): AsyncIterable<Row>;
|
|
1044
|
+
clear(table: string): Promise<void>;
|
|
1045
|
+
/**
|
|
1046
|
+
* Capture the current state and return a thunk that rolls back to it.
|
|
1047
|
+
*
|
|
1048
|
+
* @remarks
|
|
1049
|
+
* `tables` omitted clones and restores the WHOLE store, byte-identical to the
|
|
1050
|
+
* prior whole-store behavior. `tables` provided clones ONLY the named tables,
|
|
1051
|
+
* and the returned thunk restores ONLY those — every other table keeps
|
|
1052
|
+
* whatever it was mutated to after the snapshot was taken.
|
|
1053
|
+
*
|
|
1054
|
+
* @param tables - The table names to scope the snapshot to; omitted captures every table
|
|
1055
|
+
* @returns A thunk that restores the captured tables
|
|
1056
|
+
*/
|
|
1057
|
+
snapshot(tables?: readonly string[]): Promise<() => Promise<void>>;
|
|
1058
|
+
/**
|
|
1059
|
+
* Return the persisted {@link DriverMeta}, or `undefined` when the store has
|
|
1060
|
+
* never been stamped.
|
|
1061
|
+
*
|
|
1062
|
+
* @remarks
|
|
1063
|
+
* In-process only — the metadata lives in this instance's memory, exactly
|
|
1064
|
+
* like the rest of this driver's storage. A driver-conformance-valid
|
|
1065
|
+
* implementation of the optional `meta` / `stamp` pair.
|
|
1066
|
+
*
|
|
1067
|
+
* @returns The last-stamped {@link DriverMeta}, or `undefined`
|
|
1068
|
+
*/
|
|
1069
|
+
meta(): Promise<DriverMeta | undefined>;
|
|
1070
|
+
/**
|
|
1071
|
+
* Persist `meta` verbatim for a later `meta()` to return.
|
|
1072
|
+
*
|
|
1073
|
+
* @param meta - The {@link DriverMeta} to persist
|
|
1074
|
+
*/
|
|
1075
|
+
stamp(meta: DriverMeta): Promise<void>;
|
|
1076
|
+
/**
|
|
1077
|
+
* Apply a {@link Migration} plan's steps against the in-memory store.
|
|
1078
|
+
*
|
|
1079
|
+
* @remarks
|
|
1080
|
+
* A multi-step plan applies its steps sequentially and is NOT atomic — a
|
|
1081
|
+
* failure partway through a plan leaves the earlier steps already applied.
|
|
1082
|
+
*
|
|
1083
|
+
* @param plan - The migration plan to apply
|
|
1084
|
+
*/
|
|
1085
|
+
migrate(plan: Migration): Promise<void>;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
/**
|
|
1089
|
+
* Apply one table's {@link MigrationStep}s to its rows — a pure row transform.
|
|
1090
|
+
*
|
|
1091
|
+
* @remarks
|
|
1092
|
+
* `column.remove` drops that field from every row (a fresh copy — inputs are
|
|
1093
|
+
* never mutated, AGENTS §11); `column.add` leaves rows as-is (an absent field
|
|
1094
|
+
* reads as `undefined`, backfill is application policy). `table.add` /
|
|
1095
|
+
* `table.remove` / `index.add` / `index.remove` are no-ops here (they operate
|
|
1096
|
+
* on storage shape, not row shape). Steps for tables other than the one
|
|
1097
|
+
* `rows` belongs to are ignored — pass only the steps relevant to this table.
|
|
1098
|
+
*
|
|
1099
|
+
* @param rows - The table's current rows
|
|
1100
|
+
* @param steps - The migration steps to apply (typically one table's slice of a {@link Migration})
|
|
1101
|
+
* @returns A new array of transformed rows; `rows` is never mutated
|
|
1102
|
+
*
|
|
1103
|
+
* @example
|
|
1104
|
+
* ```ts
|
|
1105
|
+
* const rows = [{ id: 'a', name: 'Ada', legacy: true }]
|
|
1106
|
+
* migrateRows(rows, [{ operation: 'column.remove', table: 'users', column: 'legacy' }])
|
|
1107
|
+
* // => [{ id: 'a', name: 'Ada' }]
|
|
1108
|
+
* ```
|
|
1109
|
+
*/
|
|
1110
|
+
export declare function migrateRows(rows: readonly Row[], steps: readonly MigrationStep[]): readonly Row[];
|
|
1111
|
+
|
|
1112
|
+
/**
|
|
1113
|
+
* A schema migration plan — an ordered set of {@link MigrationStep}s moving a
|
|
1114
|
+
* database from one schema version to another.
|
|
1115
|
+
*
|
|
1116
|
+
* @remarks
|
|
1117
|
+
* `from` / `to` are the source and target schema versions; `steps` runs in
|
|
1118
|
+
* order. Applied natively via {@link DriverInterface.migrate} when a driver
|
|
1119
|
+
* implements it.
|
|
1120
|
+
*/
|
|
1121
|
+
export declare interface Migration {
|
|
1122
|
+
readonly from: number;
|
|
1123
|
+
readonly to: number;
|
|
1124
|
+
readonly steps: readonly MigrationStep[];
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
/**
|
|
1128
|
+
* One step of a {@link Migration} plan — a single schema change applied to one
|
|
1129
|
+
* table.
|
|
1130
|
+
*
|
|
1131
|
+
* @remarks
|
|
1132
|
+
* `operation` names the axis it splits on (AGENTS §4.4): adding / removing a
|
|
1133
|
+
* whole table, a column, or an index. A driver's optional `migrate` applies each
|
|
1134
|
+
* step natively; a step referencing an unknown table throws `DatabaseError`
|
|
1135
|
+
* `MIGRATION`.
|
|
1136
|
+
*/
|
|
1137
|
+
export declare type MigrationStep = {
|
|
1138
|
+
readonly operation: 'table.add';
|
|
1139
|
+
readonly table: TableSchema;
|
|
1140
|
+
} | {
|
|
1141
|
+
readonly operation: 'table.remove';
|
|
1142
|
+
readonly table: string;
|
|
1143
|
+
} | {
|
|
1144
|
+
readonly operation: 'column.add';
|
|
1145
|
+
readonly table: string;
|
|
1146
|
+
readonly column: ColumnSchema;
|
|
1147
|
+
} | {
|
|
1148
|
+
readonly operation: 'column.remove';
|
|
1149
|
+
readonly table: string;
|
|
1150
|
+
readonly column: string;
|
|
1151
|
+
} | {
|
|
1152
|
+
readonly operation: 'index.add';
|
|
1153
|
+
readonly table: string;
|
|
1154
|
+
readonly index: readonly string[];
|
|
1155
|
+
} | {
|
|
1156
|
+
readonly operation: 'index.remove';
|
|
1157
|
+
readonly table: string;
|
|
1158
|
+
readonly index: readonly string[];
|
|
1159
|
+
};
|
|
1160
|
+
|
|
1161
|
+
/** One ordering term — a column ({@link FieldPath}, flat or nested) and its direction. */
|
|
1162
|
+
export declare interface Order {
|
|
1163
|
+
readonly column: FieldPath;
|
|
1164
|
+
readonly direction: Direction;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
/**
|
|
1168
|
+
* Structurally diff a deployed and a declared table set into a {@link Migration}
|
|
1169
|
+
* plan.
|
|
1170
|
+
*
|
|
1171
|
+
* @remarks
|
|
1172
|
+
* Tables present in `declared` but not `deployed` become `table.add` steps
|
|
1173
|
+
* (carrying the full declared {@link TableSchema}); tables present in
|
|
1174
|
+
* `deployed` but not `declared` become `table.remove` steps. Tables present in
|
|
1175
|
+
* both are diffed column-by-column (by name) and index-group-by-index-group
|
|
1176
|
+
* (by deep equality of the column-name array), each producing `column.add` /
|
|
1177
|
+
* `column.remove` / `index.add` / `index.remove` steps. Step order is
|
|
1178
|
+
* deterministic: every `table.remove`, then every `table.add`, then each
|
|
1179
|
+
* shared table's column/index changes in `declared` order. `from` / `to` are
|
|
1180
|
+
* plan labels only — version tracking itself is deferred to persistent
|
|
1181
|
+
* backends.
|
|
1182
|
+
*
|
|
1183
|
+
* A column present in BOTH schemas under the same name but with a different
|
|
1184
|
+
* `type` or `nullable` throws a `MIGRATION` {@link DatabaseError} naming the
|
|
1185
|
+
* table, the column, and the from→to difference — a name-only diff would
|
|
1186
|
+
* otherwise silently produce NO step for the drift, and versioned
|
|
1187
|
+
* reconciliation would stamp over it. There is no automatic in-place
|
|
1188
|
+
* type-change step: the manual path is to add a new column, copy/convert the
|
|
1189
|
+
* data at the application layer, then remove the old column — two separate
|
|
1190
|
+
* plans, never a single implicit "alter" step.
|
|
1191
|
+
*
|
|
1192
|
+
* @param deployed - The table schemas currently applied
|
|
1193
|
+
* @param declared - The table schemas the caller wants applied
|
|
1194
|
+
* @param from - The plan's source version label (defaults to `0`)
|
|
1195
|
+
* @param to - The plan's target version label (defaults to `1`)
|
|
1196
|
+
* @returns The migration plan moving `deployed` toward `declared`
|
|
1197
|
+
* @throws A `MIGRATION` {@link DatabaseError} when a shared column's `type` or
|
|
1198
|
+
* `nullable` differs between `deployed` and `declared`
|
|
1199
|
+
*
|
|
1200
|
+
* @example
|
|
1201
|
+
* ```ts
|
|
1202
|
+
* const plan = planMigration(
|
|
1203
|
+
* [{ name: 'users', primary: 'id', columns: [], indexes: [] }],
|
|
1204
|
+
* [{ name: 'users', primary: 'id', columns: [{ name: 'age', type: 'integer', nullable: false }], indexes: [] }],
|
|
1205
|
+
* )
|
|
1206
|
+
* // plan.steps === [{ operation: 'column.add', table: 'users', column: { name: 'age', ... } }]
|
|
1207
|
+
* ```
|
|
1208
|
+
*/
|
|
1209
|
+
export declare function planMigration(deployed: readonly TableSchema[], declared: readonly TableSchema[], from?: number, to?: number): Migration;
|
|
1210
|
+
|
|
1211
|
+
/**
|
|
1212
|
+
* A fluent query builder bound to one table.
|
|
1213
|
+
*
|
|
1214
|
+
* @remarks
|
|
1215
|
+
* Accumulates conditions, ordering, JS filters, and a page; each builder method
|
|
1216
|
+
* mutates and returns the same instance, so a chain reads as one statement. The
|
|
1217
|
+
* portable parts (conditions, order, page) compile into a {@link Criteria} the
|
|
1218
|
+
* table resolves; a `filter` predicate is applied in memory after the read and
|
|
1219
|
+
* before paging, so it composes with the rest without a backend ever seeing a
|
|
1220
|
+
* JS callback.
|
|
1221
|
+
*/
|
|
1222
|
+
export declare class Query<T = Record<string, unknown>> implements QueryInterface<T> {
|
|
1223
|
+
#private;
|
|
1224
|
+
constructor(table: TableInterface<T>);
|
|
1225
|
+
where(column: FieldPath): ClauseInterface<T>;
|
|
1226
|
+
and(column: FieldPath): ClauseInterface<T>;
|
|
1227
|
+
or(column: FieldPath): ClauseInterface<T>;
|
|
1228
|
+
filter(predicate: (row: T) => boolean): QueryInterface<T>;
|
|
1229
|
+
ascending(column: FieldPath): QueryInterface<T>;
|
|
1230
|
+
descending(column: FieldPath): QueryInterface<T>;
|
|
1231
|
+
limit(count: number): QueryInterface<T>;
|
|
1232
|
+
offset(count: number): QueryInterface<T>;
|
|
1233
|
+
all(): Promise<readonly T[]>;
|
|
1234
|
+
first(): Promise<T | undefined>;
|
|
1235
|
+
count(): Promise<number>;
|
|
1236
|
+
/**
|
|
1237
|
+
* Lazy per-row evaluation of this query's conditions / filters / offset /
|
|
1238
|
+
* limit.
|
|
1239
|
+
*
|
|
1240
|
+
* @remarks
|
|
1241
|
+
* `order` and its comparators are IGNORED (streaming yields unsorted, as rows
|
|
1242
|
+
* are evaluated one at a time). Same abort semantics as
|
|
1243
|
+
* `TableInterface.scan`: the signal (if any) is checked before each yield,
|
|
1244
|
+
* and breaking out early closes the underlying source.
|
|
1245
|
+
*
|
|
1246
|
+
* @param options - `signal` to cancel the iteration; checked before each yield
|
|
1247
|
+
* @returns An async iterable of matching rows
|
|
1248
|
+
*/
|
|
1249
|
+
stream(options?: ReadOptions): AsyncIterable<T>;
|
|
1250
|
+
aggregate(operation: AggregateFunction, column: FieldPath): Promise<number | undefined>;
|
|
1251
|
+
sum(column: FieldPath): Promise<number | undefined>;
|
|
1252
|
+
average(column: FieldPath): Promise<number | undefined>;
|
|
1253
|
+
minimum(column: FieldPath): Promise<number | undefined>;
|
|
1254
|
+
maximum(column: FieldPath): Promise<number | undefined>;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
/**
|
|
1258
|
+
* A fluent query builder.
|
|
1259
|
+
*
|
|
1260
|
+
* @remarks
|
|
1261
|
+
* `where` / `and` / `or` open a {@link ClauseInterface} whose operator
|
|
1262
|
+
* closes the condition and returns the query. `filter` adds a post-fetch JS
|
|
1263
|
+
* predicate (applied after the backend read, before paging). The terminals
|
|
1264
|
+
* (`all` / `first` / `count` / the aggregates) execute against the table; each
|
|
1265
|
+
* call mutates and returns the same builder, so a chain reads as one statement.
|
|
1266
|
+
* Every `column` is a {@link FieldPath} — a string is one column, an array
|
|
1267
|
+
* descends a nested value.
|
|
1268
|
+
*/
|
|
1269
|
+
export declare interface QueryInterface<T = Row> {
|
|
1270
|
+
where(column: FieldPath): ClauseInterface<T>;
|
|
1271
|
+
and(column: FieldPath): ClauseInterface<T>;
|
|
1272
|
+
or(column: FieldPath): ClauseInterface<T>;
|
|
1273
|
+
filter(predicate: (row: T) => boolean): QueryInterface<T>;
|
|
1274
|
+
ascending(column: FieldPath): QueryInterface<T>;
|
|
1275
|
+
descending(column: FieldPath): QueryInterface<T>;
|
|
1276
|
+
limit(count: number): QueryInterface<T>;
|
|
1277
|
+
offset(count: number): QueryInterface<T>;
|
|
1278
|
+
all(): Promise<readonly T[]>;
|
|
1279
|
+
first(): Promise<T | undefined>;
|
|
1280
|
+
count(): Promise<number>;
|
|
1281
|
+
/**
|
|
1282
|
+
* Lazy per-row evaluation of this query's conditions / filters / offset /
|
|
1283
|
+
* limit.
|
|
1284
|
+
*
|
|
1285
|
+
* @remarks
|
|
1286
|
+
* `order` and its comparators are IGNORED (streaming yields unsorted, as
|
|
1287
|
+
* rows are evaluated one at a time). Same abort semantics as
|
|
1288
|
+
* {@link TableInterface.scan}: the signal (if any) is checked before each
|
|
1289
|
+
* yield, and breaking out early closes the underlying source.
|
|
1290
|
+
*/
|
|
1291
|
+
stream(options?: ReadOptions): AsyncIterable<T>;
|
|
1292
|
+
sum(column: FieldPath): Promise<number | undefined>;
|
|
1293
|
+
average(column: FieldPath): Promise<number | undefined>;
|
|
1294
|
+
minimum(column: FieldPath): Promise<number | undefined>;
|
|
1295
|
+
maximum(column: FieldPath): Promise<number | undefined>;
|
|
1296
|
+
aggregate(operation: AggregateFunction, column: FieldPath): Promise<number | undefined>;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
/**
|
|
1300
|
+
* Options for a cancellable read / iteration operation.
|
|
1301
|
+
*
|
|
1302
|
+
* @remarks
|
|
1303
|
+
* When `signal` aborts, the operation throws a {@link DatabaseError} with code
|
|
1304
|
+
* `ABORTED` carrying `signal.reason` in `context`. `TableInterface.scan` and
|
|
1305
|
+
* `QueryInterface.stream` check the signal before each yield; other read
|
|
1306
|
+
* methods check it at entry.
|
|
1307
|
+
*/
|
|
1308
|
+
export declare interface ReadOptions {
|
|
1309
|
+
readonly signal?: AbortSignal;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
/** A table row — a plain record of column values keyed by column name. */
|
|
1313
|
+
export declare type Row = Record<string, unknown>;
|
|
1314
|
+
|
|
1315
|
+
/**
|
|
1316
|
+
* The row type a table's {@link Columns} describe — `Infer` of its `objectShape`.
|
|
1317
|
+
*
|
|
1318
|
+
* @remarks
|
|
1319
|
+
* The broad `Columns` (an open `column → shape` map, e.g. when a database is held
|
|
1320
|
+
* at its default type) short-circuits to {@link Row}: there is nothing concrete to
|
|
1321
|
+
* infer, and expanding `Infer` over the open shape would trip TS's
|
|
1322
|
+
* instantiation-depth guard. Concrete column maps infer their exact row.
|
|
1323
|
+
*/
|
|
1324
|
+
export declare type RowOf<C extends Columns> = [Columns] extends [C] ? Row : Infer<{
|
|
1325
|
+
readonly type: 'object';
|
|
1326
|
+
readonly properties: C;
|
|
1327
|
+
}>;
|
|
1328
|
+
|
|
1329
|
+
/**
|
|
1330
|
+
* Map a column's {@link ContractShape} to its portable {@link ColumnType} — the
|
|
1331
|
+
* value a `TableSchema` carries so a native backend can declare a real column.
|
|
1332
|
+
*
|
|
1333
|
+
* @remarks
|
|
1334
|
+
* `string` → `text`; `number` → `integer` when the shape is integer-only, else
|
|
1335
|
+
* `real`; `boolean` → `boolean`. A `literal` takes the type of its values
|
|
1336
|
+
* (all-boolean → `boolean`, all-integer → `integer`, mixed/fractional numbers →
|
|
1337
|
+
* `real`, anything else → `text`). `optional` / `nullable` unwrap to their inner
|
|
1338
|
+
* type (nullability is tracked separately). `null` / `object` / `array` / `union` /
|
|
1339
|
+
* `json` / `raw` → `json`: a backend stores them as JSON text and can `json_extract`
|
|
1340
|
+
* for nested `FieldPath` queries. A scan-only backend ignores the result.
|
|
1341
|
+
*
|
|
1342
|
+
* @param shape - The column's contract shape
|
|
1343
|
+
* @returns The portable column type
|
|
1344
|
+
*
|
|
1345
|
+
* @example
|
|
1346
|
+
* ```ts
|
|
1347
|
+
* shapeToColumnType(stringShape()) // 'text'
|
|
1348
|
+
* shapeToColumnType(integerShape()) // 'integer'
|
|
1349
|
+
* shapeToColumnType(optionalShape(integerShape())) // 'integer'
|
|
1350
|
+
* shapeToColumnType(objectShape({ a: stringShape() })) // 'json'
|
|
1351
|
+
* ```
|
|
1352
|
+
*/
|
|
1353
|
+
export declare function shapeToColumnType(shape: ContractShape): ColumnType;
|
|
1354
|
+
|
|
1355
|
+
/**
|
|
1356
|
+
* Sort rows by an ordering specification, leaving the input untouched.
|
|
1357
|
+
*
|
|
1358
|
+
* @remarks
|
|
1359
|
+
* Applies the terms in priority order — the first term that distinguishes two
|
|
1360
|
+
* rows decides — using {@link compareValues}, reversing for `descending`.
|
|
1361
|
+
*
|
|
1362
|
+
* @param rows - The rows to sort
|
|
1363
|
+
* @param order - The ordering terms in priority order
|
|
1364
|
+
* @returns A new, sorted array
|
|
1365
|
+
*/
|
|
1366
|
+
export declare function sortRows(rows: readonly Row[], order: readonly Order[]): readonly Row[];
|
|
1367
|
+
|
|
1368
|
+
/**
|
|
1369
|
+
* A table — typed keyed CRUD plus fluent query and cursor access over a driver.
|
|
1370
|
+
*
|
|
1371
|
+
* @remarks
|
|
1372
|
+
* The table's contract is the load-bearing piece: writes go through `parse`
|
|
1373
|
+
* (coercing inputs and rejecting rows that don't fit with a `VALIDATION` throw),
|
|
1374
|
+
* reads come back through the contract guard (narrowing a stored {@link Row} to
|
|
1375
|
+
* the table's type — no assertion, AGENTS §1), and `contract` is exposed for
|
|
1376
|
+
* introspection and seeding. The driver only stores and scans; all querying is
|
|
1377
|
+
* the shared core engine in `helpers.ts`.
|
|
1378
|
+
*
|
|
1379
|
+
* @remarks
|
|
1380
|
+
* - **Observable (§13).** The owned {@link emitter} ({@link TableEventMap}) carries the
|
|
1381
|
+
* per-row mutation moments — `write` (set / add / update), `remove`, `clear` — for
|
|
1382
|
+
* fire-and-forget observers (cache invalidation, sync, an audit log), ALONGSIDE the
|
|
1383
|
+
* database-level lifecycle. Events carry the affected KEY only (no value payload, to
|
|
1384
|
+
* keep fan-out lean); reads / queries / counts are not emitted. Every event is emitted
|
|
1385
|
+
* directly, strictly AFTER the driver write / delete / clear completes; the emitter
|
|
1386
|
+
* isolates a listener throw and routes it to its `error` handler (the `error` option),
|
|
1387
|
+
* so a buggy observer can never corrupt a write or perturb a transaction.
|
|
1388
|
+
*/
|
|
1389
|
+
export declare class Table<T = Row> implements TableInterface<T> {
|
|
1390
|
+
#private;
|
|
1391
|
+
constructor(ready: () => Promise<void>, driver: DriverInterface, name: string, key: string, contract: ContractInterface<T>, generate?: KeyFunction, on?: EmitterHooks<TableEventMap>, error?: EmitterErrorHandler);
|
|
1392
|
+
get emitter(): EmitterInterface<TableEventMap>;
|
|
1393
|
+
get name(): string;
|
|
1394
|
+
get primary(): string;
|
|
1395
|
+
get contract(): ContractInterface<T>;
|
|
1396
|
+
get(key: Key): Promise<T | undefined>;
|
|
1397
|
+
get(keys: readonly Key[]): Promise<readonly (T | undefined)[]>;
|
|
1398
|
+
resolve(key: Key): Promise<T>;
|
|
1399
|
+
resolve(keys: readonly Key[]): Promise<readonly T[]>;
|
|
1400
|
+
has(key: Key): Promise<boolean>;
|
|
1401
|
+
has(keys: readonly Key[]): Promise<readonly boolean[]>;
|
|
1402
|
+
keys(): Promise<readonly Key[]>;
|
|
1403
|
+
records(criteria?: Criteria, options?: ReadOptions): Promise<readonly T[]>;
|
|
1404
|
+
/**
|
|
1405
|
+
* Count rows matching `criteria`'s conditions.
|
|
1406
|
+
*
|
|
1407
|
+
* @remarks
|
|
1408
|
+
* Unlike {@link records}, which narrows every row through the table's
|
|
1409
|
+
* contract guard before returning it, `count` operates on STORED rows
|
|
1410
|
+
* WITHOUT that guard (both the native `driver.count` hook and the
|
|
1411
|
+
* `filterRows`-over-`#collect()` fallback count raw storage) — so it can
|
|
1412
|
+
* exceed `(await records(criteria)).length` when storage holds rows that
|
|
1413
|
+
* no longer conform to the table's contract (legacy or migrated data).
|
|
1414
|
+
*
|
|
1415
|
+
* @param criteria - Optional conditions to filter by (paging is ignored)
|
|
1416
|
+
* @param options - `{ signal }` to abort
|
|
1417
|
+
* @returns The count of matching stored rows
|
|
1418
|
+
*/
|
|
1419
|
+
count(criteria?: Criteria, options?: ReadOptions): Promise<number>;
|
|
1420
|
+
/**
|
|
1421
|
+
* Compute an aggregate over `column` across rows matching `criteria`'s
|
|
1422
|
+
* conditions.
|
|
1423
|
+
*
|
|
1424
|
+
* @remarks
|
|
1425
|
+
* Like {@link count}, `aggregate` operates on STORED rows WITHOUT the
|
|
1426
|
+
* contract guard {@link records} / {@link scan} apply — a non-conforming
|
|
1427
|
+
* stored row still contributes to the computed aggregate when it matches
|
|
1428
|
+
* the conditions, even though it would never appear in `records()`'s
|
|
1429
|
+
* output.
|
|
1430
|
+
*
|
|
1431
|
+
* @param operation - The aggregate to compute
|
|
1432
|
+
* @param column - The column to aggregate
|
|
1433
|
+
* @param criteria - Optional conditions to filter by (paging is ignored)
|
|
1434
|
+
* @param options - `{ signal }` to abort
|
|
1435
|
+
* @returns The aggregate value, or `undefined` when undefined for the inputs
|
|
1436
|
+
*/
|
|
1437
|
+
aggregate(operation: AggregateFunction, column: FieldPath, criteria?: Criteria, options?: ReadOptions): Promise<number | undefined>;
|
|
1438
|
+
/**
|
|
1439
|
+
* Stream the table's rows matching `criteria`, applying offset/limit paging.
|
|
1440
|
+
*
|
|
1441
|
+
* @remarks
|
|
1442
|
+
* `criteria.limit` counts rows that pass BOTH the criteria conditions AND the
|
|
1443
|
+
* table's contract guard (a stored row that fails the guard is skipped and
|
|
1444
|
+
* does not count toward `limit`) — this can differ from {@link records}'s
|
|
1445
|
+
* `limit`, which a driver's optional native `records` hook applies BEFORE
|
|
1446
|
+
* the contract guard runs, when storage holds rows that no longer conform
|
|
1447
|
+
* to the table's contract.
|
|
1448
|
+
*
|
|
1449
|
+
* @param criteria - Optional conditions plus offset/limit paging
|
|
1450
|
+
* @param options - `{ signal }` to abort mid-stream
|
|
1451
|
+
* @returns An async iterable of matching, guard-conforming rows
|
|
1452
|
+
*/
|
|
1453
|
+
scan(criteria?: Criteria, options?: ReadOptions): AsyncIterable<T>;
|
|
1454
|
+
set(row: T, options?: ReadOptions): Promise<Key>;
|
|
1455
|
+
set(rows: readonly T[], options?: ReadOptions): Promise<readonly Key[]>;
|
|
1456
|
+
add(row: T, options?: ReadOptions): Promise<Key>;
|
|
1457
|
+
add(rows: readonly T[], options?: ReadOptions): Promise<readonly Key[]>;
|
|
1458
|
+
update(key: Key, changes: Partial<T>, options?: ReadOptions): Promise<boolean>;
|
|
1459
|
+
update(keys: readonly Key[], changes: Partial<T>, options?: ReadOptions): Promise<readonly boolean[]>;
|
|
1460
|
+
remove(key: Key, options?: ReadOptions): Promise<boolean>;
|
|
1461
|
+
remove(keys: readonly Key[], options?: ReadOptions): Promise<readonly boolean[]>;
|
|
1462
|
+
clear(): Promise<void>;
|
|
1463
|
+
query(): QueryInterface<T>;
|
|
1464
|
+
cursor(): Promise<CursorInterface<T>>;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
/**
|
|
1468
|
+
* The push observation surface of a {@link TableInterface} (AGENTS §13) — the per-row
|
|
1469
|
+
* mutation moments a fire-and-forget observer (cache invalidation, sync, an audit log)
|
|
1470
|
+
* subscribes to, ALONGSIDE the database-level {@link DatabaseEventMap}.
|
|
1471
|
+
*
|
|
1472
|
+
* @typeParam TKey - The table's primary-key type (a {@link Key}); the events carry the
|
|
1473
|
+
* affected key so the map is `TableEventMap<TKey>`.
|
|
1474
|
+
*
|
|
1475
|
+
* @remarks
|
|
1476
|
+
* Events carry the affected KEY only — never the row value — to keep fan-out lean and
|
|
1477
|
+
* avoid leaking row data through the observation channel; a consumer that needs the
|
|
1478
|
+
* value re-reads it by key. Any row put — `set`, `add`, or `update` — emits a single
|
|
1479
|
+
* `write` (the consumer re-reads if it needs to know what changed); a delete emits
|
|
1480
|
+
* `remove`; emptying the table emits `clear`. Reads / queries / counts are NOT emitted
|
|
1481
|
+
* (too hot, and a reader does not mutate). Listener isolation is the emitter's (AGENTS §13):
|
|
1482
|
+
* every event is emitted directly and a listener throw is routed to the emitter's `error`
|
|
1483
|
+
* handler (the `error` option), never onto this map, and sits AFTER the driver write / delete
|
|
1484
|
+
* / clear has completed — so a throwing observer can never corrupt a write or perturb a
|
|
1485
|
+
* transaction. Subscribe via `table.emitter.on(...)`. Declared as a `type` alias (§4.5 —
|
|
1486
|
+
* `EventMap` is a `type` kind).
|
|
1487
|
+
*/
|
|
1488
|
+
export declare type TableEventMap<TKey extends Key = Key> = {
|
|
1489
|
+
/** A row was written (set / added / updated) — the affected key (no value payload). */
|
|
1490
|
+
readonly write: readonly [key: TKey];
|
|
1491
|
+
/** A row was removed — the affected key. */
|
|
1492
|
+
readonly remove: readonly [key: TKey];
|
|
1493
|
+
/** The table was cleared (every row removed). */
|
|
1494
|
+
readonly clear: readonly [];
|
|
1495
|
+
};
|
|
1496
|
+
|
|
1497
|
+
/**
|
|
1498
|
+
* One table's portable definition, produced by `export` — the unit of schema /
|
|
1499
|
+
* migration exchange across environments.
|
|
1500
|
+
*
|
|
1501
|
+
* @remarks
|
|
1502
|
+
* `schema` is the JSON Schema (universally portable, serializable); `columns` is
|
|
1503
|
+
* the source column map, which re-imports losslessly via `import` within a
|
|
1504
|
+
* TypeScript environment. `key` is the primary-key column.
|
|
1505
|
+
*/
|
|
1506
|
+
export declare interface TableExport {
|
|
1507
|
+
readonly key: string;
|
|
1508
|
+
readonly columns: Columns;
|
|
1509
|
+
readonly schema: JSONSchema;
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
/**
|
|
1513
|
+
* Per-table secondary indexes — `{ [table]: groups }`, each group one
|
|
1514
|
+
* (possibly compound) index of column names.
|
|
1515
|
+
*
|
|
1516
|
+
* @remarks
|
|
1517
|
+
* Contracts don't express indexes, so they're declared here on `createDatabase`
|
|
1518
|
+
* and flow into each {@link TableSchema}'s `indexes` (SQLite `CREATE INDEX`,
|
|
1519
|
+
* IndexedDB `createIndex`). Mirrors {@link TableKeys}.
|
|
1520
|
+
*/
|
|
1521
|
+
export declare type TableIndexes = Readonly<Record<string, readonly (readonly string[])[]>>;
|
|
1522
|
+
|
|
1523
|
+
/**
|
|
1524
|
+
* A table — typed keyed CRUD plus fluent query and cursor access.
|
|
1525
|
+
*
|
|
1526
|
+
* @remarks
|
|
1527
|
+
* Writes are coerced through the table's contract: a string input to a numeric
|
|
1528
|
+
* column is normalized, and a row that cannot be coerced throws `VALIDATION`. A
|
|
1529
|
+
* row missing its key is assigned a generated UUID. `get` returns `undefined`
|
|
1530
|
+
* when a key is absent; `resolve` throws `NOT_FOUND`. `set` upserts; `add`
|
|
1531
|
+
* inserts and throws `CONFLICT` on a duplicate key. `contract` exposes the
|
|
1532
|
+
* compiled contract for introspection (`schema`) and fixtures (`generate`).
|
|
1533
|
+
*
|
|
1534
|
+
* The keyed methods batch by overload (AGENTS §9.2): pass one key/row for one
|
|
1535
|
+
* result, or an array for an array of results in the same order — a single verb,
|
|
1536
|
+
* never `getMany` / `setAll`. Batches run as independent sequential operations;
|
|
1537
|
+
* wrap them in `transaction` for atomicity.
|
|
1538
|
+
*/
|
|
1539
|
+
export declare interface TableInterface<T = Row> {
|
|
1540
|
+
readonly emitter: EmitterInterface<TableEventMap>;
|
|
1541
|
+
readonly name: string;
|
|
1542
|
+
readonly primary: string;
|
|
1543
|
+
readonly contract: ContractInterface<T>;
|
|
1544
|
+
get(key: Key): Promise<T | undefined>;
|
|
1545
|
+
get(keys: readonly Key[]): Promise<readonly (T | undefined)[]>;
|
|
1546
|
+
resolve(key: Key): Promise<T>;
|
|
1547
|
+
resolve(keys: readonly Key[]): Promise<readonly T[]>;
|
|
1548
|
+
has(key: Key): Promise<boolean>;
|
|
1549
|
+
has(keys: readonly Key[]): Promise<readonly boolean[]>;
|
|
1550
|
+
keys(): Promise<readonly Key[]>;
|
|
1551
|
+
records(criteria?: Criteria, options?: ReadOptions): Promise<readonly T[]>;
|
|
1552
|
+
/**
|
|
1553
|
+
* Count rows matching `criteria`'s conditions.
|
|
1554
|
+
*
|
|
1555
|
+
* @remarks
|
|
1556
|
+
* `records()` / `scan()` narrow every row through the table's contract
|
|
1557
|
+
* guard before returning it, so a non-conforming stored row (legacy data,
|
|
1558
|
+
* a row from before a migration) never appears in their results. `count`
|
|
1559
|
+
* operates on STORED rows WITHOUT that guard — it counts whatever
|
|
1560
|
+
* conditions-matches in storage, guard-conforming or not. This means
|
|
1561
|
+
* `count()` CAN exceed `(await records(criteria)).length` when storage
|
|
1562
|
+
* holds rows that no longer conform to the table's contract.
|
|
1563
|
+
*/
|
|
1564
|
+
count(criteria?: Criteria, options?: ReadOptions): Promise<number>;
|
|
1565
|
+
/**
|
|
1566
|
+
* Compute an aggregate over `column` across rows matching `criteria`'s
|
|
1567
|
+
* conditions.
|
|
1568
|
+
*
|
|
1569
|
+
* @remarks
|
|
1570
|
+
* Like {@link TableInterface.count}, `aggregate` operates on STORED rows
|
|
1571
|
+
* WITHOUT the contract guard that `records()` / `scan()` apply — a
|
|
1572
|
+
* non-conforming stored row still contributes to the aggregate (or to the
|
|
1573
|
+
* `count` operation's tally) when it matches the conditions, even though
|
|
1574
|
+
* it would never appear in `records()`'s output.
|
|
1575
|
+
*/
|
|
1576
|
+
aggregate(operation: AggregateFunction, column: FieldPath, criteria?: Criteria, options?: ReadOptions): Promise<number | undefined>;
|
|
1577
|
+
/**
|
|
1578
|
+
* Lazy filtered iteration over the table's rows.
|
|
1579
|
+
*
|
|
1580
|
+
* @remarks
|
|
1581
|
+
* `criteria`'s `conditions` / `offset` / `limit` are honored lazily as rows
|
|
1582
|
+
* stream; `order` is intentionally IGNORED — streaming yields driver
|
|
1583
|
+
* key-order, sorted output is `records()`'s job. Breaking out of the
|
|
1584
|
+
* iteration early closes the underlying source. The signal (if any) is
|
|
1585
|
+
* checked before each yield.
|
|
1586
|
+
*/
|
|
1587
|
+
scan(criteria?: Criteria, options?: ReadOptions): AsyncIterable<T>;
|
|
1588
|
+
/**
|
|
1589
|
+
* Upsert one or more rows.
|
|
1590
|
+
*
|
|
1591
|
+
* @param row - The row to upsert
|
|
1592
|
+
* @param options - Optional abort signal
|
|
1593
|
+
* @returns The row's key
|
|
1594
|
+
*/
|
|
1595
|
+
set(row: T, options?: ReadOptions): Promise<Key>;
|
|
1596
|
+
/**
|
|
1597
|
+
* Upsert one or more rows.
|
|
1598
|
+
*
|
|
1599
|
+
* @param rows - The rows to upsert
|
|
1600
|
+
* @param options - Optional abort signal, checked at entry and between items
|
|
1601
|
+
* @returns Each row's key, in order
|
|
1602
|
+
*
|
|
1603
|
+
* @remarks
|
|
1604
|
+
* The signal (if any) is checked at entry and between items; an abort
|
|
1605
|
+
* surfaces as `DatabaseError` `ABORTED`. Already-applied items stay
|
|
1606
|
+
* applied — there is no rollback. Wrap in `transaction()` for atomicity.
|
|
1607
|
+
*/
|
|
1608
|
+
set(rows: readonly T[], options?: ReadOptions): Promise<readonly Key[]>;
|
|
1609
|
+
/**
|
|
1610
|
+
* Insert one or more rows, throwing `CONFLICT` on a duplicate key.
|
|
1611
|
+
*
|
|
1612
|
+
* @param row - The row to insert
|
|
1613
|
+
* @param options - Optional abort signal
|
|
1614
|
+
* @returns The row's key
|
|
1615
|
+
*/
|
|
1616
|
+
add(row: T, options?: ReadOptions): Promise<Key>;
|
|
1617
|
+
/**
|
|
1618
|
+
* Insert one or more rows, throwing `CONFLICT` on a duplicate key.
|
|
1619
|
+
*
|
|
1620
|
+
* @param rows - The rows to insert
|
|
1621
|
+
* @param options - Optional abort signal, checked at entry and between items
|
|
1622
|
+
* @returns Each row's key, in order
|
|
1623
|
+
*
|
|
1624
|
+
* @remarks
|
|
1625
|
+
* The signal (if any) is checked at entry and between items; an abort
|
|
1626
|
+
* surfaces as `DatabaseError` `ABORTED`. Already-applied items stay
|
|
1627
|
+
* applied — there is no rollback. Wrap in `transaction()` for atomicity.
|
|
1628
|
+
*/
|
|
1629
|
+
add(rows: readonly T[], options?: ReadOptions): Promise<readonly Key[]>;
|
|
1630
|
+
/**
|
|
1631
|
+
* Apply a partial change to one or more rows.
|
|
1632
|
+
*
|
|
1633
|
+
* @param key - The key of the row to update
|
|
1634
|
+
* @param changes - The partial changes to apply
|
|
1635
|
+
* @param options - Optional abort signal
|
|
1636
|
+
* @returns `true` when the row existed and was updated
|
|
1637
|
+
*/
|
|
1638
|
+
update(key: Key, changes: Partial<T>, options?: ReadOptions): Promise<boolean>;
|
|
1639
|
+
/**
|
|
1640
|
+
* Apply a partial change to one or more rows.
|
|
1641
|
+
*
|
|
1642
|
+
* @param keys - The keys of the rows to update
|
|
1643
|
+
* @param changes - The partial changes to apply to each row
|
|
1644
|
+
* @param options - Optional abort signal, checked at entry and between items
|
|
1645
|
+
* @returns Each row's update result, in order
|
|
1646
|
+
*
|
|
1647
|
+
* @remarks
|
|
1648
|
+
* The signal (if any) is checked at entry and between items; an abort
|
|
1649
|
+
* surfaces as `DatabaseError` `ABORTED`. Already-applied items stay
|
|
1650
|
+
* applied — there is no rollback. Wrap in `transaction()` for atomicity.
|
|
1651
|
+
*/
|
|
1652
|
+
update(keys: readonly Key[], changes: Partial<T>, options?: ReadOptions): Promise<readonly boolean[]>;
|
|
1653
|
+
/**
|
|
1654
|
+
* Delete one or more rows.
|
|
1655
|
+
*
|
|
1656
|
+
* @param key - The key of the row to remove
|
|
1657
|
+
* @param options - Optional abort signal
|
|
1658
|
+
* @returns `true` when the row existed and was removed
|
|
1659
|
+
*/
|
|
1660
|
+
remove(key: Key, options?: ReadOptions): Promise<boolean>;
|
|
1661
|
+
/**
|
|
1662
|
+
* Delete one or more rows.
|
|
1663
|
+
*
|
|
1664
|
+
* @param keys - The keys of the rows to remove
|
|
1665
|
+
* @param options - Optional abort signal, checked at entry and between items
|
|
1666
|
+
* @returns Each row's removal result, in order
|
|
1667
|
+
*
|
|
1668
|
+
* @remarks
|
|
1669
|
+
* The signal (if any) is checked at entry and between items; an abort
|
|
1670
|
+
* surfaces as `DatabaseError` `ABORTED`. Already-applied items stay
|
|
1671
|
+
* applied — there is no rollback. Wrap in `transaction()` for atomicity.
|
|
1672
|
+
*/
|
|
1673
|
+
remove(keys: readonly Key[], options?: ReadOptions): Promise<readonly boolean[]>;
|
|
1674
|
+
clear(): Promise<void>;
|
|
1675
|
+
query(): QueryInterface<T>;
|
|
1676
|
+
cursor(): Promise<CursorInterface<T>>;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
/**
|
|
1680
|
+
* Per-table primary-key column overrides — `{ [table]: column }`.
|
|
1681
|
+
*
|
|
1682
|
+
* @remarks
|
|
1683
|
+
* A table absent from this map keys its rows by {@link DEFAULT_PRIMARY} (`id`).
|
|
1684
|
+
* Kept separate from {@link TablesShape} so the table map stays purely columns.
|
|
1685
|
+
*/
|
|
1686
|
+
export declare type TableKeys = Readonly<Record<string, string>>;
|
|
1687
|
+
|
|
1688
|
+
/**
|
|
1689
|
+
* A backend-agnostic description of one table — what `open` hands each driver so a
|
|
1690
|
+
* native backend can create real tables and indexes.
|
|
1691
|
+
*
|
|
1692
|
+
* @remarks
|
|
1693
|
+
* Derived by the database from its `tables` contract shapes ({@link ColumnSchema}
|
|
1694
|
+
* per column, via `shapeToColumnType`), its `keys` (`primary`), and its `indexes` option
|
|
1695
|
+
* (`indexes`, each entry one possibly-compound index of column names). A scan-only
|
|
1696
|
+
* backend (the reference `MemoryDriver`) ignores everything but `name`.
|
|
1697
|
+
*/
|
|
1698
|
+
export declare interface TableSchema {
|
|
1699
|
+
readonly name: string;
|
|
1700
|
+
readonly primary: string;
|
|
1701
|
+
readonly columns: readonly ColumnSchema[];
|
|
1702
|
+
readonly indexes: readonly (readonly string[])[];
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
/**
|
|
1706
|
+
* A database's table schema — a map of table name to its {@link Columns}.
|
|
1707
|
+
*
|
|
1708
|
+
* @remarks
|
|
1709
|
+
* Each table's row type is `Infer` of its columns (see {@link RowOf}); primary-key
|
|
1710
|
+
* columns are named separately via {@link TableKeys}.
|
|
1711
|
+
*/
|
|
1712
|
+
export declare type TablesShape = Readonly<Record<string, Columns>>;
|
|
1713
|
+
|
|
1714
|
+
/**
|
|
1715
|
+
* The handle a driver's native `transaction` hook returns.
|
|
1716
|
+
*
|
|
1717
|
+
* @remarks
|
|
1718
|
+
* `commit` finalizes the native BEGIN; `rollback` undoes it. When a driver
|
|
1719
|
+
* implements {@link DriverInterface.transaction}, the engine uses this handle
|
|
1720
|
+
* instead of the snapshot-based rollback floor.
|
|
1721
|
+
*/
|
|
1722
|
+
export declare interface TransactionInterface {
|
|
1723
|
+
commit(): Promise<void>;
|
|
1724
|
+
rollback(): Promise<void>;
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
/** The number of bytes encoded by an RFC 4122 UUID. */
|
|
1728
|
+
export declare const UUID_BYTE_COUNT = 16;
|
|
1729
|
+
|
|
1730
|
+
/** The number of distinct values one UUID byte may hold. */
|
|
1731
|
+
export declare const UUID_BYTE_RANGE = 256;
|
|
1732
|
+
|
|
1733
|
+
/**
|
|
1734
|
+
* Match a value against a wildcard pattern in LINEAR time — the shared, ReDoS-SAFE
|
|
1735
|
+
* engine behind {@link likeMatch} and {@link globMatch}.
|
|
1736
|
+
*
|
|
1737
|
+
* @remarks
|
|
1738
|
+
* A backtracking RegExp (`a%b%c` → `^a.*b.*c$`) is CATASTROPHIC on a hostile pattern:
|
|
1739
|
+
* `.*` segments separated by literals, matched against a long non-matching input, blow
|
|
1740
|
+
* up super-linearly — and JS has no atomic groups / possessive quantifiers to bound it
|
|
1741
|
+
* (AGENTS §6.5, now that the authed server runs model-supplied `list` criteria over the
|
|
1742
|
+
* wire). So this builds NO regex. It runs the classic GREEDY TWO-POINTER wildcard match:
|
|
1743
|
+
* the `any` wildcard records its position and, on a later mismatch, backtracks ONLY to
|
|
1744
|
+
* that last `any` (letting it absorb one more char) — so the work is O(value × pattern),
|
|
1745
|
+
* never the exponential / polynomial backtracking a regex would do. The pattern length
|
|
1746
|
+
* is capped at {@link MAX_PATTERN_LENGTH} (a `VALIDATION` {@link DatabaseError} over it),
|
|
1747
|
+
* bounding the pattern factor so a match stays linear in the value length whatever the
|
|
1748
|
+
* pattern.
|
|
1749
|
+
*
|
|
1750
|
+
* The `any` wildcard matches any run (including empty); `single` matches exactly one
|
|
1751
|
+
* char; every other pattern char matches itself LITERALLY (a pattern `.` / `(` / `\` is
|
|
1752
|
+
* a literal — the regex-metacharacter hazard is gone with the regex). `any` is tested
|
|
1753
|
+
* BEFORE a literal match, so a value that literally contains the wildcard char never
|
|
1754
|
+
* shadows the wildcard. Case folding is applied to BOTH sides when `fold` is set.
|
|
1755
|
+
*
|
|
1756
|
+
* @param value - The value to test
|
|
1757
|
+
* @param pattern - The wildcard pattern
|
|
1758
|
+
* @param any - The any-run wildcard char (`%` for `LIKE`, `*` for `GLOB`)
|
|
1759
|
+
* @param single - The single-char wildcard char (`_` for `LIKE`, `?` for `GLOB`)
|
|
1760
|
+
* @param fold - Whether to match case-INSENSITIVELY (`LIKE` folds; `GLOB` does not)
|
|
1761
|
+
* @returns Whether `value` matches `pattern`
|
|
1762
|
+
* @throws A `VALIDATION` {@link DatabaseError} when `pattern` exceeds {@link MAX_PATTERN_LENGTH}
|
|
1763
|
+
*/
|
|
1764
|
+
export declare function wildcardMatch(value: string, pattern: string, any: string, single: string, fold: boolean): boolean;
|
|
1765
|
+
|
|
1766
|
+
export { }
|