@jarenjs/db 0.84.3 → 0.86.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 (51) hide show
  1. package/ARCHITECTURE.md +27 -3
  2. package/README.md +35 -14
  3. package/docs/HOSTS.md +131 -4
  4. package/docs/MODEL-FORMAT.md +15 -4
  5. package/docs/NATIVE-PLANS.md +24 -7
  6. package/docs/SQLITE-RELATIONAL.md +190 -0
  7. package/package.json +24 -4
  8. package/schemas/jaren-model.authoring.schema.json +157 -0
  9. package/schemas/jaren-model.draft-07.schema.json +157 -0
  10. package/schemas/jaren-model.schema.json +157 -0
  11. package/src/capture.js +4 -2
  12. package/src/ddl.js +8 -3
  13. package/src/dialects/sqlite-relational.js +314 -0
  14. package/src/dialects/sqlite-schema.js +142 -0
  15. package/src/dialects/sqlite.js +17 -0
  16. package/src/driver.js +3 -0
  17. package/src/drivers/bun.js +26 -15
  18. package/src/drivers/node-process-endpoint.js +13 -0
  19. package/src/drivers/node-process.js +177 -0
  20. package/src/drivers/node-worker-endpoint.js +3 -103
  21. package/src/drivers/node-worker.js +6 -178
  22. package/src/drivers/node.js +3 -0
  23. package/src/drivers/snapshot.js +49 -0
  24. package/src/drivers/sqlite-endpoint.js +113 -0
  25. package/src/drivers/worker-client.js +185 -0
  26. package/src/drivers/worker-protocol.js +15 -0
  27. package/src/emit.js +37 -5
  28. package/src/engine-metadata.js +18 -0
  29. package/src/errors.js +1 -0
  30. package/src/index.js +4 -1
  31. package/src/introspect.js +1 -2
  32. package/src/jobs.js +4 -2
  33. package/src/migrate.js +12 -16
  34. package/src/model-api.js +4 -0
  35. package/src/model.js +12 -0
  36. package/src/mutation.js +66 -14
  37. package/src/physical.js +37 -7
  38. package/src/plan.js +66 -3
  39. package/src/query-api.js +5 -0
  40. package/src/query.js +39 -6
  41. package/src/relational-api.js +6 -0
  42. package/src/store.js +6 -4
  43. package/src/table-migration.js +158 -0
  44. package/types/bun.d.ts +3 -0
  45. package/types/entity.d.ts +1 -0
  46. package/types/index.d.ts +11 -2
  47. package/types/model.d.ts +1 -0
  48. package/types/node-process.d.ts +33 -0
  49. package/types/node.d.ts +3 -0
  50. package/types/query.d.ts +2 -0
  51. package/types/relational.d.ts +114 -0
package/ARCHITECTURE.md CHANGED
@@ -1167,8 +1167,10 @@ calls with different bound values.
1167
1167
  `physical.js` compiles column codecs and verifies declarations against that
1168
1168
  inventory. The existing entity core, tracker and graph row merger execute both
1169
1169
  hybrid and column layouts; there is no separate relational store. The query
1170
- planner reports decoded evaluation for physical codecs, and refuses physical
1171
- keyset continuation until its identity semantics are qualified.
1170
+ planner preserves codec semantics through qualified native plans and explicit
1171
+ residuals, and refuses physical keyset continuation until its identity semantics
1172
+ are qualified. Present-null scalar predicates and safe fluent projection
1173
+ flattening join this native subset; strict binding failures stop before a scan.
1172
1174
 
1173
1175
  `sql.js` binds trusted statements to `store.js` transaction views. It reuses the
1174
1176
  read classifier's tokenizer and the driver's scope owner. Writes invalidate all
@@ -1176,6 +1178,28 @@ clean tracked entities; pending edits and incomplete capture populations refuse.
1176
1178
  `invariants.js` uses the shared Query compiler for store rules; the dialect lowers
1177
1179
  a bounded database subset into ordered trigger bodies. `migrate.js` reuses its
1178
1180
  existing rebuild/receipt transaction and verifies preservation before publication.
1179
- The backup publisher remains shared by Node online and Bun serialized snapshots.
1181
+ The backup publisher uses Node online backup and Bun disk-backed VACUUM INTO.
1182
+ The standalone snapshot helper shares destination reservation and cleanup across
1183
+ both hosts, including transaction failures.
1180
1184
 
1181
1185
  `src/search.js` composes core lexical mechanics and the JSON predicate compiler over complete bounded entity snapshots. Committed capture and data-version checks invalidate derived state; SHA-256 source content validates persisted caches across reopen. Snapshot storage uses existing collection transactions. See [search execution](docs/SEARCH.md); native full-text dialects remain unqualified.
1186
+
1187
+ `dialects/sqlite-relational.js` owns the explicit SQLite expression and statement
1188
+ compiler; `dialects/sqlite-schema.js` reuses it for ordered tables, indexes and
1189
+ triggers. `table-migration.js` owns reviewed source/target guards, native copying
1190
+ and preservation checks, with catalog/pragma spellings in the SQLite dialect.
1191
+ The supplied driver owns transactions and cursors. These programs are explicitly
1192
+ SQLite-semantic and never enter the JSON residual evaluator. See
1193
+ [the native SQLite contract](docs/SQLITE-RELATIONAL.md).
1194
+
1195
+ `engine-metadata.js` holds inert version/table constants so schema inspection
1196
+ does not import runtime owners. `/query`, `/model` and `/entity` expose those
1197
+ mechanisms without the root store import. `compileEntityModel` performs one model
1198
+ normalization for both entities and mapping; openStore reuses that result.
1199
+
1200
+ `drivers/worker-client.js` and `drivers/sqlite-endpoint.js` own the shared bounded
1201
+ RPC contract. Thread and process entries supply their transports. The process
1202
+ driver retains owner credits until OS exit, fences lost generations, and combines
1203
+ observed native transaction state with pending statement metadata to distinguish
1204
+ rollback from an unknown commit. Supervision never serializes callbacks or replays
1205
+ transactions; Store ownership remains in the existing driver/Store gates.
package/README.md CHANGED
@@ -888,7 +888,8 @@ oracle run on both. What differs is declared rather than discovered:
888
888
  | `ALTER TABLE` | additive only | full, so a rebuild is never needed |
889
889
 
890
890
  Neither backend has a **statement timeout** (`capabilities.statementTimeout`
891
- is `false` on both) or **row estimates**. There is no replication.
891
+ is `false` on both) or **row estimates**. Portable replication uses SQLite
892
+ change capture; PostgreSQL capture and replication remain unqualified.
892
893
  Cross-process concurrency on SQLite is its own story — WAL plus a busy
893
894
  timeout, both set and visible on `store.capabilities`; on PostgreSQL it
894
895
  is the server's, and a serialization failure or a deadlock arrives as
@@ -1211,16 +1212,18 @@ same report rows — with the one difference the PostgreSQL mapping
1211
1212
  states: `numeric` carries both JSON number types, so an `integer`
1212
1213
  member reads back as `number`.
1213
1214
 
1214
- ## Sync-readiness — what exists and what does not
1215
+ ## Replication and external-write boundaries
1215
1216
 
1216
- The change stream is an ordered log of RFC 6902 patches with a
1217
- monotonic sequence, and SQLite's own changeset/conflict primitives are
1218
- available which is what a replication protocol would be *built
1219
- from*. **No replication is shipped.** There is no conflict resolution,
1220
- no site identity, no causal ordering across writers, and no capture of
1221
- writes made by another connection (the coarse `dataVersion()` signal
1222
- is the honest mitigation, not a pretend fine-grained one). Building
1223
- replication on these primitives is a roadmap item, not a hint.
1217
+ The portable replication engine supplies replica identities, causal frontiers,
1218
+ bounded logical envelopes, durable replay receipts, conflict evidence and snapshot
1219
+ resets. Data and acknowledgements commit together; hosts supply transport and any
1220
+ conflict resolver. The default preserves both contenders and rejects the write.
1221
+ See [REPLICATION-FORMAT](docs/REPLICATION-FORMAT.md) for the current contract.
1222
+
1223
+ Capture observes participating Store writes on supported SQLite hosts. Arbitrary
1224
+ writes from another connection are not fine-grained capture events; `dataVersion()`
1225
+ provides coarse invalidation. Adopted triggers, unsupported layouts and PostgreSQL
1226
+ retain the explicit capture and replication qualification limits.
1224
1227
 
1225
1228
  ## Operating a store — configuration, maintenance, backup, cancellation, the queue
1226
1229
 
@@ -1311,10 +1314,11 @@ its side-effect-free status read are in
1311
1314
 
1312
1315
  ## What this is not — every non-claim in one place
1313
1316
 
1314
- - **SQLite only.** One backend (3.45+); the dialect seam is tested
1315
- against a double but no second dialect ships. No server.
1316
- - **No replication or sync engine** (see above). No cross-connection
1317
- change capture another connection's writes are invisible locally.
1317
+ - **SQLite and PostgreSQL have different capabilities.** Both backends ship;
1318
+ PostgreSQL does not provide the SQLite capture, replication, job queue or pragma
1319
+ maintenance capabilities. See the dialect table and normative host contracts.
1320
+ - **Replication transport is supplied by the host.** Portable logical replication
1321
+ ships; arbitrary external writes are not captured as local row events.
1318
1322
  - **No statement timeout** on SQLite (the drivers expose no interrupt;
1319
1323
  the capability slot is honestly `false`), no row estimates.
1320
1324
  - **Not safe for mutually hostile tenants** without the profile's
@@ -1359,6 +1363,7 @@ Every subpath a consumer can import, derived from the manifest by
1359
1363
  <!--fact:exports.db-->
1360
1364
  | Import | Kind | Declarations |
1361
1365
  |---|---|---|
1366
+ | `@jarenjs/db/node-process` | JavaScript | declared |
1362
1367
  | `@jarenjs/db/search` | JavaScript | declared |
1363
1368
  | `@jarenjs/db` | JavaScript | declared |
1364
1369
  | `@jarenjs/db/node` | JavaScript | declared |
@@ -1379,6 +1384,10 @@ Every subpath a consumer can import, derived from the manifest by
1379
1384
  | `@jarenjs/db/package.json` | metadata | — |
1380
1385
  | `@jarenjs/db/node-worker` | JavaScript | declared |
1381
1386
  | `@jarenjs/db/node-pool` | JavaScript | declared |
1387
+ | `@jarenjs/db/relational` | JavaScript | declared |
1388
+ | `@jarenjs/db/query` | JavaScript | declared |
1389
+ | `@jarenjs/db/model` | JavaScript | declared |
1390
+ | `@jarenjs/db/entity` | JavaScript | declared |
1382
1391
  <!--/fact-->
1383
1392
 
1384
1393
 
@@ -1431,3 +1440,15 @@ SQLite constraint/audit triggers for installation through that migration boundar
1431
1440
  Native column reads and bounded mutation documents are specified in [NATIVE-PLANS](docs/NATIVE-PLANS.md), including SQL census coverage, resource accounting and refusals.
1432
1441
 
1433
1442
  `@jarenjs/db/search` composes the resident ranker with bounded authoritative entity reads and optional atomic snapshot storage. See [persisted search](docs/SEARCH.md).
1443
+
1444
+ Column-first table definitions, guarded same-connection rebuilds, exact matched writes,
1445
+ partial conflicts, raw-text JSON queries and byte-valued operations use
1446
+ [`@jarenjs/db/relational`](docs/SQLITE-RELATIONAL.md). Both host entries export
1447
+ `snapshotDatabase(connection, newPath)` for a disk-backed committed-WAL copy.
1448
+ Existing-connection consumers can import `/query`, `/model` and `/entity`;
1449
+ `compileEntityModel` shares one normalization between queries and mapping.
1450
+
1451
+ `@jarenjs/db/node-process` adds supervised native execution with finite owner
1452
+ admission, caller deadlines, generation fencing and separate process-exit and
1453
+ transaction-fate observations. See [execution hosts](docs/HOSTS.md#supervised-node-processes)
1454
+ for Store composition, receipt reconciliation and operating-system limits.
package/docs/HOSTS.md CHANGED
@@ -77,6 +77,132 @@ run on the caller. Synchronous Store methods and live queries are unavailable on
77
77
  these asynchronous connections. Bun can import both subpaths; opening a Node
78
78
  SQLite worker there reports the named unavailable-binding failure (`JD0003`).
79
79
 
80
+ ## Supervised Node processes
81
+
82
+ `nodeProcessDriver` from `@jarenjs/db/node-process` implements the same asynchronous
83
+ Driver and Connection contracts using one Node child process per connection. It
84
+ shares the worker protocol, cursor credits, transaction scopes and error handling
85
+ with the thread driver. SQLite and its native calls execute in the child; callbacks
86
+ and decoded query residuals still execute in the parent. No function is serialized.
87
+ Node.js 24 or newer is required. Bun can import the entry but `open` refuses with
88
+ `JD0003`. The native-call and process-lifecycle fixtures qualify Node 24.20.0 on
89
+ Linux; other supported operating systems require their own timing qualification.
90
+
91
+ ```js
92
+ import { nodeProcessDriver } from '@jarenjs/db/node-process';
93
+
94
+ const driver = nodeProcessDriver({ maxOwners: 2, timeoutMs: 250 });
95
+ const owner = await driver.open('application.sqlite', { timeout: 50, queueTimeout: 100 });
96
+ try {
97
+ const result = await owner.supervise(async (connection) => {
98
+ return connection.transaction(async (scope) => {
99
+ const statement = await scope.prepare('SELECT value FROM settings WHERE key = ?');
100
+ return statement.get(['theme']);
101
+ });
102
+ }, { signal: requestSignal, timeoutMs: 250 });
103
+ useResult(result);
104
+ } catch (error) {
105
+ if (error.code === 'JD2097' || error.code === 'JD2090') {
106
+ const observation = owner.settlement();
107
+ // This snapshot can still say quarantined / safeToReplace:false.
108
+ recordOwnerObservation(observation);
109
+ const exited = await owner.settled();
110
+ // Before repeating an uncertain write, inspect its durable receipt.
111
+ reconcileBeforeRetry(exited);
112
+ } else throw error;
113
+ } finally {
114
+ // Preserve the original operation outcome if closing a fenced generation fails.
115
+ await owner.close().catch(recordCleanupFailure);
116
+ }
117
+ ```
118
+
119
+ `supervise(body,{signal,timeoutMs})` admits one supervised callback per connection.
120
+ An overlapping callback refuses with `JD2091`; an already-aborted call runs no
121
+ callback and leaves the owner healthy. A deadline or abort rejects with `JD2097`,
122
+ fences all pending calls and signals process termination. Later operations on the
123
+ old generation fail with `JD2090`. A normal callback failure propagates unchanged
124
+ and does not terminate a healthy owner. `cancel(reason)` explicitly fences the
125
+ whole connection and returns its cancellation error. It affects every call sharing
126
+ that owner, so independent request lifetimes should use separate owners.
127
+
128
+ This is response cancellation and owner termination, not a SQLite statement
129
+ interrupt. `capabilities.process` and `ownerTermination` are true, while
130
+ `capabilities.cancellation.midStatement` remains false. The supervision timer cannot
131
+ preempt synchronous parent JavaScript or bound operating-system scheduling. Keep
132
+ callbacks/residuals bounded and await all admitted work. Native fixtures use a
133
+ short deadline and independently enforce a one-second caller-response ceiling;
134
+ that tested ceiling is not a universal scheduling guarantee.
135
+
136
+ `settlement()` reports the generation, PID, health, transaction fate and
137
+ `safeToReplace`. `settled()` resolves only after the child's OS exit event. An
138
+ owner awaiting termination is quarantined and still consumes its credit. If the
139
+ OS cannot settle a process promptly, the response can finish while the owner
140
+ remains quarantined; neither capacity nor writer ownership is recycled. `restart()`
141
+ waits for confirmed exit and returns a new generation; old Store handles remain
142
+ invalid. No statement, transaction or callback is automatically replayed.
143
+
144
+ The transaction observation is conservative:
145
+
146
+ - A live, acknowledged native transaction is `active`.
147
+ - An acknowledged single-statement completion is `committed`, or `rolled-back`
148
+ after rollback. A multi-statement program with no open transaction remains
149
+ `unknown`: an acknowledgement alone cannot distinguish its internal boundaries.
150
+ - Confirmed process death rolls back an observed open transaction only when no
151
+ pending operation could have committed it.
152
+ - A lost commit reply, multi-statement program or uncertain autocommit mutation
153
+ is `unknown`. Native transaction-state support is observed, never inferred from
154
+ a close promise. Reopen, verify integrity and reconcile a durable receipt before
155
+ retrying a write with an unknown outcome.
156
+
157
+ Autocommit mutation cursors remain `unknown` until their final native frame is
158
+ acknowledged. Creating an iterator or receiving an intermediate `RETURNING` row
159
+ does not acknowledge completion; early cursor return still requires reconciliation.
160
+
161
+ Driver `metrics()` reports capacity, owners, healthy owners and quarantine. A
162
+ file path already owned by the same driver cannot be opened again until exit;
163
+ this reservation uses its resolved path spelling, not filesystem inode identity.
164
+ Different driver instances, aliases and external processes remain the host's
165
+ coordination responsibility. Use one supervisor per ownership domain and retain
166
+ finite SQLite busy and transaction-queue timeouts. The host's service manager must
167
+ also own its process group: an uncatchable parent-process death is not a promise
168
+ that JavaScript can reap every descendant. Child crashes and restart are covered
169
+ by the file/receipt fixtures; power loss and service-manager policy are separate.
170
+
171
+ The ordinary Store composition uses `openStore(model,{driver,path})`. To supervise
172
+ Store calls, retain the connection in a small driver wrapper's `open` method, then
173
+ call `owner.supervise(() => store.transaction(body), options)`. This uses the same
174
+ Store and transaction APIs. After owner loss, close the invalid Store, await owner
175
+ exit and explicitly reopen/reconcile. A failed-generation `close()` may reject;
176
+ its rejection never substitutes for the separate exit observation.
177
+
178
+ All options below are positive safe integers. Row/byte/statement/cursor limits
179
+ retain the thread worker meanings. `maxOwners` includes startup and quarantine;
180
+ `timeoutMs` is the default supervised response deadline. `maxRequestBytes` bounds
181
+ each outgoing request before IPC; parameters must be SQLite scalars or byte arrays.
182
+ The process close deadline
183
+ and startup deadline can reject while termination is still pending. The reserved
184
+ owner credit remains until exit. Direct calls without `supervise`
185
+ retain the ordinary row, queue and step cancellation boundaries.
186
+
187
+ <!--fact:db.execution-options-->
188
+
189
+ | Option | Thread worker | Process owner |
190
+ |---|---:|---:|
191
+ | `windowRows` | 64 | 64 |
192
+ | `windowBytes` | 1048576 | 1048576 |
193
+ | `maxPending` | 64 | 64 |
194
+ | `maxStatements` | 1024 | 1024 |
195
+ | `maxCursors` | 64 | 64 |
196
+ | `allMaxRows` | 100000 | 100000 |
197
+ | `allMaxBytes` | 16777216 | 16777216 |
198
+ | `closeTimeoutMs` | 5000 | 1000 |
199
+ | `startupTimeoutMs` | 10000 | 10000 |
200
+ | `maxOwners` | — | 4 |
201
+ | `timeoutMs` | — | 250 |
202
+ | `maxRequestBytes` | — | 1048576 |
203
+
204
+ <!--/fact-->
205
+
80
206
  ## WAL pool policy
81
207
 
82
208
  A file pool opens exactly one writer and `readers` read-only workers, verifies WAL,
@@ -283,9 +409,10 @@ sync twin. Physical adoption on PostgreSQL refuses pending a separate mapping
283
409
  and codec qualification. Unknown application-trigger effects do not qualify
284
410
  capture or replication; those combinations refuse before an adoption claim.
285
411
 
286
- Node backups use the built-in online snapshot. Bun uses `Database.serialize()`
287
- under the store gate, writes and flushes a sibling temporary, then uses the shared
288
- atomic publisher. Both include committed WAL. Bun's snapshot holds the whole
289
- image in memory and cancellation takes effect between phases. Process-kill tests
412
+ Node backups use the built-in online snapshot. Bun uses disk-backed `VACUUM INTO`
413
+ under the store gate, flushes a sibling temporary, then uses the shared atomic
414
+ publisher. Both include committed WAL. Bun allocates no whole-database JavaScript
415
+ image; native SQLite caches and temporary storage govern working memory, and
416
+ cancellation takes effect between phases. Process-kill tests
290
417
  cover rebuild copy, table drop, commit and backup publication on both hosts;
291
418
  these tests do not establish power-loss durability or native executable packaging.
@@ -1101,6 +1101,7 @@ error.
1101
1101
  | `JD2094` | invalid or uncommitted durable snapshot; reopen the last committed version |
1102
1102
  | `JD2095` | trusted SQL or synchronous callback authority refused |
1103
1103
  | `JD2096` | persistence invariant rejected the mutation; constraint class |
1104
+ | `JD2097` | supervised native operation cancelled or past its response deadline; owner exit and transaction fate are reported separately |
1104
1105
  | `JD0060` | a replication envelope or snapshot is invalid |
1105
1106
  | `JD2100` | a replica sequence or causal dependency has a gap |
1106
1107
  | `JD2101` | an envelope identity names different content or an unknown local origin |
@@ -2352,7 +2353,9 @@ relation navigation across physical layouts is not qualified.
2352
2353
  | `decimal` | exact decimal string, including trailing zeros | TEXT |
2353
2354
  | `blob-hex` | lowercase hexadecimal string | BLOB |
2354
2355
 
2355
- Unsafe narrowing refuses `JD2003`. A byte handle never enters the public entity.
2356
+ Unsafe narrowing refuses `JD2003`. A byte handle never enters the JSON-facing entity;
2357
+ [the native SQLite channel](SQLITE-RELATIONAL.md#exact-writes-and-bytes) preserves
2358
+ Uint8Array/Buffer values on the same synchronous connection.
2356
2359
  `null: "null"` maps SQL NULL to present JSON null; `"absent"` omits the property;
2357
2360
  `"reject"` refuses it. With the JSON codec, JSON null is stored as the text `null`,
2358
2361
  so SQL NULL can independently mean absence. `default: "database"` omits an absent
@@ -2361,9 +2364,10 @@ writes to the database. A generated integer identity uses the existing
2361
2364
  `x-entity.default: "auto"` declaration. Direct updates with identical values and
2362
2365
  identical tracked saves produce no effective writes.
2363
2366
 
2364
- Mapped query documents execute through the existing decoded-row evaluator;
2365
- explanations report this residual and strict pushdown refuses it. Scalar graph
2366
- loads use mapped names; complex codec predicates require query documents.
2367
+ Mapped query documents use the qualified native subset in [NATIVE-PLANS](NATIVE-PLANS.md).
2368
+ Present-null scalar equality/ranges preserve Jaren semantics. Unsupported codecs
2369
+ and shapes retain explicit residuals; strict mode also rejects unsupported bound
2370
+ externals before fetching rows. Scalar graph loads use mapped names.
2367
2371
  Physical `page` and `after` continuation refuse until codec-aware keysets are
2368
2372
  qualified; an explicit `take`/`skip` load remains available.
2369
2373
  Capture/live/replication for adopted application triggers is not qualified and
@@ -2414,3 +2418,10 @@ must stay in the safe-number range.
2414
2418
  ## Native column mutation documents
2415
2419
 
2416
2420
  Asynchronous entity sets expose `mutate(document)` for conditional updates, conflict-aware upserts and bounded same-entity insert-select. The closed grammar, no-op/revision behavior, output bounds and transactional qualifications are specified in [NATIVE-PLANS](NATIVE-PLANS.md).
2421
+
2422
+ Explicit physical DDL metadata and live-schema migration planning are specified
2423
+ in [SQLITE-RELATIONAL](SQLITE-RELATIONAL.md#physical-schema-ownership). Opening a
2424
+ physical entity retains adoption-only behavior. Public plans create complete
2425
+ ordered declarations; an incomplete generated/default definition or view remains
2426
+ adoption metadata. SQLite expressions preserve a separate, explicit semantic
2427
+ contract for raw text, bytes, nulls, collations and floating totals.
@@ -49,6 +49,13 @@ Floating sums/averages and date/datetime grouping remain residual. Physical text
49
49
  comparisons explicitly use codepoint collation, independent of a column's declared
50
50
  collation; an incompatible index may therefore stop helping that query.
51
51
 
52
+ Safe identity/object projection chains, including a projected join followed by
53
+ filtering and ordering, flatten without crossing windows, grouping or inner
54
+ ordering. Standalone min/max over supported scalar columns and sum/avg over safe
55
+ integer columns now lower natively, preserving empty-sequence and present-null
56
+ errors. Missing or unbindable external values refuse strict execution and both
57
+ cursor APIs before any decoded fetch; a nullable slot accepts explicit null.
58
+
52
59
  `explain` reports the emitted SQL, scan narrative, profile bounds and last
53
60
  execution's admitted statement/returned-row/serialized-wire-byte counts. These
54
61
  are application admission costs. SQLite does not expose visited-row counts or
@@ -73,12 +80,13 @@ const result = await store.entity('Inventory').mutate({
73
80
  });
74
81
  ```
75
82
 
76
- The three operations are:
83
+ The operations are:
77
84
 
78
85
  | Operation | Required document members | Meaning |
79
86
  |---|---|---|
80
- | `update` | `key`, `set`; `expectedRevision` for a versioned entity | Complete primary key and revision predicate; writes only changed columns and increments the revision once. Missing/stale/identical rows yield zero affected rows. |
81
- | `upsert` | `values`, `conflict`, `update` | Insert or update the named supplied members only if their stored values differ. `conflict` is the complete ordered primary key. |
87
+ | `update` | `key` and/or `where`, `set` and/or `expressions`; `expectedRevision` for a versioned entity | Exact assignments with an atomic predicate. Default changed reporting suppresses identical writes; `reporting: 'matched'` executes them. A declared version increments once on a write. |
88
+ | `upsert` | `values`, `conflict`; `update` or `onConflict: 'nothing'` | Ordered logical conflict columns may name a non-primary UNIQUE identity; `conflictWhere` matches a partial index. Default changed reporting suppresses identical updates. |
89
+ | `delete` | `key` and/or `where`; `expectedRevision` for a versioned entity | One conditional or bulk delete, with the same transactional output bounds. |
82
90
  | `insert-select` | `source`, `where`, `select`, `conflict`, `onConflict: 'nothing'` | Same-entity scalar/literal projection, bounded source rows, conflict-ignore insertion. Source and target path codecs and NULL policies must match. |
83
91
 
84
92
  `returning` is a nonempty list of logical stored members (default: all). `maxRows`
@@ -92,14 +100,23 @@ before insertion, even if all rows would conflict. Output bounds and codec/schem
92
100
  validation run inside the transaction; failure rolls back rows and trigger effects.
93
101
  The byte bound checks decoded output, not a database allocation interrupt.
94
102
  The statement's `RETURNING` view follows SQLite timing; later AFTER-trigger
95
- modifications are not an extra readback. Same-input replay yields no effective
96
- write, no revision increment and no additional trigger effects.
103
+ modifications are not an extra readback. Changed-reporting same-input literal replay yields no effective write, revision
104
+ increment or additional trigger effects. Matched reporting and arithmetic
105
+ expressions intentionally may write on repeat.
97
106
 
98
107
  Mutations are untracked. Re-read affected rows before subsequent tracked editing;
99
108
  a previously tracked revision remains stale and retains normal conflict checks.
100
109
  Use `tx.entity(name).mutate(document)` to compose writes, receipts and jobs in one
101
- transaction. An outer failure rolls everything back. No synchronous `mutate`
102
- facade or generic bulk mutation expression language is declared.
110
+ transaction. An outer failure rolls everything back. `entityCore` on a synchronous
111
+ connection also settles these mutations synchronously. `where` accepts a native
112
+ Jaren predicate or structural `sql` expression; `expressions` maps exact logical
113
+ assignment names to `sql` expressions, with logical columns resolved to physical
114
+ names. A member cannot appear in both `set` and `expressions`.
115
+
116
+ For cross-table insert-select, expression conflict updates, raw JSON text, bytes
117
+ and complete SQLite null/collation semantics use the explicit
118
+ [native SQLite surface](SQLITE-RELATIONAL.md). It shares expression emission with
119
+ entity mutations while retaining its own native SQL result contract.
103
120
 
104
121
  ## Bounded ranges and live qualification
105
122
 
@@ -0,0 +1,190 @@
1
+ # Native SQLite programs on an existing connection
2
+
3
+ `@jarenjs/db/relational` provides structural SQL authoring, physical table DDL,
4
+ and guarded migrations. `relational(connection)` requires an available
5
+ synchronous SQLite connection from `@jarenjs/db/node` or `@jarenjs/db/bun`.
6
+ It opens no store and uses the supplied connection and transaction. Expressions
7
+ are closed `$sql` nodes built by `sql`; strings are bound values, never SQL
8
+ fragments. Identifiers are quoted as single names. `planRelational` returns
9
+ `{ sql, params, access }` for review without reading the database.
10
+
11
+ ## Choose the expression semantics
12
+
13
+ | Surface | Null, text and numeric semantics | Execution |
14
+ |---|---|---|
15
+ | LINQ / JSON query over mapped entities | Jaren equality, codepoint text comparison, declared codecs; present null remains null | Qualified native plan; `strict: true` refuses any required residual, including unsupported external bindings |
16
+ | `sql` expressions and `relational` | SQLite three-valued logic, native affinities, explicit BINARY/NOCASE/RTRIM, native aggregates | One native statement; no decoded-row fallback |
17
+
18
+ For example, Jaren equality regards two present nulls as equal, while SQLite
19
+ `=` yields SQL NULL and `IS` supplies a null-safe comparison. Jaren sum over an
20
+ empty sequence is zero and a present null is a type error. SQLite `sum` skips
21
+ null and returns null without inputs; `total` returns floating zero. SQLite
22
+ floating accumulation and ASCII NOCASE are deliberate choices on this surface.
23
+ No mapping must omit a null member to use native SQL.
24
+
25
+ ```js
26
+ import { relational, sql } from '@jarenjs/db/relational';
27
+ const r = relational(connection);
28
+ const c = sql.column;
29
+ const query = {
30
+ from: { table: 'staging', as: 's' },
31
+ columns: {
32
+ id: c('id', 's'), raw: c('body', 's'),
33
+ price: sql.cast(sql.call('json_extract', [c('body', 's'), '$.price']), 'REAL'),
34
+ },
35
+ where: sql.binary('IS', c('supplier', 's'), sql.param('supplier')),
36
+ orderBy: [{ by: sql.collate(c('sku', 's'), 'NOCASE'), nulls: 'first' }],
37
+ };
38
+ for (const row of r.iterate(query, { externals: { supplier: null } })) {
39
+ consume(row); // original raw text and native byte values are preserved
40
+ }
41
+ ```
42
+
43
+ `all`, `get`, `iterate` and `execute` settle synchronously. `iterate` uses the
44
+ shared cursor lifecycle, releases its statement on early return or failure,
45
+ and reports `streaming`/`barrier` from the driver. `all` intentionally collects
46
+ the result. SQLite may itself sort or build temporary query structures.
47
+
48
+ Selections support table and subquery sources, inner/left/cross joins, correlated
49
+ `sql.scalar`/`sql.exists`, CASE, IN/NOT IN, DISTINCT, grouped and distinct
50
+ aggregates, HAVING, UNION/UNION ALL and ordered windows. The declaration file
51
+ lists the closed operators and functions, including trim/coalesce, LIKE, JSON
52
+ extraction, casts and SQLite date functions. Neither arbitrary function names
53
+ nor a raw-expression escape hatch is accepted.
54
+
55
+ ## Exact writes and bytes
56
+
57
+ ```js
58
+ const result = r.execute({
59
+ op: 'update', table: 'claims',
60
+ set: { revision: sql.binary('+', c('revision'), 1), status: 'done' },
61
+ where: sql.binary('AND',
62
+ sql.binary('=', c('revision'), sql.param('expected')),
63
+ sql.binary('=', c('owner'), sql.param('owner'))),
64
+ }, { externals: { expected: 3, owner: 'worker-1' } });
65
+ // result.affected is SQLite's direct statement count, excluding trigger writes.
66
+ ```
67
+
68
+ UPDATE emits exactly the supplied assignments. `reporting: 'matched'` is the
69
+ native surface's default and executes identical assignments, so UPDATE OF and
70
+ immutability triggers run. `reporting: 'changed'` adds a BINARY, null-safe
71
+ comparison to suppress unchanged rows. UPDATE and DELETE require an explicit
72
+ `where`; literal `1` authorizes every row. Predicates and arithmetic belong to
73
+ one statement. The existing entity updater retains changed-row behavior;
74
+ `entity.mutate` adds explicit matched reporting and expression assignments.
75
+
76
+ INSERT accepts `values` or a `source` selection with ordered target `columns`.
77
+ A source may read another table or an intermediate legacy shape. `ignore: true`
78
+ uses INSERT OR IGNORE. `conflict` accepts ordered column/expression targets, an
79
+ optional partial-index `where`, and `action: 'nothing'` or `'update'` with an
80
+ exact `set` and optional `updateWhere`. Use `sql.column(name, 'excluded')` for
81
+ incoming values. Conflict-target expressions contain literal schema values so
82
+ SQLite can match its index. No read-then-write emulation occurs.
83
+
84
+ `Uint8Array` and Node `Buffer` values bind as bytes; returned binary values stay
85
+ bytes. Subarray offsets and zero bytes survive without hex strings or JSON
86
+ arrays. Combine metadata, image, conflict and history writes inside the existing
87
+ synchronous transaction; failure rolls back all of them. Optional `returning`
88
+ collects rows using SQLite RETURNING timing, before subsequent AFTER-trigger
89
+ changes. Unlike bounded entity mutation documents, this explicit native surface
90
+ has no automatic result row/byte limit; use a bounded selection or streaming read
91
+ when handling large results.
92
+
93
+ ## Physical schema ownership
94
+
95
+ ```js
96
+ import { defineTable, planTable, planTableMigration, applyTableMigration } from '@jarenjs/db/relational';
97
+ const history = defineTable({
98
+ name: 'history', primaryKey: ['id'],
99
+ columns: [
100
+ { name: 'id', type: 'INTEGER', identity: 'autoincrement', nullable: false },
101
+ { name: 'body', type: 'TEXT', nullable: false },
102
+ { name: 'revision', type: 'INTEGER', default: 1, nullable: false },
103
+ ],
104
+ indexes: [{ name: 'history_revision', terms: [{ by: c('revision'), direction: 'desc' }] }],
105
+ triggers: [{ name: 'history_immutable', timing: 'before', event: 'update',
106
+ steps: [{ raise: { action: 'abort', message: 'immutable history' } }] }],
107
+ });
108
+ const ddl = planTable(history).createSql;
109
+ const migration = planTableMigration(connection, history, { id: 'history-v2', allowRebuild: true });
110
+ // Inspect migration.statements and migration.finish before applying the plan.
111
+ applyTableMigration(connection, migration);
112
+ ```
113
+
114
+ Columns retain declaration order and exact INTEGER/REAL/TEXT/BLOB/NUMERIC/ANY
115
+ types. Definitions support ordered primary keys, rowid or AUTOINCREMENT identity,
116
+ nullability, database defaults, generated columns, STRICT/WITHOUT ROWID, named
117
+ UNIQUE/CHECK/foreign-key constraints and delete/update actions. Index terms
118
+ support expressions, direction and collation, with an optional partial predicate.
119
+ Triggers support BEFORE/AFTER, INSERT/UPDATE/DELETE, UPDATE OF, OLD/NEW conditions,
120
+ mutation steps and RAISE. Schema expressions use the same structural emitter,
121
+ with quoted inline literals because SQLite disallows schema parameters.
122
+
123
+ Entity `.physical(...)` metadata can also supply column `type`, `defaultValue`,
124
+ `collation`, `identity`, `check`, `generatedExpression` and `stored`, plus table
125
+ `constraints`, `indexes`, `triggers`, `strict` and `withoutRowid`.
126
+ `planEntity(...).createSql` now emits explicit table DDL for complete writable
127
+ declarations; views and incomplete generated/default definitions remain adoption
128
+ metadata. `openStore` still verifies physical tables without creating them.
129
+ Identical physical models produce an empty `planModelMigration`; changed
130
+ physical tables use the live-schema `planTableMigration` API.
131
+
132
+ ## Guarded upgrades
133
+
134
+ Planning inspects the existing schema without modifying it. A differing existing
135
+ table requires `allowRebuild: true`; additions currently use the same guarded
136
+ rebuild. Every removed column needs `dropColumns`; removing an existing explicit
137
+ index/trigger requires `dropObjects`. Unmentioned indexes and triggers survive.
138
+ An optional `copy` maps writable non-key target columns to structural expressions;
139
+ new columns otherwise use their defaults. The plan retains the source schema and
140
+ an exact canonical checksum. Applying an edited plan or a stale source refuses.
141
+ A target already matching the reviewed plan is a no-op on repeat.
142
+
143
+ Execution creates a replacement, copies natively, verifies row counts and
144
+ unchanged values/storage classes, drops/renames atomically, restores indexes and
145
+ triggers, and checks foreign keys and the target schema. Primary-key columns,
146
+ unshadowed hidden rowids, raw text, bytes and AUTOINCREMENT high-water marks are
147
+ preserved. A key change, rowid-ownership change or ambiguous rowid alias refuses.
148
+ No migration history table or model adoption is needed.
149
+
150
+ Rebuilds require SQLite's foreign-key transition outside a transaction. For a
151
+ nested upgrade use `withForeignKeysSuspended(connection, () => { ... })` as the
152
+ outer scope, then nested `connection.transaction`/migration calls share savepoints.
153
+ The helper owns an IMMEDIATE transaction, checks references, and restores
154
+ `foreign_keys` and `legacy_alter_table` after success or failure. Its callback must
155
+ settle synchronously. A rebuild inside an already open FK-enabled transaction
156
+ refuses before DDL. Node/Bun regressions cover populated history and references,
157
+ failed copies, repeat reopening, nested rollback, and process death after DROP
158
+ with WAL recovery. These tests do not establish power-loss durability.
159
+
160
+ ## Read-only inspection and disk snapshots
161
+
162
+ `readSchema` from `@jarenjs/db/model` inventories tables without adopting them,
163
+ but excludes engine bookkeeping. For a scanner that must include framework tables,
164
+ select table names directly from `sqlite_schema` with `relational`, or use the
165
+ public `connection.dialect.introspect.tables()` statement. A native selection can use `sql.call('typeof', [sql.column(name)])` and
166
+ `sql.column('rowid')` to inspect actual text storage in otherwise unknown shapes.
167
+ Declared affinity does not determine a value's storage class. WITHOUT ROWID tables
168
+ and shadowed rowid aliases require an explicit different identity selection.
169
+
170
+ Both host driver entries export `snapshotDatabase(connection, newPath)`. It
171
+ reserves a new destination, refuses any existing file (even empty), includes
172
+ committed WAL data through VACUUM INTO, syncs the output and cleans up a failed
173
+ copy. It allocates no database-sized JavaScript image. SQLite page caches and
174
+ its temporary storage govern native working memory. The copy runs synchronously
175
+ inside the asynchronous filesystem operation; it is not an incremental-progress
176
+ or interruption API. A snapshot inside an active transaction fails and cleans up.
177
+ Explicit keys, retained histories and bytes survive; [SQLite VACUUM](https://www.sqlite.org/lang_vacuum.html)
178
+ may renumber unaliased rowids, so this is not a page-identical archival copy. Bun store backups
179
+ use the same disk-backed path; Node store backup keeps its online-backup binding.
180
+
181
+ ## Lightweight engines
182
+
183
+ Use `@jarenjs/db/query`, `/model` and `/entity` for existing-connection consumers;
184
+ `/relational` needs neither entity normalization nor a query plan cache.
185
+ `compileEntityModel(model)` returns `{ entities, mapping }` from one normalization,
186
+ avoiding the duplicated work of separate `normalizeEntities` and `explainMapping`
187
+ calls. No global strong model cache is introduced. Keep connection/query caches
188
+ bounded and reuse compiled metadata. Smaller import graphs alone do not prove
189
+ the complete application meets its RSS budget; measure application memory with
190
+ representative workloads.
package/package.json CHANGED
@@ -1,12 +1,16 @@
1
1
  {
2
2
  "name": "@jarenjs/db",
3
3
  "private": false,
4
- "version": "0.84.3",
4
+ "version": "0.86.0",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
7
7
  "types": "./types/index.d.ts",
8
8
  "sideEffects": false,
9
9
  "exports": {
10
+ "./node-process": {
11
+ "types": "./types/node-process.d.ts",
12
+ "default": "./src/drivers/node-process.js"
13
+ },
10
14
  "./search": {
11
15
  "types": "./types/search.d.ts",
12
16
  "default": "./src/search.js"
@@ -48,6 +52,22 @@
48
52
  "./node-pool": {
49
53
  "types": "./types/node-pool.d.ts",
50
54
  "default": "./src/drivers/node-pool.js"
55
+ },
56
+ "./relational": {
57
+ "types": "./types/relational.d.ts",
58
+ "default": "./src/relational-api.js"
59
+ },
60
+ "./query": {
61
+ "types": "./types/query.d.ts",
62
+ "default": "./src/query-api.js"
63
+ },
64
+ "./model": {
65
+ "types": "./types/model.d.ts",
66
+ "default": "./src/model-api.js"
67
+ },
68
+ "./entity": {
69
+ "types": "./types/entity.d.ts",
70
+ "default": "./src/entity.js"
51
71
  }
52
72
  },
53
73
  "files": [
@@ -88,9 +108,9 @@
88
108
  "prepack": "npm run build:types"
89
109
  },
90
110
  "dependencies": {
91
- "@jarenjs/core": "^0.84.3",
92
- "@jarenjs/json": "^0.84.3",
93
- "@jarenjs/validate": "^0.84.3"
111
+ "@jarenjs/core": "^0.86.0",
112
+ "@jarenjs/json": "^0.86.0",
113
+ "@jarenjs/validate": "^0.86.0"
94
114
  },
95
115
  "bin": {
96
116
  "jaren-db": "./src/cli.js"