@jarenjs/db 0.49.2 → 0.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +27 -15
- package/README.md +141 -41
- package/docs/JOBS-FORMAT.md +24 -8
- package/docs/LIVE-FORMAT.md +38 -9
- package/docs/MIGRATION-FORMAT.md +118 -36
- package/docs/MODEL-FORMAT.md +232 -30
- package/package.json +4 -5
- package/schemas/jaren-migration.draft-07.schema.json +73 -0
- package/schemas/jaren-migration.schema.json +73 -0
- package/src/capture.js +66 -28
- package/src/cli.js +225 -44
- package/src/ddl.js +23 -3
- package/src/dialects/sqlite.js +2 -1
- package/src/driver.js +63 -16
- package/src/drivers/wasm.js +1 -0
- package/src/emit-model.js +14 -0
- package/src/emit.js +10 -3
- package/src/entity.js +92 -47
- package/src/errors.js +25 -0
- package/src/index.js +2 -2
- package/src/jobs.js +40 -5
- package/src/live-time.js +12 -3
- package/src/live.js +11 -1
- package/src/migrate.js +397 -191
- package/src/model.js +173 -8
- package/src/plan.js +135 -38
- package/src/query.js +138 -13
- package/src/store.js +221 -66
- package/src/tracker.js +173 -48
- package/types/index.d.ts +152 -10
- package/types/node.d.ts +3 -1
- package/types/typed.d.ts +58 -2
- package/types/wasm.d.ts +7 -0
- package/dist/types/algebra.d.ts +0 -230
- package/dist/types/app.d.ts +0 -49
- package/dist/types/capture.d.ts +0 -85
- package/dist/types/cli.d.ts +0 -2
- package/dist/types/dag-job.d.ts +0 -40
- package/dist/types/ddl.d.ts +0 -229
- package/dist/types/derive.d.ts +0 -250
- package/dist/types/dialect.d.ts +0 -154
- package/dist/types/dialects/sqlite.d.ts +0 -9
- package/dist/types/driver.d.ts +0 -110
- package/dist/types/drivers/bun.d.ts +0 -47
- package/dist/types/drivers/node.d.ts +0 -37
- package/dist/types/drivers/wasm.d.ts +0 -65
- package/dist/types/emit-model.d.ts +0 -44
- package/dist/types/emit.d.ts +0 -75
- package/dist/types/entity.d.ts +0 -23
- package/dist/types/errors.d.ts +0 -170
- package/dist/types/graph.d.ts +0 -28
- package/dist/types/index.d.ts +0 -37
- package/dist/types/jobs.d.ts +0 -140
- package/dist/types/knn.d.ts +0 -69
- package/dist/types/live-time.d.ts +0 -141
- package/dist/types/live.d.ts +0 -64
- package/dist/types/migrate.d.ts +0 -170
- package/dist/types/model.d.ts +0 -36
- package/dist/types/patch-sql.d.ts +0 -37
- package/dist/types/plan.d.ts +0 -142
- package/dist/types/profile.d.ts +0 -80
- package/dist/types/query.d.ts +0 -112
- package/dist/types/residual.d.ts +0 -64
- package/dist/types/series.d.ts +0 -227
- package/dist/types/store.d.ts +0 -60
- package/dist/types/tracker.d.ts +0 -43
- package/dist/types/typed.d.ts +0 -15
- package/dist/types/types.d.ts +0 -26
- package/dist/types/udf.d.ts +0 -75
- package/dist/types/window.d.ts +0 -52
package/docs/MODEL-FORMAT.md
CHANGED
|
@@ -163,9 +163,9 @@ Three things follow, and none of them is optional:
|
|
|
163
163
|
per-mapping cell.
|
|
164
164
|
|
|
165
165
|
**What it costs, both halves** (`benchmark/spatial.js`, the store's own
|
|
166
|
-
rows over 50 000 points): the same `$within` measures <!--
|
|
167
|
-
costs <!--
|
|
168
|
-
connection, the same probe is <!--
|
|
166
|
+
rows over 50 000 points): the same `$within` measures <!--fact:spatial.rtreeStore-->0.46 ms against 2 ms — 4.3× in the R\*Tree's favour<!--/fact-->, and loading them
|
|
167
|
+
costs <!--fact:spatial.rtreeLoad-->718 ms against 399 ms for 50,000 documents in one transaction — 1.8× the write cost<!--/fact-->. Isolated from the store on a raw
|
|
168
|
+
connection, the same probe is <!--fact:spatial.rtree-->0.3 ms against 1.9 ms — 6.4× in the R\*Tree's favour<!--/fact-->. Read speed bought with write cost and a
|
|
169
169
|
second table: choose it deliberately, per index, which is why it is
|
|
170
170
|
neither automatic nor a store-wide option.
|
|
171
171
|
|
|
@@ -198,11 +198,11 @@ Three things follow:
|
|
|
198
198
|
|
|
199
199
|
**What it buys, and what it costs.** Measured against the same query
|
|
200
200
|
over a collection with no such column — the whole embedding parsed out
|
|
201
|
-
of the stored JSON per row — the column is worth <!--
|
|
202
|
-
on the read, and costs <!--
|
|
201
|
+
of the stored JSON per row — the column is worth <!--fact:vector.jsonDoc-->15.0× the plan at 10,000 × 768<!--/fact-->
|
|
202
|
+
on the read, and costs <!--fact:vector.write-->10.6 s against 5.0 s for 50,000 documents in one transaction — 2.1× the write cost<!--/fact-->
|
|
203
203
|
on the way in, because every write pays a JSON round trip of the member
|
|
204
204
|
plus the normalize and the pack. On
|
|
205
|
-
disk it is <!--
|
|
205
|
+
disk it is <!--fact:vector.storage-->3,072 B packed against 16,141 B as a JSON number array inside the document — 5.3× smaller<!--/fact-->
|
|
206
206
|
per vector — smaller than the member, and *added* to it, since the
|
|
207
207
|
document still carries what the column is derived from. Choose it the
|
|
208
208
|
way `physical: 'rtree'` is chosen: per index, with both halves in view
|
|
@@ -617,6 +617,15 @@ returns a validation function; the function returns `true`/`false` or
|
|
|
617
617
|
and `store.capabilities.validated === false` — a declared downgrade.
|
|
618
618
|
The cost of running without one: the database constraints only see the
|
|
619
619
|
key and the indexed members; everything else is stored as given.
|
|
620
|
+
Stated more sharply, because it changes ANSWERS and not only what is
|
|
621
|
+
stored: a typed generated column carries SQLite affinity, so a value
|
|
622
|
+
that violates the collection schema — the string `'020'` under an
|
|
623
|
+
`integer` path — reads as the integer `20` in the column while the
|
|
624
|
+
document still holds text, and a pushed `$eq: ['$it.n', '20']` finds
|
|
625
|
+
a row the engine, which compares the JSON value, does not. On an
|
|
626
|
+
unvalidated store the native and residual paths agree only over
|
|
627
|
+
documents that conform to the schema; with the hook injected no other
|
|
628
|
+
document is ever stored.
|
|
620
629
|
`@jarenjs/db` never runs a validator of its own; what it imports from
|
|
621
630
|
`@jarenjs/validate` is only the pure same-document `$ref`/`$anchor`
|
|
622
631
|
resolution in `@jarenjs/validate/normalize`, for model compilation.
|
|
@@ -640,6 +649,13 @@ throw rolls back exactly its own level and rethrows — an outer
|
|
|
640
649
|
transaction that catches the error continues and its own work
|
|
641
650
|
commits. There is no implicit retry.
|
|
642
651
|
|
|
652
|
+
**On an asynchronous driver a refused write rejects.** Every write —
|
|
653
|
+
`insert`, `put`, `patch`, `delete`, the entity set's `create`/`update`/
|
|
654
|
+
`delete` and the queue's `enqueue` — answers its coded error (`JD2001`,
|
|
655
|
+
`JD2003`, `JD2005`, …) through the promise it returned: never a
|
|
656
|
+
synchronous throw, and never an unhandled rejection beside a result
|
|
657
|
+
that looks like success.
|
|
658
|
+
|
|
643
659
|
### 5.1 Transaction ownership
|
|
644
660
|
|
|
645
661
|
A SQLite connection holds ONE savepoint stack, so two transactions that
|
|
@@ -727,6 +743,7 @@ error.
|
|
|
727
743
|
| `JD0030` | an unknown x-entity member was declared |
|
|
728
744
|
| `JD0031` | relation declarations contradict each other |
|
|
729
745
|
| `JD0032` | the include specification is invalid |
|
|
746
|
+
| `JD0033` | an entity query names no entity array |
|
|
730
747
|
| `JD0040` | the save spans a relation cycle |
|
|
731
748
|
| `JD0050` | live queries require change capture |
|
|
732
749
|
| `JD0051` | the demanded live mode is unavailable |
|
|
@@ -745,6 +762,7 @@ error.
|
|
|
745
762
|
| `JD2060` | the maintained live state exceeded its bound |
|
|
746
763
|
| `JD2061` | another context owns the database |
|
|
747
764
|
| `JD2062` | the store closed with job handlers still in flight |
|
|
765
|
+
| `JD2063` | the store is closed |
|
|
748
766
|
|
|
749
767
|
The table above is proven in sync with the runtime `DB_CODES` table by
|
|
750
768
|
a test.
|
|
@@ -763,7 +781,11 @@ collection.query(doc, { profile: { maxRows: 200, externals: ['min'] } });
|
|
|
763
781
|
```
|
|
764
782
|
|
|
765
783
|
`'safe'` is the default table; a profile object overrides members over
|
|
766
|
-
it.
|
|
784
|
+
it. A per-call `profile` REPLACES the store's for that call — it is
|
|
785
|
+
normalized over the `'safe'` defaults, not over the store's profile —
|
|
786
|
+
so a store-level mandatory predicate or allow-list does not carry into
|
|
787
|
+
`execute(doc, { profile: { maxRows: 50 } })`: spell the whole profile
|
|
788
|
+
per call, or set it once on the store and pass none. The defaults: engine limits
|
|
767
789
|
`{ sequenceItems: 100000, resultItems: 10000, steps: 1000000, depth: 32 }`,
|
|
768
790
|
`maxRows: 1000`, no externals, no host functions, no collations, all
|
|
769
791
|
of the store's collections, no mandatory predicates, no scan refusal.
|
|
@@ -945,15 +967,15 @@ changing state. Measured on the same terms as the `$sqrt` rows
|
|
|
945
967
|
mean of 20 executions after a warm one; the residual comparator
|
|
946
968
|
pushes everything BUT the spatial conjunct):
|
|
947
969
|
|
|
948
|
-
<!--
|
|
970
|
+
<!--fact:spatial.udfTable-->
|
|
949
971
|
| shape | pushed (ms) | residual (ms) | verdict |
|
|
950
972
|
|---|---|---|---|
|
|
951
973
|
| solo `$within` over a full scan | 94 | 80 | ~even |
|
|
952
974
|
| indexed `$eq` **and** `$within` (~5 % pass the index) | 5.7 | 52 | push **9.1×** |
|
|
953
975
|
| `$within` with `LIMIT 10` | 1.9 | 77 | push **41.0×** |
|
|
954
|
-
<!--/
|
|
976
|
+
<!--/fact-->
|
|
955
977
|
|
|
956
|
-
So the spatial hatch <!--
|
|
978
|
+
So the spatial hatch <!--fact:spatial.udfVerdict-->earns its row: 9.1× beside the selective conjunct and 41.0× under the LIMIT<!--/fact-->,
|
|
957
979
|
by the same rule as `$sqrt`: a sole `$within` over a full scan is a
|
|
958
980
|
loss (the UDF re-parses every row in the callback, and the exact
|
|
959
981
|
containment test is dearer than a square root), a `$within` beside
|
|
@@ -1013,7 +1035,10 @@ names:
|
|
|
1013
1035
|
The schema stays a valid JSON Schema throughout: strip every
|
|
1014
1036
|
`x-entity` member and it accepts and rejects exactly the same values
|
|
1015
1037
|
(test-asserted over a corpus). The vocabulary is invisible to the
|
|
1016
|
-
validator by the same argument as `x-form`.
|
|
1038
|
+
validator by the same argument as `x-form`. A model document may also
|
|
1039
|
+
be written by code — `@jarenjs/linq/model`'s `defineModel()` emits
|
|
1040
|
+
exactly this document ([MODEL-PEN.md](../../linq/docs/MODEL-PEN.md)), and this section stays its
|
|
1041
|
+
one specification.
|
|
1017
1042
|
|
|
1018
1043
|
### 9.2 The `x-entity` vocabulary (a closed set)
|
|
1019
1044
|
|
|
@@ -1033,6 +1058,13 @@ happen, so this vocabulary is deliberately stricter than the
|
|
|
1033
1058
|
validator's ignore-unknown posture — the strictness is local to the
|
|
1034
1059
|
one namespace this package owns.
|
|
1035
1060
|
|
|
1061
|
+
The vocabulary is closed in POSITION as well as in name: `x-entity` is
|
|
1062
|
+
read on an entity's top-level properties (and through their `allOf`,
|
|
1063
|
+
`$defs` and `definitions` blocks) only. A block nested anywhere else —
|
|
1064
|
+
under a property's `properties`, `items`, `anyOf`, … — is never walked
|
|
1065
|
+
for mapping directives, so one found there is `JD0030` at its `docPath`
|
|
1066
|
+
rather than a key, an index or a relation that silently never existed.
|
|
1067
|
+
|
|
1036
1068
|
### 9.3 The hybrid mapping
|
|
1037
1069
|
|
|
1038
1070
|
Stated once, mechanically applied, and returned as data by
|
|
@@ -1042,7 +1074,8 @@ Stated once, mechanically applied, and returned as data by
|
|
|
1042
1074
|
|---|---|
|
|
1043
1075
|
| scalar (`string`/`number`/`integer`/`boolean`) at the top level | a real typed column |
|
|
1044
1076
|
| `format: date-time`/`date` with `column: "integer"` | an epoch-milliseconds `INTEGER` column; the document keeps the RFC 3339 string, the column carries the derived epoch |
|
|
1045
|
-
| `enum` of scalars | a column plus a `CHECK (column IN (…))` |
|
|
1077
|
+
| `enum` of scalars | a column plus a `CHECK (column IN (…))` — a `null` member of the enum is left to the column's nullability, never written into the list |
|
|
1078
|
+
| a union of several scalar types (`['string', 'integer']`) | the JSONB document — a column has one affinity and a union has several; `['integer', 'null']` is that scalar, nullable |
|
|
1046
1079
|
| nested object / array, or `column: "json"` | the JSONB document column, queryable by path exactly as in phase A |
|
|
1047
1080
|
| relation | a foreign-key column, or a join table for many-to-many (§9.4) |
|
|
1048
1081
|
|
|
@@ -1052,13 +1085,39 @@ everything else; a read merges them back. **The absent-versus-null
|
|
|
1052
1085
|
rule, plainly**: for a column-mapped scalar, JSON `null` and absence
|
|
1053
1086
|
both store as SQL `NULL` and read back as ABSENT. A property that
|
|
1054
1087
|
needs present-`null` semantics declares `column: "json"` and stays in
|
|
1055
|
-
the document.
|
|
1088
|
+
the document. The rule reaches the WRITE's return value, not only the
|
|
1089
|
+
read: `create()`, `update()` and `add()` answer the document a read
|
|
1090
|
+
would answer, so a `null` handed in for a column-mapped scalar is
|
|
1091
|
+
absent from what comes back — a returned object never names a member
|
|
1092
|
+
no read will show. An epoch column (§9.3's `column: "integer"` row)
|
|
1093
|
+
keeps its string in the document, present-`null` included.
|
|
1056
1094
|
|
|
1057
1095
|
### 9.4 Relations and referential integrity
|
|
1058
1096
|
|
|
1059
1097
|
Declared on one side, inferred on the other; when both sides declare,
|
|
1060
1098
|
the inverses MUST agree (`JD0031` on any contradiction).
|
|
1061
1099
|
|
|
1100
|
+
What counts as "the same edge" is decided by `via`, not by the pair of
|
|
1101
|
+
entity names: two declarations pair up when they name the same
|
|
1102
|
+
foreign-key property, and a pair is one `many` side and one `one`
|
|
1103
|
+
side. Two `many` sides, or two `one` sides, on one `via` is `JD0031`,
|
|
1104
|
+
and a paired edge must agree on `onDelete`.
|
|
1105
|
+
|
|
1106
|
+
Between one pair of entities, a `many` declaration facing a `one`
|
|
1107
|
+
declaration with a DIFFERENT `via` is `JD0031` as well (the pair
|
|
1108
|
+
disagrees on its key) — the check is per entity pair, so **two
|
|
1109
|
+
one-to-many edges between the same two entities declare their inverse
|
|
1110
|
+
on ONE side only**. The author-and-editor model is written with both
|
|
1111
|
+
edges on `User` (`authored` by `authorId`, `edited` by `editorId`) and
|
|
1112
|
+
the foreign-key properties on `Post`, or with both edges on `Post` and
|
|
1113
|
+
none on `User`; declaring `Post.author` AND `User.edited` makes
|
|
1114
|
+
`User.authored` face `Post.editor` and is refused. Two declarations of
|
|
1115
|
+
the SAME kind with different `via`s are two independent edges — which
|
|
1116
|
+
is how a legitimate cycle is written (`Post.author` by `authorId` and
|
|
1117
|
+
`User.featured` by `featuredPostId`, one-to-one each way).
|
|
1118
|
+
Many-to-many pairs must agree on the join table, and a many-to-many
|
|
1119
|
+
facing a foreign-key relation is `JD0031`.
|
|
1120
|
+
|
|
1062
1121
|
- **one-to-many** — `{ to, many: true, via, onDelete }`: `via` names
|
|
1063
1122
|
the foreign-key property on the TARGET entity (`authorId` on
|
|
1064
1123
|
`Post`). If the target declares that property it MUST be a
|
|
@@ -1096,19 +1155,34 @@ reads and deletes take `{ prop: value, … }`.
|
|
|
1096
1155
|
Applied on write in JavaScript, never by SQL `DEFAULT`, so the value
|
|
1097
1156
|
the application sees and the value stored are the same — and the
|
|
1098
1157
|
behaviour is identical on every driver. `"now"` stamps an RFC 3339
|
|
1099
|
-
UTC string on insert when the property is absent
|
|
1158
|
+
UTC string on insert when the property is absent — the calendar date
|
|
1159
|
+
alone (`YYYY-MM-DD`) on a `format: date` property, so the stamp
|
|
1160
|
+
validates against its own format; `"updated"` stamps
|
|
1100
1161
|
on insert AND on every update, always; `{ "value": … }` fills a
|
|
1101
1162
|
literal when absent; `{ "query": … }` evaluates a query document over
|
|
1102
1163
|
the document being written. Defaults run BEFORE validation, so the
|
|
1103
|
-
injected hook sees the completed document
|
|
1164
|
+
injected hook sees the completed document — with one member exempt: a
|
|
1165
|
+
store-allocated key (`default: "auto"`) is allocated by the database
|
|
1166
|
+
AFTER validation, so a write validates against the schema with that
|
|
1167
|
+
key dropped from `required` (the generated input type marks it
|
|
1168
|
+
optional for the same reason); the document the store answers carries
|
|
1169
|
+
it, and the read shape keeps it required. A `version` property is
|
|
1170
|
+
engine-owned and never defaulted by the caller: an insert without one
|
|
1171
|
+
writes `0` — not SQL `NULL`, which no `WHERE version = ?` guard could
|
|
1172
|
+
match — and every successful write bumps it (§11.5).
|
|
1104
1173
|
|
|
1105
1174
|
### 9.7 Error-code additions
|
|
1106
1175
|
|
|
1107
|
-
The entity engine adds
|
|
1108
|
-
`JD0030` — an unknown `x-entity` member; `JD0031` — relation
|
|
1109
|
-
declarations whose inverses contradict
|
|
1110
|
-
|
|
1111
|
-
|
|
1176
|
+
The entity engine adds three codes to the package's single table (§7):
|
|
1177
|
+
`JD0030` — an unknown or unread `x-entity` member; `JD0031` — relation
|
|
1178
|
+
declarations whose inverses contradict; `JD0033` — an entity query
|
|
1179
|
+
document that binds no entity array (§10.1). Everything else raises the
|
|
1180
|
+
existing codes: `JD0005` for structural model defects (a key, index or
|
|
1181
|
+
version property without a column of its own, a default on a relation,
|
|
1182
|
+
`default: "auto"` off the key, a self-referencing many-to-many, a
|
|
1183
|
+
foreign key onto a composite key), `JD2001` for a duplicate key on
|
|
1184
|
+
`create()` exactly as on a collection `insert`, and `JD2005` for
|
|
1185
|
+
database-refused writes including foreign-key violations.
|
|
1112
1186
|
|
|
1113
1187
|
## 10. Relational translation
|
|
1114
1188
|
|
|
@@ -1122,13 +1196,65 @@ translatable runs the set residual over the fetched entity root,
|
|
|
1122
1196
|
|
|
1123
1197
|
`store.execute(document)` queries the **multi-entity root**: the
|
|
1124
1198
|
engine-side value is `{ <EntityName>: [documents…], … }` and bindings
|
|
1125
|
-
range over `$.<Entity>[*]`.
|
|
1199
|
+
range over `$.<Entity>[*]`. A document that binds no entity array at
|
|
1200
|
+
all — a `$for` over a member the model does not name, or over a
|
|
1201
|
+
scalar — is `JD0033` at compile, never an empty answer. The
|
|
1202
|
+
array-constructor spelling `["$.<Entity>[*]"]`, which `@jarenjs/linq`
|
|
1203
|
+
emits so that an item that is itself an array stays one item
|
|
1204
|
+
(QUERY-PEN §3), names the same whole-entity source; the planner
|
|
1205
|
+
reads through it for collections (`["$[*]"]`) and entities alike.
|
|
1206
|
+
|
|
1207
|
+
An entity set is a provider (QUERY-PEN §8): `store.entity(name)` and
|
|
1208
|
+
`store.sync.entity(name)` carry `execute(document, options)` and
|
|
1209
|
+
`explain(document, options)` — the document is over the multi-entity
|
|
1210
|
+
root and goes to the entity translator whole, exactly as
|
|
1211
|
+
`store.execute` runs it — plus `root` (`"$.<Entity>[*]"`, the one
|
|
1212
|
+
spelling `collectEntityRoots` reads, so a chain binds its items through
|
|
1213
|
+
it) and `scope` (one identity per store, shared by every set, so two
|
|
1214
|
+
sets may be joined in one document). The store itself carries `roots`,
|
|
1215
|
+
the entity names, because it has no single root: a chain over it is
|
|
1216
|
+
refused by name (`JL0007`) rather than answering the mixed rows or
|
|
1217
|
+
counting the sets, and a document naming no entity array stays
|
|
1218
|
+
`JD0033`. A chain's element terminal hands over the one-item window
|
|
1219
|
+
`[<phrase>]` (QUERY-PEN §6); the planner reads through the window as
|
|
1220
|
+
it reads through the packed source — the phrase inside plans as if
|
|
1221
|
+
bare, and the store answers its rows as the ONE array item the engine
|
|
1222
|
+
would (`[]` for none, `[row]` for one, never singleton-unwrapped;
|
|
1223
|
+
`explain().wrapped` says so) — so `toArray()` over an entity set runs
|
|
1224
|
+
the translator, and a two-root equijoin returning a bare binding runs
|
|
1225
|
+
as one statement.
|
|
1226
|
+
This is the shape the differential oracle
|
|
1126
1227
|
can actually prove — the in-memory engine sees exactly the documents
|
|
1127
1228
|
the entity sets return (`test/db/oracle/relations/`). Relation-NAME
|
|
1128
1229
|
navigation (`$.author.name`) is deliberately not query-document sugar:
|
|
1129
1230
|
the engine has no embedded `author` member to walk, so no oracle could
|
|
1130
1231
|
vouch for it. Name-based navigation lives on the `load` surface
|
|
1131
|
-
(§10.4), where results and statement counts are the proof
|
|
1232
|
+
(§10.4), where results and statement counts are the proof — and, by
|
|
1233
|
+
code, on a query producer that lowers a hop to what the engine CAN walk.
|
|
1234
|
+
|
|
1235
|
+
For that, every entity set carries **`relations`**, the relation table
|
|
1236
|
+
a producer may read: a frozen plain record, one entry per declared
|
|
1237
|
+
relation member of the entity, `{ to, kind, via?, fkEntity?, fkTargets?,
|
|
1238
|
+
joinTable?, targetKey }` — `to` the target entity, `kind` one of
|
|
1239
|
+
`oneToOne`/`oneToMany`/`manyToMany`; for a foreign-key relation `via`
|
|
1240
|
+
the key property, `fkEntity` the entity holding it, `fkTargets` the
|
|
1241
|
+
entity it references and `targetKey` the key property it references
|
|
1242
|
+
there (the column a hop's equality compares `via` with; `kind` says
|
|
1243
|
+
which side holds the key: `oneToOne` the declaring entity, `oneToMany`
|
|
1244
|
+
the target); for a many-to-many `joinTable` and the target's `targetKey`.
|
|
1245
|
+
The store (`store.relations`) and every set's `scope` (`scope.relations`)
|
|
1246
|
+
carry the same tables keyed by entity name, so a producer holding one
|
|
1247
|
+
set can follow a hop into another root. `relationTables(entities)` is
|
|
1248
|
+
the one function that derives them. The document format is unchanged:
|
|
1249
|
+
`@jarenjs/linq`'s chain reads the table and lowers `p.author.email` to
|
|
1250
|
+
`{ $for: { r1: '$.User[*]' }, $where: { $eq: ['$r1.id', '$it.authorId'] },
|
|
1251
|
+
$return: '$r1.email' }` (QUERY-PEN §4, relation navigation) — a
|
|
1252
|
+
document a hand could have written, which this translator and the
|
|
1253
|
+
engine both run and the oracle proves (`15-linq-hops.json`). Every such
|
|
1254
|
+
lowered shape is a residual here (§10.6 — a projection, a comparison or
|
|
1255
|
+
an existence test over a phrase rather than a member path, an ordering
|
|
1256
|
+
by a phrase), named by `explain()` and refused under `strict`; none
|
|
1257
|
+
pushes natively in this version.
|
|
1132
1258
|
|
|
1133
1259
|
Per binding, predicates resolve through three reference flavors:
|
|
1134
1260
|
|
|
@@ -1212,8 +1338,11 @@ store.entity('User').load({
|
|
|
1212
1338
|
})
|
|
1213
1339
|
```
|
|
1214
1340
|
|
|
1215
|
-
Per-relation `where`/`orderBy`/`take` apply INSIDE the subquery
|
|
1216
|
-
|
|
1341
|
+
Per-relation `where`/`orderBy`/`take`/`skip` apply INSIDE the subquery
|
|
1342
|
+
(`LIMIT`/`OFFSET` per parent row) — the point where naive loaders fall
|
|
1343
|
+
back to N+1; `after` (§10.5) paginates the ROOT alone, since a keyset
|
|
1344
|
+
cursor is one position in one ordered set, and on an include it is
|
|
1345
|
+
`JD0032`. Clauses compile against
|
|
1217
1346
|
the child's own reference flavors; an untranslatable clause is a
|
|
1218
1347
|
refusal (`JD0032`) naming the include path, never a silent residual.
|
|
1219
1348
|
Include depth is bounded (default 3, override with `maxDepth`);
|
|
@@ -1242,14 +1371,33 @@ a number appears only where `capabilities.rowEstimates` is filled):
|
|
|
1242
1371
|
|
|
1243
1372
|
- three or more bindings;
|
|
1244
1373
|
- non-equality join predicates, and disjunctions spanning bindings;
|
|
1245
|
-
- `$groupby
|
|
1246
|
-
|
|
1247
|
-
|
|
1374
|
+
- `$groupby`, except the `$time-bucket` ladder the series plan pushes
|
|
1375
|
+
(README, *Time series*) — the engine's post-group cardinality
|
|
1376
|
+
rebinding deserves its own order; the count-of-related-rows case
|
|
1377
|
+
ORMs are bad at is already native via `count: true` includes;
|
|
1248
1378
|
- projections (`$return` objects) — over one binding or across a join;
|
|
1249
1379
|
- externals against document paths; booleans and `null` at bind time;
|
|
1250
1380
|
- everything phase A already listed (§8 of `QUERY-FORMAT.md`
|
|
1251
1381
|
notwithstanding, the truth table is the contract).
|
|
1252
1382
|
|
|
1383
|
+
Two deviations between a pushed answer and the engine's are DECLARED
|
|
1384
|
+
rather than refused, because in both the database is right by its own
|
|
1385
|
+
arithmetic:
|
|
1386
|
+
|
|
1387
|
+
- `$sum`/`$avg` over `number` paths — SQLite sums with Kahan–Babuška
|
|
1388
|
+
compensation and the engine sums naively, so over `0.1, 0.2, 0.3`
|
|
1389
|
+
the store answers `0.6` and the residual `0.6000000000000001`: equal
|
|
1390
|
+
to within an ulp, never equal by `===` (the differential oracle
|
|
1391
|
+
draws dyadic fractions, which are exact on both sides).
|
|
1392
|
+
- a `$time-bucket` `$groupby` whose aggregated path admits `null`
|
|
1393
|
+
(`['number', 'null']`): the pushed `AVG` skips a `null` reading
|
|
1394
|
+
exactly as the series kernel — and therefore the `$resample`
|
|
1395
|
+
spelling — does, while the engine's `$avg` over a sequence holding
|
|
1396
|
+
`null` raises `JQ2001`; under this one spelling the store answers
|
|
1397
|
+
where the in-memory engine refuses. Everywhere else an ordering or
|
|
1398
|
+
an aggregate over a path that admits `null`, or over a boolean path,
|
|
1399
|
+
is a named residual, so the two paths keep answering alike.
|
|
1400
|
+
|
|
1253
1401
|
|
|
1254
1402
|
## 11. The unit of work
|
|
1255
1403
|
|
|
@@ -1293,8 +1441,19 @@ const report = await store.saveChanges(); // one transaction
|
|
|
1293
1441
|
|
|
1294
1442
|
### 11.2 Explicit updates (the other mode)
|
|
1295
1443
|
|
|
1296
|
-
`set.update(key, changes)` and `set.delete(key)`
|
|
1297
|
-
immediate statement, last-write-wins by contract.
|
|
1444
|
+
`set.create(doc)`, `set.update(key, changes)` and `set.delete(key)`
|
|
1445
|
+
skip tracking: one immediate statement, last-write-wins by contract.
|
|
1446
|
+
`create` applies defaults, validates, inserts, and — for a
|
|
1447
|
+
many-to-many relation whose member the document carries as an array
|
|
1448
|
+
of target keys — writes the join rows in the same transaction, which
|
|
1449
|
+
rolls back whole when a membership names a row that does not exist; a
|
|
1450
|
+
duplicate key is `JD2001`. `update` takes the document's own members
|
|
1451
|
+
only: a relation member (`posts`) is `JD2003` exactly as on `create`,
|
|
1452
|
+
and a membership array (`labels`) — which `create` writes — is
|
|
1453
|
+
`JD2003` on `update` as well, because an explicit update never writes
|
|
1454
|
+
join rows or child rows and never echoes back a member it did not
|
|
1455
|
+
store; a change to a key member is `JD2003` too (the key column would
|
|
1456
|
+
go stale — delete and create). An explicit update
|
|
1298
1457
|
still bumps a declared version property, so optimistic savers observe
|
|
1299
1458
|
the row changed. This is the path reactive layers and job runners use.
|
|
1300
1459
|
|
|
@@ -1337,7 +1496,10 @@ the 100× statement reduction is the point for anything remote.
|
|
|
1337
1496
|
|
|
1338
1497
|
### 11.5 Optimistic concurrency
|
|
1339
1498
|
|
|
1340
|
-
Declare a token with `version: true` (§9.2).
|
|
1499
|
+
Declare a token with `version: true` (§9.2). A row inserted without
|
|
1500
|
+
one starts at `0` — never SQL `NULL`, which no guard could match
|
|
1501
|
+
(§9.6) — so the first tracked save after `create()`/`add()` carries
|
|
1502
|
+
`WHERE version = 0`. Every `saveChanges()`
|
|
1341
1503
|
update and guarded delete carries `WHERE version = ?` (the SNAPSHOT
|
|
1342
1504
|
version) and writes snapshot + 1; a zero-row result is **`JD2040`**
|
|
1343
1505
|
carrying the entity and key, and the whole save rolls back. Without a
|
|
@@ -1367,3 +1529,43 @@ The return value is data, not a boolean:
|
|
|
1367
1529
|
elapsedMs,
|
|
1368
1530
|
}
|
|
1369
1531
|
```
|
|
1532
|
+
|
|
1533
|
+
### 11.7 Membership: `link` and `unlink`
|
|
1534
|
+
|
|
1535
|
+
A many-to-many membership is attached or detached one row at a time
|
|
1536
|
+
through the unit of work, without carrying the whole membership array:
|
|
1537
|
+
|
|
1538
|
+
```js
|
|
1539
|
+
users.link('u1', 'labels', 'admin'); // a key, or a document carrying the key
|
|
1540
|
+
users.unlink(ada, 'labels', { name: 'dev' }); // the own side is a key or a document too
|
|
1541
|
+
const report = await store.saveChanges(); // { joinInserted: 1, joinDeleted: 1, … }
|
|
1542
|
+
```
|
|
1543
|
+
|
|
1544
|
+
- `link`/`unlink` are local, synchronous bookkeeping, like `add`/`put`/
|
|
1545
|
+
`remove`; nothing reaches the database until `saveChanges()`.
|
|
1546
|
+
- The member MUST be a many-to-many relation of the entity — `JD2003`
|
|
1547
|
+
otherwise, naming the relation's kind or the missing declaration. The
|
|
1548
|
+
target is read exactly as a membership array's element is: a key, or
|
|
1549
|
+
a document carrying the target's key (`JD2003` when it carries none).
|
|
1550
|
+
- The own side needs the entity's key. A pending insert whose `auto`
|
|
1551
|
+
key the save allocates has none to attach to and is refused (`JD2003`:
|
|
1552
|
+
"save the entity first, then attach"); a pending insert with a
|
|
1553
|
+
caller-supplied key may be linked in the same save, since join rows
|
|
1554
|
+
run after the inserts (§11.4).
|
|
1555
|
+
- The baseline is the join table **as read at save time**, not a
|
|
1556
|
+
snapshot: linking a member that already exists and unlinking one that
|
|
1557
|
+
does not are no-ops, so a save repeated with the same calls changes
|
|
1558
|
+
nothing (asserted). The last word on one target wins — `unlink` after
|
|
1559
|
+
`link` means unlink.
|
|
1560
|
+
- A `link`/`unlink` beside a `put` carrying the SAME member's membership
|
|
1561
|
+
array folds into that array's key-set difference (§11.3): one intent
|
|
1562
|
+
per entity, own key and member, never two statements racing for one
|
|
1563
|
+
row.
|
|
1564
|
+
- A tracked snapshot's loaded membership array is not rewritten by a
|
|
1565
|
+
saved `link`/`unlink` — a projection stays what it was read as;
|
|
1566
|
+
re-read (`load({ include })`) to see the membership. `discard(key)`
|
|
1567
|
+
drops the key's pending membership changes together with its
|
|
1568
|
+
tracking, and a failed save leaves them pending, as §11.6 promises.
|
|
1569
|
+
- The report counts the rows written under `joinInserted`/`joinDeleted`
|
|
1570
|
+
and `stats().tracker.pendingMemberships` counts the pending
|
|
1571
|
+
(entity, key, member) records.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jarenjs/db",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.56.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"types": "./types/index.d.ts",
|
|
@@ -36,7 +36,6 @@
|
|
|
36
36
|
},
|
|
37
37
|
"files": [
|
|
38
38
|
"types/",
|
|
39
|
-
"dist/types/",
|
|
40
39
|
"src/",
|
|
41
40
|
"schemas/",
|
|
42
41
|
"docs/",
|
|
@@ -71,9 +70,9 @@
|
|
|
71
70
|
"prepack": "npm run build:types"
|
|
72
71
|
},
|
|
73
72
|
"dependencies": {
|
|
74
|
-
"@jarenjs/core": "^0.
|
|
75
|
-
"@jarenjs/json": "^0.
|
|
76
|
-
"@jarenjs/validate": "^0.
|
|
73
|
+
"@jarenjs/core": "^0.56.0",
|
|
74
|
+
"@jarenjs/json": "^0.56.0",
|
|
75
|
+
"@jarenjs/validate": "^0.56.0"
|
|
77
76
|
},
|
|
78
77
|
"bin": {
|
|
79
78
|
"jaren-db": "./src/cli.js"
|
|
@@ -58,6 +58,12 @@
|
|
|
58
58
|
},
|
|
59
59
|
{
|
|
60
60
|
"$ref": "#/definitions/deriveStep"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"$ref": "#/definitions/sqlStep"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"$ref": "#/definitions/rebuildStep"
|
|
61
67
|
}
|
|
62
68
|
]
|
|
63
69
|
},
|
|
@@ -216,6 +222,73 @@
|
|
|
216
222
|
"columns"
|
|
217
223
|
],
|
|
218
224
|
"additionalProperties": false
|
|
225
|
+
},
|
|
226
|
+
"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.",
|
|
228
|
+
"type": "object",
|
|
229
|
+
"properties": {
|
|
230
|
+
"kind": {
|
|
231
|
+
"const": "sql"
|
|
232
|
+
},
|
|
233
|
+
"sql": {
|
|
234
|
+
"type": "string",
|
|
235
|
+
"minLength": 1
|
|
236
|
+
},
|
|
237
|
+
"note": {
|
|
238
|
+
"type": "string"
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
"required": [
|
|
242
|
+
"kind",
|
|
243
|
+
"sql"
|
|
244
|
+
],
|
|
245
|
+
"additionalProperties": false
|
|
246
|
+
},
|
|
247
|
+
"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.",
|
|
249
|
+
"type": "object",
|
|
250
|
+
"properties": {
|
|
251
|
+
"kind": {
|
|
252
|
+
"const": "rebuild"
|
|
253
|
+
},
|
|
254
|
+
"table": {
|
|
255
|
+
"type": "string",
|
|
256
|
+
"pattern": "^[A-Za-z_][A-Za-z0-9_]*$"
|
|
257
|
+
},
|
|
258
|
+
"create": {
|
|
259
|
+
"description": "The CREATE statements of the temporary table.",
|
|
260
|
+
"type": "array",
|
|
261
|
+
"minItems": 1,
|
|
262
|
+
"items": {
|
|
263
|
+
"type": "string",
|
|
264
|
+
"minLength": 1
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
"copy": {
|
|
268
|
+
"description": "The INSERT … SELECT that carries every row across the column mapping.",
|
|
269
|
+
"type": "string",
|
|
270
|
+
"minLength": 1
|
|
271
|
+
},
|
|
272
|
+
"indexes": {
|
|
273
|
+
"description": "The index statements recreated under the final name.",
|
|
274
|
+
"type": "array",
|
|
275
|
+
"items": {
|
|
276
|
+
"type": "string",
|
|
277
|
+
"minLength": 1
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
"note": {
|
|
281
|
+
"type": "string"
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
"required": [
|
|
285
|
+
"kind",
|
|
286
|
+
"table",
|
|
287
|
+
"create",
|
|
288
|
+
"copy",
|
|
289
|
+
"indexes"
|
|
290
|
+
],
|
|
291
|
+
"additionalProperties": false
|
|
219
292
|
}
|
|
220
293
|
}
|
|
221
294
|
}
|
|
@@ -58,6 +58,12 @@
|
|
|
58
58
|
},
|
|
59
59
|
{
|
|
60
60
|
"$ref": "#/$defs/deriveStep"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"$ref": "#/$defs/sqlStep"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"$ref": "#/$defs/rebuildStep"
|
|
61
67
|
}
|
|
62
68
|
]
|
|
63
69
|
},
|
|
@@ -216,6 +222,73 @@
|
|
|
216
222
|
"columns"
|
|
217
223
|
],
|
|
218
224
|
"additionalProperties": false
|
|
225
|
+
},
|
|
226
|
+
"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.",
|
|
228
|
+
"type": "object",
|
|
229
|
+
"properties": {
|
|
230
|
+
"kind": {
|
|
231
|
+
"const": "sql"
|
|
232
|
+
},
|
|
233
|
+
"sql": {
|
|
234
|
+
"type": "string",
|
|
235
|
+
"minLength": 1
|
|
236
|
+
},
|
|
237
|
+
"note": {
|
|
238
|
+
"type": "string"
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
"required": [
|
|
242
|
+
"kind",
|
|
243
|
+
"sql"
|
|
244
|
+
],
|
|
245
|
+
"additionalProperties": false
|
|
246
|
+
},
|
|
247
|
+
"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.",
|
|
249
|
+
"type": "object",
|
|
250
|
+
"properties": {
|
|
251
|
+
"kind": {
|
|
252
|
+
"const": "rebuild"
|
|
253
|
+
},
|
|
254
|
+
"table": {
|
|
255
|
+
"type": "string",
|
|
256
|
+
"pattern": "^[A-Za-z_][A-Za-z0-9_]*$"
|
|
257
|
+
},
|
|
258
|
+
"create": {
|
|
259
|
+
"description": "The CREATE statements of the temporary table.",
|
|
260
|
+
"type": "array",
|
|
261
|
+
"minItems": 1,
|
|
262
|
+
"items": {
|
|
263
|
+
"type": "string",
|
|
264
|
+
"minLength": 1
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
"copy": {
|
|
268
|
+
"description": "The INSERT … SELECT that carries every row across the column mapping.",
|
|
269
|
+
"type": "string",
|
|
270
|
+
"minLength": 1
|
|
271
|
+
},
|
|
272
|
+
"indexes": {
|
|
273
|
+
"description": "The index statements recreated under the final name.",
|
|
274
|
+
"type": "array",
|
|
275
|
+
"items": {
|
|
276
|
+
"type": "string",
|
|
277
|
+
"minLength": 1
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
"note": {
|
|
281
|
+
"type": "string"
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
"required": [
|
|
285
|
+
"kind",
|
|
286
|
+
"table",
|
|
287
|
+
"create",
|
|
288
|
+
"copy",
|
|
289
|
+
"indexes"
|
|
290
|
+
],
|
|
291
|
+
"additionalProperties": false
|
|
219
292
|
}
|
|
220
293
|
}
|
|
221
294
|
}
|