@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.
- package/ARCHITECTURE.md +70 -7
- package/README.md +69 -6
- package/docs/HOSTS.md +17 -0
- package/docs/JOBS-FORMAT.md +26 -0
- package/docs/LIVE-FORMAT.md +52 -13
- package/docs/MIGRATION-FORMAT.md +34 -0
- package/docs/MODEL-FORMAT.md +163 -15
- package/docs/NATIVE-PLANS.md +111 -0
- package/docs/REPLICATION-FORMAT.md +19 -13
- 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 +26 -4
- 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/derive.js +14 -3
- package/src/dialect.js +12 -0
- package/src/dialects/check-read.js +151 -0
- package/src/dialects/invariant-sql.js +117 -0
- package/src/dialects/postgres.js +28 -4
- package/src/dialects/sqlite.js +23 -3
- package/src/driver.js +1 -0
- package/src/drivers/bun.js +22 -4
- package/src/emit.js +133 -25
- 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 +81 -12
- package/src/invariants.js +45 -0
- package/src/jobs.js +39 -6
- package/src/live-nested.js +27 -10
- package/src/live.js +51 -136
- 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 +275 -64
- package/src/query.js +175 -78
- 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 +59 -4
- package/types/search.d.ts +20 -0
- package/types/typed.d.ts +1 -0
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 |
|
|
@@ -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
|
|
1507
|
-
|
|
1508
|
-
a window. Both answer what the engine answers,
|
|
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
|
-
|
|
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
|
|
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
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
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-
|
|
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 |
|
|
192
|
-
| selective join | rerun |
|
|
193
|
-
| high fan-out join | join |
|
|
194
|
-
| high fan-out join | rerun | 0.
|
|
195
|
-
| graph | graph | 7.
|
|
196
|
-
| graph | rerun | 4.
|
|
197
|
-
|
|
|
198
|
-
|
|
|
199
|
-
|
|
|
200
|
-
|
|
|
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.
|
|
205
|
-
| journal | 15 | 15 | 3169 | 0.
|
|
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.
|
|
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
|
},
|
|
@@ -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": [
|