@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.
- package/ARCHITECTURE.md +27 -3
- package/README.md +35 -14
- package/docs/HOSTS.md +131 -4
- package/docs/MODEL-FORMAT.md +15 -4
- package/docs/NATIVE-PLANS.md +24 -7
- package/docs/SQLITE-RELATIONAL.md +190 -0
- package/package.json +24 -4
- package/schemas/jaren-model.authoring.schema.json +157 -0
- package/schemas/jaren-model.draft-07.schema.json +157 -0
- package/schemas/jaren-model.schema.json +157 -0
- package/src/capture.js +4 -2
- package/src/ddl.js +8 -3
- package/src/dialects/sqlite-relational.js +314 -0
- package/src/dialects/sqlite-schema.js +142 -0
- package/src/dialects/sqlite.js +17 -0
- package/src/driver.js +3 -0
- package/src/drivers/bun.js +26 -15
- package/src/drivers/node-process-endpoint.js +13 -0
- package/src/drivers/node-process.js +177 -0
- package/src/drivers/node-worker-endpoint.js +3 -103
- package/src/drivers/node-worker.js +6 -178
- package/src/drivers/node.js +3 -0
- package/src/drivers/snapshot.js +49 -0
- package/src/drivers/sqlite-endpoint.js +113 -0
- package/src/drivers/worker-client.js +185 -0
- package/src/drivers/worker-protocol.js +15 -0
- package/src/emit.js +37 -5
- package/src/engine-metadata.js +18 -0
- package/src/errors.js +1 -0
- package/src/index.js +4 -1
- package/src/introspect.js +1 -2
- package/src/jobs.js +4 -2
- package/src/migrate.js +12 -16
- package/src/model-api.js +4 -0
- package/src/model.js +12 -0
- package/src/mutation.js +66 -14
- package/src/physical.js +37 -7
- package/src/plan.js +66 -3
- package/src/query-api.js +5 -0
- package/src/query.js +39 -6
- package/src/relational-api.js +6 -0
- package/src/store.js +6 -4
- package/src/table-migration.js +158 -0
- package/types/bun.d.ts +3 -0
- package/types/entity.d.ts +1 -0
- package/types/index.d.ts +11 -2
- package/types/model.d.ts +1 -0
- package/types/node-process.d.ts +33 -0
- package/types/node.d.ts +3 -0
- package/types/query.d.ts +2 -0
- 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
|
|
1171
|
-
keyset continuation until its identity semantics
|
|
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
|
|
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**.
|
|
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
|
-
##
|
|
1215
|
+
## Replication and external-write boundaries
|
|
1215
1216
|
|
|
1216
|
-
The
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
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
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
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 `
|
|
287
|
-
under the store gate,
|
|
288
|
-
|
|
289
|
-
image
|
|
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.
|
package/docs/MODEL-FORMAT.md
CHANGED
|
@@ -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
|
|
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
|
|
2365
|
-
|
|
2366
|
-
|
|
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.
|
package/docs/NATIVE-PLANS.md
CHANGED
|
@@ -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
|
|
83
|
+
The operations are:
|
|
77
84
|
|
|
78
85
|
| Operation | Required document members | Meaning |
|
|
79
86
|
|---|---|---|
|
|
80
|
-
| `update` | `key`, `set`; `expectedRevision` for a versioned entity |
|
|
81
|
-
| `upsert` | `values`, `conflict
|
|
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.
|
|
96
|
-
|
|
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.
|
|
102
|
-
|
|
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.
|
|
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.
|
|
92
|
-
"@jarenjs/json": "^0.
|
|
93
|
-
"@jarenjs/validate": "^0.
|
|
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"
|