@jarenjs/db 0.75.0 → 0.83.2
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 +20 -0
- package/README.md +25 -0
- package/docs/HOSTS.md +17 -0
- package/docs/JOBS-FORMAT.md +26 -0
- package/docs/LIVE-FORMAT.md +4 -0
- package/docs/MIGRATION-FORMAT.md +34 -0
- package/docs/MODEL-FORMAT.md +132 -1
- package/docs/NATIVE-PLANS.md +111 -0
- package/docs/SEARCH.md +55 -0
- package/package.json +8 -4
- package/schemas/jaren-migration.draft-07.schema.json +54 -5
- package/schemas/jaren-migration.schema.json +49 -0
- package/schemas/jaren-model.authoring.schema.json +360 -0
- package/schemas/jaren-model.draft-07.schema.json +128 -0
- package/schemas/jaren-model.schema.json +128 -0
- package/src/algebra.js +17 -1
- package/src/backup.js +12 -7
- package/src/cursor.js +27 -4
- package/src/dag-job.js +2 -1
- package/src/ddl.js +13 -0
- package/src/dialect.js +10 -0
- package/src/dialects/check-read.js +3 -3
- package/src/dialects/invariant-sql.js +117 -0
- package/src/dialects/postgres.js +14 -2
- package/src/dialects/sqlite.js +18 -2
- package/src/driver.js +1 -0
- package/src/drivers/bun.js +22 -4
- package/src/emit.js +80 -13
- package/src/entity.js +98 -41
- package/src/errors.js +8 -0
- package/src/graph.js +8 -1
- package/src/index.js +3 -0
- package/src/introspect.js +44 -7
- package/src/invariants.js +45 -0
- package/src/jobs.js +39 -6
- package/src/live.js +4 -1
- package/src/migrate.js +136 -22
- package/src/model.js +12 -0
- package/src/mutation.js +165 -0
- package/src/physical.js +147 -0
- package/src/plan.js +114 -20
- package/src/query.js +144 -70
- package/src/search.js +144 -0
- package/src/sql.js +60 -0
- package/src/store.js +49 -13
- package/src/tracker.js +63 -39
- package/src/window.js +1 -0
- package/types/index.d.ts +58 -3
- package/types/search.d.ts +20 -0
- package/types/typed.d.ts +1 -0
package/ARCHITECTURE.md
CHANGED
|
@@ -1159,3 +1159,23 @@ query for missing/invalid values, negative radii, polar or antimeridian boxes.
|
|
|
1159
1159
|
The exact distance predicate still refines the candidates. The shared oracle
|
|
1160
1160
|
covers SQLite column/R*Tree indexes and PostgreSQL, including repeated cached
|
|
1161
1161
|
calls with different bound values.
|
|
1162
|
+
|
|
1163
|
+
|
|
1164
|
+
## Explicit relational adoption
|
|
1165
|
+
|
|
1166
|
+
`introspect.js` owns physical inventory independently of model derivation.
|
|
1167
|
+
`physical.js` compiles column codecs and verifies declarations against that
|
|
1168
|
+
inventory. The existing entity core, tracker and graph row merger execute both
|
|
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.
|
|
1172
|
+
|
|
1173
|
+
`sql.js` binds trusted statements to `store.js` transaction views. It reuses the
|
|
1174
|
+
read classifier's tokenizer and the driver's scope owner. Writes invalidate all
|
|
1175
|
+
clean tracked entities; pending edits and incomplete capture populations refuse.
|
|
1176
|
+
`invariants.js` uses the shared Query compiler for store rules; the dialect lowers
|
|
1177
|
+
a bounded database subset into ordered trigger bodies. `migrate.js` reuses its
|
|
1178
|
+
existing rebuild/receipt transaction and verifies preservation before publication.
|
|
1179
|
+
The backup publisher remains shared by Node online and Bun serialized snapshots.
|
|
1180
|
+
|
|
1181
|
+
`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.
|
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# @jarenjs/db
|
|
2
2
|
|
|
3
|
+
For existing-file adoption with native queries, receipts and jobs, start with the [combined public recipe and evidence ledger](../../docs/ADOPTION-EVIDENCE.md). Adopted-trigger capture, physical keysets and PostgreSQL subsystem gaps retain their documented refusals; synthetic SQLite proof does not retire downstream SQL.
|
|
4
|
+
|
|
3
5
|
Documents AND entities in SQLite. A **model document** declares
|
|
4
6
|
collections (a JSON Schema, a key, indexes) and — since phase B —
|
|
5
7
|
**entities**: keys, typed columns, relations, defaults and an
|
|
@@ -1357,6 +1359,7 @@ Every subpath a consumer can import, derived from the manifest by
|
|
|
1357
1359
|
<!--fact:exports.db-->
|
|
1358
1360
|
| Import | Kind | Declarations |
|
|
1359
1361
|
|---|---|---|
|
|
1362
|
+
| `@jarenjs/db/search` | JavaScript | declared |
|
|
1360
1363
|
| `@jarenjs/db` | JavaScript | declared |
|
|
1361
1364
|
| `@jarenjs/db/node` | JavaScript | declared |
|
|
1362
1365
|
| `@jarenjs/db/postgres` | JavaScript | declared |
|
|
@@ -1366,6 +1369,7 @@ Every subpath a consumer can import, derived from the manifest by
|
|
|
1366
1369
|
| `@jarenjs/db/app` | JavaScript | declared |
|
|
1367
1370
|
| `@jarenjs/db/schemas/jaren-migration.draft-07.schema.json` | schema | — |
|
|
1368
1371
|
| `@jarenjs/db/schemas/jaren-migration.schema.json` | schema | — |
|
|
1372
|
+
| `@jarenjs/db/schemas/jaren-model.authoring.schema.json` | schema | — |
|
|
1369
1373
|
| `@jarenjs/db/schemas/jaren-model.draft-07.schema.json` | schema | — |
|
|
1370
1374
|
| `@jarenjs/db/schemas/jaren-model.schema.json` | schema | — |
|
|
1371
1375
|
| `@jarenjs/db/schemas/jaren-replication-snapshot.draft-07.schema.json` | schema | — |
|
|
@@ -1406,3 +1410,24 @@ Only affected groups are reevaluated in source order; a final aggregate folds
|
|
|
1406
1410
|
the retained group outputs in first-appearance order. Input documents and
|
|
1407
1411
|
group outputs consume both state-entry and byte credits. See
|
|
1408
1412
|
[LIVE-FORMAT](docs/LIVE-FORMAT.md) for the supported shapes and measured costs.
|
|
1413
|
+
|
|
1414
|
+
|
|
1415
|
+
## Existing relational SQLite files
|
|
1416
|
+
|
|
1417
|
+
Use `readSchema(connection)` for physical inventory, then declare an entity's
|
|
1418
|
+
`physical` table, ordered keys and column codecs and open with `{ adopt: true }`.
|
|
1419
|
+
Opening verifies the existing shape and emits no DDL. Ordinary column tables need
|
|
1420
|
+
no document column; integer identities, exact hexadecimal BLOBs, database defaults
|
|
1421
|
+
and read-only views have explicit contracts in [MODEL-FORMAT](docs/MODEL-FORMAT.md#12-existing-column-layouts).
|
|
1422
|
+
|
|
1423
|
+
Inside `store.transaction`, `tx.sql.prepare(text, { access: 'read' | 'write' })`
|
|
1424
|
+
shares the entity/outbox connection and savepoint owner. Statements expire with
|
|
1425
|
+
the scope. [The client recipe](../linq/docs/DB-CLIENT.md#trusted-sql-during-adoption)
|
|
1426
|
+
documents trust, invalidation and synchronous execution. Schema changes use
|
|
1427
|
+
`planPhysicalMigration` and `migrate`; [preservation and forward recovery](docs/MIGRATION-FORMAT.md#existing-physical-files-and-forward-recovery)
|
|
1428
|
+
require explicit dispositions and assertions. `planInvariants` supplies declared
|
|
1429
|
+
SQLite constraint/audit triggers for installation through that migration boundary.
|
|
1430
|
+
|
|
1431
|
+
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
|
+
|
|
1433
|
+
`@jarenjs/db/search` composes the resident ranker with bounded authoritative entity reads and optional atomic snapshot storage. See [persisted search](docs/SEARCH.md).
|
package/docs/HOSTS.md
CHANGED
|
@@ -272,3 +272,20 @@ Checked and dropped:
|
|
|
272
272
|
- Read-only WAL workers do not accept writes, nested scopes do not migrate,
|
|
273
273
|
transient cursors do not exhaust the statement cap, and failed writes are not
|
|
274
274
|
automatically replayed. The lifecycle and fault corpus exercises each boundary.
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
## Existing-file adoption and backup
|
|
278
|
+
|
|
279
|
+
Node and Bun qualify explicit SQLite column mappings and scoped prepared SQL.
|
|
280
|
+
The synchronous transaction API exists only when the driver's observed
|
|
281
|
+
`synchronous` capability is true; worker and other async-only hosts expose no
|
|
282
|
+
sync twin. Physical adoption on PostgreSQL refuses pending a separate mapping
|
|
283
|
+
and codec qualification. Unknown application-trigger effects do not qualify
|
|
284
|
+
capture or replication; those combinations refuse before an adoption claim.
|
|
285
|
+
|
|
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
|
|
290
|
+
cover rebuild copy, table drop, commit and backup publication on both hosts;
|
|
291
|
+
these tests do not establish power-loss durability or native executable packaging.
|
package/docs/JOBS-FORMAT.md
CHANGED
|
@@ -504,3 +504,29 @@ a cancellation policy, a retry policy are the host's.
|
|
|
504
504
|
only by `cancel()`; `counts()` reports it. A transaction view's `jobs`
|
|
505
505
|
(the outbox, §3) carries none of the four: an administration call is a
|
|
506
506
|
root call.
|
|
507
|
+
|
|
508
|
+
## Business receipt and external-effect composition
|
|
509
|
+
|
|
510
|
+
`jobs.assertLease(lease)` checks current execution authority without a write or
|
|
511
|
+
renewal. It uses the same token/expiry guard and `JD2065`/`JD2066`/`JD2067` refusals
|
|
512
|
+
as settlement; malformed authority is `JD2068`. Inside `tx.jobs`, this read and
|
|
513
|
+
subsequent mapped writes share the transaction lock. Workers receive
|
|
514
|
+
`context.lease()` to read their current token after automatic renewal.
|
|
515
|
+
|
|
516
|
+
`createWorker({ effectSafety(job, context), ... })` optionally gates each handler
|
|
517
|
+
admission. Only `true` admits; any other resolved value pauses the queue attempt
|
|
518
|
+
in its existing `cancelled` state without running the handler. The worker
|
|
519
|
+
`context.pause()` exposes the same operation. An authorized operator can
|
|
520
|
+
explicitly `requeue` and claim it for reconciliation; it never becomes an
|
|
521
|
+
unreclaimable completed job merely because its external outcome is unknown.
|
|
522
|
+
A thrown policy error follows the ordinary failure path and is checked again on
|
|
523
|
+
a later attempt. `createDagJobRunner` forwards the same hook. The hook supplements
|
|
524
|
+
the mapped effect store's mandatory pre-dispatch fence and durable intent; it is
|
|
525
|
+
not a replacement for them.
|
|
526
|
+
|
|
527
|
+
Business receipts and external intents belong to application-mapped tables via
|
|
528
|
+
`@jarenjs/linq/db`. Job reset/sweep operate on queue/checkpoint rows only and do
|
|
529
|
+
not erase those facts. An expired lease, reset or retryable job failure cannot
|
|
530
|
+
provide permission to resend unresolved external writes. Local co-commit does
|
|
531
|
+
not mean exactly-once remote delivery. See the contract package's
|
|
532
|
+
[durable composition](../../contract/docs/DURABLE.md).
|
package/docs/LIVE-FORMAT.md
CHANGED
|
@@ -692,3 +692,7 @@ It reports initialization allocations and both median and tail mutation time;
|
|
|
692
692
|
heap deltas before collection are not precise retained-state sizes. The fixture
|
|
693
693
|
has 200 initial rows and 15 mutations per case, so these measurements establish
|
|
694
694
|
correctness and costs for that fixture rather than a universal crossover.
|
|
695
|
+
|
|
696
|
+
## Structural range adapter
|
|
697
|
+
|
|
698
|
+
`@jarenjs/linq/db` exposes bounded ranges over captured hybrid entity roots, using the existing keyset pager and committed observer. Its live event is an explicit source reset with monotone revision; this does not promote offset windows or the remaining incremental shapes. See [COLLECTION-PROVIDER](../../app/docs/COLLECTION-PROVIDER.md) for credits, snapshot identity and disposal. Maintained top windows check offscreen rows against their bound and release retained state on close.
|
package/docs/MIGRATION-FORMAT.md
CHANGED
|
@@ -598,3 +598,37 @@ difference between a puzzled afternoon and a five-minute fix.
|
|
|
598
598
|
Down migrations REMAIN a non-goal (§7's reasoning is unchanged): a
|
|
599
599
|
down migration is a data-loss generator wearing a seatbelt; recovery
|
|
600
600
|
is a backup restored plus the forward chain.
|
|
601
|
+
|
|
602
|
+
## Existing physical files and forward recovery
|
|
603
|
+
|
|
604
|
+
`planPhysicalMigration(connection, fromModel, toModel, options)` records the
|
|
605
|
+
source schema and explicit DDL/SQL/rebuild steps as an ordinary migration document.
|
|
606
|
+
`options` supplies an `id`, `steps`, a disposition for every source `type:name`
|
|
607
|
+
(`preserve`, `replace`, or `drop`), and optional `{ sql, params, expected }`
|
|
608
|
+
preservation assertions. Assertions are SELECTs evaluated before and after the
|
|
609
|
+
steps. They should cover committed identities, exact BLOB hex and application
|
|
610
|
+
history facts. Unknown objects cannot disappear without a declared disposition.
|
|
611
|
+
Automatic hybrid model diffing refuses column layouts; an explicit plan is required.
|
|
612
|
+
|
|
613
|
+
Apply through `migrate(target, [plan], { baseline, model, shadow: false })`.
|
|
614
|
+
A physical plan must be qualified on an explicit backup and fresh-target fixture;
|
|
615
|
+
an empty model-generated shadow cannot recreate the original file's application
|
|
616
|
+
programs. The runner uses its existing immediate transaction, ordered steps,
|
|
617
|
+
checksummed receipt and FK checks. It checks the source schema before destructive
|
|
618
|
+
steps and preserved objects and assertions before publication. Target mapped
|
|
619
|
+
columns and declared invariant triggers are verified. A changed source is
|
|
620
|
+
`JD0020`, a lost object/fact is `JD0023`, and an edited applied receipt is `JD0022`.
|
|
621
|
+
|
|
622
|
+
Migration history is created inside the applying transaction only when needed.
|
|
623
|
+
An identical second run executes no DDL or DML. Failed steps and failed commits
|
|
624
|
+
roll back; after a process kill SQLite recovery leaves the source or the committed
|
|
625
|
+
target. Re-running resumes from committed receipts. Forward repair plans start
|
|
626
|
+
from the newest file, including later application edits; restoring an older
|
|
627
|
+
backup does not qualify as forward repair.
|
|
628
|
+
|
|
629
|
+
`backupTo()` publishes a sibling temporary only after a complete snapshot.
|
|
630
|
+
Node uses online backup. Bun uses its native serialized SQLite snapshot under
|
|
631
|
+
the store gate, then flushes and atomically renames through the same publisher.
|
|
632
|
+
Bun holds a full database image in memory and cannot offer page-granular copy
|
|
633
|
+
cancellation. Both snapshots include committed WAL; interruption before rename
|
|
634
|
+
leaves the previous destination valid, while a leftover temporary is not published.
|
package/docs/MODEL-FORMAT.md
CHANGED
|
@@ -12,6 +12,17 @@ entities, §10 relational translation, §11 the unit of work.
|
|
|
12
12
|
|
|
13
13
|
## 1. Scope
|
|
14
14
|
|
|
15
|
+
`readSchema(connection)` inventories physical facts independently of model
|
|
16
|
+
representability. SQLite keys include rowid aliases and ordered composite keys;
|
|
17
|
+
columns retain defaults, nullability and key ordinals, foreign keys retain groups
|
|
18
|
+
and update/delete actions. `tables[].sql` and `objects[].sql` retain source DDL,
|
|
19
|
+
including collations, generated expressions, predicates and trigger programs.
|
|
20
|
+
PostgreSQL catalogs retain available object definitions; unavailable source table
|
|
21
|
+
DDL is explicitly `null`. `introspectModel()` also returns an `inventory` with
|
|
22
|
+
`derived` or `preserve` dispositions. A disposition never authorizes a drop.
|
|
23
|
+
Application triggers appear as `unmapped-object` losses with their owner and SQL.
|
|
24
|
+
Repeated inspection is read-only; `strict: true` refuses losses with `JD0002`.
|
|
25
|
+
|
|
15
26
|
A **model document** declares the collections of a store: each
|
|
16
27
|
collection is a JSON Schema for its documents, a key declaration, and
|
|
17
28
|
a set of declared indexes. `openStore(model, { driver, ... })` opens
|
|
@@ -808,7 +819,7 @@ cannot retry; with the lock taken first that wait is an ordinary busy
|
|
|
808
819
|
wait the `busyTimeout` covers, and two processes claiming one key see
|
|
809
820
|
one `new`. The default `'deferred'` is unchanged, `tx.transaction()`
|
|
810
821
|
inside either mode is a savepoint, `signal` and `unitOfWork` behave the
|
|
811
|
-
same
|
|
822
|
+
same; the synchronous twin also accepts writer admission mode and refuses thenable callbacks. The open path already
|
|
812
823
|
brackets every first-open object — collection, entity and join tables,
|
|
813
824
|
indexes, the change log and its state row, the job tables — the same
|
|
814
825
|
way (§2.4).
|
|
@@ -1037,6 +1048,7 @@ error.
|
|
|
1037
1048
|
| `JD0035` | the continuation does not belong to this ordering |
|
|
1038
1049
|
| `JD0036` | a snapshot page needs an immutable ordering |
|
|
1039
1050
|
| `JD0037` | strictStreaming refused a plan that buffers |
|
|
1051
|
+
| `JD0038` | the native mutation document is unsupported or invalid |
|
|
1040
1052
|
| `JD0040` | the save spans a relation cycle |
|
|
1041
1053
|
| `JD0050` | live queries require change capture |
|
|
1042
1054
|
| `JD0051` | the demanded live mode is unavailable |
|
|
@@ -1087,6 +1099,8 @@ error.
|
|
|
1087
1099
|
| `JD2092` | a worker row, compatibility result or remote identity count exceeds its declared bound |
|
|
1088
1100
|
| `JD2093` | malformed worker protocol request |
|
|
1089
1101
|
| `JD2094` | invalid or uncommitted durable snapshot; reopen the last committed version |
|
|
1102
|
+
| `JD2095` | trusted SQL or synchronous callback authority refused |
|
|
1103
|
+
| `JD2096` | persistence invariant rejected the mutation; constraint class |
|
|
1090
1104
|
| `JD0060` | a replication envelope or snapshot is invalid |
|
|
1091
1105
|
| `JD2100` | a replica sequence or causal dependency has a gap |
|
|
1092
1106
|
| `JD2101` | an envelope identity names different content or an unknown local origin |
|
|
@@ -2283,3 +2297,120 @@ const report = await store.saveChanges(); // { joinInserted: 1, joinDeleted
|
|
|
2283
2297
|
|
|
2284
2298
|
Worker/pool options, synchronous cursors, wasm session probing and the browser
|
|
2285
2299
|
persistence ladder are specified in [execution hosts](HOSTS.md).
|
|
2300
|
+
|
|
2301
|
+
## 12. Existing column layouts
|
|
2302
|
+
|
|
2303
|
+
An entity's optional `physical` member declares a column-only SQLite layout.
|
|
2304
|
+
Omitting it retains the hybrid mapping. Opening a physical entity MUST verify
|
|
2305
|
+
its existing table or view and MUST NOT create it. `adopt: true` also prevents
|
|
2306
|
+
creation of hybrid tables and infrastructure. Inspection, adoption and explicit
|
|
2307
|
+
migration are separate operations.
|
|
2308
|
+
|
|
2309
|
+
`adopt: true` with `jobs: true` can attach the existing current SQLite job
|
|
2310
|
+
tables and claim index without executing DDL. The engine checks its own schema
|
|
2311
|
+
declaration, preserving SQL literal values; a missing or differently shaped
|
|
2312
|
+
object refuses `JD0002` and requires an explicit migration. Adoption never
|
|
2313
|
+
provisions or upgrades the queue. Capture and replication still refuse in
|
|
2314
|
+
adopted mode; existing application triggers do not imply complete live capture.
|
|
2315
|
+
|
|
2316
|
+
```json
|
|
2317
|
+
{
|
|
2318
|
+
"$model": "0.1",
|
|
2319
|
+
"entities": {
|
|
2320
|
+
"Setting": {
|
|
2321
|
+
"schema": { "type": "object", "properties": {
|
|
2322
|
+
"id": { "type": "string", "x-entity": { "key": true } },
|
|
2323
|
+
"value": {}, "updated": { "type": "string" }
|
|
2324
|
+
} },
|
|
2325
|
+
"physical": { "table": "app_settings", "columns": {
|
|
2326
|
+
"id": { "name": "key", "codec": "text", "null": "reject" },
|
|
2327
|
+
"value": { "name": "value", "codec": "json", "null": "absent" },
|
|
2328
|
+
"updated": { "name": "updated_at", "codec": "datetime", "null": "reject", "default": "database" }
|
|
2329
|
+
} }
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
```
|
|
2334
|
+
|
|
2335
|
+
Every stored property MUST have a distinct physical column name, an explicit
|
|
2336
|
+
codec and SQL NULL policy. `physical.keys` orders the declared key properties;
|
|
2337
|
+
without it, property declaration order applies. The physical primary key must
|
|
2338
|
+
agree in order. `kind: "view"` requires declared logical keys and refuses writes.
|
|
2339
|
+
Join tables can be declared as ordinary entities with ordered composite keys;
|
|
2340
|
+
relation navigation across physical layouts is not qualified.
|
|
2341
|
+
|
|
2342
|
+
| Codec | Public representation | Required SQLite affinity |
|
|
2343
|
+
|---|---|---|
|
|
2344
|
+
| `text` | string | TEXT |
|
|
2345
|
+
| `integer` | safe integer | INTEGER |
|
|
2346
|
+
| `number` | finite number, safe when integral | REAL |
|
|
2347
|
+
| `boolean` | boolean | INTEGER |
|
|
2348
|
+
| `json` | JSON value | TEXT |
|
|
2349
|
+
| `date`, `datetime` | validated RFC 3339 text | TEXT |
|
|
2350
|
+
| `epoch-ms` | canonical UTC date-time text | INTEGER |
|
|
2351
|
+
| `bigint` | signed integer decimal string within SQLite's integer range | INTEGER |
|
|
2352
|
+
| `decimal` | exact decimal string, including trailing zeros | TEXT |
|
|
2353
|
+
| `blob-hex` | lowercase hexadecimal string | BLOB |
|
|
2354
|
+
|
|
2355
|
+
Unsafe narrowing refuses `JD2003`. A byte handle never enters the public entity.
|
|
2356
|
+
`null: "null"` maps SQL NULL to present JSON null; `"absent"` omits the property;
|
|
2357
|
+
`"reject"` refuses it. With the JSON codec, JSON null is stored as the text `null`,
|
|
2358
|
+
so SQL NULL can independently mean absence. `default: "database"` omits an absent
|
|
2359
|
+
insert column and reads back the database result. `generated: true` gives column
|
|
2360
|
+
writes to the database. A generated integer identity uses the existing
|
|
2361
|
+
`x-entity.default: "auto"` declaration. Direct updates with identical values and
|
|
2362
|
+
identical tracked saves produce no effective writes.
|
|
2363
|
+
|
|
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
|
+
Physical `page` and `after` continuation refuse until codec-aware keysets are
|
|
2368
|
+
qualified; an explicit `take`/`skip` load remains available.
|
|
2369
|
+
Capture/live/replication for adopted application triggers is not qualified and
|
|
2370
|
+
is refused, rather than advertised as a complete change stream. PostgreSQL
|
|
2371
|
+
column adoption is not qualified; physical inventory remains available.
|
|
2372
|
+
|
|
2373
|
+
## 13. Persistence invariants
|
|
2374
|
+
|
|
2375
|
+
An entity may declare `invariants`: each has `name`, `on` (insert/update/delete),
|
|
2376
|
+
`assert` (a Query expression), and explicit `enforcement` (`database` or `store`).
|
|
2377
|
+
The evaluator receives `{ old, new, op }`; the missing record is null. A rule
|
|
2378
|
+
passes only on boolean true. Update rules and audit effects skip identical rows.
|
|
2379
|
+
Store enforcement covers direct and tracked model writes; arbitrary external
|
|
2380
|
+
SQL is outside that population. Trusted SQL writes refuse while store rules are
|
|
2381
|
+
present. Failed rules use `JD2096`, with constraint classification.
|
|
2382
|
+
|
|
2383
|
+
Database enforcement requires a writable physical layout and a bounded scalar
|
|
2384
|
+
query expression: `$eq`, `$ne`, `$lt`, `$le`, `$gt`, `$ge`, `$and`, `$or`, `$not`,
|
|
2385
|
+
scalar literals, `$.op`, and `$.old.member` / `$.new.member` references. Unsupported
|
|
2386
|
+
expressions refuse at planning. `planInvariants(model, { dialect })` returns
|
|
2387
|
+
reviewable trigger DDL; an explicit migration installs it. Opening verifies those
|
|
2388
|
+
programs. A rule such as `{ "$le": ["$.new.start", "$.new.end"] }` declares an
|
|
2389
|
+
interval constraint without claiming interval indexing.
|
|
2390
|
+
|
|
2391
|
+
An optional database `audit: { entity, values }` inserts into an application-owned
|
|
2392
|
+
mapped table after the accepted mutation, after identity allocation. Values use
|
|
2393
|
+
old/new scalar references. Self-referential or chained audit effects refuse;
|
|
2394
|
+
all effects share the writer transaction. Existing unrecognized triggers remain
|
|
2395
|
+
application-owned physical objects requiring preservation dispositions.
|
|
2396
|
+
|
|
2397
|
+
|
|
2398
|
+
One AFTER trigger per operation evaluates assertions in declaration order, then
|
|
2399
|
+
runs audit inserts in declaration order. Assertions see the allocated identity
|
|
2400
|
+
and generated columns. A failed assertion aborts the entire statement, including
|
|
2401
|
+
its trigger effects. Existing application triggers keep SQLite's ordering relative to these
|
|
2402
|
+
programs. Equality uses JSON-style scalar types and null equality; an ordered
|
|
2403
|
+
comparison involving SQL NULL is false; `$not` negates that boolean. An absent-column
|
|
2404
|
+
policy cannot be lowered to a database rule and refuses. Existing optimistic
|
|
2405
|
+
version properties remain owned by the model writer; an external SQL writer must
|
|
2406
|
+
supply its own declared revision discipline. Store validation and codec checks
|
|
2407
|
+
are not a substitute for database constraints on external inputs.
|
|
2408
|
+
|
|
2409
|
+
References to a property of the unavailable old/insert or new/delete record
|
|
2410
|
+
refuse database lowering, because absence differs from SQL NULL. Referenced
|
|
2411
|
+
scalar storage types are checked by the assertion trigger; numeric references
|
|
2412
|
+
must stay in the safe-number range.
|
|
2413
|
+
|
|
2414
|
+
## Native column mutation documents
|
|
2415
|
+
|
|
2416
|
+
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).
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Native plans over adopted columns
|
|
2
|
+
|
|
3
|
+
The executable census in `test/db/fixtures/adoption-sql.json` retains its SQL
|
|
4
|
+
reference alongside public documents. Five read families execute as one native
|
|
5
|
+
statement: settings projections, environment joins, correlated counts, nullable
|
|
6
|
+
grouped sums/counts, and compound-key point reads. Receipt history retains its SQL
|
|
7
|
+
reference because the source table has no declared primary key. Inventing an
|
|
8
|
+
entity identity would change the adoption contract.
|
|
9
|
+
|
|
10
|
+
## Reads
|
|
11
|
+
|
|
12
|
+
Use `store.execute(document, { strict: true })` to require a native plan;
|
|
13
|
+
`{ pushdown: false }` executes the decoded-row reference path. The JSON language
|
|
14
|
+
is unchanged: projections use object/array constructors, correlations use nested
|
|
15
|
+
`$for` and `$count`, and groups use `$groupby`. LINQ chains emit the same documents.
|
|
16
|
+
For example, a projection with a correlated count is:
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
const document = [{
|
|
20
|
+
$for: { c: '$.Catalog[*]' },
|
|
21
|
+
$orderby: '$c.sku',
|
|
22
|
+
$return: { sku: '$c.sku', inventories: { $count: {
|
|
23
|
+
$for: { i: '$.Inventory[*]' },
|
|
24
|
+
$where: { $eq: ['$i.sku', '$c.sku'] }, $return: '$i'
|
|
25
|
+
} } }
|
|
26
|
+
}];
|
|
27
|
+
const rows = await store.execute(document, { strict: true });
|
|
28
|
+
const explanation = await store.explain(document);
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Native correlations require one inner entity root, equality edges to outer
|
|
32
|
+
bindings, a bare inner return, and supported scalar predicates. Windows, grouping
|
|
33
|
+
and ordering inside a correlation, and arbitrary computed inner returns remain residual. Mandatory profile filters
|
|
34
|
+
apply to correlated roots too. Scalar column projections preserve declared NULL
|
|
35
|
+
policies. Codecs needing transformed representations remain decoded-row work.
|
|
36
|
+
|
|
37
|
+
Grouping is restricted to one root and reconstructible keys/aggregates. A nullable
|
|
38
|
+
SQL sum needs an explicit query expression: test existence of non-null values,
|
|
39
|
+
then sum those values, otherwise return literal null. An ordinary JSON sum over
|
|
40
|
+
an empty sequence remains zero. Compound physical identities without explicit
|
|
41
|
+
group ordering remain residual; the first-seen order cannot be replaced with
|
|
42
|
+
independent SQL minima. Unsupported shapes give a reason in `explain`; strict
|
|
43
|
+
mode refuses them with `JD0010`.
|
|
44
|
+
Physical integer sums/averages also prove the group's magnitude/count bound
|
|
45
|
+
inside SQL. When intermediate exactness cannot be guaranteed, ordinary execution
|
|
46
|
+
uses the existing decoded evaluator and reports that diversion; strict execution
|
|
47
|
+
refuses `JD0010`. This can cost an additional statement and full decoded scans.
|
|
48
|
+
Floating sums/averages and date/datetime grouping remain residual. Physical text
|
|
49
|
+
comparisons explicitly use codepoint collation, independent of a column's declared
|
|
50
|
+
collation; an incompatible index may therefore stop helping that query.
|
|
51
|
+
|
|
52
|
+
`explain` reports the emitted SQL, scan narrative, profile bounds and last
|
|
53
|
+
execution's admitted statement/returned-row/serialized-wire-byte counts. These
|
|
54
|
+
are application admission costs. SQLite does not expose visited-row counts or
|
|
55
|
+
statement time enforcement; a correlated subquery can visit many rows despite
|
|
56
|
+
returning a small projection. Use indexes and inspect the scan narrative. Profile
|
|
57
|
+
row limits bound fetched results; byte limits bound each decoded result item.
|
|
58
|
+
|
|
59
|
+
## Mutations
|
|
60
|
+
|
|
61
|
+
The asynchronous entity set exposes `mutate(document)`. It compiles and caches a
|
|
62
|
+
closed document into one parameterized SQLite data statement, inside the same
|
|
63
|
+
guarded transaction used by the entity writer. It supports adopted writable
|
|
64
|
+
column layouts. Hybrid entities, PostgreSQL physical layouts, arbitrary SQL,
|
|
65
|
+
store-enforced before/after invariants and unsupported expression shapes refuse
|
|
66
|
+
with `JD0038`. Database constraints and invariant triggers retain enforcement.
|
|
67
|
+
|
|
68
|
+
```js
|
|
69
|
+
const result = await store.entity('Inventory').mutate({
|
|
70
|
+
op: 'update', key: { environment: 'test', sku: '0012' },
|
|
71
|
+
expectedRevision: 1, set: { quantity: 4 },
|
|
72
|
+
returning: ['quantity', 'revision'], maxRows: 16, maxBytes: 16384
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The three operations are:
|
|
77
|
+
|
|
78
|
+
| Operation | Required document members | Meaning |
|
|
79
|
+
|---|---|---|
|
|
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. |
|
|
82
|
+
| `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
|
+
|
|
84
|
+
`returning` is a nonempty list of logical stored members (default: all). `maxRows`
|
|
85
|
+
defaults to 100 and `maxBytes` to 1048576; both must be finite positive safe
|
|
86
|
+
integers. The result is `{ mode: 'native', affected, rows, admitted }`.
|
|
87
|
+
`admitted` names one data statement, returned rows and full decoded-row bytes,
|
|
88
|
+
including columns omitted by `returning`. SQL transaction control is separate.
|
|
89
|
+
|
|
90
|
+
Insert-select materializes at most `maxRows + 1` source rows and refuses overflow
|
|
91
|
+
before insertion, even if all rows would conflict. Output bounds and codec/schema
|
|
92
|
+
validation run inside the transaction; failure rolls back rows and trigger effects.
|
|
93
|
+
The byte bound checks decoded output, not a database allocation interrupt.
|
|
94
|
+
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.
|
|
97
|
+
|
|
98
|
+
Mutations are untracked. Re-read affected rows before subsequent tracked editing;
|
|
99
|
+
a previously tracked revision remains stale and retains normal conflict checks.
|
|
100
|
+
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.
|
|
103
|
+
|
|
104
|
+
## Bounded ranges and live qualification
|
|
105
|
+
|
|
106
|
+
The headless adapter is exported by `@jarenjs/linq/db`; its structural contract is
|
|
107
|
+
[COLLECTION-PROVIDER.md](../../app/docs/COLLECTION-PROVIDER.md). It uses existing
|
|
108
|
+
entity cursors, keyset pages and committed capture, with no app import.
|
|
109
|
+
Initial qualification is captured hybrid entity roots. Physical keyset identities
|
|
110
|
+
and physical capture retain their explicit refusals. Live notifications are
|
|
111
|
+
source resets; offset windows retain their existing rerun classification.
|
package/docs/SEARCH.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Persisted lexical search
|
|
2
|
+
|
|
3
|
+
`@jarenjs/db/search` exports `createDbSearchStorage` and `createDbSearch`.
|
|
4
|
+
The adapter imports the same core ranker and JSON predicate compiler as resident
|
|
5
|
+
execution. `explain()` reports resident residual work and finite source credits;
|
|
6
|
+
SQLite/PostgreSQL native FTS is unqualified and is not substituted silently.
|
|
7
|
+
|
|
8
|
+
```js
|
|
9
|
+
import { createDbSearch, createDbSearchStorage } from '@jarenjs/db/search';
|
|
10
|
+
// The model declares an Item entity and a snapshots collection keyed by /id.
|
|
11
|
+
const storage = createDbSearchStorage(store, 'snapshots', { maxBytes: 8388608 });
|
|
12
|
+
const search = await createDbSearch(store, 'Item', {
|
|
13
|
+
version: 1, fields: ['title', 'sku'],
|
|
14
|
+
}, { source: 'catalog', maxRows: 10000, maxBytes: 8388608, storage });
|
|
15
|
+
const page = await search.search('gren tea', {
|
|
16
|
+
where: { $eq: ['$.available', true] }, facets: ['category'], limit: 20,
|
|
17
|
+
});
|
|
18
|
+
await search.dispose();
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The source entity must expose a string `id`; reads order by that key to make cold
|
|
22
|
+
build order reproducible. The store must have committed capture. A complete
|
|
23
|
+
bounded source is read inside a store transaction, and its canonical JSON is
|
|
24
|
+
hashed by host WebCrypto SHA-256 into a source-backed revision. The digest includes
|
|
25
|
+
non-indexed fields, so changed filter/facet facts invalidate search results too.
|
|
26
|
+
Captured source writes issue resets; `dataVersion` checks detect external-connection
|
|
27
|
+
SQL on requests. A cold open reads the source again, so uncaptured edits while the
|
|
28
|
+
adapter was closed cannot validate a stale cache. A host without capture or the
|
|
29
|
+
required data-version capability refuses freshness. PostgreSQL external capture
|
|
30
|
+
remains unqualified; no native full-text claim is made for either dialect.
|
|
31
|
+
|
|
32
|
+
`search(text, request)` refreshes a dirty source, compiles the request through
|
|
33
|
+
`createLexicalProvider`, then evaluates complete membership before result limits.
|
|
34
|
+
`refresh()` reports zero changes on identical input. `row(id, revision)` supplies
|
|
35
|
+
a detached authoritative row only under the current clean snapshot. `subscribe`
|
|
36
|
+
receives explicit resets, not a claim of incremental SQL maintenance. Refresh
|
|
37
|
+
rebuilds rank statistics from the complete bounded source; core incremental
|
|
38
|
+
updates remain available to other hosts. Source rows and index costs are separate
|
|
39
|
+
from a collection's page cache and mounted cells.
|
|
40
|
+
|
|
41
|
+
The optional storage collection holds `{id, payload}` derived snapshots. Publication
|
|
42
|
+
is one transaction, and equal payloads perform zero writes. Corruption, stale
|
|
43
|
+
source and incompatible configuration trigger a source rebuild. Failed storage
|
|
44
|
+
publication is reported and retried; an incomplete snapshot cannot count as a
|
|
45
|
+
successful publication. Storage can be the same store or another declared store.
|
|
46
|
+
It is never queried as a catalog. `stats()` reports reads, writes, restores,
|
|
47
|
+
rebuilds, recovery reasons and retained resources. Disposal unsubscribes, cancels
|
|
48
|
+
and drains in-flight reads, clears rows and indexes, and leaves store ownership
|
|
49
|
+
with the caller.
|
|
50
|
+
|
|
51
|
+
`createLexicalRangeProvider` from `@jarenjs/linq/db` composes this source with the
|
|
52
|
+
shared range provider. It requires complete membership within explicit match and
|
|
53
|
+
source-byte credits; a truncated top-k refuses construction. The adapter preserves
|
|
54
|
+
query and source identities, exposes exact counts only for complete membership,
|
|
55
|
+
and invalidates continuations after changes. Call `refresh()` after invalidation.
|
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.83.2",
|
|
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
|
+
"./search": {
|
|
11
|
+
"types": "./types/search.d.ts",
|
|
12
|
+
"default": "./src/search.js"
|
|
13
|
+
},
|
|
10
14
|
".": {
|
|
11
15
|
"types": "./types/index.d.ts",
|
|
12
16
|
"default": "./src/index.js"
|
|
@@ -84,9 +88,9 @@
|
|
|
84
88
|
"prepack": "npm run build:types"
|
|
85
89
|
},
|
|
86
90
|
"dependencies": {
|
|
87
|
-
"@jarenjs/core": "^0.
|
|
88
|
-
"@jarenjs/json": "^0.
|
|
89
|
-
"@jarenjs/validate": "^0.
|
|
91
|
+
"@jarenjs/core": "^0.83.2",
|
|
92
|
+
"@jarenjs/json": "^0.83.2",
|
|
93
|
+
"@jarenjs/validate": "^0.83.2"
|
|
90
94
|
},
|
|
91
95
|
"bin": {
|
|
92
96
|
"jaren-db": "./src/cli.js"
|
|
@@ -29,11 +29,60 @@
|
|
|
29
29
|
"type": "string"
|
|
30
30
|
},
|
|
31
31
|
"steps": {
|
|
32
|
-
"description": "Ordered steps; the order is the contract. A pure widening may carry NO steps
|
|
32
|
+
"description": "Ordered steps; the order is the contract. A pure widening may carry NO steps \u2014 the migration then only moves the recorded shape.",
|
|
33
33
|
"type": "array",
|
|
34
34
|
"items": {
|
|
35
35
|
"$ref": "#/definitions/step"
|
|
36
36
|
}
|
|
37
|
+
},
|
|
38
|
+
"physical": {
|
|
39
|
+
"type": "object",
|
|
40
|
+
"required": [
|
|
41
|
+
"source",
|
|
42
|
+
"dispositions",
|
|
43
|
+
"assertions"
|
|
44
|
+
],
|
|
45
|
+
"properties": {
|
|
46
|
+
"source": {
|
|
47
|
+
"type": "array",
|
|
48
|
+
"items": {
|
|
49
|
+
"type": "object"
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"dispositions": {
|
|
53
|
+
"type": "object",
|
|
54
|
+
"additionalProperties": {
|
|
55
|
+
"enum": [
|
|
56
|
+
"preserve",
|
|
57
|
+
"replace",
|
|
58
|
+
"drop"
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"assertions": {
|
|
63
|
+
"type": "array",
|
|
64
|
+
"items": {
|
|
65
|
+
"type": "object",
|
|
66
|
+
"required": [
|
|
67
|
+
"sql",
|
|
68
|
+
"expected"
|
|
69
|
+
],
|
|
70
|
+
"properties": {
|
|
71
|
+
"sql": {
|
|
72
|
+
"type": "string"
|
|
73
|
+
},
|
|
74
|
+
"params": {
|
|
75
|
+
"type": "array"
|
|
76
|
+
},
|
|
77
|
+
"expected": {
|
|
78
|
+
"type": "array"
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
"additionalProperties": false
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
"additionalProperties": false
|
|
37
86
|
}
|
|
38
87
|
},
|
|
39
88
|
"required": [
|
|
@@ -150,7 +199,7 @@
|
|
|
150
199
|
"additionalProperties": false
|
|
151
200
|
},
|
|
152
201
|
"deriveStep": {
|
|
153
|
-
"description": "Recompute named STORED derived index columns from the documents already in a collection. A spatial column is stored only under the physical mapping that cannot index a registered deterministic function (the generated mapping populates itself); a vector column is stored under both. The step is idempotent
|
|
202
|
+
"description": "Recompute named STORED derived index columns from the documents already in a collection. A spatial column is stored only under the physical mapping that cannot index a registered deterministic function (the generated mapping populates itself); a vector column is stored under both. The step is idempotent \u2014 a derived value is a pure function of the document \u2014 so a replay writes what the first run wrote.",
|
|
154
203
|
"type": "object",
|
|
155
204
|
"properties": {
|
|
156
205
|
"kind": {
|
|
@@ -224,7 +273,7 @@
|
|
|
224
273
|
"additionalProperties": false
|
|
225
274
|
},
|
|
226
275
|
"sqlStep": {
|
|
227
|
-
"description": "A DATA step spelled directly as one SQL statement (MIGRATION-FORMAT
|
|
276
|
+
"description": "A DATA step spelled directly as one SQL statement (MIGRATION-FORMAT \u00a79.4): the planner's fold of a dropped column into the document, the move of a document member into its new column, an R*Tree backfill. Executed exactly as a ddl step; distinct in kind so a reviewer reads intent and a dry run shows it as data.",
|
|
228
277
|
"type": "object",
|
|
229
278
|
"properties": {
|
|
230
279
|
"kind": {
|
|
@@ -245,7 +294,7 @@
|
|
|
245
294
|
"additionalProperties": false
|
|
246
295
|
},
|
|
247
296
|
"rebuildStep": {
|
|
248
|
-
"description": "The ALTER TABLE procedure SQLite lacks (MIGRATION-FORMAT
|
|
297
|
+
"description": "The ALTER TABLE procedure SQLite lacks (MIGRATION-FORMAT \u00a710), self-contained: create the target shape under `<table>__rebuild`, copy the rows through the rendered column mapping, drop the old table, rename, recreate the indexes, then `PRAGMA foreign_key_check` inside the transaction.",
|
|
249
298
|
"type": "object",
|
|
250
299
|
"properties": {
|
|
251
300
|
"kind": {
|
|
@@ -265,7 +314,7 @@
|
|
|
265
314
|
}
|
|
266
315
|
},
|
|
267
316
|
"copy": {
|
|
268
|
-
"description": "The INSERT
|
|
317
|
+
"description": "The INSERT \u2026 SELECT that carries every row across the column mapping.",
|
|
269
318
|
"type": "string",
|
|
270
319
|
"minLength": 1
|
|
271
320
|
},
|