@orkestrel/database 0.0.1

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.
@@ -0,0 +1,383 @@
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[]>;
@@ -0,0 +1,11 @@
1
+ export type * from './types.js';
2
+ export * from './constants.js';
3
+ export * from './errors.js';
4
+ export * from './helpers.js';
5
+ export * from './factories.js';
6
+ export * from './Database.js';
7
+ export * from './drivers/MemoryDriver.js';
8
+ export * from './Table.js';
9
+ export * from './Query.js';
10
+ export * from './Clause.js';
11
+ export * from './Cursor.js';