@jarenjs/db 0.56.0 → 0.67.0

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 (79) hide show
  1. package/ARCHITECTURE.md +412 -56
  2. package/README.md +600 -57
  3. package/docs/HOSTS.md +269 -0
  4. package/docs/JOBS-FORMAT.md +293 -45
  5. package/docs/LIVE-FORMAT.md +169 -20
  6. package/docs/MIGRATION-FORMAT.md +142 -17
  7. package/docs/MODEL-FORMAT.md +752 -64
  8. package/docs/REPLICATION-FORMAT.md +208 -0
  9. package/package.json +21 -7
  10. package/schemas/jaren-model.draft-07.schema.json +224 -162
  11. package/schemas/jaren-model.schema.json +224 -162
  12. package/schemas/jaren-replication-snapshot.draft-07.schema.json +83 -0
  13. package/schemas/jaren-replication-snapshot.schema.json +83 -0
  14. package/schemas/jaren-replication.draft-07.schema.json +82 -0
  15. package/schemas/jaren-replication.schema.json +82 -0
  16. package/src/algebra.js +227 -9
  17. package/src/backup.js +161 -0
  18. package/src/cancellation.js +48 -0
  19. package/src/capture.js +230 -47
  20. package/src/cli.js +165 -59
  21. package/src/cursor.js +417 -0
  22. package/src/dag-job.js +154 -21
  23. package/src/ddl.js +102 -8
  24. package/src/dialect.js +268 -113
  25. package/src/dialects/expression-read.js +158 -0
  26. package/src/dialects/postgres.js +618 -0
  27. package/src/dialects/rtree-ddl.js +129 -0
  28. package/src/dialects/sqlite.js +244 -11
  29. package/src/document-files.js +311 -0
  30. package/src/document-steps.js +422 -0
  31. package/src/documents.js +335 -0
  32. package/src/driver.js +448 -61
  33. package/src/drivers/bun.js +37 -1
  34. package/src/drivers/indexeddb-snapshot.js +149 -0
  35. package/src/drivers/node-pool.js +11 -0
  36. package/src/drivers/node-worker-endpoint.js +105 -0
  37. package/src/drivers/node-worker.js +204 -0
  38. package/src/drivers/node.js +41 -7
  39. package/src/drivers/postgres.js +331 -0
  40. package/src/drivers/wasm-oo1.js +97 -0
  41. package/src/drivers/wasm-session.js +67 -0
  42. package/src/drivers/wasm.js +17 -83
  43. package/src/drivers/worker-pool.js +183 -0
  44. package/src/drivers/worker-protocol.js +79 -0
  45. package/src/drivers/worker-queue.js +60 -0
  46. package/src/emit.js +339 -48
  47. package/src/entity.js +20 -22
  48. package/src/errors.js +430 -19
  49. package/src/expression.js +284 -0
  50. package/src/graph.js +64 -8
  51. package/src/index.js +48 -17
  52. package/src/introspect.js +583 -0
  53. package/src/jobs.js +843 -107
  54. package/src/json-bytes.js +58 -0
  55. package/src/live-join.js +250 -0
  56. package/src/live-nested.js +120 -0
  57. package/src/live.js +18 -4
  58. package/src/logical-rows.js +90 -0
  59. package/src/maintenance.js +175 -0
  60. package/src/migrate.js +248 -181
  61. package/src/model.js +68 -0
  62. package/src/plan.js +1119 -138
  63. package/src/pragmas.js +314 -0
  64. package/src/profile.js +151 -3
  65. package/src/query.js +1634 -323
  66. package/src/replication-format.js +115 -0
  67. package/src/replication.js +332 -0
  68. package/src/residual.js +17 -0
  69. package/src/series.js +12 -4
  70. package/src/store.js +1567 -273
  71. package/src/tracker.js +203 -29
  72. package/src/udf.js +88 -7
  73. package/types/index.d.ts +1158 -27
  74. package/types/node-pool.d.ts +28 -0
  75. package/types/node-worker.d.ts +54 -0
  76. package/types/node.d.ts +69 -2
  77. package/types/postgres.d.ts +46 -0
  78. package/types/typed.d.ts +27 -4
  79. package/types/wasm.d.ts +14 -0
package/docs/HOSTS.md ADDED
@@ -0,0 +1,269 @@
1
+ # Database execution hosts
2
+
3
+ All hosts implement the same Driver and Connection contracts. Model, query,
4
+ transaction, cursor, include and capture behavior stays in the Store. Host
5
+ capabilities are observed when the connection opens. The common oracle and
6
+ lifecycle corpus is `test/db/store-hosts.test.js`; it checks values and coded
7
+ errors across Node, worker, pool, wasm sessions and wasm journal fallback.
8
+
9
+ ## Node workers
10
+
11
+ ```js
12
+ import { openStore } from '@jarenjs/db';
13
+ import { nodeWorkerDriver } from '@jarenjs/db/node-worker';
14
+ import { nodeWorkerPoolDriver } from '@jarenjs/db/node-pool';
15
+
16
+ const driver = nodeWorkerDriver({ windowRows: 64, windowBytes: 1024 * 1024 });
17
+ const store = await openStore(model, { driver, path: 'application.sqlite' });
18
+ // The ordinary Store methods and transaction scopes apply here.
19
+ await store.close();
20
+
21
+ const pool = nodeWorkerPoolDriver({ readers: 2, queueCapacity: 64, graceMs: 5000 });
22
+ const pooledStore = await openStore(model, { driver: pool, path: 'application.sqlite' });
23
+ await pooledStore.close();
24
+ ```
25
+
26
+ Each worker owns one `DatabaseSync`. Requests use structured clone, versioned
27
+ frames, opaque statement/cursor IDs and a connection generation. A cursor receives
28
+ at most its row and byte credits per frame; the endpoint may retain one lookahead
29
+ row. Credits measure JSON keys/scalars plus raw blob bytes, excluding fixed frame
30
+ metadata. One oversized row is `JD2092`. Abort takes effect at a row boundary;
31
+ `return()` awaits remote cleanup. Neither serializes a function or replays a write.
32
+
33
+ | Worker option | Default | Meaning |
34
+ |---|---:|---|
35
+ | `windowRows` | 64 | Rows per credit frame |
36
+ | `windowBytes` | 1048576 | Counted row bytes per frame |
37
+ | `maxPending` | 64 | Admitted non-cleanup requests |
38
+ | `maxStatements` | 1024 | Remote prepared statements per connection |
39
+ | `maxCursors` | 64 | Remote open cursors |
40
+ | `allMaxRows` | 100000 | Compatibility `all()` maximum rows |
41
+ | `allMaxBytes` | 16777216 | Compatibility `all()` maximum counted bytes |
42
+ | `startupTimeoutMs` | 10000 | Worker readiness deadline |
43
+ | `closeTimeoutMs` | 5000 | Cleanup acknowledgement deadline |
44
+
45
+ All worker options are positive safe integers. Cleanup reserves at most one
46
+ request per cursor plus one close beyond `maxPending`; total pending admission is
47
+ also capped. A compiled transient cursor marks its statement ephemeral, releasing
48
+ its remote ID on exhaustion or return. Cached statements remain subject to the
49
+ statement cap. `all()` drains credited frames under both limits and refuses excess;
50
+ it never receives an unbounded whole-result frame. `metrics()` on an opened driver
51
+ connection reports pending requests, frame/row high-water marks, generation and
52
+ health. It contains no SQL or parameter values.
53
+
54
+ Worker exit or restart fences every old statement, cursor and transaction with
55
+ `JD2090`. The failure is retryable only outside a transaction; this is permission
56
+ to reopen, **not evidence that an uncertain write can be retried safely**. The
57
+ connection's `restart()` returns a new connection; old Store handles stay invalid.
58
+ A failed transaction is never migrated or replayed. Queue overflow is `JD2091`,
59
+ with `retryable: true` and the observed queue `depth`. `JD2093` identifies malformed
60
+ requests; malformed responses invalidate the generation.
61
+
62
+ Close stops admission and awaits cleanup, then requests worker termination at the
63
+ deadline and rejects with `JD2090`. Node cannot preempt a synchronous native SQLite
64
+ call with V8 worker termination. The expired generation is fenced immediately,
65
+ but the native call and its locks may outlive that refusal; process exit can wait
66
+ for the native call too. A timeout therefore has an unknown write outcome, not a
67
+ promised rollback. `capabilities.cancellation.midStatement` remains false.
68
+
69
+ Worker connections declare sessions, user functions, aggregates and online backup
70
+ unavailable: journal capture provides the same logical patches, and query residuals
71
+ run on the caller. Synchronous Store methods and live queries are unavailable on
72
+ these asynchronous connections. Bun can import both subpaths; opening a Node
73
+ SQLite worker there reports the named unavailable-binding failure (`JD0003`).
74
+
75
+ ## WAL pool policy
76
+
77
+ A file pool opens exactly one writer and `readers` read-only workers, verifies WAL,
78
+ and routes explicitly classified reads to readers. Unclassified work goes to the
79
+ writer. `:memory:` uses the writer alone. A pool opened with `readOnly: true` has
80
+ only read-only workers and requires an already-WAL file. Default reader count is
81
+ two (allowed range zero through 32); the queue admits at most `queueCapacity`
82
+ waiting requests (default 64, zero allowed), in strict FIFO order. A blocked writer
83
+ at the head can leave a reader idle. Metrics report active/idle/queued, worker
84
+ health/generation/role/executions, and wait p50/p95 over the last 1024 admissions.
85
+
86
+ Compiled operation metadata supplies read classification, and SQLite itself
87
+ refuses a write on a read-only worker. Transactions pin one worker for their entire
88
+ lifetime, including nested savepoints. Normal writable Store transactions pin the
89
+ writer; a read-only opened pool pins a reader. There is no extra transport-specific
90
+ transaction API. Store root admission still serializes unrelated operations to
91
+ preserve existing ownership rules. Multiple classified Connection reads can run
92
+ concurrently; the pool does not promise parallel root Store calls.
93
+
94
+ Worker loss settles in-flight work once and discards the slot's prepared cache.
95
+ Replacement occurs only after its lease/transaction settles. Failed replacement
96
+ stops further admission with its actual error rather than leaving callers queued
97
+ forever. Close refuses queued work, grants active work `graceMs`, and closes the
98
+ workers after that grace. A commit already submitted to SQLite owns its settlement;
99
+ close waits for its acknowledgement even beyond grace, without interrupting that
100
+ writer. Native-call termination has the limitation described above.
101
+
102
+ ## Synchronous cursors and include accounting
103
+
104
+ `store.sync.entity(name).cursor(document)`, `.loadCursor(spec)` and `.page(spec,
105
+ options)` use the same plans, classification, keysets and bounds as the asynchronous
106
+ entity set. Cursors implement `Symbol.iterator`, `next`, `return` and
107
+ `Symbol.dispose`; page methods return values. Early break, consumer throw, abort,
108
+ error and Store close release the row source exactly once. A scope cannot outlive
109
+ its transaction, and root cursors acquire admission per pull.
110
+
111
+ Include text is byte-checked before decoding. JSON parsing's construction traversal
112
+ records serialized UTF-8 sizes bottom-up; nested bounds read those sizes before
113
+ attaching reconstructed children. There is no second full nested stringify.
114
+ Numbers, control escapes, paired and lone surrogates have differential coverage
115
+ against the native JSON serializer. This is a bounded decoded graph, not a streaming
116
+ JSON parser: SQLite still produces the outer include text and JSON.parse constructs
117
+ its bounded value. The measurement below records the cost of the counting traversal
118
+ and WeakMap, including its timing and heap-growth regressions.
119
+
120
+ ## Wasm capture and browser persistence
121
+
122
+ The wasm adapter probes create/attach/changeset/delete on a disposable live session.
123
+ Only success declares `sessions: true`; missing or failing bindings select journal
124
+ capture with `sessionReason`. Each changeset is copied out of wasm memory before its
125
+ native allocation is freed, so its buffer can be transferred. Every capture extent
126
+ and connection close deletes its owned session handles. Large integral JavaScript
127
+ numbers outside the safe integer range bind as doubles, avoiding the pinned oo1
128
+ binding's int64 truncation.
129
+
130
+ The data studio probes this ordered ladder independently of user agent:
131
+
132
+ | Mode | Requirements and durability | Synchronous/live |
133
+ |---|---|---|
134
+ | `opfs-sab` | COOP/COEP isolation, SharedArrayBuffer, registered VFS and write/reopen probe | Yes |
135
+ | `opfs-sahpool` | SAH-pool APIs and write/reopen probe; no isolation headers required | Yes |
136
+ | `indexeddb-snapshot` | IndexedDB atomic write/reopen probe and an exclusive Web Lock | No |
137
+ | `memory` | Last fallback; visibly non-durable, lost on reload | Yes |
138
+
139
+ A selected durable mode has an owner worker; other tabs use its channel. The UI
140
+ names the selected mode, durability and each rejected rung. The existing five boot
141
+ stages still name hung or failed work. The isolated fixture sets COOP `same-origin`
142
+ and COEP `require-corp`; this does not change production hosting headers.
143
+
144
+ `indexedDbSnapshotHandle(sqlite3, { name, indexedDB, maxBytes })` is an injected wasm
145
+ handle, not an OPFS VFS. It runs SQLite in memory and atomically replaces a versioned
146
+ IndexedDB snapshot after an autocommit write or transaction commit. A compare-and-swap
147
+ revision prevents independent connections overwriting a newer snapshot. The promise
148
+ resolves only on IndexedDB transaction completion. The default whole-database bound
149
+ is 16 MiB, checked before export and on restore. Each acknowledged write can therefore
150
+ copy the whole database: this fallback favors bounded durability over throughput.
151
+
152
+ Quota, revision conflict or persistence failure preserves the previously committed
153
+ snapshot and invalidates the in-memory connection (`JD2094`, non-retryable). Reopen
154
+ reads the last committed version. Interrupted replacements never publish partial
155
+ bytes. Read-only opens refuse writes. Live maintenance is explicitly unavailable
156
+ because commit acknowledgements are asynchronous; the studio Store pane refreshes
157
+ after writes. `openSnapshotStorage` exposes the versioned storage primitive for
158
+ hosts that need explicit snapshot cleanup.
159
+
160
+ ## Measurements and reproducibility
161
+
162
+ <!--fact:db.hosts-->
163
+
164
+ Measured 2026-09-08, v24.19.0, AMD Ryzen 9 5900HX with Radeon Graphics; 7 samples per latency/include case.
165
+
166
+ | Host | Open p50 ms | Slow SQL p50 ms | Event-loop max ms | Tiny reads/s | Mixed work ms | Wait p95 ms |
167
+ |---|---:|---:|---:|---:|---:|---:|
168
+ | node | 0.19 | 87.04 | 92.34 | 876870 | 207.12 | 0.00 |
169
+ | worker | 52.99 | 85.23 | 3.52 | 19168 | 198.57 | 0.00 |
170
+ | pool-1-reader | 76.73 | 81.95 | 1.62 | 25109 | 207.11 | 190.32 |
171
+ | pool-3-readers | 152.95 | 82.50 | 1.86 | 26201 | 74.79 | 63.91 |
172
+
173
+ | Host | Cursor rows | Cursor ms | Sampled heap growth MiB | Sampled total RSS MiB |
174
+ |---|---:|---:|---:|---:|
175
+ | node | 100000 | 113.84 | 8.39 | 70.26 |
176
+ | worker | 100000 | 316.64 | 5.60 | 100.64 |
177
+ | pool-1-reader | 100000 | 295.61 | 8.45 | 124.79 |
178
+ | pool-3-readers | 100000 | 287.91 | 13.29 | 168.73 |
179
+
180
+ | Include accounting | Encoded bytes | Time p50 ms | Uncollected heap growth p50 MiB |
181
+ |---|---:|---:|---:|
182
+ | serialize-again | 3218891 | 12.38 | 8.00 |
183
+ | count-during-decode | 3218891 | 21.36 | 8.84 |
184
+
185
+ The worker event-loop acceptance bound is 50 ms. The original in-process baseline measured p50/p95/max event-loop delay of 1.07/86.97/87.62 ms, bare worker startup p50 27.48 ms, and duplicate include serialization 14.68 ms with 8.00 MiB uncollected heap growth. The current open measurement also includes driver probing; its startup cost is broader than that bare-worker baseline.
186
+
187
+ <!--/fact-->
188
+
189
+ The tiny workload is sequential `SELECT 7`; the mixed workload is 24 classified
190
+ reads with six interspersed writes. RSS includes worker heaps. Heap-growth figures
191
+ are uncollected allocations sampled in this process, not retained memory and not
192
+ portable heap limits. Cursor sampling occurs every 1024 rows. The final runner
193
+ measures all hosts on the same file-based workload. Timing, memory and throughput
194
+ losses are kept beside event-loop gains; these are observations, not speed promises.
195
+
196
+ Run `node --expose-gc benchmark/store-hosts.js` for the current measurements.
197
+ `benchmark/store-hosts-baseline.js` reproduces the separate initial recipe; do not
198
+ replace the baseline when measuring a change. The JSON records include runtime,
199
+ host, SQL and sample count. `npm run docs:derive` bakes these tables;
200
+ `npm run docs:check` rejects drift.
201
+
202
+ <!--fact:db.browserHosts-->
203
+
204
+ 15 storage scenarios passed without skips, measured 2026-09-08.
205
+
206
+ | Engine | Version | Isolated | Ordinary | OPFS denied | All storage denied / quota |
207
+ |---|---|---|---|---|---|
208
+ | chromium | 149.0.7827.55 | opfs-sab | opfs-sahpool | indexeddb-snapshot | memory (non-durable) |
209
+ | firefox | 151.0 | opfs-sab | opfs-sahpool | indexeddb-snapshot | memory (non-durable) |
210
+ | webkit | 26.5 | indexeddb-snapshot | indexeddb-snapshot | indexeddb-snapshot | memory (non-durable) |
211
+
212
+ webkit isolated fallback: JD2061 — opfs-sab: the SharedArrayBuffer OPFS VFS is not registered; opfs-sahpool: Missing required OPFS APIs.. Selected indexeddb-snapshot (persistent).
213
+
214
+ <!--/fact-->
215
+
216
+ Build the website, run `storage.spec.js` with Playwright's JSON reporter, then pass
217
+ the report to `node benchmark/store-hosts-browser.js /tmp/storehosts-browser.json`.
218
+ The committed JSON includes the exact recipe. On a host lacking WebKit libraries,
219
+ run the browser command in the Ubuntu Playwright container as the workspace user.
220
+ The matrix tests actual write/reopen, aborted snapshot replacement, denied storage
221
+ and quota failure. Both initial capability observations and the final storage
222
+ results are retained under `benchmark/store-hosts-browser-*.json`.
223
+
224
+ ## Scoped quirk review
225
+
226
+ Confirmed and fixed in the host path:
227
+
228
+ - Cursor abort/open races and late rows after return now release the eventual
229
+ source exactly once; asynchronous failure/disposal waits for cleanup.
230
+ - Lost transaction generations unwind only when each owning rollback settles;
231
+ failed commits retain their lease for rollback. Replacement failure stops admission
232
+ with its actual error.
233
+ - Connection close releases active iterators. Remote shutdown starts its deadline
234
+ before waiting for a native row; closed pools report no healthy or active worker.
235
+ - A failed session allocation no longer leaves capture depth open for the next
236
+ write. Wasm session bytes have independent ownership and deterministic deletion.
237
+ - The pinned oo1 numeric binder truncates huge integral Numbers through int64;
238
+ the adapter binds these as doubles, proven by the shared adversarial oracle.
239
+ - IndexedDB synchronous put failures and blocked opens settle and clean up;
240
+ failed replacement preserves the previous committed bytes and invalidates the
241
+ in-memory state. Browser tests prove the same behavior in actual storage.
242
+ - The pinned wasm build removes its private OPFS helper after initialization;
243
+ the SAB path now removes closed files through the public storage API.
244
+ - Storage selection can finish before the Store opens. Query and insert controls
245
+ now wait for the open acknowledgement, preserving input during slower IndexedDB
246
+ boots. Browser assertions use stored rows when live maintenance is unavailable.
247
+ - The requested generation-code slot was already assigned to seek-anchor typing.
248
+ Generation fencing uses the fresh `JD2090` code and preserves `JD2086`.
249
+
250
+ Confirmed beside this path, left unchanged:
251
+
252
+ - Root Store admission intentionally serializes unrelated operations; concurrent
253
+ classified Connection reads are the pool's current parallelism boundary.
254
+ - V8 termination cannot preempt native SQLite execution. A true statement interrupt
255
+ would need binding support; shutdown documents the unknown outcome explicitly.
256
+ - Asynchronous live maintenance remains unavailable; IndexedDB and Node workers
257
+ expose that capability limitation instead of returning stale live results.
258
+
259
+ Checked and dropped:
260
+
261
+ - Browser database work already runs outside the UI thread; adding another Store
262
+ or another query/transaction API was unnecessary.
263
+ - Build flags alone do not establish session support, and isolation headers alone
264
+ do not establish a working VFS. Disposable live probes decide both.
265
+ - A second include serialization was unnecessary for exact Unicode byte bounds;
266
+ the differential counter and deliberate off-by-one mutation detected disagreement.
267
+ - Read-only WAL workers do not accept writes, nested scopes do not migrate,
268
+ transient cursors do not exhaust the statement cap, and failed writes are not
269
+ automatically replayed. The lifecycle and fault corpus exercises each boundary.