@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.
Files changed (50) hide show
  1. package/ARCHITECTURE.md +20 -0
  2. package/README.md +25 -0
  3. package/docs/HOSTS.md +17 -0
  4. package/docs/JOBS-FORMAT.md +26 -0
  5. package/docs/LIVE-FORMAT.md +4 -0
  6. package/docs/MIGRATION-FORMAT.md +34 -0
  7. package/docs/MODEL-FORMAT.md +132 -1
  8. package/docs/NATIVE-PLANS.md +111 -0
  9. package/docs/SEARCH.md +55 -0
  10. package/package.json +8 -4
  11. package/schemas/jaren-migration.draft-07.schema.json +54 -5
  12. package/schemas/jaren-migration.schema.json +49 -0
  13. package/schemas/jaren-model.authoring.schema.json +360 -0
  14. package/schemas/jaren-model.draft-07.schema.json +128 -0
  15. package/schemas/jaren-model.schema.json +128 -0
  16. package/src/algebra.js +17 -1
  17. package/src/backup.js +12 -7
  18. package/src/cursor.js +27 -4
  19. package/src/dag-job.js +2 -1
  20. package/src/ddl.js +13 -0
  21. package/src/dialect.js +10 -0
  22. package/src/dialects/check-read.js +3 -3
  23. package/src/dialects/invariant-sql.js +117 -0
  24. package/src/dialects/postgres.js +14 -2
  25. package/src/dialects/sqlite.js +18 -2
  26. package/src/driver.js +1 -0
  27. package/src/drivers/bun.js +22 -4
  28. package/src/emit.js +80 -13
  29. package/src/entity.js +98 -41
  30. package/src/errors.js +8 -0
  31. package/src/graph.js +8 -1
  32. package/src/index.js +3 -0
  33. package/src/introspect.js +44 -7
  34. package/src/invariants.js +45 -0
  35. package/src/jobs.js +39 -6
  36. package/src/live.js +4 -1
  37. package/src/migrate.js +136 -22
  38. package/src/model.js +12 -0
  39. package/src/mutation.js +165 -0
  40. package/src/physical.js +147 -0
  41. package/src/plan.js +114 -20
  42. package/src/query.js +144 -70
  43. package/src/search.js +144 -0
  44. package/src/sql.js +60 -0
  45. package/src/store.js +49 -13
  46. package/src/tracker.js +63 -39
  47. package/src/window.js +1 -0
  48. package/types/index.d.ts +58 -3
  49. package/types/search.d.ts +20 -0
  50. 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.
@@ -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).
@@ -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.
@@ -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.
@@ -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, and the synchronous twin has no mode. The open path already
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.75.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.75.0",
88
- "@jarenjs/json": "^0.75.0",
89
- "@jarenjs/validate": "^0.75.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 the migration then only moves the recorded shape.",
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 a derived value is a pure function of the document so a replay writes what the first run wrote.",
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 §9.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.",
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 §10), 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.",
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 SELECT that carries every row across the column mapping.",
317
+ "description": "The INSERT \u2026 SELECT that carries every row across the column mapping.",
269
318
  "type": "string",
270
319
  "minLength": 1
271
320
  },