@jarenjs/db 0.73.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 (53) hide show
  1. package/ARCHITECTURE.md +70 -7
  2. package/README.md +69 -6
  3. package/docs/HOSTS.md +17 -0
  4. package/docs/JOBS-FORMAT.md +26 -0
  5. package/docs/LIVE-FORMAT.md +52 -13
  6. package/docs/MIGRATION-FORMAT.md +34 -0
  7. package/docs/MODEL-FORMAT.md +163 -15
  8. package/docs/NATIVE-PLANS.md +111 -0
  9. package/docs/REPLICATION-FORMAT.md +19 -13
  10. package/docs/SEARCH.md +55 -0
  11. package/package.json +8 -4
  12. package/schemas/jaren-migration.draft-07.schema.json +54 -5
  13. package/schemas/jaren-migration.schema.json +49 -0
  14. package/schemas/jaren-model.authoring.schema.json +360 -0
  15. package/schemas/jaren-model.draft-07.schema.json +128 -0
  16. package/schemas/jaren-model.schema.json +128 -0
  17. package/src/algebra.js +26 -4
  18. package/src/backup.js +12 -7
  19. package/src/cursor.js +27 -4
  20. package/src/dag-job.js +2 -1
  21. package/src/ddl.js +13 -0
  22. package/src/derive.js +14 -3
  23. package/src/dialect.js +12 -0
  24. package/src/dialects/check-read.js +151 -0
  25. package/src/dialects/invariant-sql.js +117 -0
  26. package/src/dialects/postgres.js +28 -4
  27. package/src/dialects/sqlite.js +23 -3
  28. package/src/driver.js +1 -0
  29. package/src/drivers/bun.js +22 -4
  30. package/src/emit.js +133 -25
  31. package/src/entity.js +98 -41
  32. package/src/errors.js +8 -0
  33. package/src/graph.js +8 -1
  34. package/src/index.js +3 -0
  35. package/src/introspect.js +81 -12
  36. package/src/invariants.js +45 -0
  37. package/src/jobs.js +39 -6
  38. package/src/live-nested.js +27 -10
  39. package/src/live.js +51 -136
  40. package/src/migrate.js +136 -22
  41. package/src/model.js +12 -0
  42. package/src/mutation.js +165 -0
  43. package/src/physical.js +147 -0
  44. package/src/plan.js +275 -64
  45. package/src/query.js +175 -78
  46. package/src/search.js +144 -0
  47. package/src/sql.js +60 -0
  48. package/src/store.js +49 -13
  49. package/src/tracker.js +63 -39
  50. package/src/window.js +1 -0
  51. package/types/index.d.ts +59 -4
  52. package/types/search.d.ts +20 -0
  53. package/types/typed.d.ts +1 -0
@@ -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 |
@@ -1503,10 +1517,10 @@ The token is a promise about the FOLD, and three rules enforce it:
1503
1517
  the promotion needs the same schema-typed path the core `$sum` and
1504
1518
  `$avg` need. An absent member contributes nothing on either side.
1505
1519
 
1506
- A grouped fold is not promoted — a `$groupby` outside the fixed
1507
- temporal bucket is engine work (§6) and neither is an aggregate under
1508
- a window. Both answer what the engine answers, and `explain()` names
1509
- the reason. As with the scalar hatch, a profiled document triggers no
1520
+ A registered fold inside a `$groupby` return is not promoted — the
1521
+ grouped SQL fold currently handles the built-in aggregate set only and
1522
+ neither is an aggregate under a window. Both answer what the engine answers,
1523
+ and `explain()` names the reason. As with the scalar hatch, a profiled document triggers no
1510
1524
  registration: the same `$mean` under a profile folds in the residual,
1511
1525
  and `strict: true` refuses it by name (`JD0010`).
1512
1526
 
@@ -1826,16 +1840,24 @@ paths stay residual.
1826
1840
 
1827
1841
  ### 10.2 Joins
1828
1842
 
1829
- Two bindings joined by one equality between their column references
1830
- become an INNER equijoin — exactly the engine's
1843
+ Bindings connected by equalities between their column references
1844
+ become an INNER equijoin graph — exactly the engine's
1831
1845
  cross-product-plus-filter semantics. Result order is deterministic:
1832
- any `$orderby` keys first, then BOTH bindings' row identities in
1846
+ any `$orderby` keys first, then all bindings' row identities in
1833
1847
  binding order, which is the engine's nested-loop order. `explain()`
1834
1848
  reports the join (`{ left, right }`) and the `EXPLAIN QUERY PLAN`
1835
1849
  narrative; the paired foreign key carries an index (every foreign key
1836
1850
  does — unique for a strict one-to-one, plain otherwise), so the probe
1837
1851
  side of the join is a `SEARCH`, never a second scan.
1838
1852
 
1853
+ Projection trees of singular member paths and constants lower over one or
1854
+ several bindings. Counts over those trees count their items; a single-path
1855
+ count or window first excludes rows where that path is absent. Boolean
1856
+ predicates may span bindings when each leaf uses one binding's proven total
1857
+ forms. The mandatory column equalities still establish the join graph;
1858
+ a disjunction is never treated as a mandatory join edge. Compatible path
1859
+ comparisons within one binding also lower for non-null numbers and strings.
1860
+
1839
1861
  On the `load` surface the join KIND is derived from the schema
1840
1862
  (§10.4): a `oneToOne` include reports `inner (fk required)` when the
1841
1863
  `via` property is in `required`, `left (fk optional)` otherwise —
@@ -2034,17 +2056,26 @@ a number appears only where `capabilities.rowEstimates` is filled):
2034
2056
  must never emit by accident; every binding past the first attaches by
2035
2057
  a column equality to one already joined, and a graph that does not
2036
2058
  close is the engine's;
2037
- - non-equality join predicates, and disjunctions spanning bindings;
2038
- - a `$groupby` whose key is untyped or admits `null`, whose `$return`
2039
- reads the binding (after a grouping it holds the group's ROWS), or
2040
- whose `$orderby` names anything but a group key; a window over the
2041
- groups, or an aggregate of them;
2042
- - projections (`$return` objects) ACROSS a join over one binding a
2043
- nested shape of member paths lowers the projection tree);
2059
+ - a non-equality predicate used as the only join anchor, or a boolean
2060
+ predicate whose individual leaf cannot be assigned to one binding;
2061
+ - collection grouping with untyped keys, row-binding returns, or ordering by
2062
+ anything except an orderable key or count/min/max expression; entity grouping; aggregates over groups
2063
+ beyond a count of singleton constructors containing keys, literals and row
2064
+ counts; windows over group returns that may omit an item;
2065
+ - projection trees containing operators, whole entity binding references or
2066
+ non-singular paths; windows over those opaque projections;
2067
+ - `$distinct` over untyped/compound projections or ordering by other paths;
2068
+ typed scalar projections lower when unordered or ordered solely by the
2069
+ projected path, preserving first occurrence or that declared order;
2044
2070
  - externals against document paths; booleans and `null` at bind time;
2045
2071
  - everything phase A already listed (§8 of `QUERY-FORMAT.md`
2046
2072
  notwithstanding, the truth table is the contract).
2047
2073
 
2074
+ Collection group keys may include null or boolean values, but ordering those
2075
+ keys stays residual to preserve `JQ2005`. Group ordering is stable under ties.
2076
+ Literal windows over singleton group constructors lower directly, and the
2077
+ proven constructor count wraps the grouped statement in `COUNT(*)`.
2078
+
2048
2079
  Two deviations between a pushed answer and the engine's are DECLARED
2049
2080
  rather than refused, because in both the database is right by its own
2050
2081
  arithmetic:
@@ -2266,3 +2297,120 @@ const report = await store.saveChanges(); // { joinInserted: 1, joinDeleted
2266
2297
 
2267
2298
  Worker/pool options, synchronous cursors, wasm session probing and the browser
2268
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.
@@ -184,25 +184,31 @@ allocations observed after initialization, not precise retained-heap sizes.
184
184
 
185
185
  <!--fact:db.changeflow-->
186
186
 
187
- Measured 2026-09-08, v24.19.0, AMD Ryzen 9 5900HX with Radeon Graphics; 15 mutations per case.
187
+ Measured 2026-09-09, v24.20.0, AMD Ryzen 9 5900HX with Radeon Graphics; 15 mutations per case.
188
188
 
189
189
  | Shape | Strategy | Initialize ms | Mutation p50 ms | Mutation p95 ms | Initialization heap bytes |
190
190
  |---|---|---:|---:|---:|---:|
191
- | selective join | join | 16.663 | 0.323 | 2.119 | 4878848 |
192
- | selective join | rerun | 0.899 | 0.746 | 1.622 | 347160 |
193
- | high fan-out join | join | 7.314 | 1.901 | 3.179 | 4851232 |
194
- | high fan-out join | rerun | 0.860 | 0.850 | 1.188 | 340328 |
195
- | graph | graph | 7.462 | 0.459 | 0.750 | 4811712 |
196
- | graph | rerun | 4.539 | 4.042 | 4.979 | 1417928 |
197
- | nested groups | nested-group | 6.775 | 0.252 | 0.795 | 2057832 |
198
- | nested groups | rerun | 0.897 | 0.472 | 0.685 | 295808 |
199
- | offset groups | rerun | 1.519 | 0.459 | 0.547 | 383648 |
200
- | offset groups | rerun | 0.884 | 0.426 | 0.585 | 282280 |
191
+ | selective join | join | 15.740 | 0.349 | 2.173 | 4854712 |
192
+ | selective join | rerun | 1.040 | 0.850 | 1.321 | 347640 |
193
+ | high fan-out join | join | 8.115 | 1.984 | 3.040 | 4769632 |
194
+ | high fan-out join | rerun | 0.744 | 0.973 | 1.303 | 353768 |
195
+ | graph | graph | 7.742 | 0.429 | 0.728 | 4811904 |
196
+ | graph | rerun | 4.606 | 4.170 | 7.953 | 1418576 |
197
+ | multiple-key groups | group | 8.417 | 0.285 | 0.882 | 2107240 |
198
+ | multiple-key groups | rerun | 0.663 | 0.405 | 0.464 | 116960 |
199
+ | group aggregate | group | 3.839 | 0.236 | 0.398 | 1932544 |
200
+ | group aggregate | rerun | 0.846 | 0.449 | 0.670 | 275560 |
201
+ | distinct | distinct | 3.731 | 0.179 | 0.317 | 1616800 |
202
+ | distinct | rerun | 0.480 | 0.238 | 0.333 | 86128 |
203
+ | nested groups | nested-group | 4.565 | 0.206 | 0.377 | 1757528 |
204
+ | nested groups | rerun | 0.779 | 0.551 | 0.675 | 293560 |
205
+ | offset groups | rerun | 1.274 | 0.474 | 0.623 | 383792 |
206
+ | offset groups | rerun | 0.773 | 0.527 | 1.158 | 299416 |
201
207
 
202
208
  | Capture | Envelopes | Operations | Bytes | Apply p50 ms | Replay p50 ms | Conflict p50 ms |
203
209
  |---|---:|---:|---:|---:|---:|---:|---:|
204
- | session | 15 | 15 | 3169 | 0.458 | 0.118 | 0.204 |
205
- | journal | 15 | 15 | 3169 | 0.294 | 0.092 | 0.161 |
210
+ | session | 15 | 15 | 3169 | 0.463 | 0.119 | 0.213 |
211
+ | journal | 15 | 15 | 3169 | 0.333 | 0.097 | 0.160 |
206
212
 
207
213
  Selective maintenance avoids repeated full SQL evaluation. Initialization and high-fan-out maintenance can cost more than rerunning; the table includes both. Offset groups remain rerun in both requested modes.
208
214
 
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.73.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.73.0",
88
- "@jarenjs/json": "^0.73.0",
89
- "@jarenjs/validate": "^0.73.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
  },
@@ -34,6 +34,55 @@
34
34
  "items": {
35
35
  "$ref": "#/$defs/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": [