@jarenjs/linq 0.73.0 → 0.75.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 +8 -10
- package/README.md +7 -0
- package/docs/DB-CLIENT.md +2 -2
- package/docs/LINQ-FORMAT.md +8 -8
- package/docs/MESSAGES-PEN.md +1 -1
- package/docs/QUERY-PEN.md +56 -46
- package/package.json +6 -6
- package/src/federate.js +157 -139
- package/src/messages/vocabulary.js +2 -1
- package/types/index.d.ts +10 -2
- package/types/message-vocabulary.d.ts +2 -1
package/ARCHITECTURE.md
CHANGED
|
@@ -202,16 +202,14 @@ residual runs locally, and `explain()` reports the split.
|
|
|
202
202
|
(`JL0005`) rather than silently materialised — except two providers
|
|
203
203
|
sharing a `scope` (one store's entity sets), whose roots are two
|
|
204
204
|
bindings of one multi-entity input.
|
|
205
|
-
- **Federation is
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
key is kept rather than guessed at — and a budget is a refusal
|
|
214
|
-
(`JL2008`), never a spill.
|
|
205
|
+
- **Federation is explicit.** `federate()` (QUERY-PEN §12.1) accepts a
|
|
206
|
+
connected equality graph across named providers. Source-local work runs at
|
|
207
|
+
its provider; estimates choose a connected fetch order, hash sets reduce
|
|
208
|
+
candidates, and the engine retains the original tuple semantics. Packed
|
|
209
|
+
joins run inside out under the same cumulative row/byte admission credits.
|
|
210
|
+
Per-side budgets also cap intermediates; buffered production and final
|
|
211
|
+
results have the explicit limits documented in §12.1. Aliases receive
|
|
212
|
+
independent input members, and array framing preserves array-valued rows.
|
|
215
213
|
- **The engine result shape leaks nowhere.** Every surface — sync,
|
|
216
214
|
async, provider — reproduces `undefined | item | items` exactly,
|
|
217
215
|
which is why the window-wrapper trick exists at all.
|
package/README.md
CHANGED
|
@@ -730,3 +730,10 @@ Every subpath a consumer can import, derived from the manifest by
|
|
|
730
730
|
| `@jarenjs/linq/messages` | JavaScript | declared |
|
|
731
731
|
| `@jarenjs/linq/ai` | JavaScript | declared |
|
|
732
732
|
<!--/fact-->
|
|
733
|
+
|
|
734
|
+
Explicit cross-provider joins use `federate({ sources, maxRows, maxBytes,
|
|
735
|
+
maxTotalRows, maxTotalBytes })`. Connected joins across three or more sources,
|
|
736
|
+
including successive fluent `.join()` calls, preserve the engine's result
|
|
737
|
+
order while sharing cumulative admission credits. See
|
|
738
|
+
[QUERY-PEN §12.1](docs/QUERY-PEN.md#121-the-federation-boundary-federate) for
|
|
739
|
+
planning, buffering limits and cleanup guarantees.
|
package/docs/DB-CLIENT.md
CHANGED
|
@@ -826,10 +826,10 @@ never builds one; the migration between two of them is
|
|
|
826
826
|
|
|
827
827
|
## 7. Cost
|
|
828
828
|
|
|
829
|
-
`@jarenjs/linq/db` builds to **<!--fact:bundle.db-->
|
|
829
|
+
`@jarenjs/linq/db` builds to **<!--fact:bundle.db-->633,786<!--/fact--> bytes** as a minified,
|
|
830
830
|
tree-shaken ESM bundle — the figure `scripts/check-tree-shaking.js`
|
|
831
831
|
measures and `npm run test:tree-shaking` reports, published rounded
|
|
832
|
-
(<!--fact:bundle.db.kb-->
|
|
832
|
+
(<!--fact:bundle.db.kb-->634<!--/fact--> kB) beside the other nine subpath prices in
|
|
833
833
|
[docs/CONSUMING.md](../../../docs/CONSUMING.md).
|
|
834
834
|
|
|
835
835
|
It is by far the largest of the ten, and the reason is §1.1's edge rather
|
package/docs/LINQ-FORMAT.md
CHANGED
|
@@ -37,7 +37,7 @@ is the index of those guides, and it is how a reader reaches any of them.
|
|
|
37
37
|
| Document | Lines | What it writes, and when to open it |
|
|
38
38
|
|---|---:|---|
|
|
39
39
|
| [LINQ-FORMAT.md](LINQ-FORMAT.md) | 948 | this file, the binder and the family's **normative reference**: what a pen is, the rules all of them keep, the shared `JL01xx` table, and the cross-pen views derived from the guides it indexes. **Read it when** you want a rule that is true of every pen, an index of the documents, or one place to look up a method without knowing which pen owns it |
|
|
40
|
-
| [QUERY-PEN.md](QUERY-PEN.md) | 1,
|
|
40
|
+
| [QUERY-PEN.md](QUERY-PEN.md) | 1,735 | the chain, `.` — query documents (`jaren-query`) and the provider seam. **Read it when** you are querying data, or implementing a provider that answers a query document |
|
|
41
41
|
| [SCHEMA-PEN.md](SCHEMA-PEN.md) | 1,205 | `./schema` — JSON Schema 2020-12: the structural keywords, the constraints and the annotations, each with a method of its own, plus `$query`, `$defs`/`$ref` recursion and the normalizer's per-field predicates. **Read it when** you are describing the shape of data — for validation, for a form, or as the base of an entity |
|
|
42
42
|
| [MODEL-PEN.md](MODEL-PEN.md) | 1,092 | `./model` — the `x-entity` vocabulary on JSON Schema, and the `$model` 0.1 document `openStore` accepts unchanged. **Read it when** you are declaring a store's entities, their keys and their relations |
|
|
43
43
|
| [JSLT-PEN.md](JSLT-PEN.md) | 955 | `./jslt` — `$jslt` 0.1 stylesheets: the envelope and its rules, whose bodies are captured over the matched value. **Read it when** you are transforming one document into another |
|
|
@@ -205,7 +205,7 @@ and the bundle is the byte count the tree-shaking probe builds.
|
|
|
205
205
|
| Document | Subpath | Lines | Mapping rows | Worked examples | Refusals | Bundle |
|
|
206
206
|
|---|---|---:|---:|---:|---:|---:|
|
|
207
207
|
| [LINQ-FORMAT.md](LINQ-FORMAT.md) | — | 948 | — | — | — | — |
|
|
208
|
-
| [QUERY-PEN.md](QUERY-PEN.md) | `.` | 1,
|
|
208
|
+
| [QUERY-PEN.md](QUERY-PEN.md) | `.` | 1,735 | 34 | 8 | 15 | 174,241 B |
|
|
209
209
|
| [SCHEMA-PEN.md](SCHEMA-PEN.md) | `./schema` | 1,205 | 82 | 10 | 4 | 36,717 B |
|
|
210
210
|
| [MODEL-PEN.md](MODEL-PEN.md) | `./model` | 1,092 | 28 | 6 | 3 | 45,143 B |
|
|
211
211
|
| [JSLT-PEN.md](JSLT-PEN.md) | `./jslt` | 955 | 17 | 8 | 3 | 19,856 B |
|
|
@@ -215,12 +215,12 @@ and the bundle is the byte count the tree-shaking probe builds.
|
|
|
215
215
|
| [APP-PEN.md](APP-PEN.md) | `./app` | 1,143 | 22 | 7 | 3 | 51,006 B |
|
|
216
216
|
| [FORMS-PEN.md](FORMS-PEN.md) | `./forms` | 940 | 18 | 6 | 3 | 40,873 B |
|
|
217
217
|
| [AI-PEN.md](AI-PEN.md) | `./ai` | 98 | 12 | 1 | 3 | 16,904 B |
|
|
218
|
-
| [MESSAGES-PEN.md](MESSAGES-PEN.md) | `./messages` | 105 | 9 | 2 | 1 | 18,
|
|
218
|
+
| [MESSAGES-PEN.md](MESSAGES-PEN.md) | `./messages` | 105 | 9 | 2 | 1 | 18,363 B |
|
|
219
219
|
| [JTLT-PEN.md](JTLT-PEN.md) | `./jtlt` | 83 | 13 | 1 | 2 | 16,725 B |
|
|
220
220
|
| [PROJECT-PEN.md](PROJECT-PEN.md) | `./project` | 75 | 9 | 1 | 1 | 15,132 B |
|
|
221
221
|
| [CHARTS-PEN.md](CHARTS-PEN.md) | `./charts` | 94 | 21 | 1 | 1 | 17,014 B |
|
|
222
|
-
| [DB-CLIENT.md](DB-CLIENT.md) | `./db` | 893 | 41 | 4 | 2 |
|
|
223
|
-
| **16 documents** | | **12,
|
|
222
|
+
| [DB-CLIENT.md](DB-CLIENT.md) | `./db` | 893 | 41 | 4 | 2 | 633,786 B |
|
|
223
|
+
| **16 documents** | | **12,401** | **371** | **73** | | |
|
|
224
224
|
<!--/fact-->
|
|
225
225
|
|
|
226
226
|
A pen whose mapping rows are far below its worked examples is a pen
|
|
@@ -262,7 +262,7 @@ it and each document publishes it. The rounded column is what
|
|
|
262
262
|
<!--fact:pens.cost-->
|
|
263
263
|
| Subpath | Document | Bundle | Rounded |
|
|
264
264
|
|---|---|---:|---:|
|
|
265
|
-
| `@jarenjs/linq` | [QUERY-PEN.md](QUERY-PEN.md) | 174,
|
|
265
|
+
| `@jarenjs/linq` | [QUERY-PEN.md](QUERY-PEN.md) | 174,241 B | 174 kB |
|
|
266
266
|
| `@jarenjs/linq/schema` | [SCHEMA-PEN.md](SCHEMA-PEN.md) | 36,717 B | 37 kB |
|
|
267
267
|
| `@jarenjs/linq/model` | [MODEL-PEN.md](MODEL-PEN.md) | 45,143 B | 45 kB |
|
|
268
268
|
| `@jarenjs/linq/jslt` | [JSLT-PEN.md](JSLT-PEN.md) | 19,856 B | 20 kB |
|
|
@@ -272,11 +272,11 @@ it and each document publishes it. The rounded column is what
|
|
|
272
272
|
| `@jarenjs/linq/app` | [APP-PEN.md](APP-PEN.md) | 51,006 B | 51 kB |
|
|
273
273
|
| `@jarenjs/linq/forms` | [FORMS-PEN.md](FORMS-PEN.md) | 40,873 B | 41 kB |
|
|
274
274
|
| `@jarenjs/linq/ai` | [AI-PEN.md](AI-PEN.md) | 16,904 B | 17 kB |
|
|
275
|
-
| `@jarenjs/linq/messages` | [MESSAGES-PEN.md](MESSAGES-PEN.md) | 18,
|
|
275
|
+
| `@jarenjs/linq/messages` | [MESSAGES-PEN.md](MESSAGES-PEN.md) | 18,363 B | 18 kB |
|
|
276
276
|
| `@jarenjs/linq/jtlt` | [JTLT-PEN.md](JTLT-PEN.md) | 16,725 B | 17 kB |
|
|
277
277
|
| `@jarenjs/linq/project` | [PROJECT-PEN.md](PROJECT-PEN.md) | 15,132 B | 15 kB |
|
|
278
278
|
| `@jarenjs/linq/charts` | [CHARTS-PEN.md](CHARTS-PEN.md) | 17,014 B | 17 kB |
|
|
279
|
-
| `@jarenjs/linq/db` | [DB-CLIENT.md](DB-CLIENT.md) |
|
|
279
|
+
| `@jarenjs/linq/db` | [DB-CLIENT.md](DB-CLIENT.md) | 633,786 B | 634 kB |
|
|
280
280
|
<!--/fact-->
|
|
281
281
|
|
|
282
282
|
Read these as prices, not as scores. `./db` is the largest by an order of
|
package/docs/MESSAGES-PEN.md
CHANGED
|
@@ -100,6 +100,6 @@ gate compares generated files and all locale parameter sets with their sources.
|
|
|
100
100
|
|
|
101
101
|
## 7. Cost
|
|
102
102
|
|
|
103
|
-
The isolated messages pen costs **<!--fact:bundle.messages-->18,
|
|
103
|
+
The isolated messages pen costs **<!--fact:bundle.messages-->18,363<!--/fact--> bytes**.
|
|
104
104
|
It carries the shared template compiler and generated vocabulary, with no locale,
|
|
105
105
|
validator, forms or contract engine and no query chain.
|
package/docs/QUERY-PEN.md
CHANGED
|
@@ -472,8 +472,9 @@ time:
|
|
|
472
472
|
emits `{ "$for": { "it": "$.Post[*]", "it2": "$.User[*]" }, "$where":
|
|
473
473
|
{ "$eq": ["$it.authorId", "$it2.id"] }, "$return": "$it" }` — the shape
|
|
474
474
|
the store's translator answers in ONE statement when the result is a
|
|
475
|
-
bare binding
|
|
476
|
-
|
|
475
|
+
bare binding or a projection tree of member paths and constants
|
|
476
|
+
(MODEL-FORMAT §10.2). An opaque projection remains a declared residual
|
|
477
|
+
over the fetched roots (§10.6). `concat` stays
|
|
477
478
|
same-source even within a scope: one input per document. A scope MAY
|
|
478
479
|
carry `relations` — the relation tables of every root of the scope,
|
|
479
480
|
keyed by root name (a store's does) — which is where a chained hop
|
|
@@ -732,46 +733,55 @@ const rows = await fromAsync(fed.source('orders'))
|
|
|
732
733
|
spaces or other characters), and all of a federation's sources share one scope — which
|
|
733
734
|
is exactly what admits the join. The federation is what executes it:
|
|
734
735
|
|
|
735
|
-
1.
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
2.
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
the `resident
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
`
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
736
|
+
1. Each side's own filters and projection run at its source. Cursor sources
|
|
737
|
+
receive that document directly; buffered sources receive an array wrapper
|
|
738
|
+
so array-valued projected rows retain their item boundaries.
|
|
739
|
+
2. Mandatory equality predicates form a connected binding graph. The smallest
|
|
740
|
+
declared `estimatedRows` starts the fetch. Each subsequent side is the
|
|
741
|
+
smallest estimated side connected to an already fetched side. Missing or
|
|
742
|
+
equal estimates preserve binding declaration order. Equality inside an OR
|
|
743
|
+
does not establish an edge; disconnected bindings refuse `JL0005` before
|
|
744
|
+
any source opens.
|
|
745
|
+
3. Each next side is reduced against the retained keys of its already fetched
|
|
746
|
+
neighbours. Singular member/index paths are supported, including quoted
|
|
747
|
+
names and negative array indexes. Compound key values are conservatively
|
|
748
|
+
retained for the engine to compare.
|
|
749
|
+
4. The original query engine decides the result over those sets, in the
|
|
750
|
+
original binding order. Fetch order never changes tuple or result order.
|
|
751
|
+
Packed joins emitted by successive `.join()` calls execute inside out;
|
|
752
|
+
their projected results become bounded intermediate sides. Aliases of one
|
|
753
|
+
source retain independent fetched sets.
|
|
754
|
+
|
|
755
|
+
`explain(document)` reports `strategy`, per-side `budget`, `combinedBudget`,
|
|
756
|
+
`order` (the ordered side descriptions), and `resident.document`. Each side
|
|
757
|
+
names its source, estimate, join key, child document and `row`/`buffered`
|
|
758
|
+
streaming mode. Nested sides additionally expose `children`. `build` and
|
|
759
|
+
`probe` remain the first two sides for compatibility.
|
|
760
|
+
|
|
761
|
+
**Budgets are admission credits.** `maxRows` and `maxBytes` apply separately
|
|
762
|
+
at each source or intermediate. `maxTotalRows` and `maxTotalBytes` cover all
|
|
763
|
+
retained source and intermediate admissions in the entire call, and default
|
|
764
|
+
to twice their corresponding per-side budget. All four are positive safe
|
|
765
|
+
integers. Credits are cumulative: freeing an intermediate's inputs does not
|
|
766
|
+
refund them. This conservative rule makes a nested plan's total explicit.
|
|
767
|
+
|
|
768
|
+
A cursor refuses with `JL2008` before retaining the row that would exceed a
|
|
769
|
+
credit. Buffered sources are checked after they produce their array. Nested
|
|
770
|
+
resident phrases additionally cap `sequenceItems` and `resultItems` at
|
|
771
|
+
`maxRows` (or a stricter caller limit); exceeding those intermediate limits
|
|
772
|
+
raises `JL2008`. Intermediate bytes are checked after projection. These are
|
|
773
|
+
bounds on admitted data and intermediate item counts, **not** a promise to
|
|
774
|
+
bound a buffered provider's heap, the size of one constructed value, or the
|
|
775
|
+
final resident result. A caller can also supply query-engine limits for that
|
|
776
|
+
final result. Estimates affect planning only, never admission.
|
|
777
|
+
|
|
778
|
+
Every opened cursor closes exactly once on success, budget refusal, child
|
|
779
|
+
failure or cancellation. `signal` is checked at row boundaries and passed to
|
|
780
|
+
providers. Cleanup failures preserve a primary execution failure and never
|
|
781
|
+
strand another opened cursor.
|
|
782
|
+
|
|
783
|
+
There is no spill, distributed transaction or cross-source write. A merge
|
|
784
|
+
strategy remains unavailable because providers declare no ordering guarantee.
|
|
775
785
|
|
|
776
786
|
## 13. Worked examples
|
|
777
787
|
|
|
@@ -1195,7 +1205,7 @@ proves that too, so the exclusion cannot hide a chain refusal).
|
|
|
1195
1205
|
| `JL2004` | a provider's `execute()` answered a promise on the synchronous surface |
|
|
1196
1206
|
| `JL2005` | a push queue was fed after `end()` |
|
|
1197
1207
|
| `JL2006` | a provider answered an element terminal with something other than one array |
|
|
1198
|
-
| `JL2008` | a federated fetch reached
|
|
1208
|
+
| `JL2008` | a federated fetch or intermediate reached a per-side or combined row/byte budget (§12.1) |
|
|
1199
1209
|
|
|
1200
1210
|
`JL2007` is the client door's, not the chain's: `createDbLedger`'s stale
|
|
1201
1211
|
settlement ([DB-CLIENT.md §2.6](DB-CLIENT.md#26-the-ledger)); it is
|
|
@@ -1672,7 +1682,7 @@ are shorter:
|
|
|
1672
1682
|
## 17. Cost
|
|
1673
1683
|
|
|
1674
1684
|
A consumer importing `from` from `@jarenjs/linq` and calling one
|
|
1675
|
-
terminal bundles **<!--fact:bundle.chain-->174,
|
|
1685
|
+
terminal bundles **<!--fact:bundle.chain-->174,241<!--/fact--> bytes** (esbuild, ESM, minified, tree-shaken,
|
|
1676
1686
|
`platform: 'neutral'`). The figure is measured by
|
|
1677
1687
|
`scripts/check-tree-shaking.js`'s chain probe and compared with this
|
|
1678
1688
|
section on every `npm run test:tree-shaking`: it is derived, never typed,
|
|
@@ -1706,7 +1716,7 @@ making:
|
|
|
1706
1716
|
`docs/CONSUMING.md` states the rounded price of all ten subpaths in one
|
|
1707
1717
|
table, each figure held equal to the same measurements. Two of its rows
|
|
1708
1718
|
are the ones to read together: the chain at <!--fact:bundle.chain.kb-->174<!--/fact--> kB and
|
|
1709
|
-
`./db` at <!--fact:bundle.db.kb-->
|
|
1719
|
+
`./db` at <!--fact:bundle.db.kb-->634<!--/fact--> kB.
|
|
1710
1720
|
The client costs what the store costs, by construction, and the chain
|
|
1711
1721
|
costs what running a query costs.
|
|
1712
1722
|
|
|
@@ -1715,7 +1725,7 @@ the reason is worth knowing: a bundler counts a shared module once, and
|
|
|
1715
1725
|
the chain and every pen share the expression capture (`expression.js`)
|
|
1716
1726
|
and the coded errors under it (`errors.js`, and `@jarenjs/core`'s error
|
|
1717
1727
|
and object helpers). A consumer importing the chain AND the schema pen
|
|
1718
|
-
bundles **<!--fact:bundle.chain.withSchemaPen-->198,
|
|
1728
|
+
bundles **<!--fact:bundle.chain.withSchemaPen-->198,911<!--/fact--> bytes** — **<!--fact:bundle.chain.shared-->12,047<!--/fact--> bytes** less than the sum of the
|
|
1719
1729
|
figure above and [SCHEMA-PEN.md](SCHEMA-PEN.md#7-cost) §7's, which is
|
|
1720
1730
|
what those shared modules weigh. The probe measures that pair too, so
|
|
1721
1731
|
the saving is derived like everything else here. What the chain does NOT
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jarenjs/linq",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.75.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"types": "./types/index.d.ts",
|
|
@@ -104,13 +104,13 @@
|
|
|
104
104
|
"prepack": "npm run build:types"
|
|
105
105
|
},
|
|
106
106
|
"dependencies": {
|
|
107
|
-
"@jarenjs/core": "^0.
|
|
108
|
-
"@jarenjs/json": "^0.
|
|
107
|
+
"@jarenjs/core": "^0.75.0",
|
|
108
|
+
"@jarenjs/json": "^0.75.0"
|
|
109
109
|
},
|
|
110
110
|
"peerDependencies": {
|
|
111
|
-
"@jarenjs/db": "^0.
|
|
112
|
-
"@jarenjs/formats": "^0.
|
|
113
|
-
"@jarenjs/validate": "^0.
|
|
111
|
+
"@jarenjs/db": "^0.75.0",
|
|
112
|
+
"@jarenjs/formats": "^0.75.0",
|
|
113
|
+
"@jarenjs/validate": "^0.75.0"
|
|
114
114
|
},
|
|
115
115
|
"peerDependenciesMeta": {
|
|
116
116
|
"@jarenjs/db": {
|
package/src/federate.js
CHANGED
|
@@ -1,51 +1,26 @@
|
|
|
1
1
|
//@ts-check
|
|
2
2
|
/**
|
|
3
|
-
* @file
|
|
4
|
-
*
|
|
3
|
+
* @file Explicit bounded federation (QUERY-PEN §12.1). Source-local
|
|
4
|
+
* documents execute at their providers; a connected mandatory equality
|
|
5
|
+
* graph chooses the fetch order by declared estimates, with binding order
|
|
6
|
+
* breaking ties. Hash membership only removes impossible candidates: the
|
|
7
|
+
* query engine decides the resident result and its original tuple order.
|
|
5
8
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* would turn a one-line chain into an unbounded fetch of two
|
|
13
|
-
* databases.
|
|
9
|
+
* Packed joins emitted by successive LINQ joins execute inside out. Every
|
|
10
|
+
* source and intermediate has a per-side budget, and one shared admission
|
|
11
|
+
* counter covers the entire call. Intermediate phrase materialization is
|
|
12
|
+
* capped through the query engine's own limits. Buffered providers and
|
|
13
|
+
* intermediate byte sizes can only be checked after they produce an array;
|
|
14
|
+
* cursor providers admit each retained row before holding it.
|
|
14
15
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* one provider-compatible source per name, all sharing one scope — so
|
|
18
|
-
* the join the chain already knows how to build is admitted, and the
|
|
19
|
-
* federation is what executes it:
|
|
20
|
-
*
|
|
21
|
-
* 1. each binding's own document — the filters and the projection the
|
|
22
|
-
* chain already packed per side — runs against ITS source;
|
|
23
|
-
* 2. the smaller side (declared estimate, else the first named) is
|
|
24
|
-
* streamed into a hash table keyed by the join key, counting rows
|
|
25
|
-
* and serialized bytes against the budget as it fills;
|
|
26
|
-
* 3. the other side is streamed and PROBED: a row whose key no build
|
|
27
|
-
* row carries cannot join, so it is dropped before it costs
|
|
28
|
-
* anything;
|
|
29
|
-
* 4. the caller's own document runs in the engine over the two
|
|
30
|
-
* reduced sets — the resident join, which is what decides.
|
|
31
|
-
*
|
|
32
|
-
* Step 4 is why this file spells no join semantics of its own. The
|
|
33
|
-
* engine's `$eq` decides which rows pair, its ordering orders them and
|
|
34
|
-
* its projection shapes them; the hash table exists to bound the FETCH,
|
|
35
|
-
* never to answer the query. A reduction that dropped a row the engine
|
|
36
|
-
* would have joined would be a wrong answer, so the probe keeps
|
|
37
|
-
* anything it cannot key (a compound key value) rather than guessing.
|
|
38
|
-
*
|
|
39
|
-
* Non-goals, named rather than discovered: no spill (a budget is a
|
|
40
|
-
* refusal, not a disk), no distributed transaction, no cross-source
|
|
41
|
-
* write, and no non-equality join — without an equality key the fetch
|
|
42
|
-
* is the cross product, which is exactly what the budget exists to
|
|
43
|
-
* refuse.
|
|
16
|
+
* No spill, distributed transaction, cross-source writes, or disconnected
|
|
17
|
+
* cartesian products. Ordinary unrelated-source join() still refuses JL0005.
|
|
44
18
|
*/
|
|
45
19
|
|
|
46
20
|
import { LinqBuildError, LinqRuntimeError } from './errors.js';
|
|
47
21
|
import { compileDocument, isProviderSource, providerRoot } from './provider.js';
|
|
48
22
|
import { memberSegment } from './expression.js';
|
|
23
|
+
import { parseJSONPath } from '@jarenjs/json/path';
|
|
49
24
|
|
|
50
25
|
/** The strategies this boundary knows. One, for now, and it says so. */
|
|
51
26
|
const STRATEGIES = new Set(['hash']);
|
|
@@ -57,7 +32,7 @@ const STRATEGIES = new Set(['hash']);
|
|
|
57
32
|
* @returns {number}
|
|
58
33
|
*/
|
|
59
34
|
function budgetOf(value, what) {
|
|
60
|
-
if (!Number.
|
|
35
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
61
36
|
throw new LinqBuildError('JL0005',
|
|
62
37
|
`federate() needs a positive integer ${what} — a federated fetch holds rows in memory, `
|
|
63
38
|
+ 'and a bound is what makes that finite');
|
|
@@ -69,15 +44,22 @@ function budgetOf(value, what) {
|
|
|
69
44
|
* The one member path a `'$it.a.b'` operand names, or `null` for
|
|
70
45
|
* anything else (an operator call, a literal, the binding itself).
|
|
71
46
|
* @param {any} node
|
|
72
|
-
* @returns {{ binding: string, path: string[] } | null}
|
|
47
|
+
* @returns {{ binding: string, path: (string | number)[] } | null}
|
|
73
48
|
*/
|
|
74
49
|
function memberPathOf(node) {
|
|
75
50
|
if (typeof node !== 'string' || !node.startsWith('$')) return null;
|
|
76
|
-
const
|
|
77
|
-
if (
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
51
|
+
const match = /^\$([^.[\]]+)([.[])/.exec(node);
|
|
52
|
+
if (match === null) return null;
|
|
53
|
+
try {
|
|
54
|
+
const { segments } = parseJSONPath(`$${node.slice(match[1].length + 1)}`);
|
|
55
|
+
if (!segments.every((segment) => !segment.descendant && segment.selectors.length === 1
|
|
56
|
+
&& ['name', 'index'].includes(segment.selectors[0].kind))) return null;
|
|
57
|
+
return { binding: match[1], path: segments.map((segment) => {
|
|
58
|
+
const selector = segment.selectors[0];
|
|
59
|
+
return selector.kind === 'name' ? selector.name : selector.index;
|
|
60
|
+
}) };
|
|
61
|
+
}
|
|
62
|
+
catch { return null; }
|
|
81
63
|
}
|
|
82
64
|
|
|
83
65
|
/** Read one member path out of a row; `undefined` where it is absent. */
|
|
@@ -85,7 +67,14 @@ function valueAt(row, path) {
|
|
|
85
67
|
let value = row;
|
|
86
68
|
for (const name of path) {
|
|
87
69
|
if (value === null || typeof value !== 'object') return undefined;
|
|
88
|
-
|
|
70
|
+
if (typeof name === 'number') {
|
|
71
|
+
if (!Array.isArray(value)) return undefined;
|
|
72
|
+
value = value[name < 0 ? value.length + name : name];
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
if (Array.isArray(value) || !Object.hasOwn(value, name)) return undefined;
|
|
76
|
+
value = value[name];
|
|
77
|
+
}
|
|
89
78
|
}
|
|
90
79
|
return value;
|
|
91
80
|
}
|
|
@@ -169,7 +158,7 @@ function locateFlwor(document) {
|
|
|
169
158
|
* @param {Map<string, any>} members - federated name → member record
|
|
170
159
|
* @returns {any}
|
|
171
160
|
*/
|
|
172
|
-
function planFederation(document, members) {
|
|
161
|
+
function planFederation(document, members, nested = false) {
|
|
173
162
|
const located = locateFlwor(document);
|
|
174
163
|
const query = located === null ? document : located.flwor;
|
|
175
164
|
const bindings = query?.$for;
|
|
@@ -178,10 +167,8 @@ function planFederation(document, members) {
|
|
|
178
167
|
'a federated document ranges over its sources with $for — this one has no bindings');
|
|
179
168
|
}
|
|
180
169
|
const names = Object.keys(bindings);
|
|
181
|
-
if (names.length
|
|
182
|
-
throw new LinqBuildError('JL0005',
|
|
183
|
-
`a federated fetch joins exactly two sources, not ${names.length} — `
|
|
184
|
-
+ 'federate one pair at a time, or load the third side yourself');
|
|
170
|
+
if (names.length < (nested ? 1 : 2)) {
|
|
171
|
+
throw new LinqBuildError('JL0005', 'a federated fetch needs at least two source bindings');
|
|
185
172
|
}
|
|
186
173
|
|
|
187
174
|
/** @type {any[]} */
|
|
@@ -191,15 +178,20 @@ function planFederation(document, members) {
|
|
|
191
178
|
// the two spellings the chain builds: a bare root, or the side's
|
|
192
179
|
// own packed document (its `$where`, its `$orderby`, its `$return`)
|
|
193
180
|
const packed = Array.isArray(value) && value.length === 1 ? value[0] : null;
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
// plan and will not guess at
|
|
181
|
+
// A source-local phrase has one bare root; a packed join is planned
|
|
182
|
+
// recursively and becomes a bounded intermediate side.
|
|
197
183
|
const inner = packed === null ? [] : Object.keys(packed.$for ?? {});
|
|
198
184
|
const root = packed === null ? value
|
|
199
185
|
: (inner.length === 1 ? packed.$for[inner[0]] : null);
|
|
200
186
|
if (typeof root !== 'string') {
|
|
201
|
-
|
|
202
|
-
|
|
187
|
+
if (packed === null || inner.length === 0) {
|
|
188
|
+
throw new LinqBuildError('JL0005',
|
|
189
|
+
`the binding '${binding}' does not range over a federated source`);
|
|
190
|
+
}
|
|
191
|
+
const child = planFederation(packed, members, true);
|
|
192
|
+
sides.push({ binding, nested: child, packed, root: null,
|
|
193
|
+
member: { name: binding, estimatedRows: undefined } });
|
|
194
|
+
continue;
|
|
203
195
|
}
|
|
204
196
|
const member = members.get(root);
|
|
205
197
|
if (member === undefined) {
|
|
@@ -210,52 +202,57 @@ function planFederation(document, members) {
|
|
|
210
202
|
sides.push({ binding, member, packed, root });
|
|
211
203
|
}
|
|
212
204
|
|
|
213
|
-
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const probe = build === first ? second : first;
|
|
222
|
-
return {
|
|
223
|
-
strategy: 'hash',
|
|
224
|
-
build: { ...build, key: key[build.binding] },
|
|
225
|
-
probe: { ...probe, key: key[probe.binding] },
|
|
226
|
-
// the resident document is the caller's own with each side reduced
|
|
227
|
-
// to its root: the packed work has already run at the source, and
|
|
228
|
-
// whatever the terminal wrapped around the query is put back
|
|
229
|
-
resident: located.rewrap({ ...query,
|
|
230
|
-
$for: Object.fromEntries(sides.map((side) => [side.binding, side.root])) }),
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* The equality key that links the two sides, or the refusal.
|
|
236
|
-
* @param {any} where
|
|
237
|
-
* @param {any[]} sides
|
|
238
|
-
* @returns {Record<string, string[]>}
|
|
239
|
-
*/
|
|
240
|
-
function joinKey(where, sides) {
|
|
241
|
-
const conjuncts = where === undefined || where === null ? []
|
|
242
|
-
: (Array.isArray(where?.$and) ? where.$and : [where]);
|
|
243
|
-
const [a, b] = sides;
|
|
244
|
-
for (const conjunct of conjuncts) {
|
|
245
|
-
const operands = conjunct?.$eq;
|
|
246
|
-
if (!Array.isArray(operands) || operands.length !== 2) continue;
|
|
205
|
+
// A mandatory equality graph supplies the fetch order. OR branches
|
|
206
|
+
// never prove an edge. Estimates choose among connected candidates;
|
|
207
|
+
// equal or absent estimates retain binding declaration order.
|
|
208
|
+
const edges = [];
|
|
209
|
+
const visit = (where) => {
|
|
210
|
+
if (Array.isArray(where?.$and)) { where.$and.forEach(visit); return; }
|
|
211
|
+
const operands = where?.$eq;
|
|
212
|
+
if (!Array.isArray(operands) || operands.length !== 2) return;
|
|
247
213
|
const left = memberPathOf(operands[0]);
|
|
248
214
|
const right = memberPathOf(operands[1]);
|
|
249
|
-
if (left
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
215
|
+
if (left && right && left.binding !== right.binding
|
|
216
|
+
&& names.includes(left.binding) && names.includes(right.binding)) edges.push({ left, right });
|
|
217
|
+
};
|
|
218
|
+
visit(query.$where);
|
|
219
|
+
const remaining = [...sides];
|
|
220
|
+
const order = [];
|
|
221
|
+
const selected = new Set();
|
|
222
|
+
while (remaining.length > 0) {
|
|
223
|
+
const candidates = remaining.filter((side) => selected.size === 0 || edges.some(({ left, right }) =>
|
|
224
|
+
(left.binding === side.binding && selected.has(right.binding))
|
|
225
|
+
|| (right.binding === side.binding && selected.has(left.binding))));
|
|
226
|
+
if (candidates.length === 0) throw new LinqBuildError('JL0005',
|
|
227
|
+
'a federated join needs an equality between one member of each side in a connected binding graph');
|
|
228
|
+
candidates.sort((a, b) => (a.member.estimatedRows ?? Infinity) - (b.member.estimatedRows ?? Infinity));
|
|
229
|
+
const side = candidates[0];
|
|
230
|
+
side.links = edges.flatMap(({ left, right }) => {
|
|
231
|
+
const [own, other] = left.binding === side.binding ? [left, right] : [right, left];
|
|
232
|
+
return own.binding === side.binding && selected.has(other.binding)
|
|
233
|
+
? [{ key: own.path, binding: other.binding, otherKey: other.path }] : [];
|
|
234
|
+
});
|
|
235
|
+
const edge = edges.find(({ left, right }) => left.binding === side.binding || right.binding === side.binding);
|
|
236
|
+
side.key = edge === undefined ? [] : (edge.left.binding === side.binding ? edge.left.path : edge.right.path);
|
|
237
|
+
order.push(side); selected.add(side.binding); remaining.splice(remaining.indexOf(side), 1);
|
|
238
|
+
}
|
|
239
|
+
// Bindings may independently project or alias the same source. Give
|
|
240
|
+
// those sets separate input members so one cannot overwrite another.
|
|
241
|
+
const used = new Set(sides.filter((side) => side.root !== null).map((side) => side.member.name));
|
|
242
|
+
const roots = new Set();
|
|
243
|
+
for (const side of sides) {
|
|
244
|
+
side.inputName = side.member.name;
|
|
245
|
+
if (side.root === null || roots.has(side.root)) {
|
|
246
|
+
let name = `_federated${sides.indexOf(side)}`;
|
|
247
|
+
while (used.has(name)) name += '_';
|
|
248
|
+
used.add(name); side.inputName = name; side.root = `$${memberSegment(name)}[*]`;
|
|
249
|
+
}
|
|
250
|
+
roots.add(side.root);
|
|
254
251
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
252
|
+
return { strategy: 'hash', sides: order, build: order[0], probe: order[1],
|
|
253
|
+
resident: located.rewrap({ ...query,
|
|
254
|
+
$for: Object.fromEntries(sides.map((side) => [side.binding,
|
|
255
|
+
side.packed === null ? side.root : [side.root]])) }) };
|
|
259
256
|
}
|
|
260
257
|
|
|
261
258
|
/**
|
|
@@ -274,7 +271,7 @@ function joinKey(where, sides) {
|
|
|
274
271
|
* @param {any[]} open - cursors to close, in order
|
|
275
272
|
* @returns {Promise<{ rows: any[], read: number, bytes: number, streamed: boolean }>}
|
|
276
273
|
*/
|
|
277
|
-
async function fetchSide(member, document, options, budget, keep, open) {
|
|
274
|
+
async function fetchSide(member, document, options, budget, keep, open, combined) {
|
|
278
275
|
const kept = [];
|
|
279
276
|
let read = 0;
|
|
280
277
|
let bytes = 0;
|
|
@@ -292,6 +289,11 @@ async function fetchSide(member, document, options, budget, keep, open) {
|
|
|
292
289
|
`the federated fetch of '${member.name}' reached its ${budget.maxBytes}-byte budget `
|
|
293
290
|
+ `at row ${kept.length + 1} — narrow the sides, or raise maxBytes`);
|
|
294
291
|
}
|
|
292
|
+
if (combined.rows + 1 > combined.maxRows || combined.bytes + size > combined.maxBytes) {
|
|
293
|
+
throw new LinqRuntimeError('JL2008',
|
|
294
|
+
'the federated fetch reached its combined row or byte budget — narrow the sides, or raise maxTotalRows/maxTotalBytes');
|
|
295
|
+
}
|
|
296
|
+
combined.rows++; combined.bytes += size;
|
|
295
297
|
bytes += size;
|
|
296
298
|
kept.push(row);
|
|
297
299
|
};
|
|
@@ -321,19 +323,18 @@ async function fetchSide(member, document, options, budget, keep, open) {
|
|
|
321
323
|
return { rows: kept, read, bytes, streamed: true };
|
|
322
324
|
}
|
|
323
325
|
stopIfAborted();
|
|
324
|
-
|
|
325
|
-
|
|
326
|
+
// Frame the sequence as one array so an array-valued row is still
|
|
327
|
+
// one row. Cursor sources already supply that item boundary.
|
|
328
|
+
const answer = await member.provider.execute([document], options);
|
|
329
|
+
if (!Array.isArray(answer)) throw new LinqRuntimeError('JL2008',
|
|
330
|
+
`the federated source '${member.name}' did not answer the requested array frame`);
|
|
331
|
+
for (const row of answer) { stopIfAborted(); admit(row); }
|
|
326
332
|
return { rows: kept, read, bytes, streamed: false };
|
|
327
333
|
}
|
|
328
334
|
|
|
329
|
-
/** The engine's answer shape as a row list: none, one, or many. */
|
|
330
|
-
function itemsOf(answer) {
|
|
331
|
-
if (answer === undefined) return [];
|
|
332
|
-
return Array.isArray(answer) ? answer : [answer];
|
|
333
|
-
}
|
|
334
|
-
|
|
335
335
|
/** One side's document, with the federated root rewritten to the source's own. */
|
|
336
336
|
function childDocument(side) {
|
|
337
|
+
if (side.nested) return side.packed;
|
|
337
338
|
const own = providerRoot(side.member.provider);
|
|
338
339
|
if (side.packed === null) return { $for: { it: own }, $return: '$it' };
|
|
339
340
|
const binding = Object.keys(side.packed.$for)[0];
|
|
@@ -344,7 +345,7 @@ function childDocument(side) {
|
|
|
344
345
|
* An explicit federation boundary over two or more provider sources.
|
|
345
346
|
*
|
|
346
347
|
* @param {{ sources: Record<string, any>, maxRows: number, maxBytes: number,
|
|
347
|
-
* strategy?: string }} spec
|
|
348
|
+
* maxTotalRows?: number, maxTotalBytes?: number, strategy?: string }} spec
|
|
348
349
|
* @returns {{ source: (name: string) => any, names: readonly string[] }}
|
|
349
350
|
* @example
|
|
350
351
|
* const fed = federate({
|
|
@@ -378,6 +379,11 @@ export function federate(spec) {
|
|
|
378
379
|
maxBytes: budgetOf(spec.maxBytes, 'maxBytes'),
|
|
379
380
|
};
|
|
380
381
|
|
|
382
|
+
const totals = {
|
|
383
|
+
maxRows: budgetOf(spec.maxTotalRows ?? budget.maxRows * 2, 'maxTotalRows'),
|
|
384
|
+
maxBytes: budgetOf(spec.maxTotalBytes ?? budget.maxBytes * 2, 'maxTotalBytes'),
|
|
385
|
+
};
|
|
386
|
+
|
|
381
387
|
/** The scope every member shares: what admits the join, and nothing else. */
|
|
382
388
|
const scope = Object.freeze({ federation: true });
|
|
383
389
|
/** @type {Map<string, any>} federated root → member */
|
|
@@ -415,34 +421,43 @@ export function federate(spec) {
|
|
|
415
421
|
const plan = planFederation(document, members);
|
|
416
422
|
/** @type {any[]} */
|
|
417
423
|
const open = [];
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
424
|
+
const combined = { ...totals, rows: 0, bytes: 0 };
|
|
425
|
+
const run = async (current, intermediate = false) => {
|
|
426
|
+
const fetched = new Map();
|
|
427
|
+
for (const side of current.sides) {
|
|
428
|
+
const tables = side.links.map((link) => {
|
|
429
|
+
const keys = new Set();
|
|
430
|
+
let unkeyed = false;
|
|
431
|
+
for (const row of fetched.get(link.binding)) {
|
|
432
|
+
const key = hashKey(valueAt(row, link.otherKey));
|
|
433
|
+
if (key === null) unkeyed = true; else keys.add(key);
|
|
434
|
+
}
|
|
435
|
+
return { ...link, keys, unkeyed };
|
|
436
|
+
});
|
|
437
|
+
const keep = (row) => tables.every((table) => {
|
|
438
|
+
const key = hashKey(valueAt(row, table.key));
|
|
439
|
+
return table.unkeyed || key === null || table.keys.has(key);
|
|
440
|
+
});
|
|
441
|
+
const member = side.nested ? { ...side.member,
|
|
442
|
+
provider: { execute: () => run(side.nested, true) } } : side.member;
|
|
443
|
+
const found = await fetchSide(member, childDocument(side), options, budget, keep, open, combined);
|
|
444
|
+
fetched.set(side.binding, found.rows);
|
|
430
445
|
}
|
|
431
|
-
const
|
|
432
|
-
const
|
|
433
|
-
(
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
const answer =
|
|
446
|
+
const input = Object.fromEntries(current.sides.map((side) => [side.inputName, fetched.get(side.binding)]));
|
|
447
|
+
const limits = intermediate ? { ...options?.limits,
|
|
448
|
+
sequenceItems: Math.min(options?.limits?.sequenceItems ?? Infinity, budget.maxRows),
|
|
449
|
+
resultItems: Math.min(options?.limits?.resultItems ?? Infinity, budget.maxRows) } : options?.limits;
|
|
450
|
+
const compiled = compileDocument(intermediate ? [current.resident] : current.resident,
|
|
451
|
+
{ ...options, limits, externals: options?.externalNames ?? [] });
|
|
452
|
+
try { return compiled(input, options?.externals ?? {}); }
|
|
453
|
+
catch (error) {
|
|
454
|
+
if (intermediate && error.code === 'JQ2009' && /sequenceItems|resultItems/.test(error.message)) throw new LinqRuntimeError('JL2008',
|
|
455
|
+
'a federated intermediate join reached its row budget');
|
|
456
|
+
throw error;
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
try {
|
|
460
|
+
const answer = await run(plan);
|
|
446
461
|
// the fetch answered, so a cursor that will not close IS this
|
|
447
462
|
// call's failure rather than something to swallow
|
|
448
463
|
await closeAll(open);
|
|
@@ -484,13 +499,16 @@ export function federate(spec) {
|
|
|
484
499
|
source: side.member.name,
|
|
485
500
|
root: side.root,
|
|
486
501
|
estimatedRows: side.member.estimatedRows ?? null,
|
|
487
|
-
key: `$${side.binding}
|
|
502
|
+
key: `$${side.binding}${side.key.map((part) => typeof part === 'number' ? `[${part}]` : memberSegment(part)).join('')}`,
|
|
488
503
|
document: childDocument(side),
|
|
489
|
-
streaming: typeof side.member.provider
|
|
504
|
+
streaming: typeof side.member.provider?.cursor === 'function' ? 'row' : 'buffered',
|
|
505
|
+
...(side.nested ? { children: side.nested.sides.map(describe) } : {}),
|
|
490
506
|
});
|
|
491
507
|
return {
|
|
492
508
|
strategy: plan.strategy,
|
|
493
509
|
budget: { ...budget },
|
|
510
|
+
combinedBudget: { maxTotalRows: totals.maxRows, maxTotalBytes: totals.maxBytes },
|
|
511
|
+
order: plan.sides.map(describe),
|
|
494
512
|
build: describe(plan.build),
|
|
495
513
|
probe: describe(plan.probe),
|
|
496
514
|
// the join itself is the engine's, over what the two fetches
|
package/types/index.d.ts
CHANGED
|
@@ -795,15 +795,18 @@ export interface FederatedSide {
|
|
|
795
795
|
readonly document: unknown;
|
|
796
796
|
/** Whether this side is pulled row by row, or answered whole. */
|
|
797
797
|
readonly streaming: 'row' | 'buffered';
|
|
798
|
+
readonly children?: readonly FederatedSide[];
|
|
798
799
|
}
|
|
799
800
|
|
|
800
801
|
/** What a federated document will do, without doing any of it (§12.1). */
|
|
801
802
|
export interface FederationPlan {
|
|
802
803
|
readonly strategy: 'hash';
|
|
803
804
|
readonly budget: { readonly maxRows: number; readonly maxBytes: number };
|
|
805
|
+
readonly combinedBudget: { readonly maxTotalRows: number; readonly maxTotalBytes: number };
|
|
806
|
+
readonly order: readonly FederatedSide[];
|
|
804
807
|
readonly build: FederatedSide;
|
|
805
808
|
readonly probe: FederatedSide;
|
|
806
|
-
/** The
|
|
809
|
+
/** The engine evaluates the original binding order over the reduced sets. */
|
|
807
810
|
readonly resident: { readonly document: unknown };
|
|
808
811
|
}
|
|
809
812
|
|
|
@@ -822,13 +825,18 @@ export interface FederatedSource<T = unknown> extends AsyncProvider<T> {
|
|
|
822
825
|
* out of that, by naming the sources and the bounds together: each
|
|
823
826
|
* side's own filters and projection run at its source, the smaller side
|
|
824
827
|
* fills a bounded hash table, the other is probed against it, and the
|
|
825
|
-
* caller's own document decides over the
|
|
828
|
+
* caller's own document decides over the reduced sets. Connected N-way
|
|
829
|
+
* graphs and nested fluent joins share cumulative admission credits. A side that
|
|
826
830
|
* reaches `maxRows` or `maxBytes` raises `JL2008` at the row that would
|
|
827
831
|
* have broken the bound. */
|
|
828
832
|
export function federate(spec: {
|
|
829
833
|
sources: Record<string, AsyncProvider | { provider: AsyncProvider; estimatedRows?: number }>;
|
|
830
834
|
maxRows: number;
|
|
831
835
|
maxBytes: number;
|
|
836
|
+
/** Shared admission credits across source reads and intermediate sets; defaults to 2 * maxRows. */
|
|
837
|
+
maxTotalRows?: number;
|
|
838
|
+
/** Defaults to 2 * maxBytes. Buffered providers/intermediates are checked after production. */
|
|
839
|
+
maxTotalBytes?: number;
|
|
832
840
|
strategy?: 'hash';
|
|
833
841
|
}): {
|
|
834
842
|
source<T = unknown>(name: string): FederatedSource<T>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Derived from the English catalogs by scripts/generate-message-pen.js.
|
|
2
2
|
export interface CatalogIds {
|
|
3
3
|
validate: "type" | "required" | "minimum" | "maximum" | "exclusiveMinimum" | "exclusiveMaximum" | "multipleOf" | "minLength" | "maxLength" | "pattern" | "additionalProperties" | "minProperties" | "maxProperties" | "minItems" | "maxItems" | "uniqueItems" | "contains" | "items" | "allOf" | "anyOf" | "oneOf" | "not" | "format" | "if" | "then" | "else" | "false schema" | "$query" | "JQ2001" | "JQ2003";
|
|
4
|
-
forms: "form/required" | "form/type" | "form/const" | "form/enum" | "form/minLength" | "form/maxLength" | "form/pattern" | "form/format" | "form/minimum" | "form/maximum" | "form/exclusiveMinimum" | "form/exclusiveMaximum" | "form/multipleOf" | "form/minItems" | "form/maxItems" | "form/uniqueItems" | "form/minProperties" | "form/maxProperties" | "x-form/assert" | "form/addItem" | "form/removeItem";
|
|
4
|
+
forms: "form/required" | "form/type" | "form/const" | "form/enum" | "form/minLength" | "form/maxLength" | "form/pattern" | "form/format" | "form/minimum" | "form/maximum" | "form/exclusiveMinimum" | "form/exclusiveMaximum" | "form/multipleOf" | "form/minItems" | "form/maxItems" | "form/uniqueItems" | "form/minProperties" | "form/maxProperties" | "x-form/assert" | "form/addItem" | "form/removeItem" | "form/jsonPlaceholder";
|
|
5
5
|
contract: "contract/not-found" | "contract/method-not-allowed" | "contract/body-too-large" | "contract/unsupported-media" | "contract/malformed-json" | "contract/invalid-input" | "contract/idempotency-key-required" | "contract/handler-failed" | "contract/idempotency-conflict" | "contract/invalid-output" | "contract/malformed-path" | "contract/malformed-query" | "contract/not-implemented" | "contract/precondition-failed" | "contract/invalid-header" | "contract/handler-error" | "contract/client-invalid-input" | "contract/network" | "contract/cancelled" | "contract/invalid-response" | "contract/key-storage-failed" | "contract/undeclared-response" | "contract/not-a-contract" | "contract/incompatible" | "contract/host-failed" | "contract/local-handler-failed" | "contract/unknown-operation" | "contract/port-timeout" | "contract/malformed-frame" | "contract/channel-closed" | "contract/not-a-stream" | "contract/invalid-snapshot" | "contract/seq-regression" | "contract/stream-error" | "contract/heartbeat-missed" | "contract/slow-consumer" | "contract/reconnect-exhausted";
|
|
6
6
|
}
|
|
7
7
|
export interface MessageParameters {
|
|
@@ -56,6 +56,7 @@ export interface MessageParameters {
|
|
|
56
56
|
"x-form/assert": never;
|
|
57
57
|
"form/addItem": never;
|
|
58
58
|
"form/removeItem": never;
|
|
59
|
+
"form/jsonPlaceholder": never;
|
|
59
60
|
"contract/not-found": never;
|
|
60
61
|
"contract/method-not-allowed": "allow";
|
|
61
62
|
"contract/body-too-large": "limit" | "op";
|