@orkestrel/database 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,69 +0,0 @@
1
- import type { ContractInterface, FieldPath } from '@orkestrel/contract';
2
- import type { EmitterErrorHandler, EmitterHooks, EmitterInterface } from '@orkestrel/emitter';
3
- import type { AggregateFunction, Criteria, CursorInterface, DriverInterface, Key, KeyFunction, QueryInterface, ReadOptions, Row, TableEventMap, TableInterface } from './types.js';
4
- /**
5
- * A table — typed keyed CRUD plus fluent query and cursor access over a driver.
6
- *
7
- * @remarks
8
- * The table's contract is the load-bearing piece: writes go through `parse`
9
- * (coercing inputs and rejecting rows that don't fit with a `VALIDATION` throw),
10
- * reads come back through the contract guard (narrowing a stored {@link Row} to
11
- * the table's type — no assertion, AGENTS §1), and `contract` is exposed for
12
- * introspection and seeding. The driver only stores and scans; all querying is
13
- * the shared core engine in `helpers.ts`.
14
- *
15
- * @remarks
16
- * - **Observable (§13).** The owned {@link emitter} ({@link TableEventMap}) carries the
17
- * per-row mutation moments — `write` (set / add / update), `remove`, `clear` — for
18
- * fire-and-forget observers (cache invalidation, sync, an audit log), ALONGSIDE the
19
- * database-level lifecycle. Events carry the affected KEY only (no value payload, to
20
- * keep fan-out lean); reads / queries / counts are not emitted. Every event is emitted
21
- * directly, strictly AFTER the driver write / delete / clear completes; the emitter
22
- * isolates a listener throw and routes it to its `error` handler (the `error` option),
23
- * so a buggy observer can never corrupt a write or perturb a transaction.
24
- */
25
- export declare class Table<T = Row> implements TableInterface<T> {
26
- #private;
27
- constructor(ready: () => Promise<void>, driver: DriverInterface, name: string, key: string, contract: ContractInterface<T>, generate?: KeyFunction, on?: EmitterHooks<TableEventMap>, error?: EmitterErrorHandler);
28
- get emitter(): EmitterInterface<TableEventMap>;
29
- get name(): string;
30
- get primary(): string;
31
- get contract(): ContractInterface<T>;
32
- get(key: Key): Promise<T | undefined>;
33
- get(keys: readonly Key[]): Promise<readonly (T | undefined)[]>;
34
- resolve(key: Key): Promise<T>;
35
- resolve(keys: readonly Key[]): Promise<readonly T[]>;
36
- has(key: Key): Promise<boolean>;
37
- has(keys: readonly Key[]): Promise<readonly boolean[]>;
38
- keys(): Promise<readonly Key[]>;
39
- records(criteria?: Criteria, options?: ReadOptions): Promise<readonly T[]>;
40
- count(criteria?: Criteria, options?: ReadOptions): Promise<number>;
41
- aggregate(operation: AggregateFunction, column: FieldPath, criteria?: Criteria, options?: ReadOptions): Promise<number | undefined>;
42
- /**
43
- * Stream the table's rows matching `criteria`, applying offset/limit paging.
44
- *
45
- * @remarks
46
- * `criteria.limit` counts rows that pass BOTH the criteria conditions AND the
47
- * table's contract guard (a stored row that fails the guard is skipped and
48
- * does not count toward `limit`) — this can differ from {@link records}'s
49
- * `limit`, which a driver's optional native `records` hook applies BEFORE
50
- * the contract guard runs, when storage holds rows that no longer conform
51
- * to the table's contract.
52
- *
53
- * @param criteria - Optional conditions plus offset/limit paging
54
- * @param options - `{ signal }` to abort mid-stream
55
- * @returns An async generator of matching, guard-conforming rows
56
- */
57
- scan(criteria?: Criteria, options?: ReadOptions): AsyncGenerator<T>;
58
- set(row: T, options?: ReadOptions): Promise<Key>;
59
- set(rows: readonly T[], options?: ReadOptions): Promise<readonly Key[]>;
60
- add(row: T, options?: ReadOptions): Promise<Key>;
61
- add(rows: readonly T[], options?: ReadOptions): Promise<readonly Key[]>;
62
- update(key: Key, changes: Partial<T>, options?: ReadOptions): Promise<boolean>;
63
- update(keys: readonly Key[], changes: Partial<T>, options?: ReadOptions): Promise<readonly boolean[]>;
64
- remove(key: Key, options?: ReadOptions): Promise<boolean>;
65
- remove(keys: readonly Key[], options?: ReadOptions): Promise<readonly boolean[]>;
66
- clear(): Promise<void>;
67
- query(): QueryInterface<T>;
68
- cursor(): Promise<CursorInterface<T>>;
69
- }
@@ -1,22 +0,0 @@
1
- /**
2
- * The primary-key column assumed when {@link TableKeys} does not name one.
3
- *
4
- * @remarks
5
- * `id` is the convention IndexedDB (`keyPath: 'id'`) and SQL (`id` / rowid) both
6
- * lean on, so a table that omits `key` keys its rows by `id`.
7
- */
8
- export declare const DEFAULT_PRIMARY = "id";
9
- /**
10
- * The longest `LIKE` / `GLOB` pattern the wildcard matcher accepts before rejecting it.
11
- *
12
- * @remarks
13
- * A ReDoS bound (AGENTS §6.5): the SA1–SA4 migration lets a model supply `list`
14
- * criteria over the wire, so `likeMatch` / `globMatch` run attacker-controlled
15
- * patterns. The matcher is the LINEAR greedy two-pointer wildcard match — never a
16
- * backtracking regex (`.*`-segments-separated-by-literals against a long input is the
17
- * catastrophic shape JS cannot bound without atomic groups), so it is O(value ×
18
- * pattern). Capping the pattern length bounds that pattern factor, leaving a match
19
- * linear in the value length whatever the pattern. A longer pattern throws a
20
- * `VALIDATION` {@link DatabaseError}; the cap is generous for any legitimate search.
21
- */
22
- export declare const MAX_PATTERN_LENGTH = 1024;
@@ -1,94 +0,0 @@
1
- import type { Criteria, DriverInterface, DriverMeta, Key, Migration, Row, TableSchema } from '../types.js';
2
- /**
3
- * The reference {@link DriverInterface} — nested maps, no I/O.
4
- *
5
- * @remarks
6
- * The in-between made concrete: it runs identically in a browser or on a server,
7
- * so it is the storage behind tests, ephemeral caches, and any code that wants
8
- * the database API without a persistent backend. Rows are copied in and out so a
9
- * caller can never mutate stored state by reference (AGENTS §11), and `snapshot`
10
- * clones every table to give transactions an exact rollback point. `scan` and
11
- * `keys` yield in key order — sorted by the core {@link compareValues} total
12
- * order, the same contract the SQLite (`ORDER BY`) and IndexedDB (key-ordered
13
- * reads) backends honor, so an unordered read agrees across every backend rather
14
- * than leaking Map insertion order. A persistent backend (IndexedDB, SQLite)
15
- * implements the same nine methods over real storage.
16
- */
17
- export declare class MemoryDriver implements DriverInterface {
18
- #private;
19
- open(schema: readonly TableSchema[]): Promise<void>;
20
- close(): Promise<void>;
21
- read(table: string, key: Key): Promise<Row | undefined>;
22
- write(table: string, key: Key, row: Row): Promise<void>;
23
- delete(table: string, key: Key): Promise<boolean>;
24
- keys(table: string): Promise<readonly Key[]>;
25
- scan(table: string): AsyncIterable<Row>;
26
- /**
27
- * Natively filtered lazy iteration — the {@link DriverInterface.stream} hook.
28
- *
29
- * @remarks
30
- * Iterates the table's keys in the same key order `scan` and `keys` yield
31
- * (sorted by {@link compareValues}), testing each row against
32
- * `criteria.conditions` (via {@link matchesCriteria}) before counting it
33
- * toward `offset` / `limit`. Both are applied lazily as matches are found —
34
- * `offset` matches are skipped without being yielded, and iteration stops the
35
- * instant `limit` yields have been produced, so a large table is never fully
36
- * walked for a small page. `criteria.order` is IGNORED (the same contract as
37
- * `TableInterface.scan` and `QueryInterface.stream`): streaming yields key
38
- * order, sorted output is `records()`'s job. Rows yield copy-out (AGENTS
39
- * §11), and an unknown table mirrors `scan`'s empty-yield behavior.
40
- *
41
- * @param table - The table to stream
42
- * @param criteria - The filter / offset / limit to apply lazily
43
- *
44
- * @example
45
- * ```ts
46
- * for await (const row of driver.stream('users', { conditions, limit: 10 })) {
47
- * // one matched row at a time, in key order
48
- * }
49
- * ```
50
- */
51
- stream(table: string, criteria: Criteria): AsyncIterable<Row>;
52
- clear(table: string): Promise<void>;
53
- /**
54
- * Capture the current state and return a thunk that rolls back to it.
55
- *
56
- * @remarks
57
- * `tables` omitted clones and restores the WHOLE store, byte-identical to the
58
- * prior whole-store behavior. `tables` provided clones ONLY the named tables,
59
- * and the returned thunk restores ONLY those — every other table keeps
60
- * whatever it was mutated to after the snapshot was taken.
61
- *
62
- * @param tables - The table names to scope the snapshot to; omitted captures every table
63
- * @returns A thunk that restores the captured tables
64
- */
65
- snapshot(tables?: readonly string[]): Promise<() => Promise<void>>;
66
- /**
67
- * Return the persisted {@link DriverMeta}, or `undefined` when the store has
68
- * never been stamped.
69
- *
70
- * @remarks
71
- * In-process only — the metadata lives in this instance's memory, exactly
72
- * like the rest of this driver's storage. A driver-conformance-valid
73
- * implementation of the optional `meta` / `stamp` pair.
74
- *
75
- * @returns The last-stamped {@link DriverMeta}, or `undefined`
76
- */
77
- meta(): Promise<DriverMeta | undefined>;
78
- /**
79
- * Persist `meta` verbatim for a later `meta()` to return.
80
- *
81
- * @param meta - The {@link DriverMeta} to persist
82
- */
83
- stamp(meta: DriverMeta): Promise<void>;
84
- /**
85
- * Apply a {@link Migration} plan's steps against the in-memory store.
86
- *
87
- * @remarks
88
- * A multi-step plan applies its steps sequentially and is NOT atomic — a
89
- * failure partway through a plan leaves the earlier steps already applied.
90
- *
91
- * @param plan - The migration plan to apply
92
- */
93
- migrate(plan: Migration): Promise<void>;
94
- }
@@ -1,38 +0,0 @@
1
- import type { DatabaseErrorCode } from './types.js';
2
- /**
3
- * An error thrown by the database layer.
4
- *
5
- * @remarks
6
- * Carries a {@link DatabaseErrorCode} and an optional `context` bag naming the
7
- * offending table / key. Thrown for: operating on a closed database (`CLOSED`), a
8
- * `resolve` miss (`NOT_FOUND`), an `add` onto an existing key (`CONFLICT`), a
9
- * row that fails its table's contract (`VALIDATION`), a cancelled operation whose
10
- * {@link ReadOptions.signal} aborted (`ABORTED`, carrying `signal.reason` in
11
- * `context`), an inapplicable {@link Migration} plan (`MIGRATION`), a
12
- * driver that violates a {@link DriverInterface} invariant, thrown by the
13
- * `conformDriver` helper (`CONFORMANCE`), and an unexpected infrastructure
14
- * fault surfaced by a driver seam — e.g. a filesystem failure while
15
- * persisting (`DRIVER`) — as opposed to expected domain conditions, which
16
- * keep their specific codes.
17
- */
18
- export declare class DatabaseError extends Error {
19
- readonly code: DatabaseErrorCode;
20
- readonly context?: Readonly<Record<string, unknown>>;
21
- constructor(code: DatabaseErrorCode, message: string, context?: Readonly<Record<string, unknown>>);
22
- }
23
- /**
24
- * Narrow an unknown caught value to a {@link DatabaseError}.
25
- *
26
- * @param value - The value to test (typically a `catch` binding)
27
- * @returns `true` when `value` is a {@link DatabaseError}
28
- *
29
- * @example
30
- * ```ts
31
- * try {
32
- * await users.add(row)
33
- * } catch (error) {
34
- * if (isDatabaseError(error) && error.code === 'CONFLICT') await users.set(row)
35
- * }
36
- * ```
37
- */
38
- export declare function isDatabaseError(value: unknown): value is DatabaseError;
@@ -1,43 +0,0 @@
1
- import type { DatabaseInterface, DatabaseOptions, DriverInterface, TablesShape } from './types.js';
2
- /**
3
- * Create a database over a driver and a declared `tables` schema.
4
- *
5
- * @remarks
6
- * `tables` maps each name to its columns (a `column → shape` map); the database
7
- * wraps each in an `objectShape`, so you never write `objectShape` at the table
8
- * level. The `const` type parameter captures the literal names and columns, so
9
- * `db.table('users')` is checked against the schema and typed by `Infer` of its
10
- * columns — no annotations. Name a non-`id` primary-key column per table via the
11
- * optional `keys` map.
12
- *
13
- * @param options - The driver, the `tables` column map, optional `keys`, and an
14
- * optional `name`
15
- * @returns A typed {@link DatabaseInterface}
16
- *
17
- * @example
18
- * ```ts
19
- * import { createDatabase, createMemoryDriver } from '@orkestrel/database'
20
- * import { integerShape, stringShape } from '@orkestrel/contract'
21
- *
22
- * const db = createDatabase({
23
- * driver: createMemoryDriver(),
24
- * tables: {
25
- * users: { id: stringShape(), age: integerShape() },
26
- * posts: { slug: stringShape(), title: stringShape() },
27
- * },
28
- * keys: { posts: 'slug' },
29
- * })
30
- * await db.table('users').set({ id: 'u1', age: 36 }) // typed; coerced + validated
31
- * ```
32
- */
33
- export declare function createDatabase<const T extends TablesShape>(options: DatabaseOptions<T>): DatabaseInterface<T>;
34
- /**
35
- * Create the in-memory reference {@link DriverInterface}.
36
- *
37
- * @remarks
38
- * Backed by nested maps with no I/O — the same driver runs in a browser or on a
39
- * server, making it the natural choice for tests and ephemeral storage.
40
- *
41
- * @returns A fresh in-memory driver
42
- */
43
- export declare function createMemoryDriver(): DriverInterface;
@@ -1,383 +0,0 @@
1
- import type { ContractShape, FieldPath } from '@orkestrel/contract';
2
- import type { AggregateFunction, ColumnType, ConformanceFinding, Condition, Criteria, DriverInterface, Key, Migration, MigrationStep, Order, Row, TableSchema } from './types.js';
3
- /**
4
- * A total ordering over arbitrary values — the comparator behind sorting and the
5
- * range operators.
6
- *
7
- * @remarks
8
- * Values of different types order by a fixed type rank (`undefined` < `null` <
9
- * boolean < number < string < other); same-typed values compare naturally.
10
- * `NaN` sorts after every other number and equal to itself, so the comparator
11
- * is total and never returns `NaN`.
12
- *
13
- * @param left - The left value
14
- * @param right - The right value
15
- * @returns `-1`, `0`, or `1`
16
- */
17
- export declare function compareValues(left: unknown, right: unknown): number;
18
- /**
19
- * Structural equality by SameValueZero leaves — the comparator behind conformance
20
- * checks and any test/fixture that needs "same data", not "same reference".
21
- *
22
- * @remarks
23
- * Primitives compare by SameValueZero (`NaN` equals itself; `+0` equals `-0`).
24
- * Arrays compare by index (same length, every element `deepEqual`). Plain
25
- * records (via `isRecord`) compare by their OWN enumerable keys: same key
26
- * COUNT and, for every key in `left`, `right` has that key (`Object.hasOwn`)
27
- * with a `deepEqual` value — so a key present with value `undefined` is NOT
28
- * equal to that key being absent (both differ in `Object.keys` membership).
29
- * Anything else (functions, class instances, mismatched shapes) falls through
30
- * to `false`. There is no cycle detection — a cyclic input recurses forever;
31
- * callers pass acyclic data (rows, plans, config).
32
- *
33
- * @param left - The left value
34
- * @param right - The right value
35
- * @returns Whether `left` and `right` are structurally equal
36
- *
37
- * @example
38
- * ```ts
39
- * deepEqual(Number.NaN, Number.NaN) // true
40
- * deepEqual({ a: [1, { b: 2 }] }, { a: [1, { b: 2 }] }) // true
41
- * deepEqual({ a: undefined }, {}) // false — present-undefined ≠ absent
42
- * ```
43
- */
44
- export declare function deepEqual(left: unknown, right: unknown): boolean;
45
- /**
46
- * Match a value against a wildcard pattern in LINEAR time — the shared, ReDoS-SAFE
47
- * engine behind {@link likeMatch} and {@link globMatch}.
48
- *
49
- * @remarks
50
- * A backtracking RegExp (`a%b%c` → `^a.*b.*c$`) is CATASTROPHIC on a hostile pattern:
51
- * `.*` segments separated by literals, matched against a long non-matching input, blow
52
- * up super-linearly — and JS has no atomic groups / possessive quantifiers to bound it
53
- * (AGENTS §6.5, now that the authed server runs model-supplied `list` criteria over the
54
- * wire). So this builds NO regex. It runs the classic GREEDY TWO-POINTER wildcard match:
55
- * the `any` wildcard records its position and, on a later mismatch, backtracks ONLY to
56
- * that last `any` (letting it absorb one more char) — so the work is O(value × pattern),
57
- * never the exponential / polynomial backtracking a regex would do. The pattern length
58
- * is capped at {@link MAX_PATTERN_LENGTH} (a `VALIDATION` {@link DatabaseError} over it),
59
- * bounding the pattern factor so a match stays linear in the value length whatever the
60
- * pattern.
61
- *
62
- * The `any` wildcard matches any run (including empty); `single` matches exactly one
63
- * char; every other pattern char matches itself LITERALLY (a pattern `.` / `(` / `\` is
64
- * a literal — the regex-metacharacter hazard is gone with the regex). `any` is tested
65
- * BEFORE a literal match, so a value that literally contains the wildcard char never
66
- * shadows the wildcard. Case folding is applied to BOTH sides when `fold` is set.
67
- *
68
- * @param value - The value to test
69
- * @param pattern - The wildcard pattern
70
- * @param any - The any-run wildcard char (`%` for `LIKE`, `*` for `GLOB`)
71
- * @param single - The single-char wildcard char (`_` for `LIKE`, `?` for `GLOB`)
72
- * @param fold - Whether to match case-INSENSITIVELY (`LIKE` folds; `GLOB` does not)
73
- * @returns Whether `value` matches `pattern`
74
- * @throws A `VALIDATION` {@link DatabaseError} when `pattern` exceeds {@link MAX_PATTERN_LENGTH}
75
- */
76
- export declare function wildcardMatch(value: string, pattern: string, any: string, single: string, fold: boolean): boolean;
77
- export declare function likeMatch(value: string, pattern: string): boolean;
78
- export declare function globMatch(value: string, pattern: string): boolean;
79
- /**
80
- * Evaluate one {@link Condition} against a row — the per-operator predicate.
81
- *
82
- * @remarks
83
- * Reads the condition's column — a `FieldPath`, resolved with `resolveField` (a
84
- * string is one column; an array descends a nested value) — and applies the
85
- * operator. Range operators use {@link compareValues}; `like` / `glob` / `starts`
86
- * / `ends` match only strings; `any` / `none` test membership by value equality.
87
- * Total — a type mismatch is simply a non-match.
88
- *
89
- * @param row - The row to test
90
- * @param condition - The condition to apply
91
- * @returns Whether the row satisfies the condition
92
- */
93
- export declare function matchesCondition(row: Row, condition: Condition): boolean;
94
- /**
95
- * Fold a row through a list of conditions, joining each by its connector.
96
- *
97
- * @remarks
98
- * Evaluated left-to-right: the first condition seeds the result, and each later
99
- * condition combines with `&&` (`and`) or `||` (`or`). An empty list matches
100
- * every row. There is no operator precedence — conditions combine in the order
101
- * the query builder recorded them.
102
- *
103
- * @param row - The row to test
104
- * @param conditions - The conditions to fold
105
- * @returns Whether the row satisfies the combined conditions
106
- */
107
- export declare function matchesCriteria(row: Row, conditions: readonly Condition[]): boolean;
108
- /**
109
- * Filter rows by a list of conditions — the shared basis for a table's count
110
- * and aggregate paths (no sort/page, unlike {@link applyCriteria}).
111
- *
112
- * @remarks
113
- * An empty condition list matches every row (returned as-is, no copy). Folds
114
- * each row through {@link matchesCriteria}.
115
- *
116
- * @param rows - The rows to filter
117
- * @param conditions - The conditions to apply (empty matches everything)
118
- * @returns The matching rows
119
- *
120
- * @example
121
- * ```ts
122
- * filterRows(
123
- * [{ age: 30 }, { age: 12 }],
124
- * [{ column: 'age', operator: 'above', values: [18], connector: 'and' }],
125
- * ) // => [{ age: 30 }]
126
- * ```
127
- */
128
- export declare function filterRows(rows: readonly Row[], conditions: readonly Condition[]): readonly Row[];
129
- /**
130
- * Sort rows by an ordering specification, leaving the input untouched.
131
- *
132
- * @remarks
133
- * Applies the terms in priority order — the first term that distinguishes two
134
- * rows decides — using {@link compareValues}, reversing for `descending`.
135
- *
136
- * @param rows - The rows to sort
137
- * @param order - The ordering terms in priority order
138
- * @returns A new, sorted array
139
- */
140
- export declare function sortRows(rows: readonly Row[], order: readonly Order[]): readonly Row[];
141
- /**
142
- * Apply a {@link Criteria} to rows — filter, then sort, then page.
143
- *
144
- * @remarks
145
- * The whole portable read pipeline in one place: conditions filter, `order`
146
- * sorts, and `offset` / `limit` window the result. Each step is skipped when its
147
- * part of the criteria is absent. The reference {@link DriverInterface} backends
148
- * lean on this rather than each re-deriving it.
149
- *
150
- * @param rows - The rows to process (typically a table's full `scan`)
151
- * @param criteria - The read specification, or `undefined` for all rows as-is
152
- * @returns The filtered, sorted, paged rows
153
- */
154
- export declare function applyCriteria(rows: readonly Row[], criteria?: Criteria): readonly Row[];
155
- /**
156
- * Compute an aggregate over a column across rows.
157
- *
158
- * @remarks
159
- * `count` returns the row count. The numeric aggregates coerce each cell with
160
- * the contracts `parseNumber` (so `'42'` counts) and ignore non-numeric cells;
161
- * over zero numeric values they return `undefined` — the SQL `NULL` of an empty
162
- * aggregate.
163
- *
164
- * @param rows - The rows to aggregate (non-record entries are ignored)
165
- * @param operation - The aggregate to compute
166
- * @param column - The column to aggregate
167
- * @returns The aggregate value, or `undefined` when undefined for the inputs
168
- */
169
- export declare function computeAggregate(rows: readonly unknown[], operation: AggregateFunction, column: FieldPath): number | undefined;
170
- /**
171
- * Read a row's primary key from a column, when it is a usable {@link Key}.
172
- *
173
- * @param row - The row to read
174
- * @param column - The primary-key column name
175
- * @returns The key (a string or finite number), or `undefined`
176
- */
177
- export declare function extractKey(row: Row, column: string): Key | undefined;
178
- /**
179
- * Map a column's {@link ContractShape} to its portable {@link ColumnType} — the
180
- * value a `TableSchema` carries so a native backend can declare a real column.
181
- *
182
- * @remarks
183
- * `string` → `text`; `number` → `integer` when the shape is integer-only, else
184
- * `real`; `boolean` → `boolean`. A `literal` takes the type of its values
185
- * (all-boolean → `boolean`, all-integer → `integer`, mixed/fractional numbers →
186
- * `real`, anything else → `text`). `optional` / `nullable` unwrap to their inner
187
- * type (nullability is tracked separately). `null` / `object` / `array` / `union` /
188
- * `json` / `raw` → `json`: a backend stores them as JSON text and can `json_extract`
189
- * for nested `FieldPath` queries. A scan-only backend ignores the result.
190
- *
191
- * @param shape - The column's contract shape
192
- * @returns The portable column type
193
- *
194
- * @example
195
- * ```ts
196
- * shapeToColumnType(stringShape()) // 'text'
197
- * shapeToColumnType(integerShape()) // 'integer'
198
- * shapeToColumnType(optionalShape(integerShape())) // 'integer'
199
- * shapeToColumnType(objectShape({ a: stringShape() })) // 'json'
200
- * ```
201
- */
202
- export declare function shapeToColumnType(shape: ContractShape): ColumnType;
203
- /**
204
- * Throw when an {@link ReadOptions.signal | AbortSignal} has fired — the shared
205
- * cancellation gate checked at operation boundaries and between streamed rows.
206
- *
207
- * @remarks
208
- * A no-op for `undefined` or a live signal, so callers thread `options?.signal`
209
- * straight through. When the signal has aborted, throws an `ABORTED`
210
- * {@link DatabaseError} carrying the signal's `reason` in its context — callers
211
- * mint signals with whatever tool they like (`AbortSignal.timeout(ms)`,
212
- * `new AbortController()`, `@orkestrel/abort`).
213
- *
214
- * @param signal - The signal to check, if any
215
- * @returns Nothing — returns normally while the signal is live
216
- * @throws An `ABORTED` {@link DatabaseError} when the signal has aborted
217
- *
218
- * @example
219
- * ```ts
220
- * import { checkAbort } from '@orkestrel/database'
221
- *
222
- * const controller = new AbortController()
223
- * checkAbort(controller.signal) // returns
224
- * controller.abort('too slow')
225
- * checkAbort(controller.signal) // throws DatabaseError('ABORTED', …)
226
- * ```
227
- */
228
- export declare function checkAbort(signal: AbortSignal | undefined): void;
229
- /**
230
- * Structurally diff a deployed and a declared table set into a {@link Migration}
231
- * plan.
232
- *
233
- * @remarks
234
- * Tables present in `declared` but not `deployed` become `table.add` steps
235
- * (carrying the full declared {@link TableSchema}); tables present in
236
- * `deployed` but not `declared` become `table.remove` steps. Tables present in
237
- * both are diffed column-by-column (by name) and index-group-by-index-group
238
- * (by deep equality of the column-name array), each producing `column.add` /
239
- * `column.remove` / `index.add` / `index.remove` steps. Step order is
240
- * deterministic: every `table.remove`, then every `table.add`, then each
241
- * shared table's column/index changes in `declared` order. `from` / `to` are
242
- * plan labels only — version tracking itself is deferred to persistent
243
- * backends.
244
- *
245
- * @param deployed - The table schemas currently applied
246
- * @param declared - The table schemas the caller wants applied
247
- * @param from - The plan's source version label (defaults to `0`)
248
- * @param to - The plan's target version label (defaults to `1`)
249
- * @returns The migration plan moving `deployed` toward `declared`
250
- *
251
- * @example
252
- * ```ts
253
- * const plan = planMigration(
254
- * [{ name: 'users', primary: 'id', columns: [], indexes: [] }],
255
- * [{ name: 'users', primary: 'id', columns: [{ name: 'age', type: 'integer', nullable: false }], indexes: [] }],
256
- * )
257
- * // plan.steps === [{ operation: 'column.add', table: 'users', column: { name: 'age', ... } }]
258
- * ```
259
- */
260
- export declare function planMigration(deployed: readonly TableSchema[], declared: readonly TableSchema[], from?: number, to?: number): Migration;
261
- /**
262
- * Apply one table's {@link MigrationStep}s to its rows — a pure row transform.
263
- *
264
- * @remarks
265
- * `column.remove` drops that field from every row (a fresh copy — inputs are
266
- * never mutated, AGENTS §11); `column.add` leaves rows as-is (an absent field
267
- * reads as `undefined`, backfill is application policy). `table.add` /
268
- * `table.remove` / `index.add` / `index.remove` are no-ops here (they operate
269
- * on storage shape, not row shape). Steps for tables other than the one
270
- * `rows` belongs to are ignored — pass only the steps relevant to this table.
271
- *
272
- * @param rows - The table's current rows
273
- * @param steps - The migration steps to apply (typically one table's slice of a {@link Migration})
274
- * @returns A new array of transformed rows; `rows` is never mutated
275
- *
276
- * @example
277
- * ```ts
278
- * const rows = [{ id: 'a', name: 'Ada', legacy: true }]
279
- * migrateRows(rows, [{ operation: 'column.remove', table: 'users', column: 'legacy' }])
280
- * // => [{ id: 'a', name: 'Ada' }]
281
- * ```
282
- */
283
- export declare function migrateRows(rows: readonly Row[], steps: readonly MigrationStep[]): readonly Row[];
284
- /**
285
- * Run the driver-conformance battery against a fresh {@link DriverInterface}
286
- * per phase, yielding one {@link ConformanceFinding} per violated invariant —
287
- * the shared invariant suite every backend (in-memory, SQLite, IndexedDB)
288
- * must uphold to be a drop-in {@link DriverInterface}.
289
- *
290
- * @remarks
291
- * Framework-agnostic: no test-runner or Node imports, only sibling core
292
- * modules — so it runs equally from a unit test, a smoke script, or a new
293
- * driver's own README. Opens a fixed two-table schema (`users` keyed by the
294
- * default `id`, `posts` keyed by a non-id `slug`) and, calling `factory()`
295
- * fresh for each phase so failures stay isolated, verifies: `open`/`close`;
296
- * `read` of a missing key returns `undefined`; `write`/`read` round-trip with
297
- * copy-in/copy-out isolation (mutating the caller's row after `write`, or the
298
- * row `read` returns, never perturbs stored state) and upsert-overwrite;
299
- * `delete` returns `true` then `false`; `keys`/`scan` yield in ascending key
300
- * order; `clear` empties only its target table; `snapshot`'s rollback thunk
301
- * restores pre-snapshot state; a scoped `snapshot(['users'])` rolls back only
302
- * the named table, leaving a concurrent mutation to another table intact; a
303
- * non-`id` primary key (`posts.slug`) round-trips; a nested-object row
304
- * round-trips structurally (via {@link deepEqual}). The optional surface is
305
- * presence-gated: when `migrate` exists, a `column.remove` plan strips the
306
- * column from stored rows and a plan referencing an unknown table throws
307
- * `DatabaseError` `MIGRATION`; when `stream` exists, it yields only
308
- * condition-matching rows and honors `offset`/`limit`; when `transaction`
309
- * exists, `commit` persists and `rollback` restores; when both `meta` and
310
- * `stamp` exist, a fresh store's `meta()` is `undefined`, and after
311
- * `stamp({ version, schema })`, `meta()` returns the exact stamped value.
312
- *
313
- * Each phase runs within a `try`/`catch`: an EXPECTED mismatch yields a
314
- * finding built from the assertion, while an UNEXPECTED throw (a driver
315
- * crash mid-phase) is caught and yielded as a finding too, naming the phase
316
- * as `check` and carrying the caught error in `context.error` — a broken
317
- * driver can never escape the battery as an unhandled rejection. Within a
318
- * phase, the FIRST violated assertion yields and the phase stops (matching
319
- * the historical fail-fast shape at phase granularity); the generator then
320
- * moves on to the next phase regardless. Because this is a **generator**,
321
- * consuming only the first yielded value reproduces true fail-fast (later
322
- * phases never run) — that is exactly what {@link conformDriver} does.
323
- *
324
- * @param factory - Mints a fresh, unopened driver instance (called once per phase)
325
- * @yields One {@link ConformanceFinding} per violated invariant, in phase order
326
- *
327
- * @example
328
- * ```ts
329
- * import { createMemoryDriver, driverFindings } from '@orkestrel/database'
330
- *
331
- * for await (const finding of driverFindings(() => createMemoryDriver())) {
332
- * console.log(finding.check, finding.message)
333
- * }
334
- * ```
335
- */
336
- export declare function driverFindings(factory: () => DriverInterface): AsyncGenerator<ConformanceFinding>;
337
- /**
338
- * Run the driver-conformance battery, throwing on the first violated
339
- * invariant — the fail-fast entry point most callers (test setup, CI smoke
340
- * checks) want.
341
- *
342
- * @remarks
343
- * A thin driver over {@link driverFindings}: because that generator is
344
- * lazy, consuming only its first yielded value means every LATER phase
345
- * never runs — true fail-fast, not merely "report only the first". The
346
- * thrown error is byte-compatible with the historical shape: a
347
- * `CONFORMANCE` {@link DatabaseError} whose `message` is the finding's
348
- * `message` and whose `context` is `{ check, ...finding.context }`.
349
- *
350
- * @param factory - Mints a fresh, unopened driver instance (called once per phase)
351
- * @returns Nothing — resolves once every phase has passed
352
- * @throws A `CONFORMANCE` {@link DatabaseError} on the first violated invariant
353
- *
354
- * @example
355
- * ```ts
356
- * import { conformDriver, createMemoryDriver } from '@orkestrel/database'
357
- *
358
- * await conformDriver(() => createMemoryDriver()) // resolves when every invariant holds
359
- * ```
360
- */
361
- export declare function conformDriver(factory: () => DriverInterface): Promise<void>;
362
- /**
363
- * Run the FULL driver-conformance battery and collect every violation — the
364
- * audit entry point for a driver author who wants a complete report rather
365
- * than a single fail-fast throw.
366
- *
367
- * @remarks
368
- * Drains {@link driverFindings} to completion: every phase runs regardless
369
- * of earlier violations, so a driver breaking two independent invariants
370
- * reports both. An empty array means the driver is fully conformant.
371
- *
372
- * @param factory - Mints a fresh, unopened driver instance (called once per phase)
373
- * @returns Every violated invariant found, in phase order (empty when fully conformant)
374
- *
375
- * @example
376
- * ```ts
377
- * import { auditDriver, createMemoryDriver } from '@orkestrel/database'
378
- *
379
- * const findings = await auditDriver(() => createMemoryDriver())
380
- * for (const finding of findings) console.log(`${finding.check}: ${finding.message}`)
381
- * ```
382
- */
383
- export declare function auditDriver(factory: () => DriverInterface): Promise<readonly ConformanceFinding[]>;