@orkestrel/scaffold 0.0.66 → 0.0.68

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.
Files changed (74) hide show
  1. package/dist/bin/main.js +67 -44
  2. package/dist/bin/main.js.map +1 -1
  3. package/dist/host/agents/templates/brief.md +9 -0
  4. package/dist/host/claude/agents/orkestrel.md +8 -8
  5. package/dist/host/claude/rules/names.md +15 -0
  6. package/dist/host/claude/rules/tests.md +33 -4
  7. package/dist/host/claude/rules/workspace.md +14 -2
  8. package/dist/host/dotfiles/prettierignore +3 -0
  9. package/dist/host/guides/README.md +65 -0
  10. package/dist/host/guides/abort.md +169 -0
  11. package/dist/host/guides/agent.md +1509 -0
  12. package/dist/host/guides/brief.md +1266 -0
  13. package/dist/host/guides/browser.md +2200 -0
  14. package/dist/host/guides/budget.md +196 -0
  15. package/dist/host/guides/codec.md +519 -0
  16. package/dist/host/guides/console.md +785 -0
  17. package/dist/host/guides/contract.md +1193 -0
  18. package/dist/host/guides/csv.md +541 -0
  19. package/dist/host/guides/database.md +2518 -0
  20. package/dist/host/guides/emitter.md +233 -0
  21. package/dist/host/guides/form.md +1791 -0
  22. package/dist/host/guides/html.md +717 -0
  23. package/dist/host/guides/indexeddb.md +505 -0
  24. package/dist/host/guides/interpret.md +1029 -0
  25. package/dist/host/guides/lsp.md +515 -0
  26. package/dist/host/guides/markdown.md +964 -0
  27. package/dist/host/guides/mcp.md +5554 -0
  28. package/dist/host/guides/middleware.md +927 -0
  29. package/dist/host/guides/msg.md +440 -0
  30. package/dist/host/guides/ndjson.md +120 -0
  31. package/dist/host/guides/ollama.md +380 -0
  32. package/dist/host/guides/pool.md +280 -0
  33. package/dist/host/guides/probe.md +1210 -0
  34. package/dist/host/guides/process.md +1620 -0
  35. package/dist/host/guides/program.md +1110 -0
  36. package/dist/host/guides/qualifier.md +854 -0
  37. package/dist/host/guides/queue.md +370 -0
  38. package/dist/host/guides/rater.md +330 -0
  39. package/dist/host/guides/reason.md +1122 -0
  40. package/dist/host/guides/relation.md +373 -0
  41. package/dist/host/guides/router.md +753 -0
  42. package/dist/host/guides/scaffold.md +192 -31
  43. package/dist/host/guides/sea.md +383 -0
  44. package/dist/host/guides/server.md +752 -0
  45. package/dist/host/guides/sqlite.md +330 -0
  46. package/dist/host/guides/sse.md +187 -0
  47. package/dist/host/guides/supervisor.md +4890 -0
  48. package/dist/host/guides/table.md +1556 -0
  49. package/dist/host/guides/template.md +280 -0
  50. package/dist/host/guides/terminal.md +1145 -0
  51. package/dist/host/guides/test.md +2969 -0
  52. package/dist/host/guides/timeout.md +252 -0
  53. package/dist/host/guides/tool.md +311 -0
  54. package/dist/host/guides/toolbox.md +1038 -0
  55. package/dist/host/guides/websocket.md +282 -0
  56. package/dist/host/guides/worker.md +615 -0
  57. package/dist/host/guides/workflow.md +1507 -0
  58. package/dist/host/guides/workspace.md +595 -0
  59. package/dist/host/manifest.json +1218 -10
  60. package/dist/host/tests/policy.test.ts +279 -2
  61. package/dist/host/tests/setupPolicy.ts +437 -6
  62. package/dist/src/core/index.cjs +44 -22
  63. package/dist/src/core/index.cjs.map +1 -1
  64. package/dist/src/core/index.d.cts +33 -9
  65. package/dist/src/core/index.d.ts +33 -9
  66. package/dist/src/core/index.js +43 -23
  67. package/dist/src/core/index.js.map +1 -1
  68. package/dist/src/server/index.cjs +1750 -1567
  69. package/dist/src/server/index.cjs.map +1 -1
  70. package/dist/src/server/index.d.cts +106 -24
  71. package/dist/src/server/index.d.ts +106 -24
  72. package/dist/src/server/index.js +1751 -1570
  73. package/dist/src/server/index.js.map +1 -1
  74. package/package.json +9 -9
@@ -0,0 +1,2518 @@
1
+ # Database
2
+
3
+ > One typed database API for keyed rows, fluent queries, cursors, and
4
+ > whole-store transactions, running unchanged over an in-memory map, a JSON
5
+ > file, SQLite, or IndexedDB.
6
+
7
+ A table is a contract. Declare a `tables` map of
8
+ [`ContractShape`](contract.md)s once, and the row type, write-time coercion and
9
+ validation, JSON-Schema introspection, and seed data all flow from that one
10
+ declaration — no separate schema, no annotations, no `as`.
11
+
12
+ The design stance is one engine, thin drivers. A backend implements only an
13
+ irreducible storage primitive — keyed read/write/insert/delete, an ordered
14
+ `scan`, key listing, and a `snapshot` — and inherits the entire WHERE / order /
15
+ page / aggregate surface from a single pure query engine in the core. A backend
16
+ that can go faster (a SQL `WHERE`, an index range) implements the optional
17
+ native hooks the engine falls back from; it never re-derives query semantics.
18
+ So this is deliberately not an ORM and not a query abstraction layer: there is
19
+ no entity graph, no migration runner, and no raw-SQL escape hatch — only the
20
+ smallest cross-environment core that earns its keep.
21
+
22
+ Source: [`src/core`](../src/core), published through `@orkestrel/database`. The
23
+ persistent drivers ship alongside it: a trusted-mode SQLite driver in
24
+ [`src/server`](../src/server) (surfaced through `@orkestrel/database/server`)
25
+ with native querying, paging, aggregation, transactions, and atomic migration,
26
+ and a narrow-then-refine IndexedDB driver in [`src/browser`](../src/browser)
27
+ (surfaced through `@orkestrel/database/browser`) that pushes a key-range
28
+ candidate set down to the index and lets the core engine refine it to the exact
29
+ result — beside the I/O-free `MemoryDriver` and the file-persisted
30
+ `JSONDriver`.
31
+
32
+ ## Surface
33
+
34
+ Declare a `tables` shape map (keys are table names) once, and reach each
35
+ table — fully typed, no annotations — with `table(name)`.
36
+
37
+ ### Create a database
38
+
39
+ Declares two tables, opens a memory-backed database over them, and runs a keyed write, a keyed read, and a fluent query:
40
+
41
+ ```ts
42
+ import { createDatabase, createMemoryDriver } from '@orkestrel/database'
43
+ import { integerShape, stringShape } from '@orkestrel/contract'
44
+
45
+ const db = createDatabase({
46
+ driver: createMemoryDriver(), // any DriverInterface — a persistent backend swaps in, same API
47
+ tables: {
48
+ users: { id: stringShape(), name: stringShape(), age: integerShape() },
49
+ posts: { slug: stringShape(), title: stringShape() },
50
+ },
51
+ primary: { posts: 'slug' }, // non-`id` primary-key columns, per table
52
+ })
53
+
54
+ const users = db.table('users') // hold the handle; TableInterface<{ id; name; age }>
55
+
56
+ await users.set({ id: 'u1', name: 'Ada', age: 36 }) // coerced + validated through the contract
57
+ await users.get('u1') // typed { id; name; age } | undefined — narrowed, never `as`
58
+ await users
59
+ .query()
60
+ .condition({ column: 'age', operator: 'from', values: [18], connector: 'and' })
61
+ .order({ column: 'age', direction: 'descending' })
62
+ .collect() // typed rows
63
+ ```
64
+
65
+ Each `tables` value is a column map (a `column → shape` map) — a table row is
66
+ always an object, so the database wraps it in an `objectShape` for you; you
67
+ never write `objectShape` at the table level. The row type is `Infer` of
68
+ those columns, so `db.table('users')` is checked against the schema (a
69
+ typo'd column name or a wrong-typed write fails at compile time) and returns
70
+ a `TableInterface` typed by that row. That one declaration is the single
71
+ source of truth: it types the table, drives write coercion + validation,
72
+ produces the JSON Schema, and seeds fixtures.
73
+
74
+ ### Factories
75
+
76
+ | API | Kind | Summary |
77
+ | ----------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------- |
78
+ | `createDatabase` | function | Creates a database over a driver and a declared `tables` schema. |
79
+ | `createMemoryDriver` | function | Creates the in-memory reference `DriverInterface`. |
80
+ | `createJSONDriver` | function | Creates a persistent JSON-file `DriverInterface` for a given path. |
81
+ | `createSQLiteDriver` | function | Creates a trusted-mode, server-native SQLite `DriverInterface` for a database path, or for `:memory:` when the options bag omits one. |
82
+ | `createIndexedDBDriver` | function | Creates a persistent IndexedDB `DriverInterface` for a browser database name. |
83
+
84
+ ### Classes
85
+
86
+ | Class | Kind | Summary |
87
+ | ----------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
88
+ | `Database` | class | Exposes a typed view over one shared internal lifecycle and storage context. |
89
+ | `DriverIterator` | class | Forms the internal continuation boundary for a root driver async iterator. |
90
+ | `MemoryDriver` | class | Implements the reference `DriverInterface` — nested maps, no I/O. |
91
+ | `JSONDriver` | class | Implements a persistent `DriverInterface` backed by a single JSON file — the reference `MemoryDriver` plus file load / flush. |
92
+ | `SQLiteDriver` | class | Implements the `DriverInterface` over SQLite — the server-native, trusted-mode backend built on the published `@orkestrel/sqlite` synchronous wrapper. |
93
+ | `IndexedDBDriver` | class | Implements the `DriverInterface` over IndexedDB — the persistent browser backend, built on the published `@orkestrel/indexeddb` wrapper. |
94
+
95
+ ### Server
96
+
97
+ | API | Kind | Summary |
98
+ | ---------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
99
+ | `METADATA_TABLE` | const | Names the reserved metadata table the `SQLiteDriver` creates on `open` to persist its stamped `DriverMetadata` (`version` + declared schema JSON) — the SQLite realization of the `metadata` / `stamp` driver hooks. |
100
+ | `matchesConditionExactly` | function | Reports whether one `Condition` compiles to SQL that is provably identical to the core engine's `matchesCondition` for every value its column's declared type can store. |
101
+ | `matchesOrderExactly` | function | Reports whether one `Order` term's column compiles to an `ORDER BY` that matches the engine's `sortRows` exactly. |
102
+ | `matchesQueryExactly` | function | Reports whether a whole `QueryInput` is exact — every condition and every order term is exact. `limit` / `offset` never affect exactness (SQL `LIMIT` / `OFFSET` are always engine-identical). |
103
+ | `matchesDeclaredStorage` | function | Reports whether a value's runtime type matches a column's declared exact type — the operand side of the declared-type-trust proof. |
104
+ | `EXACT_COLUMN_STORAGE` | const | Lists the declared `ColumnStorage`s whose SQL equality comparisons (`equals` / `not` / `any` / `none`) and `starts` / `ends` compiles are provably engine-exact under declared-type trust — `text` / `integer` / `real` / `boolean`; a `json` or `blob` column always refines instead. |
105
+ | `EXACT_RANGE_COLUMN_STORAGE` | const | Lists the declared `ColumnStorage`s whose SQL range comparisons (`above` / `below` / `from` / `to` / `between`) and `ORDER BY` compiles are provably engine-exact — `integer` / `real` / `boolean` only. `text` is excluded: see `EXACT_COLUMN_STORAGE`'s remarks for the BINARY-collation (code-point) vs. JS `<` (code-unit) divergence on supplementary-plane characters. |
106
+ | `extractValues` | function | Extracts a stored row's values in a declared positional order. |
107
+ | `deriveSQLiteIndexName` | function | Builds a collision-free SQL index name for a table + column-group index — shared by the compiler module's `schemaToIndexes` and `stepToSQL`, so a plan-built index name always matches one `open` would have created. |
108
+
109
+ ### SQL compilation
110
+
111
+ Pure, server-only functions that turn a core `QueryInput` / `TableSchema` into
112
+ parameterized SQL text — the native-query payoff for a SQLite-backed driver.
113
+ None of these import a SQLite package; they speak strings and values only.
114
+
115
+ | API | Kind | Summary |
116
+ | ------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
117
+ | `inferValueStorage` | function | Reads the storage type a nested (`json_extract`) operand encodes as from its runtime value, never as `json`. |
118
+ | `compileJSONTypeSQL` | function | Compiles a nested `FieldPath` to the `json_type(<col>, <path>)` SQL expression — the `compileFieldSQL` `json_extract` sibling used to tell a present JSON `null` apart from an absent path (both read back as SQL `NULL` through `json_extract`, but `json_type` reports `'null'` for the former and SQL `NULL` for the latter). |
119
+ | `compileConditionSQL` | function | Compiles one condition to its `<column> <operator>` SQL fragment and the parameters it binds — engine-exact under SQL's three-valued NULL logic. |
120
+ | `compileWhereSQL` | function | Folds the conditions into one WHERE clause, parenthesizing progressively left-to-right so the grouping matches the engine's `matchesQuery` fold. |
121
+ | `compileOrderSQL` | function | Compiles the ORDER BY clause from the order terms, always ending with the primary key as the final determinant. |
122
+ | `compilePageSQL` | function | Compiles the LIMIT / OFFSET clause. |
123
+ | `compileQuerySQL` | function | Compiles a `QueryInput` into the SQL clause that follows a table name, with its bound parameters in clause order. |
124
+ | `quoteIdentifier` | function | Quotes a SQL identifier (a table or column name) so any characters are literal. |
125
+ | `compileFieldSQL` | function | Compiles a `FieldPath` to the SQL expression that reads it. |
126
+ | `compileColumnSQL` | function | Maps a portable `ColumnStorage` to its SQLite column type. |
127
+ | `compileAggregateSQL` | function | Compiles an `AggregateOperation` over a `FieldPath`. |
128
+ | `matchesAggregateExactly` | function | Reports whether SQLite can execute an aggregate exactly like the core engine. |
129
+ | `matchesSQLiteAffinity` | function | Checks a declared SQLite type against a portable storage affinity. |
130
+ | `matchesAbsentPath` | function | Reports whether a caught filesystem error says that nothing is there to read. |
131
+ | `encodeValue` | function | Encodes a JS value to its stored `SQLiteValue` for a declared column. |
132
+ | `decodeValue` | function | Decodes a stored `SQLiteValue` back to its JS value for a declared column — the exact inverse of `encodeValue`. |
133
+ | `encodeRow` | function | Encodes a whole `Row` to a `SQLiteRow` by its table's schema. |
134
+ | `decodeRow` | function | Decodes a stored `SQLiteRow` back to a `Row` by its table's schema. |
135
+ | `schemaToTable` | function | Projects a `TableSchema` to its `CREATE TABLE IF NOT EXISTS` statement. |
136
+ | `schemaToIndexes` | function | Projects a `TableSchema` to its declared SQLite indexes. |
137
+ | `stepToSQL` | function | Projects one `MigrationStep` to SQLite DDL. |
138
+
139
+ ### Browser
140
+
141
+ Pure functions behind the IndexedDB driver's key-range pushdown planner — a
142
+ candidate superset the core engine then refines to the exact result, never
143
+ lossy.
144
+
145
+ | API | Kind | Summary |
146
+ | -------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
147
+ | `selectPlan` | function | Plans an IndexedDB read for a `QueryInput` — picks the index (or the primary store) and `IDBKeyRange` to narrow by, falling back to a full scan. |
148
+ | `conditionToRange` | function | Translates one `Condition` to the `IDBKeyRange` it maps to, when its operator is one of the exact key comparisons over scalar operands; otherwise returns `undefined`. |
149
+ | `INDEXABLE_STORAGE` | const | Lists the declared `ColumnStorage`s that are valid, orderable IndexedDB keys. |
150
+ | `METADATA_STORE` | const | Names the reserved out-of-line store `__metadata__` the `IndexedDBDriver` stamps its `DriverMetadata` into. |
151
+ | `mapIndexedDBError` | function | Maps a backend `IndexedDBError` to the portable `DatabaseError` taxonomy — the default mapping used everywhere except inside `migrate()`. |
152
+ | `mapMigrationError` | function | Maps a backend `IndexedDBError` to the portable `DatabaseError` taxonomy for use inside `migrate()` — the one context where `UPGRADE` means the migration itself failed, not a generic driver fault. |
153
+ | `deriveIndexedDBIndexName` | function | Derives an IndexedDB index name for a declared column group — a bare column name for a single-column index, a deterministic collision-free encoding for a compound one. |
154
+ | `schemaToStore` | function | Projects a table schema into the IndexedDB wrapper's store definition. |
155
+
156
+ ### Errors
157
+
158
+ | API | Kind | Summary |
159
+ | ----------------- | -------- | ----------------------------------------------------- |
160
+ | `DatabaseError` | class | Represents an error thrown by the database layer. |
161
+ | `isDatabaseError` | function | Narrows an unknown caught value to a `DatabaseError`. |
162
+
163
+ ### Query engine
164
+
165
+ The portable semantics every backend shares — pure, total functions the
166
+ driver never re-implements.
167
+
168
+ | Helper | Kind | Summary |
169
+ | ---------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
170
+ | `compareValues` | function | Compares two arbitrary values under one total order — the comparator behind sorting and the range operators. |
171
+ | `matchesCondition` | function | Evaluates one `Condition` against a row — the per-operator predicate. |
172
+ | `matchesQuery` | function | Folds a row through a list of conditions, joining each by its connector. |
173
+ | `sortRows` | function | Sorts rows by an ordering specification, leaving the input untouched. |
174
+ | `applyQuery` | function | Applies a `QueryInput` to rows — filter, then sort, then page. |
175
+ | `validatePage` | function | Validates the paging fields of a portable query. |
176
+ | `computeAggregate` | function | Computes an aggregate over a column across rows. |
177
+ | `extractKey` | function | Reads a row's primary key from a column, when it is a usable `Key`. |
178
+ | `bindRowKey` | function | Returns a fresh row whose primary column is authoritatively bound to its storage key. |
179
+ | `shapeToColumnSchema` | function | Projects one contract shape into a portable column schema. |
180
+ | `findColumn` | function | Reads one flat column's declaration out of a table schema. |
181
+ | `resolvePrimary` | function | Resolves the primary-key column one table keys its rows by. |
182
+ | `requireColumns` | function | Requires one declared table's columns out of a table map. |
183
+ | `shapeToColumnStorage` | function | Maps a column's `ContractShape` to its portable `ColumnStorage` — the value a `TableSchema` carries so a native backend can declare a real column. |
184
+ | `filterRows` | function | Filters rows by a list of conditions — the shared basis for a table's count and aggregate paths (no sort/page, unlike `applyQuery`). |
185
+ | `equalsValue` | function | Compares two values structurally by SameValueZero leaves — the comparator behind conformance checks and any test/fixture that needs "same data", not "same reference". |
186
+
187
+ For `minimum` and `maximum`, `computeAggregate` compares each numeric value
188
+ with a scalar `Math.min` or `Math.max` call. The helper never passes a
189
+ row-sized argument list.
190
+
191
+ ### Abort
192
+
193
+ | API | Kind | Summary |
194
+ | ------------ | -------- | ------------------------------------------------------------------------------------------------------------------------- |
195
+ | `checkAbort` | function | Throws when an `AbortSignal` has fired — the shared abort gate checked at operation boundaries and between streamed rows. |
196
+
197
+ ### Migrations
198
+
199
+ Caller-driven schema migration — a pure structural diff plus a pure row
200
+ transform. Versioning drivers persist reconciliation metadata through the paired
201
+ `metadata` / `stamp` hooks.
202
+
203
+ | API | Kind | Summary |
204
+ | ------------------------ | -------- | ------------------------------------------------------------------------------- |
205
+ | `planMigration` | function | Diffs a deployed and a declared table set structurally into a `Migration` plan. |
206
+ | `migrateRows` | function | Applies one table's `MigrationStep`s to its rows — a pure row transform. |
207
+ | `projectMigrationSchema` | function | Projects migration steps sequentially over a canonical validated owned schema. |
208
+ | `normalizeDriverSchema` | function | Canonicalizes an unknown driver schema into a distinct deeply frozen snapshot. |
209
+
210
+ ### Conformance
211
+
212
+ | API | Kind | Summary |
213
+ | --------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
214
+ | `conformDriver` | function | Runs the driver-conformance battery, throwing on the first violated invariant — the fail-fast entry point most callers (test setup, CI smoke checks) want. |
215
+ | `scanDriver` | function | Walks the driver-conformance battery against a fresh `DriverInterface` per phase, yielding one `ConformanceFinding` per violated invariant — the shared invariant suite every backend (in-memory, SQLite, IndexedDB) must uphold to be a drop-in `DriverInterface`. |
216
+ | `auditDriver` | function | Runs the full driver-conformance battery and collects every violation — the audit entry point for a driver author who wants a complete report rather than a single fail-fast throw. |
217
+
218
+ ### Helpers & guards
219
+
220
+ Pure helpers behind the query engine's pattern matching.
221
+
222
+ | API | Kind | Summary |
223
+ | ------------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
224
+ | `cloneDriverMetadata` | function | Clones unknown driver metadata into a distinct deeply frozen snapshot. |
225
+ | `matchesWildcardPattern` | function | Matches a value against a wildcard pattern in linear time — the shared, ReDoS-safe engine behind `matchesLikePattern` and `matchesGlobPattern`. |
226
+ | `matchesLikePattern` | function | Matches a value against a SQL `LIKE` pattern, folding case. |
227
+ | `matchesGlobPattern` | function | Matches a value against a `GLOB` pattern, preserving case. |
228
+ | `isDriverMetadata` | function | Checks whether a value is persisted driver metadata. |
229
+ | `isDriverSchema` | function | Checks whether a value is a complete portable driver schema. |
230
+ | `isColumnSchema` | function | Checks whether a value is a portable column schema. |
231
+ | `isTableSchema` | function | Checks whether a value is a portable table schema. |
232
+ | `isMigrationStep` | function | Checks whether a value is one ordered migration step. |
233
+ | `isMigration` | function | Checks whether a value is an ordered migration plan. |
234
+ | `isMigrationInput` | function | Checks whether a value is one atomic migration request. |
235
+ | `isKey` | function | Checks whether a value is a usable database key. |
236
+ | `cloneDriverSchema` | function | Clones unknown driver schema into a distinct deeply frozen snapshot. |
237
+ | `cloneMigrationInput` | function | Clones unknown migration input into a distinct deeply frozen snapshot. |
238
+
239
+ ### Constants
240
+
241
+ A `Shape` cell holds the constant's declared type.
242
+
243
+ | Constant | Kind | Shape | Summary |
244
+ | -------------------------- | ----- | ------------------------ | ------------------------------------------------------------------------------------------------------------- |
245
+ | `DEFAULT_PRIMARY` | const | `string` | Supplies the primary-key column, `'id'`, assumed when `PrimaryMap` does not name one. |
246
+ | `MAX_PATTERN_LENGTH` | const | `number` | Sets the longest `LIKE` / `GLOB` pattern the wildcard matcher accepts, 1024 characters, before rejecting it. |
247
+ | `CONFORMANCE_USERS_SCHEMA` | const | `TableSchema` | Describes the `users` table the driver-conformance battery opens — keyed by the default `id` primary column. |
248
+ | `CONFORMANCE_POSTS_SCHEMA` | const | `TableSchema` | Describes the `posts` table the driver-conformance battery opens — keyed by a non-`id` `slug` primary column. |
249
+ | `CONFORMANCE_SCHEMA` | const | `readonly TableSchema[]` | Holds the fixed `users` and `posts` schema every driver-conformance phase opens. |
250
+
251
+ ### Types
252
+
253
+ A `Shape` cell holds an interface's data members as bare names in braces, `?` marking an optional member and `plus` introducing its call-signature members, and a type alias's own type literal with a union's arms escaped as `\|`. An extended interface's name comes before `plus`, with the members it adds after.
254
+
255
+ | Type | Kind | Shape | Summary |
256
+ | -------------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
257
+ | `Key` | type | `string \| number` | Represents a primary key — the value identifying a row within its table. |
258
+ | `KeyFunction` | type | `() => Key` | Represents a key-generating function. |
259
+ | `Row` | type | `Record<string, unknown>` | Represents a table row — a plain record of column values keyed by column name. |
260
+ | `ConditionOperator` | type | `'equals' \| 'not' \| 'above' \| 'below' \| 'from' \| 'to' \| 'between' \| 'like' \| 'glob' \| 'starts' \| 'ends' \| 'any' \| 'none' \| 'absent' \| 'present'` | Represents a WHERE operator — the comparison a single `Condition` applies. |
261
+ | `ConditionConnector` | type | `'and' \| 'or'` | Names how a `Condition` joins to the running result of the conditions before it. |
262
+ | `Condition` | interface | `{ column, operator, values, connector }` | Represents one compiled WHERE condition. |
263
+ | `OrderDirection` | type | `'ascending' \| 'descending'` | Names a sort direction. |
264
+ | `Order` | interface | `{ column, direction }` | Represents one ordering term — a column (`FieldPath`, flat or nested) and its direction. |
265
+ | `QueryInput` | interface | `{ conditions?, order?, limit?, offset? }` | Represents a serializable read specification — everything a backend needs to compile one read, free of JS callbacks so any backend can honor it. |
266
+ | `AggregateOperation` | type | `'count' \| 'sum' \| 'average' \| 'minimum' \| 'maximum'` | Names an aggregate computed over a numeric column. |
267
+ | `OperationOptions` | interface | `{ signal? }` | Options for an abortable operation. |
268
+ | `DatabaseStatus` | type | `'idle' \| 'open' \| 'closed'` | Names the lifecycle state of a `DatabaseInterface`. |
269
+ | `AdmissionInterface` | interface | `{ accepting } plus track` | Represents the admission boundary a scoped operation enters before it runs. |
270
+ | `DatabaseErrorCode` | type | `'CLOSED' \| 'NOT_FOUND' \| 'CONFLICT' \| 'VALIDATION' \| 'ABORTED' \| 'MIGRATION' \| 'CONFORMANCE' \| 'DRIVER'` | Names a machine-readable `DatabaseError` code. |
271
+ | `ConformanceFinding` | interface | `{ check, message, context }` | Represents one violated invariant from the driver-conformance battery. |
272
+ | `DatabaseEventMap` | type | `{ open, close, transaction, commit, rollback, migrate }` | Describes the push observation surface of a `DatabaseInterface` — the connection + transaction lifecycle a fire-and-forget observer (logging, metrics, tracing, cache invalidation) subscribes to. |
273
+ | `TableEventMap` | type | `{ write, remove, clear }` | Describes the push observation surface of a `TableInterface` — the per-row mutation moments a fire-and-forget observer (cache invalidation, sync, an audit log) subscribes to, alongside the database-level `DatabaseEventMap`. |
274
+ | `ColumnMap` | type | `Readonly<Record<string, ContractShape>>` | Represents one table's columns — a map of column name to its value `ContractShape`. |
275
+ | `TableMap` | type | `Readonly<Record<string, ColumnMap>>` | Represents a database's table schema — a map of table name to its `ColumnMap`. |
276
+ | `RowOf` | type | `Infer<{ category: 'object'; properties: C }>` | Represents the row type a table's `ColumnMap` describe — `Infer` of the `objectShape` the database wraps them in. |
277
+ | `PrimaryMap` | type | `Readonly<Record<string, string>>` | Holds per-table primary-key column overrides — `{ [table]: column }`. |
278
+ | `IndexMap` | type | `Readonly<Record<string, ReadonlyArray<readonly string[]>>>` | Holds per-table secondary indexes — `{ [table]: groups }`, each group one (possibly compound) index of column names. |
279
+ | `ColumnStorage` | type | `'text' \| 'integer' \| 'real' \| 'boolean' \| 'json' \| 'blob'` | Names a portable storage type for a column — the backend maps it to its native type (SQLite affinity, an IndexedDB value). Derived from a column's `ContractShape` by `shapeToColumnStorage`; `json` covers object/array/union/raw values a backend stores as JSON text and can `json_extract` for nested-field queries. |
280
+ | `ColumnSchema` | interface | `{ name, storage, optional, nullable }` | Represents one column of a `TableSchema` — its name, portable `ColumnStorage`, and whether it independently accepts absence (`optional`) and explicit `null` (`nullable`). |
281
+ | `TableSchema` | interface | `{ name, primary, columns, indexes }` | Represents a backend-agnostic description of one table — what `open` hands each driver so a native backend can create real tables and indexes. |
282
+ | `MigrationStep` | type | `{ operation: 'table.add', table } \| { operation: 'table.remove', table } \| { operation: 'column.add', table, column } \| { operation: 'column.remove', table, column } \| { operation: 'index.add', table, index } \| { operation: 'index.remove', table, index }` | Represents one step of a `Migration` plan — a single schema change applied to one table. |
283
+ | `Migration` | interface | `{ from, to, steps }` | Represents a schema migration plan — an ordered set of `MigrationStep`s moving a database from one schema version to another. |
284
+ | `MigrationInput` | interface | `{ plan, metadata? }` | Represents one atomic migration request. |
285
+ | `StorageInterface` | interface | `{} plus read, write, insert, delete, keys, scan, clear, records?, aggregate?, stream?, migrate?, metadata?, stamp?` | Declares the storage operations available only inside a driver's transaction scope. |
286
+ | `DriverMetadata` | interface | `{ version, schema }` | Represents persisted schema metadata a versioning driver owns as an immutable snapshot. |
287
+ | `DriverInterface` | interface | `StorageInterface plus {} plus open, close, snapshot, transaction?` | Declares the storage primitive every backend implements — the whole of the bridge. |
288
+ | `DatabaseOptions` | interface | `{ on?, error?, driver, tables, primary?, indexes?, name?, generator?, version? }` | Options for `createDatabase`. |
289
+ | `CompiledSQL` | interface | `{ sql, parameters }` | Represents a parameterized SQL fragment or statement plus its bind values. The `@orkestrel/database/server` entry point exports this type. |
290
+ | `SQLiteDriverOptions` | interface | `{ path?, readonly?, timeout?, references?, pragmas? }` | Configures `createSQLiteDriver`. The `@orkestrel/database/server` entry point exports this type. |
291
+ | `QueryPlan` | interface | `{ index?, range? }` | Represents a pushdown plan — an optional index and optional `IDBKeyRange` used to narrow a read. An omitted `index` selects the primary store; an omitted `range` performs a full scan. The plan is always a superset of the matching rows; the core engine refines it to the exact result. An empty plan (`{}`) is a primary-store full scan. The `@orkestrel/database/browser` entry point exports this type. |
292
+ | `TableDefinition` | interface | `{ primary, columns, schema }` | Represents one table's portable definition, produced by `export` — the unit of schema / migration exchange across environments. |
293
+ | `DatabaseStorageInterface` | interface | `{} plus table` | Represents a database view valid only inside one `DatabaseInterface.transaction` scope. |
294
+ | `DatabaseInterface` | interface | `{ emitter, name, status } plus table, import, export, open, close, transaction, migrate` | Represents a database — the ergonomic entry point that owns the driver and its tables. |
295
+ | `TableInterface` | interface | `{ emitter, name, primary, contract } plus get, resolve, has, keys, records, count, aggregate, scan, set, add, update, remove, clear, query, cursor` | Exposes typed keyed CRUD plus fluent query and cursor access. |
296
+ | `QueryInterface` | interface | `{} plus condition, order, filter, limit, offset, collect, find, count, stream, aggregate` | Builds a read through a fluent chain. |
297
+ | `CursorInterface` | interface | `{ value, index, done } plus next, update, remove, close` | Walks a table's rows forward for bulk in-place mutation. |
298
+
299
+ ## Methods
300
+
301
+ The public methods of each behavioral interface — one table per type, keyed
302
+ by its backticked name, every call-signature member listed (its `readonly`
303
+ data members, for example `emitter` / `name` / `status` / `primary` / `contract` /
304
+ `value` / `index` / `done`, stay in the preceding Surface rows — `emitter` is the
305
+ typed push observation surface, see [Observing](#observing)). The database and
306
+ driver classes in `### Classes` implement their interfaces exactly, so this
307
+ doubles as the per-instance method surface; `DriverIterator` is the internal
308
+ continuation boundary and implements none of these interfaces (see
309
+ `.claude/rules/documentation.md` § Parity).
310
+
311
+ #### `StorageInterface`
312
+
313
+ The storage capability a native driver passes into one transaction scope.
314
+ It exposes work, not settlement: the driver commits when the callback fulfills,
315
+ rolls back when it rejects, and invalidates the capability afterward. Every
316
+ method below runs inside that scope, and each carries the same contract its
317
+ `DriverInterface` twin carries against the whole backend, so the
318
+ `StorageInterface` and `DriverInterface` tables share one description per
319
+ method.
320
+
321
+ | Method | Returns | Summary |
322
+ | ----------- | -------------------------------------- | -------------------------------------------------------------------------------- |
323
+ | `read` | `Promise<Row \| undefined>` | Reads one row by key. |
324
+ | `write` | `Promise<void>` | Writes one row at a key. |
325
+ | `insert` | `Promise<void>` | Inserts one row atomically, rejecting `CONFLICT` when its key already exists. |
326
+ | `delete` | `Promise<boolean>` | Deletes one row by key. |
327
+ | `keys` | `Promise<readonly Key[]>` | Lists a table's keys. |
328
+ | `scan` | `AsyncIterable<Row>` | Iterates a table's rows in ascending key order. |
329
+ | `clear` | `Promise<void>` | Empties a table. |
330
+ | `records` | `Promise<readonly Row[]>` | Reads the rows matching a `QueryInput` natively — an optional hook. |
331
+ | `aggregate` | `Promise<number \| undefined>` | Computes an aggregate over a column natively — an optional hook. |
332
+ | `stream` | `AsyncIterable<Row>` | Iterates the natively filtered rows lazily — an optional hook. |
333
+ | `migrate` | `Promise<void>` | Applies one atomic `MigrationInput` — an optional hook. |
334
+ | `metadata` | `Promise<DriverMetadata \| undefined>` | Reads the persisted `DriverMetadata` as a deeply frozen copy — an optional hook. |
335
+ | `stamp` | `Promise<void>` | Writes the persisted `DriverMetadata`, snapshot at entry — an optional hook. |
336
+
337
+ #### `DriverInterface`
338
+
339
+ The complete backend extends `StorageInterface` with lifecycle, the
340
+ snapshot floor, and an optional native transaction callback. The inherited
341
+ storage/query/migration/metadata methods carry the same contract they carry on
342
+ `StorageInterface`, addressed against the whole backend rather than one
343
+ transaction scope. `open` receives the derived `TableSchema` list: a native
344
+ backend builds real tables and indexes from it, a scan-only backend reads
345
+ `name` alone. `snapshot` with no `tables` captures the whole store, while a
346
+ list scopes capture and restore to those tables. An omitted optional hook
347
+ costs nothing — the core query engine answers `records`, `aggregate`, and
348
+ `stream` over `scan` instead, and a driver without `transaction` runs a scope
349
+ on the snapshot floor (see [Native transactions](#native-transactions)).
350
+
351
+ | Method | Returns | Summary |
352
+ | ------------- | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
353
+ | `open` | `Promise<void>` | Readies the tables from a derived `TableSchema` list. |
354
+ | `close` | `Promise<void>` | Releases the backend. |
355
+ | `snapshot` | `Promise<() => Promise<void>>` | Captures table rows and returns a repeatable thunk that restores those rows — the primitive transactions are built on. |
356
+ | `read` | `Promise<Row \| undefined>` | Reads one row by key. |
357
+ | `write` | `Promise<void>` | Writes one row at a key. |
358
+ | `insert` | `Promise<void>` | Inserts one row atomically, rejecting `CONFLICT` when its key already exists. |
359
+ | `delete` | `Promise<boolean>` | Deletes one row by key. |
360
+ | `keys` | `Promise<readonly Key[]>` | Lists a table's keys. |
361
+ | `scan` | `AsyncIterable<Row>` | Iterates a table's rows in ascending key order. |
362
+ | `clear` | `Promise<void>` | Empties a table. |
363
+ | `records` | `Promise<readonly Row[]>` | Reads the rows matching a `QueryInput` natively — an optional hook. |
364
+ | `aggregate` | `Promise<number \| undefined>` | Computes an aggregate over a column natively — an optional hook. |
365
+ | `stream` | `AsyncIterable<Row>` | Iterates the natively filtered rows lazily — an optional hook. |
366
+ | `migrate` | `Promise<void>` | Applies one atomic `MigrationInput` — an optional hook. |
367
+ | `metadata` | `Promise<DriverMetadata \| undefined>` | Reads the persisted `DriverMetadata` as a deeply frozen copy — an optional hook. |
368
+ | `stamp` | `Promise<void>` | Writes the persisted `DriverMetadata`, snapshot at entry — an optional hook. |
369
+ | `transaction` | `Promise<R>` | Opens a native transaction scope — an optional driver hook. The driver owns acquisition, commit or rollback, release, and invalidation of the scoped capability. |
370
+
371
+ #### `DatabaseInterface`
372
+
373
+ `transaction` and `migrate` each take an optional `OperationOptions`, whose
374
+ `signal` is checked once, at entry.
375
+
376
+ | Method | Returns | Summary |
377
+ | ------------- | ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
378
+ | `table` | `TableInterface<RowOf<T[K]>>` | Returns the typed handle for a declared table. |
379
+ | `import` | `DatabaseInterface<U>` | Defines a further shape map of tables as a typed view over the same driver and storage. |
380
+ | `export` | `Readonly<Record<string, TableDefinition>>` | Returns one portable `TableDefinition` per declared table. |
381
+ | `open` | `Promise<void>` | Connects the driver eagerly, ahead of the lazy connect on first use. |
382
+ | `close` | `Promise<void>` | Closes the database and releases its driver. |
383
+ | `transaction` | `Promise<R>` | Runs a scope over a `DatabaseStorageInterface`, committing when the callback fulfills and rolling back when it rejects. |
384
+ | `migrate` | `Promise<Migration>` | Diffs a caller-supplied deployed schema against this database's declared schema (its `tables`, as configured) through `planMigration`, applies the resulting plan through the driver's optional `migrate` hook, and returns the applied plan. |
385
+
386
+ #### `DatabaseStorageInterface`
387
+
388
+ The scoped view and every table taken from it throw `CONFLICT` for work started
389
+ after the transaction settles.
390
+
391
+ | Method | Returns | Summary |
392
+ | ------- | ----------------------------- | ------------------------------------------------------ |
393
+ | `table` | `TableInterface<RowOf<T[K]>>` | Returns a table bound to the active transaction scope. |
394
+
395
+ #### `AdmissionInterface`
396
+
397
+ The admission boundary the root database context and a transaction scope both
398
+ expose; its `accepting` data member stays in the Surface row above.
399
+
400
+ | Method | Returns | Summary |
401
+ | ------- | ------------ | ------------------------------------------------------------------------------------------------------------------- |
402
+ | `track` | `Promise<R>` | Enters one operation into the boundary's ledger so whoever stops the boundary contains everything already accepted. |
403
+
404
+ #### `TableInterface`
405
+
406
+ The keyed methods batch by overload (one in → one out; array in → array
407
+ out) — a single verb, never `getMany` / `setAll`. `records()` / `scan()`
408
+ narrow every row through the table's contract guard, so a non-conforming
409
+ stored row (legacy data, a row from before a migration) never appears in
410
+ their results. `count()` uses the same contract-valid candidate semantics as
411
+ `records()` while ignoring paging, so invalid stored rows do not consume the
412
+ count. `aggregate()` remains a stored-row operation; its `count` aggregate may
413
+ therefore include a row that `TableInterface.count()` excludes. `records`,
414
+ `count`, `aggregate`, `scan`, `set`, `add`, `update`, and `remove` each take an
415
+ optional `OperationOptions`: its `signal` reaches every backend commit point,
416
+ and an aborted batch keeps the items already committed.
417
+
418
+ | Method | Returns | Summary |
419
+ | ----------- | ------------------------------------ | ------------------------------------------------------------------------------------ |
420
+ | `get` | `Promise<T \| undefined>` (or array) | Reads one row by key, or one row per key for a list — `undefined` for each miss. |
421
+ | `resolve` | `Promise<T>` (or array) | Reads one row by key, or one row per key for a list, throwing `NOT_FOUND` on a miss. |
422
+ | `has` | `Promise<boolean>` (or array) | Reports whether one key exists, or one result per key for a list. |
423
+ | `keys` | `Promise<readonly Key[]>` | Lists every primary key in order. |
424
+ | `records` | `Promise<readonly T[]>` | Reads the contract-valid rows matching an optional `QueryInput`. |
425
+ | `count` | `Promise<number>` | Counts contract-valid rows matching `input`'s conditions. |
426
+ | `aggregate` | `Promise<number \| undefined>` | Computes an aggregate over `column` across rows matching `input`'s conditions. |
427
+ | `scan` | `AsyncIterable<T>` | Iterates the table's rows lazily with filtering. |
428
+ | `set` | `Promise<Key>` (or array) | Upserts one or more rows. |
429
+ | `add` | `Promise<Key>` (or array) | Inserts one or more rows, throwing `CONFLICT` on a duplicate key. |
430
+ | `update` | `Promise<boolean>` (or array) | Applies a partial change to one or more rows. |
431
+ | `remove` | `Promise<boolean>` (or array) | Deletes one or more rows. |
432
+ | `clear` | `Promise<void>` | Empties the table. |
433
+ | `query` | `QueryInterface<T>` | Opens a fluent query builder over the table. |
434
+ | `cursor` | `Promise<CursorInterface<T>>` | Opens a forward row cursor for bulk mutation. |
435
+
436
+ #### `QueryInterface`
437
+
438
+ Each modifier mutates and returns the same builder. `condition` accepts the
439
+ portable condition directly, `order` accepts one portable order, and the
440
+ terminal methods execute the accumulated `QueryInput`. `stream` takes an
441
+ optional `OperationOptions` and ignores `order`, because rows are evaluated one
442
+ at a time.
443
+
444
+ | Method | Returns | Summary |
445
+ | ----------- | ------------------------------ | -------------------------------------------------------------------------------- |
446
+ | `condition` | `QueryInterface<T>` | Adds one portable condition, including its explicit connector. |
447
+ | `order` | `QueryInterface<T>` | Adds one portable ordering term — a column and a direction. |
448
+ | `filter` | `QueryInterface<T>` | Adds a post-fetch JavaScript predicate. |
449
+ | `limit` | `QueryInterface<T>` | Caps the result count. |
450
+ | `offset` | `QueryInterface<T>` | Skips the leading rows. |
451
+ | `collect` | `Promise<readonly T[]>` | Executes the accumulated read and collects every matching row. |
452
+ | `find` | `Promise<T \| undefined>` | Executes the accumulated read and returns the first match, or `undefined`. |
453
+ | `count` | `Promise<number>` | Executes the accumulated read and returns the match count. |
454
+ | `stream` | `AsyncIterable<T>` | Evaluates this query's conditions / filters / offset / limit lazily, row by row. |
455
+ | `aggregate` | `Promise<number \| undefined>` | Executes a named aggregate over one column. |
456
+
457
+ #### `CursorInterface`
458
+
459
+ | Method | Returns | Summary |
460
+ | -------- | --------------- | ------------------------------------------------------------------ |
461
+ | `next` | `Promise<void>` | Advances to the next present row. |
462
+ | `update` | `Promise<void>` | Merges changes into the row at the current position. |
463
+ | `remove` | `Promise<void>` | Deletes the row at the current position. |
464
+ | `close` | `void` | Closes the cursor terminally, so every later operation is a no-op. |
465
+
466
+ ## Contract
467
+
468
+ These invariants hold across the core database source tree ↔ this guide:
469
+
470
+ 1. **Doc ↔ public entry bijection.** Every `function` / `const` / `class` /
471
+ `interface` / `type` row in the `## Surface` tables is reachable from the
472
+ `src/core`, `src/server`, or `src/browser` entry barrel, and every reachable
473
+ public export appears as a Surface row — compiler-resolved and exhaustive in
474
+ both directions (see `.claude/rules/documentation.md` § Parity). Exported
475
+ implementation declarations outside
476
+ an entry barrel remain internal.
477
+ 2. **A table is a contract.** Every write is coerced **and** validated
478
+ through the table's compiled contract — `set` / `add` / `update` run the
479
+ row through the installed Contract 0.0.9 `contract.parse`, one step that
480
+ both coerces and enforces constraints such as `min` / `pattern`. Every
481
+ parsed result already satisfies `contract.is`, so `Table` does not run a
482
+ second guard after parsing; a row that fails throws `VALIDATION`. Reads are
483
+ narrowed back to the table's row type through the guard — never an `as`
484
+ (see `AGENTS.md` § Non-negotiable rules). The
485
+ row type is the shape's `Infer`, so a `tables` map types every table from
486
+ one declaration. A contract rejection reports only the table plus the first
487
+ bounded contract fault (`field` and `reason` when one exists); the rejected
488
+ row, received value, and parser cause never enter the message, context,
489
+ serialized error, or table events.
490
+ 3. **Thin driver, one engine, native overrides.** The required
491
+ `DriverInterface` surface is the irreducible storage primitive — keyed
492
+ read/write/atomic-insert/delete, an ordered `scan`, key listing, and `snapshot`. `open`
493
+ hands the driver a derived `TableSchema[]` (each table's `columns`, their
494
+ portable `ColumnStorage` through `shapeToColumnStorage`, the `primary` key, and declared
495
+ `indexes`) so a native backend can build real tables and indexes; a
496
+ scan-only backend reads only `name`. The pure, total query engine
497
+ (`applyQuery` / `matchesQuery` / `computeAggregate` / …) over `scan`
498
+ is the default and the only required path. A backend can implement the
499
+ optional native `records?` / `aggregate?` / `transaction?` /
500
+ `stream?` / `migrate?` where it has a faster or more native path, and the
501
+ engine prefers each when present — falling back to the portable path
502
+ otherwise (see `.claude/rules/architecture.md` § System constraints).
503
+ Because `aggregate?` legitimately resolves to
504
+ `undefined` (a sum over zero rows), `Table.aggregate` decides the hook ran
505
+ by its **presence** (a present method returns a Promise; `?.()` is
506
+ `undefined` only when the method is absent), never by the resolved value.
507
+ Neither reference driver implements `records?` / `aggregate?`,
508
+ so every query runs the engine over key-ordered `scan`; both do implement
509
+ `stream?` and `migrate?` (`MemoryDriver.stream` lazily filters `scan` through
510
+ the engine; `JSONDriver.stream` delegates to its inner `MemoryDriver`).
511
+ `MemoryDriver` still lacks a native `transaction?`, so its transactions
512
+ always use the snapshot floor; `JSONDriver` does implement
513
+ `transaction?` — it clones the committed memory/schema/metadata into an
514
+ isolated candidate, passes only that candidate's capability to the callback,
515
+ and publishes it with one atomic file replacement only when the callback
516
+ fulfills. Rejection or persistence failure discards the candidate, leaving
517
+ committed memory, metadata, and file bytes exact; nesting and root operations
518
+ while active throw `CONFLICT`. Root `scan` / `stream` iterators created
519
+ before a JSON or SQLite transaction guard every continuation before and
520
+ after the underlying read. Resuming one while the transaction is active
521
+ throws `CONFLICT`, discards any concurrently produced row, cleans the source
522
+ exactly once, and terminalizes the iterator; the transaction and driver
523
+ remain usable. Memory and IndexedDB need no equivalent root wrapper because
524
+ neither exposes a callback transaction. `Database.transaction` over a `JSONDriver`
525
+ therefore prefers this native path over the snapshot floor. Both reference drivers also
526
+ implement the paired `metadata?` / `stamp?` — `MemoryDriver` in-process only
527
+ (the owned `DriverMetadata` snapshot lives in instance memory), `JSONDriver` persisted:
528
+ the file is `{ metadata?: DriverMetadata, tables }`, with `metadata` present only
529
+ once the store has been `stamp`ed at least once (an old, pre-versioning
530
+ file — bare `{ tables }` — reads back as unstamped, that is, `metadata()`
531
+ resolves `undefined`; a bare `{ tables }` document is therefore unstamped). A `JSONDriver` write-path
532
+ fault (`mkdir` / `writeFile` / `rename` failing during `#serialize`)
533
+ surfaces as a `DatabaseError` `DRIVER` after temporary-file cleanup; its
534
+ context carries `path` and the native `cause`. If that cleanup also fails,
535
+ the top-level `DRIVER` context is exactly the persistence evidence
536
+ `{ path, temp, cause, cleanup }`; a precommit abort remains an `ABORTED`
537
+ `DatabaseError` nested at `cause`, while cleanup failure determines the
538
+ top-level code. Durable reads fail closed: only a proven absent JSON path or
539
+ absent native metadata record is fresh. A fresh SQLite/IndexedDB store may create
540
+ missing declared tables/stores while retaining unrelated physical objects. Once
541
+ metadata exists, every table/store in its persisted schema must already exist
542
+ physically: SQLite throws
543
+ `DatabaseError('DRIVER', 'Stored SQLite table is missing')` before any DDL in its
544
+ open transaction, and IndexedDB throws
545
+ `DatabaseError('DRIVER', 'Stored IndexedDB store is missing')` before its final
546
+ open. Extra physical objects remain allowed, and SQLite may reconstruct a
547
+ missing declared index because the table and its records still exist. IndexedDB
548
+ captures the bootstrap connection's exact stores and version in the same
549
+ lifetime, then pins the persisted final open to that version; a concurrent
550
+ versionchange makes the stale open reject instead of silently bumping and
551
+ recreating storage. Existing unreadable, malformed, structurally incompatible,
552
+ or physically incomplete state throws `DRIVER` without publishing a handle or
553
+ attempting table/store repair; after external repair, the same driver instance
554
+ may retry `open`.
555
+ `SQLiteDriver` and `IndexedDBDriver` complete the native-override
556
+ picture from opposite ends, each earning trust its own way (see
557
+ `.claude/rules/architecture.md` § System constraints).
558
+ `SQLiteDriver` is **prove-exactness-or-refine**: real `CREATE TABLE` /
559
+ `CREATE INDEX` DDL backs every table, but `records?` /
560
+ `aggregate?` / `stream?` compile a `QueryInput` straight to SQL
561
+ (`compileQuerySQL`, `compileAggregateSQL`) and run it natively only when
562
+ `matchesQueryExactly` (built from `matchesConditionExactly` / `matchesOrderExactly`) first
563
+ proves the SQL and the engine's semantics are identical for every
564
+ condition and order term — otherwise the driver falls back to a full
565
+ `scan` refined through the same core engine every scan-only driver uses
566
+ (`applyQuery` / `filterRows` / `computeAggregate` / `matchesQuery`).
567
+ Refine, not native SQL, is the path for: `like` / `glob` patterns (SQL
568
+ `LIKE`/`GLOB` semantics can diverge from the engine's `matchesWildcardPattern`),
569
+ every scalar condition over a column that is optional or nullable,
570
+ `absent` / `present` only when the column is optional and nullable, a `null`
571
+ or `undefined` scalar operand, an empty `any` / `none` operand list,
572
+ mismatched operand types, `json` / `blob` columns, and any nested
573
+ `FieldPath` — and a
574
+ range operator (`above` / `below` / `from` / `to` / `between`) or an
575
+ `ORDER BY` term over a `text` column: SQLite's default BINARY collation
576
+ orders `TEXT` by Unicode code point while the core engine's `compareValues`
577
+ orders JS strings by UTF-16 code unit, and the two diverge on
578
+ supplementary-plane characters (code points ≥ U+10000, for example many emoji) —
579
+ so text ranges and text ordering always refine through the engine, even
580
+ though text equality (`equals`/`not`/`any`/`none`) and `starts`/`ends` stay
581
+ native on a required non-null text column. `starts` / `ends` compile
582
+ case-sensitively (a `substr` comparison plus a
583
+ `typeof text === 'string'` guard; an empty operand falls back to a
584
+ `typeof` check) when they do qualify as exact. `IndexedDBDriver` is **narrow-then-refine**:
585
+ `records?` / `stream?` first ask `selectPlan` for a key-range pushdown over
586
+ the primary key or a single-column secondary index — a candidate superset,
587
+ never lossy — then hands that superset to the same core engine
588
+ (`applyQuery` / `matchesQuery`) every scan-only driver uses, which
589
+ refines it to the exact result; a plan that cannot prove itself range-exact
590
+ (a nested path, a non-orderable column type, an `or`-joined condition, a
591
+ non-comparison operator) falls back to a full scan. `below` / `to` push
592
+ down only onto the primary store, never a secondary index — a secondary
593
+ index has no entry for a row whose indexed column is absent or `null`,
594
+ while the engine's total order (`compareValues`) lets those rows match a
595
+ `below` / `to` bound; `equals` / `above` / `from` / `between` remain
596
+ index-eligible on either. `conditionToRange` returns `undefined` for a `between`
597
+ whose bounds are reversed (`compareValues(first, second) > 0`), so
598
+ `selectPlan` falls back to a full scan instead of handing a raw
599
+ backwards `IDBKeyRange` to the store (which would throw a `DataError`).
600
+ `IndexedDBDriver.snapshot()` captures every store in one read transaction,
601
+ so the capture is point-in-time consistent across stores even under
602
+ concurrent writers; `restore` was already atomic. `SQLiteDriver` and
603
+ `IndexedDBDriver` each implement `migrate?` natively and treat `MigrationInput` as one commit unit:
604
+ `SQLiteDriver` applies schema, rows, and optional metadata inside one native
605
+ SQLite transaction (`stepToSQL` projects each step's DDL), while a migration
606
+ invoked inside an existing callback transaction uses one fixed internal
607
+ savepoint SQL literal. The published `@orkestrel/sqlite` wrapper
608
+ intentionally exposes raw `execute` but no savepoint manager; the savepoint
609
+ contains a caught inner migration so it cannot leak partial DDL and the
610
+ outer transaction remains active for unrelated work.
611
+ `IndexedDBDriver` performs a non-empty plan in one versionchange transaction,
612
+ writing `metadata` in that same upgrade; a metadata-only input uses one ordinary
613
+ `__metadata__` readwrite transaction. A `column.remove` step that meets a stored
614
+ value which is not a record fails the migration closed with `MIGRATION` and
615
+ nothing is rewritten. Both implement the paired `metadata?` /
616
+ `stamp?` into a reserved store name a user table must avoid:
617
+ `SQLiteDriver` uses a single-row `_metadata` table (`METADATA_TABLE`), and
618
+ `IndexedDBDriver` uses an out-of-line `__metadata__` store (`METADATA_STORE`), both
619
+ excluded from a whole-store `snapshot`. `SQLiteDriver` implements
620
+ `transaction?` as a callback-scoped real `BEGIN` / `COMMIT` / `ROLLBACK`;
621
+ the capability performs reads, writes, migration, and metadata work inside
622
+ that one native transaction, then becomes invalid. `IndexedDBDriver`
623
+ deliberately omits `transaction?`: an `IDBTransaction` can auto-commit when
624
+ control yields to a non-IDB `await`, so arbitrary callback awaits cannot
625
+ truthfully remain inside one native transaction. It also omits `aggregate?`
626
+ because IndexedDB has no native SUM/AVG/MIN/MAX; the engine over the narrowed
627
+ `records?` covers it. A new backend implements a handful of small methods and
628
+ inherits the entire query surface unchanged.
629
+ 4. **Total query helpers; the equality family is structural, not ranked.**
630
+ `compareValues`, `matchesCondition`, and `matchesQuery` never throw — a
631
+ type mismatch is a non-match and the comparator is a total order (it
632
+ never returns `NaN`), mirroring the contracts guards' totality (see
633
+ `.claude/rules/patterns.md` § Validation and contracts). The range
634
+ operators (`above` / `below` / `from` / `to` /
635
+ `between`) still rank through `compareValues`'s total order (which
636
+ collapses every object/array to one rank-5 bucket). The equality-family
637
+ operators (`equals` / `not` / `any` / `none`) instead compare through
638
+ `equalsValue` — structural equality by SameValueZero leaves, so `equals` on
639
+ an object/array compares field-by-field rather than by reference or rank,
640
+ and `NaN` equals `NaN` under `equals` / `any` (it never matched anything
641
+ under the old rank-based comparison).
642
+ 5. **Scoped transactions, with explicit admission and drain.**
643
+ `transaction(scope, options?)` checks `options?.signal` once at entry, then
644
+ gives `scope` a `DatabaseStorageInterface`: a table-only view backed by a
645
+ scoped `StorageInterface`. Every operation accepted while the callback
646
+ is active is tracked, and settlement waits for that whole accepted operation
647
+ graph to drain — not only the promise the callback returns. A rejected
648
+ accepted operation aborts the transaction even when caller code catches that
649
+ rejection. If the callback itself throws synchronously or rejects
650
+ asynchronously, that exact reason wins over a drain error; otherwise the
651
+ first tracked failure becomes the transaction error.
652
+ Admission closes when the callback returns, so later work conflicts rather
653
+ than escaping settlement. Each `scan` / `stream` continuation is tracked
654
+ independently: an in-flight `next()` drains, but an idle iterator does not pin
655
+ commit, and a continuation requested after settlement throws `CONFLICT`.
656
+ Root tables (including imported views), `open`, `close`, `migrate`, and
657
+ nesting throw `CONFLICT` while the scope is active; the scoped view, its
658
+ tables, queries, cursors, and streams also throw `CONFLICT` after settlement.
659
+ When the driver implements `transaction?(scope)`, the driver owns
660
+ acquisition, commit on fulfillment, rollback on rejection, release, and
661
+ invalidation. Otherwise the universal single-writer floor snapshots the
662
+ whole store, runs the same scoped callback, and restores the snapshot on
663
+ rejection. Either path emits the identical `transaction` / `commit`
664
+ lifecycle; `rollback(error)` is emitted only when rollback completed and the
665
+ original scope/drain error remains the propagated rejection, never when
666
+ cleanup itself replaced that error.
667
+ 6. **Observation is a pure side-channel.** The core `Database` owns a
668
+ typed `emitter` (`DatabaseEventMap` — `open` / `close` / `transaction` /
669
+ `commit` / `rollback` / `migrate`) and each `Table` owns one (`TableEventMap` —
670
+ `write` / `remove` / `clear`, key only, no value payload to avoid heavy
671
+ fan-out / leaking row data). Every event is emitted directly (the
672
+ `.claude/rules/patterns.md` § Listener isolation convention: the emitter
673
+ isolates a listener throw, routing it to its
674
+ own `error` handler — the `error` option, surfaced as `(error, event)`,
675
+ not a domain event — itself re-entrancy-guarded) strictly after the
676
+ relevant transition — `commit` only after a scope succeeds, `rollback`
677
+ only after restoration succeeds and the observed error is the same rejection
678
+ that propagates (a cleanup failure is not mislabeled as a rollback), a
679
+ `write` / `remove` / `clear` only after the driver op
680
+ completes. So a buggy observer can never corrupt a write or a
681
+ transaction: the committed state stays intact, the rollback still
682
+ restores, and the original transaction error still propagates (proven by
683
+ the emit-safety tests). Reads / queries / counts are not emitted (a
684
+ reader does not mutate, and those paths are too hot). The observation
685
+ lives in the core layer; the drivers stay storage primitives.
686
+ 7. **Views share a driver.** A database is a typed view over a set of tables
687
+ on one driver. `import(tables)` returns a new view of only those tables
688
+ over the **same** driver (sharing storage and transactions); `export()`
689
+ emits a portable `TableDefinition` per table — `schema` is the universally
690
+ portable JSON Schema, `columns` re-imports losslessly through `import` within
691
+ a TypeScript environment.
692
+ 8. **Doc ↔ source method bijection.** Every behavioral interface's
693
+ `## Methods` table lists exactly its public methods (call-signature
694
+ members) — exhaustive, both directions — and each implementing class
695
+ (`Database` / `MemoryDriver` / `JSONDriver` / `SQLiteDriver` /
696
+ `IndexedDBDriver` / `Table` / `Query`) implements
697
+ every required method and adds none beyond the interface (optional members
698
+ like `records?` / `aggregate?` / `transaction?` / `stream?` /
699
+ `migrate?` / `metadata?` / `stamp?` may be omitted). `MemoryDriver` and
700
+ `JSONDriver` both omit `records?` / `aggregate?`, and both
701
+ implement `stream?` / `migrate?` / `metadata?` / `stamp?`; `MemoryDriver` still
702
+ omits `transaction?` (snapshot floor only) while `JSONDriver`
703
+ implements `transaction?` too (isolated candidate state plus one atomic
704
+ publish on callback fulfillment). `SQLiteDriver` implements every optional hook — `records?` /
705
+ `aggregate?` / `transaction?` / `stream?` / `migrate?` / `metadata?`
706
+ / `stamp?` — the fully-native backend. `IndexedDBDriver` implements
707
+ `records?` / `stream?` / `migrate?` / `metadata?` / `stamp?` but
708
+ omits `transaction?` and `aggregate?` by IndexedDB's nature, not by
709
+ choice (see `.claude/rules/documentation.md` § Parity). A renamed / added /
710
+ removed method breaks the gate
711
+ until the table is reconciled.
712
+ 9. **Abort is a shared gate, not per-method reinvention.**
713
+ `checkAbort(signal)` is the one place `ABORTED` is thrown — a no-op for
714
+ `undefined` or a live signal. `records` / `count` / `aggregate` check it
715
+ at entry; `TableInterface.scan` and `QueryInterface.stream` check it
716
+ before each yield (so an abort mid-iteration stops promptly) and ignore
717
+ `order` (streaming yields driver key-order; sorted output stays
718
+ `records()`'s job). Breaking out of a stream early (`break`) closes the
719
+ underlying source. Each call to `scan` / `stream` returns a fresh
720
+ iterable — reusing a `QueryInterface` across calls never leaks state
721
+ between them. `set` / `update` route through `write`, `add` through the
722
+ required atomic `insert`, and `remove` through `delete`; all carry the same
723
+ `OperationOptions` to the real backend commit point. An abort while the shared lazy open is pending
724
+ rejects that mutation promptly; the open may finish for other waiters, but
725
+ the rejected mutation never dispatches later. Memory and SQLite re-check
726
+ immediately before their synchronous mutation. IndexedDB runs each point
727
+ mutation in one explicit readwrite transaction and aborts that transaction
728
+ only while it is active. JSON queues each nontransactional point mutation
729
+ through preimage capture, staging, atomic rename, and success or restoration;
730
+ queued aborts never start, active precommit aborts restore memory and clean
731
+ the temp file before rejection, and reads wait behind that unit. Once an
732
+ commit point that cannot be aborted (`SQLite` call entry, IndexedDB transaction
733
+ completion, JSON `rename` dispatch) has won, the operation awaits and reports its real
734
+ result; a late signal cannot convert success to `ABORTED`. Batch items remain
735
+ sequential and independent: earlier committed items survive an abort of a
736
+ later item.
737
+ 10. **Key generation is host-neutral and overridable.** When an optional
738
+ primary column is omitted, a table uses global `crypto.randomUUID()`.
739
+ `DatabaseOptions.generator` (a `KeyFunction`) authoritatively replaces
740
+ that default, which is required for numeric generated primaries. An
741
+ explicit primary value always wins and never invokes the generator.
742
+ 11. **Atomic migration input, plus opt-in versioned reconciliation.**
743
+ `planMigration(deployed, declared, from?, to?)` structurally diffs two
744
+ `TableSchema[]` into an ordered `Migration` plan (`table.add` /
745
+ `table.remove`, then each shared table's `column.add` / `column.remove`
746
+ / `index.add` / `index.remove`). A driver receives
747
+ `driver.migrate?.({ plan, metadata? })`: one `MigrationInput` whose schema
748
+ changes and optional target metadata publish atomically. A step
749
+ referencing an unknown table throws
750
+ `DatabaseError('MIGRATION')`; so does a `column.add` / `column.remove`-adjacent
751
+ shared column whose declared `storage`, `optional`, or `nullable` differs between
752
+ `deployed` and `declared` (an in-place storage/optionality/nullability change is not
753
+ auto-migrated — the JSDoc on `planMigration` documents the manual path:
754
+ add a new column, copy/convert the data, then remove the old one).
755
+ A `column.add` that is required and non-null is rejected before DDL because
756
+ existing rows cannot satisfy it without an explicit data backfill; add an
757
+ optional or nullable column first, populate it, then tighten the schema
758
+ through an explicit application-managed migration.
759
+ `Database.migrate(deployed, options?)`
760
+ is the explicit pre-open migration path. It diffs `deployed` against the
761
+ database's own declared `tables`, applies the resulting plan through the
762
+ driver's `migrate?` hook (throwing `MIGRATION` when the driver lacks
763
+ one), emits the `migrate` event on success, and returns the applied
764
+ plan — `options?.signal` is checked once, at entry, throwing `ABORTED`
765
+ on an already-fired signal. The explicit path opens the caller-declared
766
+ deployed physical schema, applies the migration (including target metadata
767
+ when `version` is configured), and publishes `open` as one readiness
768
+ transition. It is an alternative admission path, so the same handle does
769
+ not run a second automatic reconciliation. A failed explicit apply remains
770
+ the exact readiness failure for ordinary table/open work until another
771
+ explicit `migrate` succeeds; `close` remains available. `migrateRows` is the pure per-table row
772
+ transform (`column.remove` drops the field from a fresh copy of each
773
+ row; the other operations act on storage shape, not row shape, so they
774
+ are no-ops here) — a driver's own `migrate` decides how to apply it to
775
+ stored rows. This caller-driven path remains the way to migrate against
776
+ an unversioned driver (one that implements neither `metadata` nor `stamp`),
777
+ which still owns knowing what is deployed. A driver that does
778
+ implement both `metadata` and `stamp` can instead opt into automatic
779
+ reconciliation by passing `DatabaseOptions.version`. A versioning driver's
780
+ `open()` first discovers persisted `DriverMetadata.schema` and opens that
781
+ deployed physical schema; it must not pre-create the target schema before
782
+ reconciliation. `Database.open()` then compares deployed metadata with the
783
+ declared version inside the same lazy-connect chain. A fresh store
784
+ (`metadata()` is `undefined`) stamps `{ version, schema }` for next time. A
785
+ stored version below the declared one computes the plan from the persisted
786
+ schema and passes `{ plan, metadata: { version, schema } }` to `migrate`, so
787
+ schema, rows, and new metadata commit or roll back together. A stored
788
+ version above the declared one throws `MIGRATION`. At an equal version,
789
+ the persisted and declared schemas must still match; drift throws
790
+ `MIGRATION`, while an exact match is a no-op with no metadata rewrite.
791
+ Comparison canonicalizes table order, column order, and the outer index-list
792
+ order, while preserving each compound index's inner column order because
793
+ `['city', 'age']` and `['age', 'city']` are different indexes.
794
+ `version` left unset, or set against a non-versioning driver, leaves
795
+ `open()` unchanged — versioning is opt-in per driver and per database.
796
+ Versioning drivers own the migration input's atomicity even when they do
797
+ not expose a general callback `transaction` hook. On a fresh handle, call
798
+ either `open()` for metadata-driven reconciliation or `migrate(deployed)`
799
+ for caller-driven reconciliation. Both converge on the same declared schema
800
+ and publish readiness once.
801
+ 12. **Driver conformance.** `conformDriver(factory)` is a framework-agnostic
802
+ battery (no test-runner import) any new `DriverInterface` backend can run
803
+ against itself — a smoke script, a unit test, or a new driver's own
804
+ README all call it the same way. It opens the fixed `users` and `posts`
805
+ schema per phase (calling `factory()` fresh each time so failures stay
806
+ isolated) and verifies the required surface's invariants (copy-in/copy-out
807
+ isolation, upsert-overwrite, key-ordered `keys`/`scan`, `snapshot` rollback, a
808
+ non-`id` primary key, structural round-tripping through `equalsValue`), then
809
+ presence-gates the optional `migrate?` / `stream?` / `transaction?`
810
+ hooks when the driver implements them. The battery's `write-read` phase
811
+ is deepened with nested-field checks (a written row's nested object/array
812
+ fields must copy-in/copy-out isolated, not only its top-level fields),
813
+ and a dedicated `snapshot-nested` phase asserts the same nested isolation
814
+ across a `snapshot()` capture/restore round-trip — a driver that
815
+ shallow-copies anywhere in its write/read/scan/snapshot boundary
816
+ fails conformance (`MemoryDriver` passes by deep-copying through
817
+ `structuredClone` at every one of those boundaries). The first violated
818
+ invariant throws a `CONFORMANCE` `DatabaseError` naming the failed check.
819
+ 13. **Backend faults surface as `DatabaseError`, never raw.** No native
820
+ wrapper error (a SQLite fault, an IndexedDB `DOMException`) crosses a
821
+ `DriverInterface` implementation — `SQLiteDriver` and `IndexedDBDriver`
822
+ each map every backend fault to a `DatabaseError` at the boundary
823
+ (`#guard` internally on the SQLite side; `mapIndexedDBError` /
824
+ `mapMigrationError` on the IndexedDB side), preserving the original
825
+ error as `context.cause`. `SQLiteDriver`: a constraint violation maps to
826
+ `CONFLICT`, a closed-connection fault to `CLOSED`, a busy/locked database
827
+ to `DRIVER` with a `retryable` context flag, anything else to `DRIVER`.
828
+ Snapshot capture puts the open gate, every prepare/read, and captured-map
829
+ population inside one `#guard`; replay likewise contains the open gate,
830
+ native transaction, deletes, prepares, and reinserts in one `#guard`.
831
+ Public `close()` crosses that same boundary. A physically dropped declared
832
+ table therefore rejects capture as top-level `DRIVER`, and a zero-timeout
833
+ exclusive lock rejects replay as retryable `DRIVER` with
834
+ `context.code === 'BUSY'`; both retain the actual `SQLiteError` only at
835
+ `context.cause`.
836
+ `IndexedDBDriver`: a constraint violation maps to `CONFLICT`; a
837
+ closed/not-open/invalid-state fault to `CLOSED`; a quota fault to
838
+ `DRIVER` with `code: 'QUOTA'`; `migrate`'s versionchange path remaps an
839
+ upgrade fault to `MIGRATION`; anything else to `DRIVER`. A blocked open
840
+ or versionchange is nonterminal and remains pending until the competing
841
+ connection closes, rather than surfacing as an error.
842
+ 14. **The reserved metadata table/store is a hard guard, not a naming
843
+ convention.** `SQLiteDriver.open` throws `DatabaseError('VALIDATION')`
844
+ when the declared tables include one literally named `_metadata`
845
+ (`METADATA_TABLE`); `IndexedDBDriver.open` does the same for `__metadata__`
846
+ (`METADATA_STORE`) — a collision is caught at `open`, not discovered later
847
+ as corrupted metadata. Because both drivers derive their index names from
848
+ a length-prefixed scheme (`deriveSQLiteIndexName` for SQLite,
849
+ `deriveIndexedDBIndexName` for IndexedDB) to stay collision-free across
850
+ compound indexes, a database
851
+ file/store created under an older naming scheme leaves its old-named
852
+ indexes orphaned (unreferenced, harmless) on reopen under the new scheme —
853
+ they are never queried and never collide, but a storage audit may notice
854
+ them.
855
+
856
+ What ships is the **core in-between** (schema-aware: `open` receives a
857
+ derived `TableSchema[]`, with `shapeToColumnStorage` mapping each column's shape), its
858
+ reference `MemoryDriver`, and the persistent `JSONDriver`, `SQLiteDriver`, and
859
+ `IndexedDBDriver` backends. `JSONDriver` in `src/server` is a decorator over
860
+ `MemoryDriver` that loads/flushes a single JSON file — every primitive
861
+ delegates to the inner memory driver, so querying, key-order `scan` / `keys`,
862
+ and capture-replay `snapshot` are inherited unchanged; `JSONDriver.migrate`
863
+ additionally persists the migrated state, and every flush is atomic — written
864
+ to a sibling temp file and `rename`d onto the target path, so a crash mid-flush
865
+ can never truncate or corrupt the previous good file. Outside a `transaction`,
866
+ `JSONDriver` still flushes once per mutation (`write` / `insert` / `delete` /
867
+ `clear`); its native `transaction?(scope)` clones committed rows, schema, and
868
+ metadata into an isolated candidate. The callback can observe only that
869
+ candidate; fulfillment serializes it once and publishes memory only after the
870
+ atomic file replacement, while rejection or persistence failure discards it
871
+ without changing committed state. Nested transactions, root operations while
872
+ active, and a captured capability used after settlement throw `CONFLICT`.
873
+ `SQLiteDriver`, also in `src/server`, is the
874
+ fully-native, **trusted-mode** backend on the published `@orkestrel/sqlite`
875
+ wrapper — real typed `CREATE TABLE` / `CREATE INDEX` DDL, native
876
+ `records?` / `aggregate?` / `stream?` compiled straight to SQL,
877
+ real `BEGIN` / `COMMIT` / `ROLLBACK` transactions, atomic DDL migration
878
+ (`stepToSQL`; a root input uses one native transaction, while an input inside
879
+ an existing callback transaction uses the guarded fixed internal savepoint
880
+ literal for caught-inner-failure containment while the outer transaction stays
881
+ active),
882
+ and a reserved
883
+ `_metadata` table (`METADATA_TABLE`) for `metadata?` / `stamp?` versioning — every
884
+ optional `DriverInterface` hook, none skipped. `IndexedDBDriver` in
885
+ `src/browser`, on the published `@orkestrel/indexeddb` wrapper, is the
886
+ **narrow-then-refine** persistent browser backend — `selectPlan` turns a
887
+ `QueryInput` into a key-range pushdown over the primary key or a single-column
888
+ secondary index (a candidate superset, never lossy) that the same core
889
+ engine then refines to the exact result; `migrate?` applies a non-empty plan
890
+ and its metadata in one versionchange transaction, and `metadata?` / `stamp?`
891
+ persist into a reserved
892
+ `__metadata__` store (`METADATA_STORE`) — it omits `transaction?` because arbitrary
893
+ callback awaits outlive an auto-committing `IDBTransaction`, and
894
+ `aggregate?` (no native SUM/AVG/MIN/MAX) by IndexedDB's own nature. The core
895
+ `Database` / `Table` are also **observable** — each owns a typed `emitter`
896
+ (`DatabaseEventMap` / `TableEventMap`) carrying the transaction +
897
+ per-row lifecycle (see [Observing](#observing)); a driver stays a storage
898
+ primitive (the observation lives in the core layer above it).
899
+
900
+ ## Patterns
901
+
902
+ ### Declaring tables in options
903
+
904
+ Declares two tables with per-column contracts, a non-default primary key, and a secondary index, then reads back each table's resolved primary column:
905
+
906
+ ```ts
907
+ import { createDatabase, createMemoryDriver } from '@orkestrel/database'
908
+ import { integerShape, literalShape, optionalShape, stringShape } from '@orkestrel/contract'
909
+
910
+ const db = createDatabase({
911
+ driver: createMemoryDriver(),
912
+ name: 'app',
913
+ tables: {
914
+ // Each table's value is its columns — wrapped in an `objectShape` for you.
915
+ users: {
916
+ id: stringShape(),
917
+ name: stringShape({ min: 1 }),
918
+ age: integerShape({ min: 0 }),
919
+ role: literalShape(['admin', 'member', 'guest']),
920
+ bio: optionalShape(stringShape()), // nested object columns still use objectShape
921
+ },
922
+ posts: { slug: stringShape(), title: stringShape() },
923
+ },
924
+ primary: { posts: 'slug' }, // default primary column is 'id'
925
+ indexes: { posts: [['title']] }, // secondary indexes — contracts don't express them
926
+ })
927
+
928
+ const users = db.table('users') // hold the handle; reuse it
929
+ const posts = db.table('posts')
930
+
931
+ users.primary // 'id' — the default primary column
932
+ posts.primary // 'slug' — the declared override
933
+ ```
934
+
935
+ Each `indexes` entry is one (possibly compound) index of column names; they
936
+ flow into each table's derived `TableSchema`. Neither driver here declares a
937
+ native index, so both ignore them; SQLite and IndexedDB use supported
938
+ declarations for native indexes and still refine through the shared engine when
939
+ required.
940
+
941
+ ### Swapping the driver
942
+
943
+ The `tables` declaration and every call against the database are identical
944
+ across backends — only the `driver` changes, so the same code runs in tests
945
+ and in production. Pick the driver per environment and pass it to
946
+ `createDatabase`:
947
+
948
+ ```ts
949
+ import { createDatabase, createMemoryDriver } from '@orkestrel/database' // tests / ephemeral — no I/O
950
+ import { createJSONDriver } from '@orkestrel/database/server' // node — persisted to a file
951
+ import { integerShape, stringShape } from '@orkestrel/contract'
952
+
953
+ const driver =
954
+ process.env.NODE_ENV === 'test' ? createMemoryDriver() : createJSONDriver('data/app.json')
955
+ const db = createDatabase({
956
+ driver,
957
+ tables: { users: { id: stringShape(), age: integerShape() } },
958
+ indexes: { users: [['age']] },
959
+ })
960
+ void db
961
+ ```
962
+
963
+ `MemoryDriver` implements the native `stream` hook and neither `records` nor
964
+ `aggregate`, so the core engine's `matchesQuery` answers every query on either
965
+ path. It is I/O-free, making it the storage behind tests, ephemeral caches, and any code
966
+ that wants the database API without a persistent backend. Its row boundary
967
+ continues to use native `structuredClone`, retaining supported non-JSON values
968
+ such as `Blob` and `Uint8Array`; only `DriverMetadata` crosses the stricter exact-JSON
969
+ `cloneDriverMetadata` boundary. `JSONDriver` adds file persistence on top of the same
970
+ in-memory engine — both return identical query results, so the choice is purely
971
+ about where the bytes live, never about behavior.
972
+
973
+ ### Keyed CRUD
974
+
975
+ Runs every keyed operation — `set`, `add`, `update`, `get`, `resolve`, `has`, `remove`, and `clear` — against one table:
976
+
977
+ ```ts
978
+ import { createDatabase, createMemoryDriver } from '@orkestrel/database'
979
+ import { integerShape, optionalShape, stringShape } from '@orkestrel/contract'
980
+
981
+ const users = createDatabase({
982
+ driver: createMemoryDriver(),
983
+ tables: {
984
+ users: {
985
+ id: stringShape(),
986
+ name: stringShape(),
987
+ age: integerShape(),
988
+ role: stringShape(),
989
+ bio: optionalShape(stringShape()),
990
+ },
991
+ },
992
+ }).table('users')
993
+
994
+ await users.set({ id: 'u1', name: 'Ada', age: 36, role: 'admin' }) // upsert → key
995
+ await users.add({ id: 'u1', name: 'Ada', age: 36, role: 'admin' }) // throws CONFLICT (exists)
996
+ await users.update('u1', { age: 37 }) // merge + re-validate → boolean
997
+ await users.get('u1') // row or undefined (typed)
998
+ await users.resolve('u1') // row or throw NOT_FOUND
999
+ await users.has('u1') // boolean
1000
+ await users.remove('u1') // boolean
1001
+ await users.clear() // empty the table
1002
+
1003
+ // A missing primary uses global crypto.randomUUID(), or DatabaseOptions.generator when supplied.
1004
+ ```
1005
+
1006
+ `add` is a storage-level claim, not `read` followed by `write`: `Table.add`
1007
+ calls the required `DriverInterface.insert`, and each backend rejects a
1008
+ duplicate at its own atomic insertion boundary. Two concurrent adds for the
1009
+ same key therefore cannot both succeed; exactly one wins and the other rejects
1010
+ with `CONFLICT`.
1011
+
1012
+ ### Filtered records, count, and aggregate
1013
+
1014
+ `records` / `count` / `aggregate` take an optional `QueryInput` directly —
1015
+ `query()` compiles one for you, but a caller with a pre-built `QueryInput` can
1016
+ call these directly:
1017
+
1018
+ ```ts
1019
+ import { createDatabase, createMemoryDriver } from '@orkestrel/database'
1020
+ import { integerShape, stringShape } from '@orkestrel/contract'
1021
+
1022
+ const users = createDatabase({
1023
+ driver: createMemoryDriver(),
1024
+ tables: { users: { id: stringShape(), age: integerShape() } },
1025
+ }).table('users')
1026
+
1027
+ await users.records({
1028
+ conditions: [{ column: 'age', operator: 'from', values: [18], connector: 'and' }],
1029
+ }) // every row aged 18 or over
1030
+ await users.count() // every row, unfiltered
1031
+ await users.aggregate('average', 'age') // number | undefined
1032
+ ```
1033
+
1034
+ ### Streaming with early exit
1035
+
1036
+ `scan` (on a table) and `stream` (on a query) are lazy — rows are yielded one
1037
+ at a time rather than collected up front. `conditions` / `offset` / `limit`
1038
+ are honored as rows stream; `order` is ignored (sorted output is `records()`
1039
+ / `collect()`'s job — streaming yields driver key-order). Breaking out early
1040
+ closes the underlying source, and each call returns a fresh iterable:
1041
+
1042
+ ```ts
1043
+ import { createDatabase, createMemoryDriver } from '@orkestrel/database'
1044
+ import { integerShape, stringShape } from '@orkestrel/contract'
1045
+
1046
+ const users = createDatabase({
1047
+ driver: createMemoryDriver(),
1048
+ tables: {
1049
+ users: {
1050
+ id: stringShape(),
1051
+ name: stringShape(),
1052
+ age: integerShape(),
1053
+ role: stringShape(),
1054
+ },
1055
+ },
1056
+ }).table('users')
1057
+
1058
+ // Table.scan — lazy filtered iteration, no upfront collection.
1059
+ for await (const user of users.scan({
1060
+ conditions: [{ column: 'age', operator: 'from', values: [18], connector: 'and' }],
1061
+ })) {
1062
+ if (user.name === 'Ada') break // closes the source immediately — no more rows read
1063
+ }
1064
+
1065
+ // Query.stream — the fluent builder's lazy terminal (filters/offset/limit apply, order is ignored).
1066
+ for await (const user of users
1067
+ .query()
1068
+ .condition({ column: 'role', operator: 'equals', values: ['member'], connector: 'and' })
1069
+ .stream()) {
1070
+ console.log(user.name)
1071
+ }
1072
+ ```
1073
+
1074
+ ### Abort
1075
+
1076
+ Reads, iterations, and point mutations take an optional
1077
+ `OperationOptions.signal`. An already-fired signal throws `ABORTED`; `scan` /
1078
+ `stream` re-check it before each yield, while mutations carry it through the
1079
+ driver to the backend commit point:
1080
+
1081
+ ```ts
1082
+ import {
1083
+ checkAbort,
1084
+ createDatabase,
1085
+ createMemoryDriver,
1086
+ isDatabaseError,
1087
+ } from '@orkestrel/database'
1088
+ import { integerShape, stringShape } from '@orkestrel/contract'
1089
+
1090
+ const users = createDatabase({
1091
+ driver: createMemoryDriver(),
1092
+ tables: {
1093
+ users: { id: stringShape(), name: stringShape(), age: integerShape(), role: stringShape() },
1094
+ },
1095
+ }).table('users')
1096
+
1097
+ // A time-boxed read — abort after 50ms.
1098
+ try {
1099
+ await users.records(undefined, { signal: AbortSignal.timeout(50) })
1100
+ } catch (error) {
1101
+ if (isDatabaseError(error) && error.code === 'ABORTED') console.log('too slow', error.context)
1102
+ }
1103
+
1104
+ // A time-boxed scan — checked before each yielded row.
1105
+ const controller = new AbortController()
1106
+ for await (const user of users.scan(undefined, { signal: controller.signal })) {
1107
+ if (user.id === 'stop-here') controller.abort('caller aborted')
1108
+ }
1109
+
1110
+ // Every point-mutation primitive carries the signal to its backend commit point.
1111
+ await users.set({ id: 'u2', name: 'Bo', age: 41, role: 'member' }, { signal: controller.signal })
1112
+ await users.add({ id: 'u3', name: 'Cy', age: 29, role: 'member' }, { signal: controller.signal })
1113
+ await users.remove('u2', { signal: controller.signal })
1114
+
1115
+ // The shared gate every abortable boundary calls internally:
1116
+ checkAbort(controller.signal) // throws DatabaseError('ABORTED', …) once aborted
1117
+ ```
1118
+
1119
+ Abort is precommit, not a `Promise.race` over an active commit that cannot be aborted
1120
+ write. Memory and SQLite check immediately before their synchronous mutation;
1121
+ IndexedDB aborts its explicit readwrite transaction while active; JSON aborts
1122
+ staging, cleans its temp file, and restores the preimage before rejecting. If
1123
+ the native commit has already been dispatched, the method ignores a late abort
1124
+ and awaits the real success or failure. A batch passes the same signal to each
1125
+ sequential item, so already-committed earlier items remain committed.
1126
+
1127
+ ### Batch operations
1128
+
1129
+ The keyed methods batch by overload (see `.claude/rules/patterns.md`
1130
+ § Batch operations) — one key/row in, one
1131
+ result; an array in, an array of results in the same order. The verb never
1132
+ changes (no `getMany` / `setAll`):
1133
+
1134
+ ```ts
1135
+ import type { RowOf } from '@orkestrel/database'
1136
+ import { createDatabase, createMemoryDriver } from '@orkestrel/database'
1137
+ import { integerShape, stringShape } from '@orkestrel/contract'
1138
+
1139
+ const columns = {
1140
+ id: stringShape(),
1141
+ name: stringShape(),
1142
+ age: integerShape(),
1143
+ role: stringShape(),
1144
+ }
1145
+ const users = createDatabase({
1146
+ driver: createMemoryDriver(),
1147
+ tables: { users: columns },
1148
+ }).table('users')
1149
+ const row1: RowOf<typeof columns> = { id: 'u1', name: 'Ada', age: 36, role: 'admin' }
1150
+ const row2: RowOf<typeof columns> = { id: 'u2', name: 'Bo', age: 41, role: 'member' }
1151
+ const row3: RowOf<typeof columns> = { id: 'u3', name: 'Cy', age: 29, role: 'member' }
1152
+
1153
+ await users.set([row1, row2, row3]) // → readonly Key[]
1154
+ await users.add([row1, row2]) // → readonly Key[] (CONFLICT rejects the batch)
1155
+ await users.get(['u1', 'u2']) // → readonly (Row | undefined)[]
1156
+ await users.resolve(['u1', 'u2']) // → readonly Row[] (NOT_FOUND on any miss)
1157
+ await users.has(['u1', 'u2']) // → readonly boolean[]
1158
+ await users.update(['u1', 'u2'], { role: 'member' }) // same changes to each → readonly boolean[]
1159
+ await users.remove(['u1', 'u2']) // → readonly boolean[]
1160
+ ```
1161
+
1162
+ A batch runs as independent sequential operations; wrap it in `transaction`
1163
+ when it must be atomic.
1164
+
1165
+ ### Coercion through the contract
1166
+
1167
+ Parses a numeric string against the table's contract, stores the coerced number, and reads it back:
1168
+
1169
+ ```ts
1170
+ import { createDatabase, createMemoryDriver } from '@orkestrel/database'
1171
+ import { integerShape, stringShape } from '@orkestrel/contract'
1172
+
1173
+ const users = createDatabase({
1174
+ driver: createMemoryDriver(),
1175
+ tables: {
1176
+ users: { id: stringShape(), name: stringShape(), age: integerShape(), role: stringShape() },
1177
+ },
1178
+ }).table('users')
1179
+
1180
+ // A numeric column accepts a numeric string and stores the coerced number.
1181
+ const normalized = users.contract.parse({
1182
+ id: 'u2',
1183
+ name: 'Bo',
1184
+ age: '41',
1185
+ role: 'member',
1186
+ })
1187
+ if (normalized === undefined) throw new Error('Expected the row to parse')
1188
+ await users.set(normalized)
1189
+ ;(await users.get('u2'))?.age // 41 (a number) — the contract parsed it
1190
+
1191
+ // A row that cannot satisfy the shape throws DatabaseError('VALIDATION').
1192
+ ```
1193
+
1194
+ ### Fluent queries
1195
+
1196
+ Chains conditions, an order, and a limit through the query builder and collects the matching rows:
1197
+
1198
+ ```ts
1199
+ import { createDatabase, createMemoryDriver } from '@orkestrel/database'
1200
+ import { integerShape, stringShape } from '@orkestrel/contract'
1201
+
1202
+ const users = createDatabase({
1203
+ driver: createMemoryDriver(),
1204
+ tables: {
1205
+ users: { id: stringShape(), name: stringShape(), age: integerShape(), role: stringShape() },
1206
+ },
1207
+ }).table('users')
1208
+
1209
+ await users
1210
+ .query()
1211
+ .condition({ column: 'age', operator: 'from', values: [18], connector: 'and' })
1212
+ .condition({ column: 'role', operator: 'not', values: ['guest'], connector: 'and' })
1213
+ .order({ column: 'age', direction: 'descending' })
1214
+ .limit(10)
1215
+ .collect() // the first ten non-guest adults, oldest first
1216
+
1217
+ await users
1218
+ .query()
1219
+ .condition({ column: 'name', operator: 'starts', values: ['A'], connector: 'and' })
1220
+ .find() // first match or undefined
1221
+ await users
1222
+ .query()
1223
+ .condition({ column: 'role', operator: 'equals', values: ['admin'], connector: 'and' })
1224
+ .count() // number
1225
+ await users
1226
+ .query()
1227
+ .condition({ column: 'role', operator: 'equals', values: ['member'], connector: 'and' })
1228
+ .aggregate('average', 'age') // number | undefined
1229
+ await users
1230
+ .query()
1231
+ .filter((user) => user.name.includes('a'))
1232
+ .collect() // post-fetch JavaScript predicate
1233
+
1234
+ // Ordering, paging, and named aggregation:
1235
+ await users.query().order({ column: 'name', direction: 'ascending' }).offset(10).limit(5).collect() // page 3 of 5, alphabetical
1236
+ await users
1237
+ .query()
1238
+ .condition({ column: 'age', operator: 'above', values: [18], connector: 'and' })
1239
+ .aggregate('sum', 'age')
1240
+ ```
1241
+
1242
+ Every condition operator uses the same `condition` method and explicit,
1243
+ serializable input:
1244
+
1245
+ ```ts
1246
+ import { createDatabase, createMemoryDriver } from '@orkestrel/database'
1247
+ import { integerShape, optionalShape, stringShape } from '@orkestrel/contract'
1248
+
1249
+ const users = createDatabase({
1250
+ driver: createMemoryDriver(),
1251
+ tables: {
1252
+ users: {
1253
+ id: stringShape(),
1254
+ name: stringShape(),
1255
+ age: integerShape(),
1256
+ role: stringShape(),
1257
+ bio: optionalShape(stringShape()),
1258
+ },
1259
+ },
1260
+ }).table('users')
1261
+
1262
+ await users
1263
+ .query()
1264
+ .condition({ column: 'age', operator: 'equals', values: [36], connector: 'and' })
1265
+ .collect()
1266
+ await users
1267
+ .query()
1268
+ .condition({ column: 'age', operator: 'not', values: [36], connector: 'and' })
1269
+ .collect()
1270
+ await users
1271
+ .query()
1272
+ .condition({ column: 'age', operator: 'above', values: [18], connector: 'and' })
1273
+ .collect()
1274
+ await users
1275
+ .query()
1276
+ .condition({ column: 'age', operator: 'below', values: [65], connector: 'and' })
1277
+ .collect()
1278
+ await users
1279
+ .query()
1280
+ .condition({ column: 'age', operator: 'from', values: [18], connector: 'and' })
1281
+ .collect()
1282
+ await users
1283
+ .query()
1284
+ .condition({ column: 'age', operator: 'to', values: [65], connector: 'and' })
1285
+ .collect()
1286
+ await users
1287
+ .query()
1288
+ .condition({ column: 'age', operator: 'between', values: [18, 65], connector: 'and' })
1289
+ .collect()
1290
+ await users
1291
+ .query()
1292
+ .condition({ column: 'name', operator: 'like', values: ['A%'], connector: 'and' })
1293
+ .collect()
1294
+ await users
1295
+ .query()
1296
+ .condition({ column: 'name', operator: 'glob', values: ['A*'], connector: 'and' })
1297
+ .collect()
1298
+ await users
1299
+ .query()
1300
+ .condition({ column: 'name', operator: 'starts', values: ['A'], connector: 'and' })
1301
+ .collect()
1302
+ await users
1303
+ .query()
1304
+ .condition({ column: 'name', operator: 'ends', values: ['a'], connector: 'and' })
1305
+ .collect()
1306
+ await users
1307
+ .query()
1308
+ .condition({ column: 'role', operator: 'any', values: ['admin', 'member'], connector: 'and' })
1309
+ .collect()
1310
+ await users
1311
+ .query()
1312
+ .condition({ column: 'role', operator: 'none', values: ['guest'], connector: 'and' })
1313
+ .collect()
1314
+ await users
1315
+ .query()
1316
+ .condition({ column: 'bio', operator: 'absent', values: [], connector: 'and' })
1317
+ .collect()
1318
+ await users
1319
+ .query()
1320
+ .condition({ column: 'bio', operator: 'present', values: [], connector: 'and' })
1321
+ .collect()
1322
+ ```
1323
+
1324
+ The condition operators map to familiar SQL operators. The engine
1325
+ evaluates every one of them in JS over `scan`; SQLite and IndexedDB push down
1326
+ provably exact candidate work and refine through these same semantics:
1327
+
1328
+ | Operator | SQL |
1329
+ | --------- | ------------- |
1330
+ | `equals` | `=` |
1331
+ | `not` | `!=` |
1332
+ | `above` | `>` |
1333
+ | `below` | `<` |
1334
+ | `from` | `>=` |
1335
+ | `to` | `<=` |
1336
+ | `between` | `BETWEEN` |
1337
+ | `like` | `LIKE` |
1338
+ | `glob` | `GLOB` |
1339
+ | `starts` | `LIKE 'p%'` |
1340
+ | `ends` | `LIKE '%s'` |
1341
+ | `any` | `IN` |
1342
+ | `none` | `NOT IN` |
1343
+ | `absent` | `IS NULL` |
1344
+ | `present` | `IS NOT NULL` |
1345
+
1346
+ ### Nested fields
1347
+
1348
+ Every column — in a condition, order, or aggregate — is a
1349
+ [`FieldPath`](contract.md): a **single string is one
1350
+ column** (never split on `.`), while an **array descends** into a nested
1351
+ (object / `json`) value. The _shape_ of the argument says how to read it; the
1352
+ string's _value_ is never parsed — there are no magic strings here.
1353
+
1354
+ ```ts
1355
+ import { createDatabase, createMemoryDriver } from '@orkestrel/database'
1356
+ import { numberShape, objectShape, stringShape } from '@orkestrel/contract'
1357
+
1358
+ const db = createDatabase({
1359
+ driver: createMemoryDriver(),
1360
+ tables: {
1361
+ events: {
1362
+ id: stringShape(),
1363
+ payload: objectShape({
1364
+ user: objectShape({ id: stringShape() }),
1365
+ at: stringShape(),
1366
+ }),
1367
+ 'payload.id': stringShape(),
1368
+ },
1369
+ orders: {
1370
+ id: stringShape(),
1371
+ totals: objectShape({ amount: numberShape() }),
1372
+ },
1373
+ },
1374
+ })
1375
+
1376
+ await db
1377
+ .table('events')
1378
+ .query()
1379
+ .condition({
1380
+ column: ['payload', 'user', 'id'],
1381
+ operator: 'equals',
1382
+ values: ['u1'],
1383
+ connector: 'and',
1384
+ })
1385
+ .collect()
1386
+ await db
1387
+ .table('events')
1388
+ .query()
1389
+ .order({ column: ['payload', 'at'], direction: 'descending' })
1390
+ .limit(20)
1391
+ .collect()
1392
+ await db.table('orders').query().aggregate('sum', ['totals', 'amount'])
1393
+
1394
+ // A dotted string is a column literally named 'payload.id', not a path:
1395
+ await db
1396
+ .table('events')
1397
+ .query()
1398
+ .condition({ column: 'payload.id', operator: 'present', values: [], connector: 'and' })
1399
+ .collect()
1400
+ ```
1401
+
1402
+ ### Cursors
1403
+
1404
+ The concrete cursor implementation is internal. Consumers receive the public
1405
+ `CursorInterface`, whose promise operations execute serially in invocation
1406
+ order. Every call is admitted through its owning transaction ledger before
1407
+ closed-cursor no-op behavior is considered, so a retained cursor still rejects
1408
+ with `CONFLICT` after its transaction settles. One rejected operation does not
1409
+ poison later admitted work.
1410
+
1411
+ ```ts
1412
+ import { createDatabase, createMemoryDriver } from '@orkestrel/database'
1413
+ import { integerShape, stringShape } from '@orkestrel/contract'
1414
+
1415
+ const users = createDatabase({
1416
+ driver: createMemoryDriver(),
1417
+ tables: {
1418
+ users: { id: stringShape(), age: integerShape(), role: stringShape() },
1419
+ },
1420
+ }).table('users')
1421
+
1422
+ const cursor = await users.cursor()
1423
+ while (!cursor.done) {
1424
+ if (cursor.value && cursor.value.age < 18) await cursor.remove()
1425
+ else await cursor.update({ role: 'member' })
1426
+ await cursor.next()
1427
+ }
1428
+ cursor.close()
1429
+ ```
1430
+
1431
+ `close()` is the sole synchronous cursor operation. It is terminal and clears
1432
+ `value` immediately. Work queued but not yet dispatched becomes a no-op; a
1433
+ backend mutation already dispatched may settle, but no await continuation can
1434
+ publish cursor state or restore `value` after close.
1435
+
1436
+ ### Transactions
1437
+
1438
+ Runs a scoped callback across two tables that commits on success and rolls every table back when the scope throws:
1439
+
1440
+ ```ts
1441
+ import { createDatabase, createMemoryDriver } from '@orkestrel/database'
1442
+ import { integerShape, stringShape } from '@orkestrel/contract'
1443
+
1444
+ const db = createDatabase({
1445
+ driver: createMemoryDriver(),
1446
+ tables: {
1447
+ users: {
1448
+ id: stringShape(),
1449
+ name: stringShape(),
1450
+ age: integerShape(),
1451
+ role: stringShape(),
1452
+ },
1453
+ posts: { slug: stringShape(), title: stringShape() },
1454
+ },
1455
+ primary: { posts: 'slug' },
1456
+ })
1457
+ const somethingWrong = false
1458
+
1459
+ // Commits on success; rolls every table back if the scope throws.
1460
+ await db.transaction(async (transaction) => {
1461
+ await transaction.table('users').set({ id: 'u3', name: 'Cy', age: 29, role: 'member' })
1462
+ await transaction.table('posts').add({ slug: 'intro', title: 'Intro' })
1463
+ if (somethingWrong) throw new Error('abort') // → both writes undone
1464
+ })
1465
+
1466
+ // Every accepted operation drains before settlement, including work not
1467
+ // returned by the callback. Catching an accepted rejection does not rescue
1468
+ // the transaction: the tracker still rolls it back.
1469
+ await db.transaction(async (transaction) => {
1470
+ void transaction.table('users').set({ id: 'u4', name: 'Dee', age: 31, role: 'member' })
1471
+ try {
1472
+ await transaction.table('posts').add({ slug: 'intro', title: 'duplicate' })
1473
+ } catch {
1474
+ // The duplicate remains a tracked transaction failure.
1475
+ }
1476
+ }) // rejects CONFLICT and rolls back u4
1477
+
1478
+ // Iterator continuations are the tracked unit. An in-flight next() drains;
1479
+ // merely creating or pausing an iterator does not hold the transaction open.
1480
+ await db.transaction(async (transaction) => {
1481
+ const rows = transaction.table('users').scan()[Symbol.asyncIterator]()
1482
+ await rows.next()
1483
+ // A rows.next() requested after this callback settles throws CONFLICT.
1484
+ })
1485
+
1486
+ // A pre-aborted signal is checked once at entry, before anything transactional runs:
1487
+ await db.transaction(async () => {}, { signal: AbortSignal.timeout(0) }) // throws ABORTED
1488
+ ```
1489
+
1490
+ While the scope is active, root/imported tables, `open`, `close`, `migrate`,
1491
+ and nested transactions reject with `CONFLICT`. The transaction view and every
1492
+ table/query/cursor/stream derived from it are invalid after settlement. When
1493
+ both the callback and a tracked operation reject, the callback rejection takes
1494
+ precedence; otherwise the first tracked rejection becomes the transaction
1495
+ error. `rollback(error)` reports only a completed rollback whose original
1496
+ scope/drain error is still being propagated — a cleanup failure is never
1497
+ reported as a successful rollback.
1498
+
1499
+ Root promise operations enter the shared admission ledger synchronously, before
1500
+ their first `await`. `transaction()` closes root admission before it drains that
1501
+ ledger, so work accepted immediately before the transaction is included and work
1502
+ attempted immediately after the boundary conflicts; there is no unobserved gap where a
1503
+ root write can escape into the transaction. If rollback cleanup fails, the
1504
+ operation rejects `DatabaseError('DRIVER')` with exact evidence
1505
+ `{ cause: rollbackFailure, transaction: originalFailure }` and emits no
1506
+ `rollback` event.
1507
+
1508
+ `AdmissionInterface` is that ledger's published contract — the one shape the
1509
+ root context and a transaction scope both present. No public call returns an
1510
+ instance (every implementor is internal), so read it as the boundary shape a
1511
+ scoped operation is entered into:
1512
+
1513
+ ```ts
1514
+ import type { AdmissionInterface } from '@orkestrel/database'
1515
+
1516
+ const boundary: AdmissionInterface = {
1517
+ accepting: true,
1518
+ track: (operation) => operation(),
1519
+ }
1520
+ boundary.accepting // true
1521
+ await boundary.track(async () => 42) // 42
1522
+ ```
1523
+
1524
+ ### Native transactions
1525
+
1526
+ `transaction` uses a driver's optional native `transaction?(scope)` hook
1527
+ instead of the snapshot floor. The driver passes a `StorageInterface`
1528
+ capability to the callback, commits when it fulfills, rolls back when it
1529
+ rejects, and invalidates the capability after settlement. The database-level
1530
+ `transaction` / `commit` / `rollback` events fire the same either way:
1531
+
1532
+ ```ts
1533
+ import type { TableSchema } from '@orkestrel/database'
1534
+ import { createSQLiteDriver } from '@orkestrel/database/server'
1535
+
1536
+ const driver = createSQLiteDriver()
1537
+ const schema: readonly TableSchema[] = [
1538
+ {
1539
+ name: 'users',
1540
+ primary: 'id',
1541
+ columns: [
1542
+ { name: 'id', storage: 'text', optional: false, nullable: false },
1543
+ { name: 'name', storage: 'text', optional: false, nullable: false },
1544
+ ],
1545
+ indexes: [],
1546
+ },
1547
+ ]
1548
+ await driver.open(schema)
1549
+ if (driver.transaction) {
1550
+ await driver.transaction(async (transaction) => {
1551
+ await transaction.write('users', 'u1', { id: 'u1', name: 'Ada' })
1552
+ const row = await transaction.read('users', 'u1')
1553
+ if (row === undefined) throw new Error('missing scoped row')
1554
+ })
1555
+ }
1556
+ ```
1557
+
1558
+ A `scope` throw rolls back and preserves the original rejection unless backend
1559
+ cleanup itself fails. A commit failure rejects the callback operation and never
1560
+ publishes candidate state in backends such as `JSONDriver`.
1561
+
1562
+ ### Migrations
1563
+
1564
+ Migrations are caller-driven — `planMigration` structurally diffs a
1565
+ deployed and a declared `TableSchema[]` into an ordered `Migration`, which
1566
+ the caller packages as `MigrationInput` for a driver's optional native
1567
+ `migrate?`. The input's schema changes and optional target `metadata`
1568
+ commit or roll back together. `migrateRows` is the pure per-table row transform
1569
+ a driver's `migrate` can lean on. Calling `planMigration` + `driver.migrate?`
1570
+ directly is still the low-level path
1571
+ (useful outside a `Database`, for example against a bare driver):
1572
+
1573
+ ```ts
1574
+ import type { TableSchema } from '@orkestrel/database'
1575
+ import { createMemoryDriver, migrateRows, planMigration } from '@orkestrel/database'
1576
+
1577
+ const deployed: readonly TableSchema[] = [
1578
+ {
1579
+ name: 'users',
1580
+ primary: 'id',
1581
+ columns: [{ name: 'id', storage: 'text', optional: false, nullable: false }],
1582
+ indexes: [],
1583
+ },
1584
+ ]
1585
+ const declared: readonly TableSchema[] = [
1586
+ {
1587
+ name: 'users',
1588
+ primary: 'id',
1589
+ columns: [
1590
+ { name: 'id', storage: 'text', optional: false, nullable: false },
1591
+ { name: 'age', storage: 'integer', optional: true, nullable: true },
1592
+ ],
1593
+ indexes: [],
1594
+ },
1595
+ ]
1596
+ const plan = planMigration(deployed, declared) // { from: 0, to: 1, steps: [...] }
1597
+ const driver = createMemoryDriver()
1598
+ await driver.open(deployed) // open the physical schema that is actually deployed
1599
+ await driver.migrate?.({ plan }) // atomic schema + row migration
1600
+
1601
+ // The pure row-shape transform a driver's own `migrate` can apply:
1602
+ const rows = [{ id: 'a', name: 'Ada', legacy: true }]
1603
+ migrateRows(rows, [{ operation: 'column.remove', table: 'users', column: 'legacy' }])
1604
+ // => [{ id: 'a', name: 'Ada' }]
1605
+ ```
1606
+
1607
+ `Database.migrate(deployed, options?)` wraps that same diff-then-apply
1608
+ orchestration against the database's own declared `tables`, so the caller
1609
+ only has to track what is deployed:
1610
+
1611
+ ```ts
1612
+ import { createDatabase, createMemoryDriver } from '@orkestrel/database'
1613
+ import { integerShape, stringShape } from '@orkestrel/contract'
1614
+
1615
+ const db = createDatabase({
1616
+ driver: createMemoryDriver(),
1617
+ tables: { users: { id: stringShape(), name: stringShape(), age: integerShape() } },
1618
+ })
1619
+ const deployed: readonly import('@orkestrel/database').TableSchema[] = [
1620
+ {
1621
+ name: 'users',
1622
+ primary: 'id',
1623
+ columns: [{ name: 'id', storage: 'text', optional: false, nullable: false }],
1624
+ indexes: [],
1625
+ },
1626
+ ]
1627
+ const plan = await db.migrate(deployed) // diffs deployed vs. declared, applies it, emits 'migrate'
1628
+ plan.steps // the applied Migration steps
1629
+
1630
+ db.emitter.on('migrate', (applied) => console.log('migrated to', applied.to))
1631
+ ```
1632
+
1633
+ ### Versioned auto-migrate on open
1634
+
1635
+ A driver that implements the paired `metadata` / `stamp` can skip the
1636
+ caller-driven `Database.migrate` call entirely: pass
1637
+ `DatabaseOptions.version`, and `open()` reconciles the driver's persisted
1638
+ schema against the declared one for you, migrating and re-stamping as needed.
1639
+
1640
+ A partial capability is deliberately inert: when only `metadata` or only `stamp`
1641
+ exists, `open()` calls neither hook and performs no migration, stamping, or
1642
+ `migrate` event emission. Reconciliation requires `version`, `metadata`, and
1643
+ `stamp` together.
1644
+
1645
+ ```ts
1646
+ import { createDatabase } from '@orkestrel/database'
1647
+ import { createJSONDriver } from '@orkestrel/database/server'
1648
+ import { integerShape, optionalShape, stringShape } from '@orkestrel/contract'
1649
+
1650
+ const path = 'data/versioned.json'
1651
+ const db = createDatabase({
1652
+ driver: createJSONDriver(path),
1653
+ tables: { users: { id: stringShape(), name: stringShape(), age: integerShape() } },
1654
+ version: 2, // the declared schema version
1655
+ })
1656
+
1657
+ await db.open() // fresh store → stamps { version: 2, schema } for next time
1658
+ await db.open() // idempotent while this handle remains open
1659
+ db.emitter.on('migrate', (applied) => console.log('auto-migrated to', applied.to))
1660
+ await db.close()
1661
+
1662
+ // close() is terminal for this handle and every imported view. A persistent
1663
+ // reopen uses a fresh driver/database handle over the same store.
1664
+ const same = createDatabase({
1665
+ driver: createJSONDriver(path),
1666
+ tables: { users: { id: stringShape(), name: stringShape(), age: integerShape() } },
1667
+ version: 2,
1668
+ })
1669
+ await same.open() // same version + canonical schema → no migration or metadata rewrite
1670
+ await same.close()
1671
+
1672
+ // Reopen the same store with a higher version and a changed declaration.
1673
+ // The backend first opens DriverMetadata.schema as the deployed physical schema;
1674
+ // Database then diffs deployed → declared and submits one atomic
1675
+ // { plan, metadata: { version, schema } } migration input.
1676
+ const upgraded = createDatabase({
1677
+ driver: createJSONDriver(path),
1678
+ tables: {
1679
+ users: {
1680
+ id: stringShape(),
1681
+ name: stringShape(),
1682
+ age: integerShape(),
1683
+ visits: optionalShape(integerShape()),
1684
+ },
1685
+ },
1686
+ version: 3,
1687
+ })
1688
+ await upgraded.open() // schema + rows + version-3 metadata publish together
1689
+ ```
1690
+
1691
+ ### Owning driver metadata
1692
+
1693
+ `DriverMetadata` is exact JSON and crosses one public ownership boundary. A driver
1694
+ snapshots it at every `stamp` / migration ingress and returns a fresh deeply
1695
+ frozen copy from `metadata()`, so neither later mutation of the caller's input nor
1696
+ mutation attempts against a returned value can alter stored version state.
1697
+ `cloneDriverMetadata` provides that boundary to every driver:
1698
+
1699
+ ```ts
1700
+ import { cloneDriverMetadata } from '@orkestrel/database'
1701
+
1702
+ const source = {
1703
+ version: 3,
1704
+ schema: [
1705
+ {
1706
+ name: 'users',
1707
+ primary: 'id',
1708
+ columns: [{ name: 'id', storage: 'text', optional: false, nullable: false }],
1709
+ indexes: [],
1710
+ },
1711
+ ],
1712
+ }
1713
+ const metadata = cloneDriverMetadata(source)
1714
+
1715
+ Object.isFrozen(metadata) // true
1716
+ Object.isFrozen(metadata.schema[0]) // true
1717
+ metadata !== source // true
1718
+ ```
1719
+
1720
+ The total guards inspect untrusted input without throwing, while the cloners
1721
+ establish owned, deeply frozen boundaries for the complete schema or migration.
1722
+ The browser projection consumes the same portable table schema:
1723
+
1724
+ ```ts
1725
+ import {
1726
+ cloneDriverSchema,
1727
+ cloneMigrationInput,
1728
+ isColumnSchema,
1729
+ isDriverMetadata,
1730
+ isDriverSchema,
1731
+ isMigration,
1732
+ isMigrationInput,
1733
+ isMigrationStep,
1734
+ isTableSchema,
1735
+ bindRowKey,
1736
+ normalizeDriverSchema,
1737
+ projectMigrationSchema,
1738
+ shapeToColumnSchema,
1739
+ type TableSchema,
1740
+ } from '@orkestrel/database'
1741
+ import { schemaToStore } from '@orkestrel/database/browser'
1742
+ import { optionalShape, stringShape } from '@orkestrel/contract'
1743
+
1744
+ const table: TableSchema = {
1745
+ name: 'users',
1746
+ primary: 'id',
1747
+ columns: [{ name: 'id', storage: 'text', optional: false, nullable: false }],
1748
+ indexes: [],
1749
+ }
1750
+ const plan = { from: 1, to: 2, steps: [{ operation: 'table.add', table }] }
1751
+ const input = { plan, metadata: { version: 2, schema: [table] } }
1752
+
1753
+ isColumnSchema(table.columns[0])
1754
+ isTableSchema(table)
1755
+ isDriverSchema([table])
1756
+ isMigrationStep(plan.steps[0])
1757
+ isMigration(plan)
1758
+ isDriverMetadata(input.metadata)
1759
+ isMigrationInput(input)
1760
+ cloneDriverSchema([table])
1761
+ cloneMigrationInput(input)
1762
+ bindRowKey({ name: 'Ada' }, 'id', 'u1')
1763
+ normalizeDriverSchema([table])
1764
+ shapeToColumnSchema('nickname', optionalShape(stringShape()))
1765
+ projectMigrationSchema([], cloneMigrationInput(input).plan.steps)
1766
+ schemaToStore(table)
1767
+ ```
1768
+
1769
+ The helper delegates exact JSON ownership to Contract 0.0.9's
1770
+ `cloneJSONRecord`, then validates the owned output as `DriverMetadata`. A malformed
1771
+ shape, cycle, function, accessor, or hostile/revoked proxy throws
1772
+ `DatabaseError('VALIDATION')` with `context.path === 'metadata'`; clone/traversal
1773
+ failures are retained only as `context.cause`, so no raw Contract or caller
1774
+ error crosses the Database surface. Hostile values are never stringified or
1775
+ embedded in the diagnostic.
1776
+
1777
+ `JSONDriver` applies that ownership rule at every file-backed seam: valid parsed
1778
+ metadata is cloned, while a present malformed metadata value fails the whole
1779
+ open with a payload-safe `DRIVER` error. Root `stamp` / `migrate`
1780
+ and their scoped candidate equivalents clone metadata synchronously before
1781
+ queue admission or another await can yield to caller mutation. Candidate/root
1782
+ publication, serialization, and every `metadata()` copy-out clone again, so the
1783
+ serialized value is an owned validated snapshot and each returned value is
1784
+ distinct and deeply frozen. General rows retain `MemoryDriver`'s native
1785
+ structured-clone behavior and are not forced through the JSON metadata cloner.
1786
+
1787
+ `SQLiteDriver` applies the same boundary at persisted-row ingress, every
1788
+ root/scoped `stamp` and `migrate` ingress, and every `metadata()` copy-out.
1789
+ Root lifecycle and scoped token gates run before hostile metadata traversal;
1790
+ valid migration metadata is cloned before its first DDL statement. Malformed
1791
+ stored metadata fails closed with `DRIVER`, while valid copy-outs are distinct
1792
+ and deeply frozen. General SQLite rows continue through
1793
+ their declared codecs and native `SQLiteValue`s, never the JSON metadata cloner.
1794
+
1795
+ ### Driver conformance
1796
+
1797
+ `conformDriver(factory)` runs the same invariant battery every backend must
1798
+ uphold — call it from a new driver's own test suite (or a smoke script) to
1799
+ prove it is a drop-in `DriverInterface`:
1800
+
1801
+ ```ts
1802
+ import { conformDriver, createMemoryDriver } from '@orkestrel/database'
1803
+
1804
+ await conformDriver(() => createMemoryDriver()) // resolves once every phase passes
1805
+ // A driver that violates an invariant rejects with DatabaseError('CONFORMANCE', ...)
1806
+ ```
1807
+
1808
+ ### Auditing a custom driver
1809
+
1810
+ `auditDriver(factory)` drains the full battery instead of failing fast,
1811
+ collecting every violation — useful when developing a new backend and
1812
+ wanting the complete picture in one run rather than fixing one invariant at
1813
+ a time:
1814
+
1815
+ ```ts
1816
+ import { auditDriver, createMemoryDriver } from '@orkestrel/database'
1817
+
1818
+ const findings = await auditDriver(() => createMemoryDriver())
1819
+ // [] — a fully conformant driver
1820
+ for (const finding of findings) console.log(`${finding.check}: ${finding.message}`)
1821
+
1822
+ // The lower-level generator these two build on — one phase per yield, lazy:
1823
+ import { scanDriver } from '@orkestrel/database'
1824
+ for await (const finding of scanDriver(() => createMemoryDriver())) {
1825
+ console.log(finding.check, finding.context)
1826
+ }
1827
+ ```
1828
+
1829
+ ### Key factories
1830
+
1831
+ When the primary column is optional and a write omits it, the table uses
1832
+ global `crypto.randomUUID()` by default. `DatabaseOptions.generator` is an
1833
+ authoritative override; numeric primary columns require one because the
1834
+ default generator returns a string. Explicit primary values never invoke it.
1835
+ Browser consumers relying on the default require a secure context that exposes
1836
+ `crypto.randomUUID()`; otherwise supply a generator or an explicit primary.
1837
+
1838
+ ```ts
1839
+ import { createDatabase, createMemoryDriver } from '@orkestrel/database'
1840
+ import { integerShape, optionalShape, stringShape } from '@orkestrel/contract'
1841
+
1842
+ const db = createDatabase({
1843
+ driver: createMemoryDriver(),
1844
+ tables: { posts: { id: optionalShape(stringShape()), title: stringShape() } },
1845
+ })
1846
+ await db.table('posts').set({ title: 'Hello' }) // a fresh UUID
1847
+
1848
+ const numbered = createDatabase({
1849
+ driver: createMemoryDriver(),
1850
+ tables: { events: { id: optionalShape(integerShape()), name: stringShape() } },
1851
+ generator: () => 42,
1852
+ })
1853
+ await numbered.table('events').set({ name: 'opened' }) // 42
1854
+ ```
1855
+
1856
+ ### Observing
1857
+
1858
+ Both the `Database` and each `Table` expose a typed `emitter` (see
1859
+ `.claude/rules/patterns.md` § Stateful emitters)
1860
+ carrying its lifecycle for fire-and-forget observers — logging, metrics,
1861
+ **cache invalidation, a sync layer**. The vocabulary is split by audience:
1862
+ the **database** carries the connection + transaction moments, each **table**
1863
+ the per-row mutations (key only — no value payload, to keep fan-out lean; a
1864
+ consumer that needs the value re-reads it). Subscribe through
1865
+ `entity.emitter.on(...)`, or wire initial listeners through the reserved
1866
+ `on?` option. **Emitting is observation-only**: every event fires strictly
1867
+ after the relevant transition, so a listener can never change what a write
1868
+ or a transaction does.
1869
+
1870
+ ```ts
1871
+ import { createDatabase, createMemoryDriver } from '@orkestrel/database'
1872
+ import { stringShape } from '@orkestrel/contract'
1873
+
1874
+ const db = createDatabase({
1875
+ driver: createMemoryDriver(),
1876
+ tables: { users: { id: stringShape(), name: stringShape() } },
1877
+ on: { commit: () => console.log('transaction committed') },
1878
+ })
1879
+
1880
+ const users = db.table('users') // hold the handle (the documented practice) and observe it
1881
+ users.emitter.on('write', (key) => console.log('invalidate users', key))
1882
+ users.emitter.on('remove', (key) => console.log('invalidate users', key))
1883
+ db.emitter.on('rollback', (error) => console.warn('transaction rolled back', error))
1884
+ ```
1885
+
1886
+ The event vocabulary:
1887
+
1888
+ | Entity | Event map | Events |
1889
+ | ---------- | ------------------ | ----------------------------------------------------------------------------------------------- |
1890
+ | `Database` | `DatabaseEventMap` | `open()` · `close()` · `transaction()` · `commit()` · `rollback(error)` · `migrate(migration)` |
1891
+ | `Table` | `TableEventMap` | `write(key)` · `remove(key)` · `clear()` (key only — `set` / `add` / `update` all emit `write`) |
1892
+
1893
+ `open` fires once when the handle's driver connects (an explicit `open()`, or
1894
+ the lazy first-use connect); `close`
1895
+ when the driver is released; `transaction` when a scope begins after its
1896
+ native boundary or fallback snapshot is acquired; `commit` only after a scope
1897
+ succeeds; `rollback` only after a throwing scope's tables are all restored;
1898
+ `migrate` after a migration commits. A `Table` fires `write` after any
1899
+ row put (set / add / update — re-read by key if you need the new value),
1900
+ `remove` after a row is deleted (a delete of an absent key emits nothing),
1901
+ and `clear` after the table is emptied. Reads / queries / counts are **not**
1902
+ emitted — a reader does not mutate, and those paths are too hot. Each
1903
+ `db.table(name)` returns a fresh handle with its own emitter, so subscribe
1904
+ on the handle you hold and operate on that same handle.
1905
+
1906
+ **Listener isolation.** A listener throw never escapes
1907
+ into the engine: the emitter isolates it and routes it to
1908
+ its own `error` handler (the `error` option, surfaced as `(error, event)`),
1909
+ not to a domain event — so a buggy observer is isolated yet not silently
1910
+ lost. The `error` handler runs in its own try/catch, so even a throwing
1911
+ handler can't recurse or escape; with no handler, the throw is swallowed
1912
+ silently. Every throwing listener surfaces (not only the first). Because
1913
+ every emit sits after its transition and is isolated, a buggy observer
1914
+ **cannot corrupt a write or a transaction**: a throwing `commit` observer
1915
+ leaves the committed state intact, a throwing `rollback` observer cannot
1916
+ suppress the propagated transaction error (the original throw still
1917
+ propagates, the tables still roll back), and a throwing `write` observer
1918
+ leaves the written row intact — proven by the per-entity emit-safety tests.
1919
+ (A `Table` reached through the `Database` receives the same `error` handler
1920
+ the `DatabaseOptions.error` option supplies, so a `Table` listener throw
1921
+ routes there; with no `error` handler configured, the throw is swallowed
1922
+ silently.)
1923
+
1924
+ ### Importing and exporting schemas
1925
+
1926
+ `import` defines more than one table at once from a shape map (keys are
1927
+ names) and returns a typed view of those tables over the **same** driver.
1928
+ `export` emits a portable definition per table — useful for moving a schema
1929
+ between databases or environments and for diffing migrations.
1930
+
1931
+ ```ts
1932
+ import { createDatabase, createMemoryDriver } from '@orkestrel/database'
1933
+ import { integerShape, stringShape } from '@orkestrel/contract'
1934
+
1935
+ const db = createDatabase({
1936
+ driver: createMemoryDriver(),
1937
+ tables: { users: { id: stringShape(), name: stringShape() } },
1938
+ })
1939
+
1940
+ // Define more tables at runtime; the returned view is typed and shares storage.
1941
+ // Compose every imported view before the first open/use; every view shares one lifecycle context.
1942
+ const audit = db.import(
1943
+ {
1944
+ logs: { id: stringShape(), message: stringShape(), at: integerShape() },
1945
+ sessions: { id: stringShape(), user: stringShape() },
1946
+ },
1947
+ { sessions: 'id' },
1948
+ )
1949
+ await audit.table('logs').set({ id: 'l1', message: 'started', at: 1 })
1950
+
1951
+ // Export a portable schema (JSON Schema is environment-agnostic).
1952
+ const portable = db.export()
1953
+ const exported = portable.users
1954
+ if (exported === undefined) throw new Error('Expected the users definition')
1955
+ exported.schema // a JSON Schema document
1956
+ exported.columns // the source column map (re-imports through `import` in a TS environment)
1957
+ exported.primary // 'id'
1958
+ ```
1959
+
1960
+ ### Introspection & seeding
1961
+
1962
+ Reads a table's contract schema, generates a reproducible seed row, and guards an unknown value against it:
1963
+
1964
+ ```ts
1965
+ import { createDatabase, createMemoryDriver } from '@orkestrel/database'
1966
+ import { stringShape } from '@orkestrel/contract'
1967
+
1968
+ const users = createDatabase({
1969
+ driver: createMemoryDriver(),
1970
+ tables: { users: { id: stringShape(), name: stringShape() } },
1971
+ }).table('users')
1972
+ const value: unknown = { id: 'u1', name: 'Ada' }
1973
+
1974
+ users.contract.schema // the table's JSON Schema (from the shape)
1975
+ users.contract.generate() // a valid seed row — reproducible with a seeded RandomFunction
1976
+ users.contract.is(value) // the row guard
1977
+ ```
1978
+
1979
+ ### Connecting eagerly
1980
+
1981
+ The driver connects lazily on first table use; call `open` to connect
1982
+ eagerly instead (useful to fail fast at startup, before the first request):
1983
+
1984
+ ```ts
1985
+ import { createDatabase, createMemoryDriver } from '@orkestrel/database'
1986
+ import { stringShape } from '@orkestrel/contract'
1987
+
1988
+ const db = createDatabase({
1989
+ driver: createMemoryDriver(),
1990
+ tables: { users: { id: stringShape() } },
1991
+ })
1992
+ await db.open() // connects immediately — table() calls after this never wait on it
1993
+ ```
1994
+
1995
+ Concurrent eager and lazy callers share one readiness attempt. A physical
1996
+ driver-open failure clears that attempt, so a later `open()` or table operation
1997
+ retries instead of inheriting a permanently rejected promise. When physical
1998
+ open succeeds and version reconciliation then fails, `status` remains `open`
1999
+ and the single `open` event records that physical transition. The shared
2000
+ readiness promise and ordinary table work still reject until a later automatic
2001
+ retry reconciles successfully; recovery reuses the same open physical handle
2002
+ instead of creating a duplicate connection.
2003
+ An explicit migration failure is stricter: ordinary work continues to receive
2004
+ that exact failure until another explicit `migrate(deployed)` succeeds.
2005
+
2006
+ `close()` first stops new root admissions, drains every root operation admitted
2007
+ synchronously before the close/transaction boundary, waits for any shared
2008
+ readiness attempt to settle, and releases the driver. Close is idempotent but
2009
+ terminal: the database handle and every imported view remain `closed`; reopen a
2010
+ persistent store with a fresh driver/database handle.
2011
+
2012
+ ### Driver primitives
2013
+
2014
+ A `DriverInterface` is the irreducible storage primitive every backend
2015
+ implements; `Database` / `Table` are the ergonomic layer built on it. Calling
2016
+ it directly (as `Table` does internally) shows the whole required surface:
2017
+
2018
+ ```ts
2019
+ import type { TableSchema } from '@orkestrel/database'
2020
+ import { createMemoryDriver } from '@orkestrel/database'
2021
+
2022
+ const driver = createMemoryDriver()
2023
+ const schema: readonly TableSchema[] = [
2024
+ {
2025
+ name: 'users',
2026
+ primary: 'id',
2027
+ columns: [{ name: 'id', storage: 'text', optional: false, nullable: false }],
2028
+ indexes: [],
2029
+ },
2030
+ ]
2031
+ await driver.open(schema)
2032
+ await driver.stamp?.({ version: 1, schema })
2033
+ await driver.insert('users', 'u1', { id: 'u1', name: 'Ada' }) // duplicate → CONFLICT
2034
+ await driver.write('users', 'u1', { id: 'u1', name: 'Ada Lovelace' }) // upsert
2035
+ await driver.read('users', 'u1') // { id: 'u1', name: 'Ada' } | undefined
2036
+ for await (const row of driver.scan('users')) row // every row, key order
2037
+ await driver.keys('users') // readonly Key[]
2038
+ const rollback = await driver.snapshot() // capture, then...
2039
+ await driver.delete('users', 'u1') // boolean
2040
+ await rollback() // ...restore the captured state
2041
+ await driver.clear('users')
2042
+ await driver.close()
2043
+ ```
2044
+
2045
+ ### Query engine helpers
2046
+
2047
+ The pure functions behind `TableInterface` and `QueryInterface` — useful
2048
+ directly when building a new driver's native `records` / `aggregate` hook:
2049
+
2050
+ ```ts
2051
+ import type { Condition } from '@orkestrel/database'
2052
+ import { integerShape } from '@orkestrel/contract'
2053
+ import {
2054
+ applyQuery,
2055
+ compareValues,
2056
+ computeAggregate,
2057
+ equalsValue,
2058
+ extractKey,
2059
+ filterRows,
2060
+ matchesGlobPattern,
2061
+ matchesLikePattern,
2062
+ matchesCondition,
2063
+ matchesQuery,
2064
+ shapeToColumnStorage,
2065
+ sortRows,
2066
+ validatePage,
2067
+ matchesWildcardPattern,
2068
+ } from '@orkestrel/database'
2069
+
2070
+ compareValues(1, 2) // -1 — a total order over mixed types
2071
+ matchesWildcardPattern('hello', 'h%o', '%', '_', true) // true — the shared LIKE/GLOB engine
2072
+ matchesLikePattern('hello', 'h%o') // true — case-insensitive
2073
+ matchesGlobPattern('hello', 'h*o') // true — case-sensitive
2074
+
2075
+ const condition: Condition = {
2076
+ column: 'age',
2077
+ operator: 'above',
2078
+ values: [18],
2079
+ connector: 'and',
2080
+ }
2081
+ matchesCondition({ age: 36 }, condition) // true
2082
+ matchesQuery({ age: 36 }, [condition]) // true — folds every condition
2083
+ filterRows([{ age: 36 }, { age: 12 }], [condition]) // [{ age: 36 }] — the count/aggregate basis
2084
+
2085
+ sortRows([{ age: 36 }, { age: 18 }], [{ column: 'age', direction: 'ascending' }])
2086
+ applyQuery([{ age: 36 }, { age: 18 }], { conditions: [condition], limit: 1 })
2087
+ validatePage({ limit: 25, offset: 0 }) // valid; fractions, negatives, NaN, and infinity throw
2088
+ computeAggregate([{ age: 36 }, { age: 18 }], 'average', 'age') // 27
2089
+
2090
+ extractKey({ id: 'u1' }, 'id') // 'u1'
2091
+ shapeToColumnStorage(integerShape()) // 'integer' — the type `open` hands a driver
2092
+ equalsValue({ a: [1, { b: 2 }] }, { a: [1, { b: 2 }] }) // true — structural, not reference, equality
2093
+ ```
2094
+
2095
+ ### Persistence with the JSON driver
2096
+
2097
+ Opens a database over the JSON driver and writes one row, persisted to the backing file:
2098
+
2099
+ ```ts
2100
+ import { createDatabase } from '@orkestrel/database'
2101
+ import { createJSONDriver } from '@orkestrel/database/server'
2102
+ import { stringShape } from '@orkestrel/contract'
2103
+
2104
+ const db = createDatabase({
2105
+ driver: createJSONDriver('data/app.json'),
2106
+ tables: { users: { id: stringShape(), name: stringShape() } },
2107
+ })
2108
+ await db.table('users').set({ id: 'u1', name: 'Ada' }) // persisted to app.json
2109
+ ```
2110
+
2111
+ `JSONDriver` serializes `open` with every mutation. It reads persisted metadata
2112
+ first and, when versioned, adopts `DriverMetadata.schema` as the deployed schema
2113
+ before target reconciliation. Only `ENOENT` proves a fresh store. Every other
2114
+ read failure and every existing invalid document fails closed with a
2115
+ payload-safe `DRIVER` error, leaving prior bytes and in-memory publication
2116
+ unchanged. The accepted document has exactly `tables` and optional `metadata`;
2117
+ its table keys exactly match the selected deployed schema, each table is an
2118
+ array, and every entry is a record with a usable, unique declared primary key.
2119
+ A bare `{ tables }` document remains a valid unstamped legacy file. No corrupt
2120
+ document is rewritten, quarantined, or repaired automatically; external repair
2121
+ followed by `open` on the same driver retries normally. It is a decorator over
2122
+ `MemoryDriver`, but each nontransactional point mutation is one exclusive
2123
+ queue job spanning preimage capture, speculative memory change, temp-file
2124
+ staging, atomic rename, and success or restoration. Reads wait behind that
2125
+ job, so they never observe its speculative state. An abort while queued rejects
2126
+ promptly and the job later exits before touching memory; an abort during
2127
+ staging uses the native file-write signal, removes the temp file, restores the
2128
+ preimage, and only then rejects `ABORTED`. `rename` dispatch is the
2129
+ commit point that cannot be aborted: after dispatch, the driver awaits and reports the
2130
+ real rename result. A failure before or at rename restores memory before the
2131
+ next queued writer starts, so concurrent writers cannot persist an older
2132
+ snapshot over a later mutation. Callback transactions and root
2133
+ `migrate({ plan, metadata })` build an isolated candidate and publish file first:
2134
+ the temp-file replacement must succeed before committed memory, schema, or
2135
+ metadata is swapped. Thus readers never observe state that durable storage did
2136
+ not accept, and a persistence failure discards the entire candidate. After a
2137
+ persistence fault, temporary-file cleanup completes before rejection. Cleanup
2138
+ success preserves the existing precedence: a precommit fired signal rejects
2139
+ `ABORTED`, otherwise the operation rejects `DRIVER` with the native/raw fault
2140
+ only at `context.cause`. If cleanup fails too, the top-level error is `DRIVER`
2141
+ with `context` containing the exact `path`, deterministic sibling `temp`, the
2142
+ original or abort-mapped `cause`, and the native `cleanup` fault. Root memory is
2143
+ restored before that error leaves the exclusive queue, so the next operation
2144
+ can proceed after the temporary obstruction is removed. A successful staging
2145
+ write requests native `flush: true` before the same-directory atomic rename.
2146
+
2147
+ `JSONDriver.snapshot()` captures owned row data together with the table schema
2148
+ needed to decode that capture. Its rollback thunk is repeatable and restores
2149
+ data only into the driver's current schema: current metadata is never rewound,
2150
+ a captured table removed after capture is skipped, and a table added later is
2151
+ preserved. Replaying the same thunk again produces the same row result without
2152
+ replacing the current schema or metadata.
2153
+
2154
+ ### Compiling input to SQL
2155
+
2156
+ The server's pure `compilers.ts` turns a core `QueryInput` (the same one
2157
+ `applyQuery` folds) into the `WHERE` / `ORDER BY` / `LIMIT` tail of a
2158
+ `SELECT`, with `?`-bound parameters in clause order — the payload a native SQLite
2159
+ driver's `records` hook runs directly:
2160
+
2161
+ ```ts
2162
+ import {
2163
+ compileAggregateSQL,
2164
+ compileColumnSQL,
2165
+ compileFieldSQL,
2166
+ compileQuerySQL,
2167
+ deriveSQLiteIndexName,
2168
+ matchesAggregateExactly,
2169
+ matchesSQLiteAffinity,
2170
+ quoteIdentifier,
2171
+ schemaToIndexes,
2172
+ schemaToTable,
2173
+ stepToSQL,
2174
+ } from '@orkestrel/database/server'
2175
+ import type { TableSchema } from '@orkestrel/database'
2176
+
2177
+ const schema: TableSchema = {
2178
+ name: 'users',
2179
+ primary: 'id',
2180
+ columns: [
2181
+ { name: 'id', storage: 'text', optional: false, nullable: false },
2182
+ { name: 'age', storage: 'integer', optional: false, nullable: false },
2183
+ ],
2184
+ indexes: [],
2185
+ }
2186
+
2187
+ compileQuerySQL(
2188
+ { conditions: [{ column: 'age', operator: 'from', values: [18], connector: 'and' }] },
2189
+ schema,
2190
+ ) // { sql: 'WHERE "age" >= ? ORDER BY "id"', parameters: [18] }
2191
+
2192
+ quoteIdentifier('order') // '"order"'
2193
+ deriveSQLiteIndexName('users', ['age']) // 'idx_5_users_3_age'
2194
+ compileColumnSQL('integer') // 'INTEGER'
2195
+ compileFieldSQL(['profile', 'score']) // 'json_extract("profile", \'$.score\')'
2196
+ compileAggregateSQL('average', 'age') // 'AVG("age")'
2197
+ matchesAggregateExactly('minimum', 'age', schema) // true
2198
+ matchesSQLiteAffinity('INTEGER', 'integer') // true
2199
+ schemaToTable(schema) // CREATE TABLE IF NOT EXISTS …
2200
+ schemaToIndexes(schema) // []
2201
+ stepToSQL({ operation: 'index.add', table: 'users', index: ['age'] })
2202
+
2203
+ // A parameterized SQLite binding runs it directly:
2204
+ // db.prepare(`SELECT * FROM "users" ${sql}`).all(...parameters)
2205
+ ```
2206
+
2207
+ ### Exact-or-refine vs. narrow-then-refine native reads
2208
+
2209
+ A native override earns the engine's trust by proving exactness or by
2210
+ narrowing then refining (see `.claude/rules/architecture.md` § System
2211
+ constraints).
2212
+ **Prove-exactness-or-refine** (`SQLiteDriver`): the backend has real typed
2213
+ columns and indexes, so it compiles the `QueryInput` straight to SQL and runs
2214
+ it natively only when `matchesQueryExactly` first proves the SQL and the engine
2215
+ agree on every condition/order term for that schema — otherwise it falls
2216
+ back to a full scan refined through the same core engine (never a "trust
2217
+ blindly" path). **Narrow-then-refine**
2218
+ (`IndexedDBDriver`): the backend can only prove a candidate superset range-exact
2219
+ (a key-range pushdown), so it fetches that superset and hands it to the same
2220
+ core engine every scan-only driver uses (`applyQuery` / `matchesQuery`),
2221
+ which refines it down to the exact result — conformance is earned by "never
2222
+ under-fetch," not by native filtering. Both are indistinguishable from the
2223
+ caller's side: `Table.records` / `count` / `stream` return identical rows
2224
+ either way; only the path to get there differs.
2225
+
2226
+ ```ts
2227
+ import type { Condition, TableSchema } from '@orkestrel/database'
2228
+ import {
2229
+ matchesConditionExactly,
2230
+ matchesQueryExactly,
2231
+ matchesOrderExactly,
2232
+ } from '@orkestrel/database/server'
2233
+
2234
+ const schema: TableSchema = {
2235
+ name: 'users',
2236
+ primary: 'id',
2237
+ columns: [
2238
+ { name: 'id', storage: 'text', optional: false, nullable: false },
2239
+ { name: 'age', storage: 'integer', optional: false, nullable: false },
2240
+ ],
2241
+ indexes: [],
2242
+ }
2243
+
2244
+ const exact: Condition = {
2245
+ column: 'age',
2246
+ operator: 'above',
2247
+ values: [18],
2248
+ connector: 'and',
2249
+ }
2250
+ matchesConditionExactly(exact, schema) // true — a plain comparison over a typed column
2251
+
2252
+ const notExact: Condition = {
2253
+ column: 'age',
2254
+ operator: 'above',
2255
+ values: [null],
2256
+ connector: 'and',
2257
+ }
2258
+ matchesConditionExactly(notExact, schema) // false — a null operand refines instead
2259
+
2260
+ matchesOrderExactly({ column: 'age', direction: 'ascending' }, schema) // true — a flat, orderable column
2261
+
2262
+ matchesQueryExactly(
2263
+ { conditions: [exact], order: [{ column: 'age', direction: 'ascending' }] },
2264
+ schema,
2265
+ ) // true
2266
+ ```
2267
+
2268
+ ### Persistence with the SQLite driver
2269
+
2270
+ Opens a database over the SQLite driver, writes one row, and runs a query compiled to native SQL:
2271
+
2272
+ ```ts
2273
+ import { createDatabase } from '@orkestrel/database'
2274
+ import { createSQLiteDriver } from '@orkestrel/database/server'
2275
+ import { integerShape, stringShape } from '@orkestrel/contract'
2276
+
2277
+ const db = createDatabase({
2278
+ driver: createSQLiteDriver({ path: 'data/app.sqlite' }), // or createSQLiteDriver() for ':memory:'
2279
+ tables: { users: { id: stringShape(), name: stringShape(), age: integerShape() } },
2280
+ })
2281
+ await db.table('users').set({ id: 'u1', name: 'Ada', age: 36 }) // persisted to app.sqlite
2282
+
2283
+ // Native querying, paging, and aggregation — compiled to SQL, no engine re-filter:
2284
+ await db
2285
+ .table('users')
2286
+ .query()
2287
+ .condition({ column: 'age', operator: 'from', values: [18], connector: 'and' })
2288
+ .order({ column: 'age', direction: 'descending' })
2289
+ .collect()
2290
+ await db
2291
+ .table('users')
2292
+ .query()
2293
+ .condition({ column: 'age', operator: 'above', values: [18], connector: 'and' })
2294
+ .aggregate('average', 'age')
2295
+
2296
+ // Real transactions and atomic migration ship with it:
2297
+ await db.transaction(async (transaction) => {
2298
+ await transaction.table('users').update('u1', { age: 37 })
2299
+ }) // real BEGIN/COMMIT/ROLLBACK, not the snapshot floor
2300
+
2301
+ // createSQLiteDriver accepts a SQLiteDriverOptions bag:
2302
+ createSQLiteDriver({
2303
+ path: 'data/app.sqlite',
2304
+ timeout: 5000,
2305
+ references: true,
2306
+ pragmas: { journal_mode: 'WAL' }, // applied through pragma() right after connect(), in order
2307
+ })
2308
+ ```
2309
+
2310
+ `SQLiteDriver` is the fully-native backend — it implements every optional
2311
+ `DriverInterface` hook (`records?` / `aggregate?` / `transaction?`
2312
+ / `stream?` / `migrate?` / `metadata?` / `stamp?`). Reopen the same `path` with a
2313
+ higher `DatabaseOptions.version` and it reconciles automatically through its
2314
+ reserved `_metadata` table (`METADATA_TABLE`) — see
2315
+ [Versioned auto-migrate on open](#versioned-auto-migrate-on-open); `open()`
2316
+ throws `DatabaseError('VALIDATION')` if a declared table is literally named
2317
+ `_metadata`, so the collision is caught immediately rather than silently
2318
+ corrupting metadata. `open()` creates only `_metadata` first, reads
2319
+ `DriverMetadata.schema`, and then creates or validates that deployed schema before
2320
+ `Database` reconciles it with the declaration. A root
2321
+ `migrate({ plan, metadata })` uses one native transaction for DDL, row changes, and
2322
+ metadata. The same call inside a callback transaction uses a SQLite savepoint:
2323
+ the wrapper deliberately provides raw `execute`, not a savepoint manager, so the
2324
+ driver owns one guarded fixed internal SQL literal. If caller code catches its
2325
+ failure, the failed inner migration is rolled back to that savepoint while the
2326
+ surrounding transaction remains active and may continue safely. Snapshot
2327
+ capture and replay, plus public `close()`, contain their complete native bodies
2328
+ inside `#guard`; no raw SQLite fault crosses the driver boundary. Candidate
2329
+ schema state becomes live only after the native commit. Point `write` / `insert` /
2330
+ `delete` check `OperationOptions.signal`
2331
+ immediately before the synchronous SQLite call; that call entry is the commit
2332
+ point, so there is no post-check that could relabel a completed commit.
2333
+
2334
+ ### Persistence with the IndexedDB driver
2335
+
2336
+ Feature-detects `indexedDB`, opens a database over the IndexedDB driver, writes one row, and runs a query pushed down to a key range:
2337
+
2338
+ ```ts
2339
+ import { createDatabase } from '@orkestrel/database'
2340
+ import { createIndexedDBDriver } from '@orkestrel/database/browser'
2341
+ import { stringShape } from '@orkestrel/contract'
2342
+
2343
+ // Feature-detect before reaching for it — IndexedDB is a browser-only global.
2344
+ if (typeof indexedDB !== 'undefined') {
2345
+ const db = createDatabase({
2346
+ driver: createIndexedDBDriver('app'),
2347
+ tables: { users: { id: stringShape(), name: stringShape() } },
2348
+ })
2349
+ await db.table('users').set({ id: 'u1', name: 'Ada' }) // persisted to IndexedDB
2350
+ await db
2351
+ .table('users')
2352
+ .query()
2353
+ .condition({ column: 'id', operator: 'equals', values: ['u1'], connector: 'and' })
2354
+ .collect() // pushed down to a key range
2355
+ }
2356
+ ```
2357
+
2358
+ `IndexedDBDriver.open()` first makes a metadata-only bootstrap connection and,
2359
+ inside one readonly transaction, tests whether the `'metadata'` key exists and
2360
+ reads its value. Absence returns `undefined`; a present malformed value
2361
+ (including stored `undefined`) throws a payload-safe `DRIVER` error without
2362
+ publishing schema/identity/connection state or changing the record. The
2363
+ bootstrap closes in every outcome, so external repair or version activity is
2364
+ not blocked and the same driver may retry. It then connects the deployed
2365
+ `DriverMetadata.schema` plus `__metadata__`; it does not create the target declaration
2366
+ before reconciliation. The driver narrows a `QueryInput` to a key-range candidate over the
2367
+ primary key or a single-column secondary index (`selectPlan`), then lets the
2368
+ core engine refine it to the exact result — see
2369
+ [Exact-or-refine vs. narrow-then-refine native reads](#exact-or-refine-vs-narrow-then-refine-native-reads).
2370
+ It implements `records?` / `stream?` / `migrate?` / `metadata?` /
2371
+ `stamp?` (persisted into a reserved `__metadata__` store, `METADATA_STORE` —
2372
+ `open()` throws `DatabaseError('VALIDATION')` if a declared table is
2373
+ literally named `__metadata__`), but omits `transaction?` (the underlying
2374
+ `IDBTransaction` auto-commits the moment control yields to a non-IDB
2375
+ `await`) and `aggregate?` (IndexedDB has no native SUM/AVG/MIN/MAX) by
2376
+ IndexedDB's own nature. A non-empty `migrate({ plan, metadata })` performs DDL,
2377
+ row transformations, and the metadata write in the same versionchange
2378
+ transaction; a metadata-only input uses one ordinary `__metadata__` write
2379
+ transaction. A failed upgrade reconnects the old deployed schema. Each point
2380
+ `write` / `insert` / `delete` still uses one explicit
2381
+ wrapper `database.write(table, scope)` transaction: the signal aborts it only
2382
+ while active, a signal-driven rollback maps to `DatabaseError('ABORTED')`, and
2383
+ native transaction completion is the commit boundary a late abort cannot
2384
+ rewrite.
2385
+ Every public `QueryInput` boundary also calls `validatePage`: `limit` and
2386
+ `offset`, when present, must be finite nonnegative integers. Validation is
2387
+ deterministic (`limit` before `offset`), non-finite diagnostics retain
2388
+ `'NaN'` / `'Infinity'` rather than JSON-coercing to `null`, and zero is
2389
+ legal. Return kind determines timing: `Query.limit` / `Query.offset` and
2390
+ `AsyncIterable` factories (`Table.scan` and every direct driver `stream`)
2391
+ throw synchronously, while Promise terminals (`Table.records` / `count` /
2392
+ `aggregate` and native Promise hooks) return rejected promises. Every path
2393
+ reports identical `VALIDATION` evidence and applies the same page predicate.
2394
+ Failed query-builder validation does not mutate builder state. `count` and
2395
+ `aggregate` validate paging even though valid paging remains intentionally
2396
+ ignored by their unpaged semantics.
2397
+
2398
+ ### IndexedDB pushdown planning
2399
+
2400
+ The pure planner behind `IndexedDBDriver`'s native `records?` /
2401
+ `stream?` — useful directly to see what a `QueryInput` pushes down to before it
2402
+ ever touches a browser database:
2403
+
2404
+ ```ts
2405
+ import type { Condition, TableSchema } from '@orkestrel/database'
2406
+ import { isKey } from '@orkestrel/database'
2407
+ import { conditionToRange, deriveIndexedDBIndexName, selectPlan } from '@orkestrel/database/browser'
2408
+
2409
+ const schema: TableSchema = {
2410
+ name: 'users',
2411
+ primary: 'id',
2412
+ columns: [
2413
+ { name: 'id', storage: 'text', optional: false, nullable: false },
2414
+ { name: 'age', storage: 'integer', optional: false, nullable: false },
2415
+ ],
2416
+ indexes: [['age']],
2417
+ }
2418
+
2419
+ isKey('u1') // true — a string is a usable IndexedDB key
2420
+ isKey(true) // false — a boolean is not
2421
+ deriveIndexedDBIndexName(['city', 'age']) // '2#4:city3:age'
2422
+
2423
+ const equalsAge: Condition = {
2424
+ column: 'age',
2425
+ operator: 'equals',
2426
+ values: [30],
2427
+ connector: 'and',
2428
+ }
2429
+ conditionToRange(equalsAge) // an IDBKeyRange.only(30) — an exact comparison operator
2430
+
2431
+ // A full scan (no condition qualifies for pushdown) returns an empty plan —
2432
+ // the driver then reads every row and lets the core engine filter it exactly:
2433
+ selectPlan(undefined, schema, ['age']) // {}
2434
+
2435
+ // A comparison over the indexed `age` column narrows to that index's range:
2436
+ selectPlan(
2437
+ { conditions: [{ column: 'age', operator: 'from', values: [18], connector: 'and' }] },
2438
+ schema,
2439
+ ['age'],
2440
+ ) // { index: 'age', range: an IDBKeyRange bounding age >= 18 }
2441
+ ```
2442
+
2443
+ ### IndexedDB error mapping
2444
+
2445
+ `IndexedDBDriver` never lets a raw backend fault cross its `DriverInterface`
2446
+ surface — every one is mapped to a `DatabaseError`, the original preserved
2447
+ as `context.cause`:
2448
+
2449
+ ```ts
2450
+ import type { IndexedDBError } from '@orkestrel/indexeddb'
2451
+ import { mapIndexedDBError, mapMigrationError } from '@orkestrel/database/browser'
2452
+
2453
+ declare const fault: IndexedDBError // a caught backend fault
2454
+
2455
+ mapIndexedDBError(fault) // → DatabaseError('CONFLICT' | 'CLOSED' | 'DRIVER', ...)
2456
+ mapMigrationError(fault) // → the same, but an UPGRADE fault becomes 'MIGRATION'
2457
+ ```
2458
+
2459
+ ### Practices
2460
+
2461
+ - **Declare tables in `createDatabase({ tables })` and hold the handles** —
2462
+ `const users = db.table('users')`; reuse them rather than re-resolving.
2463
+ - **Writes coerce, reads narrow.** At an unknown-input boundary, normalize
2464
+ loose data (`'41'`) through `table.contract.parse` before a typed write;
2465
+ trust `get` / `records` to return the row type.
2466
+ - **Use `resolve` when absence is an error**, `get` when it is expected —
2467
+ `resolve` throws `NOT_FOUND`, `get` returns `undefined`.
2468
+ - **Reach for `query()` over `records()`** — the builder compiles a portable
2469
+ `QueryInput`; `filter` is the JS escape hatch when an operator won't express
2470
+ it.
2471
+ - **Use `import` to add tables and views to the shared store before its first
2472
+ open/use** and `export` to move a schema across environments. Once opening
2473
+ starts, `import` conflicts; after `close`, it is closed.
2474
+ - **Wrap multi-write invariants in `transaction`** — a throw rolls every
2475
+ table back.
2476
+ - **Observe, don't drive** — subscribe to `db.emitter` (transaction
2477
+ lifecycle) / `table.emitter` (per-row `write` / `remove` / `clear`, key
2478
+ only) for cache invalidation, sync, or metrics (see
2479
+ [Observing](#observing)); emitting is a pure side-channel, so a listener
2480
+ never changes what a write or transaction does (and a throwing one can't
2481
+ corrupt it).
2482
+ - **Use `MemoryDriver` for tests and ephemeral data** — no I/O, and
2483
+ `JSONDriver` swaps in unchanged when writes need to survive a restart.
2484
+
2485
+ ## Tests
2486
+
2487
+ - [`tests/guides.test.ts`](../tests/guides.test.ts) — the `## Surface` ↔ compiler-resolved public-entry bijection across `src/core`, `src/server`, and `src/browser`, including fail-closed temporary-project coverage for barrel resolution and unsupported exports, each interface ↔ implementing-class method bijection, and the equality gate: every `Summary` cell against its declaration's description paragraph, the titled `Create a database` fence against the `@example` block of that title (pinned so the titled pair cannot be retired silently), and the README pitch against this guide's tagline. It also compiles every TypeScript fence against the published entry specifiers and runs the flagship fences, asserting the values their comments claim.
2488
+ - [`tests/src/core/cloners.test.ts`](../tests/src/core/cloners.test.ts) — `cloneDriverMetadata` ownership: normalized deeply frozen distinct output, caller-mutation isolation, and `VALIDATION` translation for malformed, cyclic, functional, accessor, and hostile/revoked-proxy inputs without leaking raw Contract or caller errors.
2489
+ - [`tests/src/core/validators.test.ts`](../tests/src/core/validators.test.ts) — total boundary guards for keys, columns, tables, driver schemas, migrations, inputs, and metadata.
2490
+ - [`tests/src/core/helpers.test.ts`](../tests/src/core/helpers.test.ts) — the query engine: `validatePage`'s strict page matrix, deterministic field order, exact non-finite diagnostics, and legal zero; `findColumn`'s flat-column lookup and its `undefined` miss; `resolvePrimary`'s declared-or-default key and `requireColumns`'s typed map lookup with its `NOT_FOUND` throw; `compareValues` total order, every `matchesCondition` operator (the equality family — `equals` / `not` / `any` / `none` — through `equalsValue`, including `NaN`-equals-`NaN`; the range family through `compareValues`), `matchesQuery` folding, `filterRows`, `sortRows`, `applyQuery`, `computeAggregate`, `extractKey`, `shapeToColumnStorage`'s shape → portable-type mapping (scalars, `json` for object/array/union/raw, optional/nullable unwrap, literal-by-values), total `isDriverMetadata` rejection of malformed and hostile getter/proxy input, `equalsValue`'s structural equality, `planMigration`'s `MIGRATION` throw on a shared column's storage/nullability drift, and the `scanDriver` / `conformDriver` / `auditDriver` battery against `MemoryDriver` and a deliberately-broken driver (each check fails with a `CONFORMANCE` `DatabaseError`), including the deepened `write-read` nested-field checks and the `snapshot-nested` phase (a shallow-copying driver fails it).
2491
+ - [`tests/src/core/drivers/MemoryDriver.test.ts`](../tests/src/core/drivers/MemoryDriver.test.ts) — the driver primitive: `open(schema)` readies tables, read/write/atomic-insert/delete/keys/scan/clear + `snapshot` rollback, duplicate-insert `CONFLICT`, non-JSON row isolation through native `structuredClone`, metadata stamp/migrate/copy-out ownership through `cloneDriverMetadata`, strict stream paging, and pre-aborted point mutations rejecting `ABORTED` without changing rows.
2492
+ - [`tests/src/core/Database.test.ts`](../tests/src/core/Database.test.ts) — declared tables, lazy connect, typed CRUD, custom keys, indexes, import/export, and callback transactions: whole accepted-operation drain, synchronous-throw and asynchronous-rejection reason identity, caught-operation rejection still rolling back, callback-over-drain error precedence, root/import/lifecycle/nesting barriers, stale scoped table/query/cursor/stream invalidation, and truthful successful-rollback-only events. It also covers explicit migration and versioned open: deployed-schema-first reconciliation, fresh stamp, same-version no-op, atomic upgrade input, higher-version rejection, paired-hook enforcement (metadata-only and stamp-only are inert), and migrate-event behavior.
2493
+ - [`tests/src/core/ScopedIterator.test.ts`](../tests/src/core/ScopedIterator.test.ts) — direct internal continuation-lifetime coverage: tracked `next` / `return` / `throw`, the readiness thunk running before every advance and its failure preempting the source, synchronous source throws, missing methods, concurrent accepted continuations, idle iterators, late conflicts, and exactly-once rejected cleanup.
2494
+ - [`tests/src/core/DriverIterator.test.ts`](../tests/src/core/DriverIterator.test.ts) — direct root-driver continuation coverage: pre/post-read guards, produced-row discard, terminalization, return races, missing methods, throw delegation, and exactly-once cleanup.
2495
+ - [`tests/src/core/Table.test.ts`](../tests/src/core/Table.test.ts) — `Table`'s keyed CRUD + batch overloads, payload-safe bounded contract diagnostics, strict paging at every read boundary, coercion and error paths, `add` dispatch through atomic `insert` (including concurrent duplicate claims), sequential partial-batch abort semantics, and the emitter's post-commit and no-aborted-event behavior.
2496
+ - [`tests/src/core/Query.test.ts`](../tests/src/core/Query.test.ts) — `Query`'s where / and / or dispatch, ordering, synchronous strict page builders with no failed mutation, legal zero, `filter`, and aggregates.
2497
+ - [`tests/src/core/Cursor.test.ts`](../tests/src/core/Cursor.test.ts) — cursor behavior over a key snapshot: `value` / `index` / `done`, serialized overlapping `next` / `update` / `remove`, rejection recovery, synchronous runner admission, terminal close before queued work and during dispatched reads/mutations, plus transaction-ledger regressions for deleted-key skipping, unawaited normalized updates, validation rollback, and retained closed/active conflicts.
2498
+ - [`tests/src/core/factories.test.ts`](../tests/src/core/factories.test.ts) — `createDatabase` / `createMemoryDriver` each return a working instance of their interface (a round-trip end to end).
2499
+ - [`tests/src/core/TransactionScope.test.ts`](../tests/src/core/TransactionScope.test.ts) — direct transaction-lifetime coverage: `accepting` and the `check` refusal, synchronous admission through `track`, a synchronous operation throw captured as a rejection, unawaited work contained by `drain`, the drain loop re-reading work another tracked operation admitted, first-failure identity preserved across repeated drains, and `stream`'s per-continuation boundary leaving an idle iterator unpinned.
2500
+ - [`tests/src/core/DatabaseContext.test.ts`](../tests/src/core/DatabaseContext.test.ts) — direct shared-context coverage: `register`'s identical-schema merge, its `VALIDATION` conflict, and its `CONFLICT` / `CLOSED` refusals; idle → open → closed transitions emitted once with one shared readiness promise; root admission, its transaction-time `CONFLICT`, and close-time drain; transaction commit / rollback events and value or error propagation, nested-transaction refusal, entry-only signal checking, and a fresh `TransactionScope` per attempt; explicit migration's missing-hook and post-open refusals; and versioned reconciliation's fresh stamp, newer-store rejection, differing-schema rejection, and same-version no-op.
2501
+ - [`tests/src/core/DatabaseTransaction.test.ts`](../tests/src/core/DatabaseTransaction.test.ts) — direct transaction-view coverage: the typed table `table()` builds over the scoped driver, its writes landing straight on that driver, the scope `CONFLICT` after settlement, the `NOT_FOUND` refusal for an undeclared table, a per-table primary override, a scoped table refusing work started after settlement, and the configured generator minting a key for a keyless write.
2502
+ - [`tests/src/server/factories.test.ts`](../tests/src/server/factories.test.ts) — `createJSONDriver` / `createSQLiteDriver` each return a working `DriverInterface` instance (a round-trip end to end), drive the core `createDatabase` stack, and persist across a reopen; `createSQLiteDriver` defaults to an in-memory database when its options bag is omitted.
2503
+ - [`tests/src/server/drivers/JSONDriver.test.ts`](../tests/src/server/drivers/JSONDriver.test.ts) — `JSONDriver` persistence plus atomic insert and the exclusive point-mutation queue: queued abort/no late start, active staging restoration, cleanup-success error precedence, deterministic real-filesystem persistence-plus-cleanup dual failure with exact evidence and queue recovery, read isolation, concurrent-writer ordering, fail-closed real-filesystem coverage for non-absence reads, invalid syntax/documents/table sets/containers/rows/metadata, byte preservation, no partial publication, application-invalid row retention, payload-safe rejection, strict stream paging, and same-instance external-repair retry; synchronous root/scoped stamp and migration ownership, deeply frozen distinct copy-out, deployed-schema-first open, isolated callback/root-migration candidates whose rows/schema/metadata publish only after file replacement succeeds, transaction-time root scan/stream continuation conflicts with terminal cleanup, and post-native-rollback rejection replacement.
2504
+ - [`tests/src/server/drivers/SQLiteDriver.test.ts`](../tests/src/server/drivers/SQLiteDriver.test.ts) — `SQLiteDriver`'s native surface: deployed-metadata-first open and reconciliation; fail-closed malformed metadata, physical schema disagreement, and persisted-table loss before DDL, including deterministic first-loss evidence, physical non-recreation, external repair, and same-driver retry; root/scoped stamp/migration ownership and distinct deeply frozen copy-out; atomic insert/duplicate `CONFLICT`; point-mutation abort boundaries; strict direct records/aggregate/stream paging; native exact-or-refine query and aggregate paths; repeatable schema-aware snapshot capture/replay plus real dropped-table and exclusive-lock failure containment/recovery; atomic `MigrationInput` schema/rows/metadata at the root; fixed-literal savepoint containment for a caught migration failure while its callback transaction remains active; candidate-schema publication only after commit; callback transaction barriers/invalidation including root continuation cleanup; payload-safe rejection; post-native-rollback rejection replacement; backend-fault mapping; and engine parity.
2505
+ - [`tests/src/server/helpers.test.ts`](../tests/src/server/helpers.test.ts) — the SQLite bridge: `quoteIdentifier`, codecs, row extraction, `deriveSQLiteIndexName` exact bytes, and the `matchesConditionExactly` / `matchesOrderExactly` / `matchesQueryExactly` / `matchesAggregateExactly` / `matchesSQLiteAffinity` predicates.
2506
+ - [`tests/src/server/compilers.test.ts`](../tests/src/server/compilers.test.ts) — the coherent SQL-emitter cluster: column/field/aggregate compilation, table/index/migration DDL, and the strict-page `QueryInput` → SQL pipeline with exact statements and parameters.
2507
+ - [`tests/src/server/inferers.test.ts`](../tests/src/server/inferers.test.ts) — `inferValueStorage`'s nested-operand storage inference: the `ColumnStorage` a `json_extract` operand must encode as, over booleans, integral and fractional numbers, bigints, objects and arrays, strings, `null`, and `undefined`.
2508
+ - [`tests/src/server/integration.test.ts`](../tests/src/server/integration.test.ts) — cross-backend behavioral parity: the same `QueryInput` set run against `MemoryDriver` and `SQLiteDriver` (both exact-path and refine-path queries) produce identical rows/counts/aggregates.
2509
+ - [`tests/src/browser/drivers/IndexedDBDriver.test.ts`](../tests/src/browser/drivers/IndexedDBDriver.test.ts) — `IndexedDBDriver` against real IndexedDB: metadata-only bootstrap and deployed-schema-first reopen, one-transaction `has`/`get` absence discrimination, fail-closed malformed-metadata preservation (including stored `undefined`) and persisted-store loss with deterministic first-loss evidence, physical non-recreation, extra-store retention, external repair, and same-driver retry; bootstrap-lifetime store/version capture and a competing versionchange proving the persisted final open is pinned and retryable; payload-safe errors and table rejection, closed failed state, reserved-store validation, atomic `add` insertion and duplicate `CONFLICT`, active/pre-dispatch/late abort boundaries, strict direct records/stream paging, native narrow-then-refine queries, snapshot capture-replay, schema/rows/metadata in one versionchange migration, metadata-only migration input, failed-upgrade reconnection to the old schema, metadata persistence, backend-fault mapping, and confirmation that callback `transaction` / native `aggregate` are absent.
2510
+ - [`tests/src/browser/helpers.test.ts`](../tests/src/browser/helpers.test.ts) — the pushdown planner: `conditionToRange` over every comparison operator, `selectPlan` index/primary selection and lossless fallbacks, backend-error mapping, and exact `deriveIndexedDBIndexName` bytes.
2511
+ - [`tests/src/browser/factories.test.ts`](../tests/src/browser/factories.test.ts) — `createIndexedDBDriver` returns a working `DriverInterface` instance (a round-trip end to end).
2512
+ - [`tests/src/browser/integration.test.ts`](../tests/src/browser/integration.test.ts) — cross-backend behavioral parity: the same `QueryInput` set run against `MemoryDriver` and `IndexedDBDriver` produce identical rows/counts, including pushdown edge cases (`below`/`to` on a secondary-indexed column, a reversed `between`).
2513
+
2514
+ ## See also
2515
+
2516
+ - [`contract.md`](contract.md) — the shape DSL and `createContract` a table is built on.
2517
+ - [`AGENTS.md`](../AGENTS.md) — the rules; see § Documentation contract.
2518
+ - [`README.md`](README.md) — the guides index.