@rindle/client 0.1.6 → 0.3.1
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/README.md +70 -5
- package/dist/ast.d.ts +11 -0
- package/dist/ast.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/operators.d.ts +5 -0
- package/dist/operators.d.ts.map +1 -1
- package/dist/operators.js +5 -0
- package/dist/operators.js.map +1 -1
- package/dist/query.d.ts +155 -34
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +213 -8
- package/dist/query.js.map +1 -1
- package/dist/resolve.d.ts +40 -0
- package/dist/resolve.d.ts.map +1 -0
- package/dist/resolve.js +109 -0
- package/dist/resolve.js.map +1 -0
- package/dist/store.d.ts +58 -15
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +221 -26
- package/dist/store.js.map +1 -1
- package/dist/types.d.ts +41 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/view.d.ts +43 -5
- package/dist/view.d.ts.map +1 -1
- package/dist/view.js +109 -31
- package/dist/view.js.map +1 -1
- package/package.json +1 -1
- package/src/ast.ts +11 -0
- package/src/index.ts +5 -0
- package/src/operators.ts +5 -0
- package/src/query.ts +505 -52
- package/src/resolve.ts +136 -0
- package/src/store.ts +227 -35
- package/src/types.ts +40 -3
- package/src/view.ts +127 -30
package/src/query.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// - `where<Field>(…)` camelCase sugar (`whereClosed(false)`) is intercepted too.
|
|
7
7
|
// Both are fully typed via mapped types + template-literal keys.
|
|
8
8
|
|
|
9
|
-
import type { Ast, Bound, Condition, CorrelatedSubquery, Dir, ExistsOp, LitValue, OrderPart } from "./ast.ts";
|
|
9
|
+
import type { Ast, Bound, Condition, CorrelatedSubquery, Dir, ExistsOp, LitValue, OrderPart, SimpleOp } from "./ast.ts";
|
|
10
10
|
import { stableKey } from "./key.ts";
|
|
11
11
|
import type { Arg, Cond } from "./operators.ts";
|
|
12
12
|
import { fieldCondition } from "./operators.ts";
|
|
@@ -20,18 +20,18 @@ import type { ArrayView, SingularArrayView } from "./view.ts";
|
|
|
20
20
|
// (`data: R[]`) to a `SingularArrayView` (`data: R | null`). It threads through every chained
|
|
21
21
|
// method so the unwrap survives `.one().where(…)`, etc. Defaults `false` (plural).
|
|
22
22
|
|
|
23
|
-
type FieldFn<C extends AnyCols, K extends keyof C, Rels, One extends boolean, Sel extends string> = (
|
|
23
|
+
type FieldFn<C extends AnyCols, K extends keyof C, Rels, One extends boolean, Sel extends string, LocalRels> = (
|
|
24
24
|
arg: Arg<ColT<C[K]>>,
|
|
25
|
-
) => Query<C, Rels, One, Sel>;
|
|
25
|
+
) => Query<C, Rels, One, Sel, LocalRels>;
|
|
26
26
|
|
|
27
27
|
/** `where.closed(false)`, `where.priority(gt(3))`. */
|
|
28
|
-
type WhereProxy<C extends AnyCols, Rels, One extends boolean, Sel extends string> = {
|
|
29
|
-
[K in keyof C]: FieldFn<C, K, Rels, One, Sel>;
|
|
28
|
+
type WhereProxy<C extends AnyCols, Rels, One extends boolean, Sel extends string, LocalRels> = {
|
|
29
|
+
[K in keyof C]: FieldFn<C, K, Rels, One, Sel, LocalRels>;
|
|
30
30
|
};
|
|
31
31
|
|
|
32
32
|
/** `whereClosed(false)`, `wherePriority(gt(3))`. */
|
|
33
|
-
type WhereSugar<C extends AnyCols, Rels, One extends boolean, Sel extends string> = {
|
|
34
|
-
[K in keyof C as `where${Capitalize<string & K>}`]: FieldFn<C, K, Rels, One, Sel>;
|
|
33
|
+
type WhereSugar<C extends AnyCols, Rels, One extends boolean, Sel extends string, LocalRels> = {
|
|
34
|
+
[K in keyof C as `where${Capitalize<string & K>}`]: FieldFn<C, K, Rels, One, Sel, LocalRels>;
|
|
35
35
|
};
|
|
36
36
|
|
|
37
37
|
/** What `materialize()` returns: singular (`R | null`) for a top-level `.one()`, else plural. */
|
|
@@ -49,27 +49,56 @@ type Projected<C extends AnyCols, Sel extends string> = [Sel] extends [never]
|
|
|
49
49
|
? RowOf<C>
|
|
50
50
|
: Pick<RowOf<C>, Sel & keyof C>;
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
/**
|
|
53
|
+
* The accumulating result row of a top-level aggregate (REDUCE-DESIGN.md §8). `Agg` is `false`
|
|
54
|
+
* until {@link QueryBase.count}/{@link QueryBase.groupBy} reshapes the query; then it is the
|
|
55
|
+
* group-by columns intersected with the synthetic `{ count: number }`. {@link AggAcc} treats the
|
|
56
|
+
* `false` state as `{}` so the intersections in `count`/`groupBy` compose either way.
|
|
57
|
+
*/
|
|
58
|
+
type AggAcc<Agg> = [Agg] extends [false] ? {} : Agg;
|
|
59
|
+
|
|
60
|
+
/** `having`'s field binder: one accessor per aggregate-OUTPUT column (the group-by columns plus the
|
|
61
|
+
* synthetic numeric `count`), each producing a {@link Cond} over that aggregate row. It is the only
|
|
62
|
+
* way to name `count` in a predicate (it lives on no base table); compose several with `and`/`or`. */
|
|
63
|
+
type HavingProxy<Row> = { [K in keyof Row & string]: (arg: Arg<Row[K]>) => Cond<Row> };
|
|
64
|
+
|
|
65
|
+
/** The `countAs` relationship aliases of a query — the {@link Rels} keys whose value is the scalar
|
|
66
|
+
* `number` a count aggregate surfaces. These are the only aliases {@link QueryBase.having}'s
|
|
67
|
+
* parent-by-child-aggregate overload accepts (a plain `sub` alias carries a row object, not a
|
|
68
|
+
* number, so it is rejected). */
|
|
69
|
+
type AggregateAlias<Rels> = { [K in keyof Rels]: Rels[K] extends number ? K & string : never }[keyof Rels];
|
|
70
|
+
|
|
71
|
+
/** What a query materializes: the post-aggregation row once reshaped by {@link QueryBase.count}
|
|
72
|
+
* (`Agg`), else the projected base row plus its relationship values. */
|
|
73
|
+
type ResultRow<C extends AnyCols, Rels, Sel extends string, Agg> = [Agg] extends [false]
|
|
74
|
+
? Projected<C, Sel> & Rels
|
|
75
|
+
: Agg;
|
|
76
|
+
|
|
77
|
+
type FragmentEdge<C extends AnyCols, Rels, Sel extends string, LocalRels> = (
|
|
78
|
+
q: Query<C, Rels, false, Sel, LocalRels>,
|
|
79
|
+
) => Query<C, Rels, boolean, Sel, LocalRels>;
|
|
80
|
+
|
|
81
|
+
interface QueryBase<C extends AnyCols, Rels, One extends boolean, Sel extends string, LocalRels, Agg = false> {
|
|
53
82
|
/** Present when this local query came from `defineQuery`; used as the remote identity. */
|
|
54
83
|
readonly name?: string;
|
|
55
84
|
/** Present when this local query came from `defineQuery`; sent with `name` upstream. */
|
|
56
85
|
readonly args?: unknown;
|
|
57
86
|
/** Condition form — consumes `or()`/`and()`/`exists()`/field conditions (AND-ed across calls).
|
|
58
87
|
* Filters over the FULL column set (`RowOf<C>`), independent of what's `select`ed/masked. */
|
|
59
|
-
where(cond: Cond<RowOf<C>>): Query<C, Rels, One, Sel>;
|
|
60
|
-
orderBy<K extends keyof C>(col: K, dir: Dir): Query<C, Rels, One, Sel>;
|
|
88
|
+
where(cond: Cond<RowOf<C>>): Query<C, Rels, One, Sel, LocalRels>;
|
|
89
|
+
orderBy<K extends keyof C>(col: K, dir: Dir): Query<C, Rels, One, Sel, LocalRels>;
|
|
61
90
|
/** Project a subset of columns (PROJECTION-SUPPORT-DESIGN.md §6, masking §6/§10.6). Chainable
|
|
62
91
|
* (each call unions into `Sel`); omit to select all. Drives what the server syncs, what the
|
|
63
92
|
* view reports, AND — once any column is named — narrows the result row TYPE to exactly the
|
|
64
93
|
* selection (masking). `where`/`orderBy`/`start`/`sub` still see the full column set. */
|
|
65
|
-
select<K extends keyof C & string>(...cols: K[]): Query<C, Rels, One, Sel | K>;
|
|
66
|
-
limit(n: number): Query<C, Rels, One, Sel>;
|
|
94
|
+
select<K extends keyof C & string>(...cols: K[]): Query<C, Rels, One, Sel | K, LocalRels>;
|
|
95
|
+
limit(n: number): Query<C, Rels, One, Sel, LocalRels>;
|
|
67
96
|
/** Cursor paging: start at (or, with `exclusive`, after) the partial `cursor` row over the
|
|
68
97
|
* sort columns. Lowers to a `Skip` in the engine. */
|
|
69
|
-
start(cursor: Partial<RowOf<C>>, opts?: { exclusive?: boolean }): Query<C, Rels, One, Sel>;
|
|
98
|
+
start(cursor: Partial<RowOf<C>>, opts?: { exclusive?: boolean }): Query<C, Rels, One, Sel, LocalRels>;
|
|
70
99
|
/** Return a single row: `materialize()` yields a {@link SingularArrayView} (`data: R | null`)
|
|
71
100
|
* and the engine caps the query to `limit = 1`. */
|
|
72
|
-
one(): Query<C, Rels, true, Sel>;
|
|
101
|
+
one(): Query<C, Rels, true, Sel, LocalRels>;
|
|
73
102
|
/** **Merge** a {@link Fragment} into THIS node — Relay's "spread on the same type" (§5). The
|
|
74
103
|
* fragment must be over the same table (enforced in the result type: a fragment over a different
|
|
75
104
|
* table yields `never`, plus a runtime throw). Unions the projection (`Sel | FSel`), folds in the
|
|
@@ -84,9 +113,90 @@ interface QueryBase<C extends AnyCols, Rels, One extends boolean, Sel extends st
|
|
|
84
113
|
* `include` keeps `C` out of any contravariant position — preserving `Query<Concrete>`'s
|
|
85
114
|
* assignability to `Query<AnyCols>` (needed by `Store<ColsMap>`/`QueryRoot`). Same-table is then
|
|
86
115
|
* checked as the mutual-assignability of `C` and `FC` in the return type. */
|
|
87
|
-
include<FC extends AnyCols, FRels = {}, FSel extends string = never>(
|
|
88
|
-
fragment: Fragment<FC, FRels, FSel>,
|
|
89
|
-
): [C] extends [FC] ? ([FC] extends [C] ? Query<C, Rels & FRels, One, Sel | FSel> : never) : never;
|
|
116
|
+
include<FC extends AnyCols, FRels = {}, FSel extends string = never, FLocalRels = FRels>(
|
|
117
|
+
fragment: Fragment<FC, FRels, FSel, FLocalRels>,
|
|
118
|
+
): [C] extends [FC] ? ([FC] extends [C] ? Query<C, Rels & FRels, One, Sel | FSel, LocalRels & FLocalRels> : never) : never;
|
|
119
|
+
/** Nest a child fragment as an opaque local-read ref. The coverage query still composes the
|
|
120
|
+
* child's full AST at runtime; the React-facing fragment data exposes only refs at this
|
|
121
|
+
* boundary so the child component owns its local subscription. */
|
|
122
|
+
sub<
|
|
123
|
+
A extends string,
|
|
124
|
+
CC extends AnyCols,
|
|
125
|
+
CRels = {},
|
|
126
|
+
CSel extends string = never,
|
|
127
|
+
CLocalRels = CRels,
|
|
128
|
+
F extends Fragment<CC, CRels, CSel, CLocalRels> = Fragment<CC, CRels, CSel, CLocalRels>,
|
|
129
|
+
>(
|
|
130
|
+
alias: A,
|
|
131
|
+
child: TableLike<CC>,
|
|
132
|
+
corr: { parent: Array<keyof C & string>; child: Array<keyof CC & string> },
|
|
133
|
+
build: F,
|
|
134
|
+
edge: FragmentEdge<CC, CRels, CSel, CLocalRels>,
|
|
135
|
+
): Query<
|
|
136
|
+
C,
|
|
137
|
+
Rels & { [P in A]: Array<Projected<CC, CSel> & CRels> },
|
|
138
|
+
One,
|
|
139
|
+
Sel,
|
|
140
|
+
LocalRels & { [P in A]: Array<FragmentRef<F>> }
|
|
141
|
+
>;
|
|
142
|
+
sub<
|
|
143
|
+
A extends string,
|
|
144
|
+
CC extends AnyCols,
|
|
145
|
+
CRels = {},
|
|
146
|
+
CSel extends string = never,
|
|
147
|
+
CLocalRels = CRels,
|
|
148
|
+
F extends Fragment<CC, CRels, CSel, CLocalRels> = Fragment<CC, CRels, CSel, CLocalRels>,
|
|
149
|
+
>(
|
|
150
|
+
alias: A,
|
|
151
|
+
child: TableLike<CC>,
|
|
152
|
+
corr: { parent: Array<keyof C & string>; child: Array<keyof CC & string> },
|
|
153
|
+
build: F,
|
|
154
|
+
): Query<
|
|
155
|
+
C,
|
|
156
|
+
Rels & { [P in A]: Array<Projected<CC, CSel> & CRels> },
|
|
157
|
+
One,
|
|
158
|
+
Sel,
|
|
159
|
+
LocalRels & { [P in A]: Array<FragmentRef<F>> }
|
|
160
|
+
>;
|
|
161
|
+
/** Relationship-value form of the fragment-ref overload above. */
|
|
162
|
+
sub<
|
|
163
|
+
A extends string,
|
|
164
|
+
CC extends AnyCols,
|
|
165
|
+
CRels = {},
|
|
166
|
+
CSel extends string = never,
|
|
167
|
+
CLocalRels = CRels,
|
|
168
|
+
F extends Fragment<CC, CRels, CSel, CLocalRels> = Fragment<CC, CRels, CSel, CLocalRels>,
|
|
169
|
+
>(
|
|
170
|
+
alias: A,
|
|
171
|
+
relationship: Relationship<C, CC>,
|
|
172
|
+
build: F,
|
|
173
|
+
edge: FragmentEdge<CC, CRels, CSel, CLocalRels>,
|
|
174
|
+
): Query<
|
|
175
|
+
C,
|
|
176
|
+
Rels & { [P in A]: Array<Projected<CC, CSel> & CRels> },
|
|
177
|
+
One,
|
|
178
|
+
Sel,
|
|
179
|
+
LocalRels & { [P in A]: Array<FragmentRef<F>> }
|
|
180
|
+
>;
|
|
181
|
+
/** Relationship-value form of the fragment-ref overload above. */
|
|
182
|
+
sub<
|
|
183
|
+
A extends string,
|
|
184
|
+
CC extends AnyCols,
|
|
185
|
+
CRels = {},
|
|
186
|
+
CSel extends string = never,
|
|
187
|
+
CLocalRels = CRels,
|
|
188
|
+
F extends Fragment<CC, CRels, CSel, CLocalRels> = Fragment<CC, CRels, CSel, CLocalRels>,
|
|
189
|
+
>(
|
|
190
|
+
alias: A,
|
|
191
|
+
relationship: Relationship<C, CC>,
|
|
192
|
+
build: F,
|
|
193
|
+
): Query<
|
|
194
|
+
C,
|
|
195
|
+
Rels & { [P in A]: Array<Projected<CC, CSel> & CRels> },
|
|
196
|
+
One,
|
|
197
|
+
Sel,
|
|
198
|
+
LocalRels & { [P in A]: Array<FragmentRef<F>> }
|
|
199
|
+
>;
|
|
90
200
|
/** Nest a child by EXPLICIT correlation (no schema relationship). `alias` is the result key.
|
|
91
201
|
* The child's own projection (`CSel`) masks the nested row, so a fragment spread here
|
|
92
202
|
* contributes exactly the columns it declared. */
|
|
@@ -95,7 +205,13 @@ interface QueryBase<C extends AnyCols, Rels, One extends boolean, Sel extends st
|
|
|
95
205
|
child: TableLike<CC>,
|
|
96
206
|
corr: { parent: Array<keyof C & string>; child: Array<keyof CC & string> },
|
|
97
207
|
build?: (q: Query<CC>) => Query<CC, CRels, boolean, CSel>,
|
|
98
|
-
): Query<
|
|
208
|
+
): Query<
|
|
209
|
+
C,
|
|
210
|
+
Rels & { [P in A]: Array<Projected<CC, CSel> & CRels> },
|
|
211
|
+
One,
|
|
212
|
+
Sel,
|
|
213
|
+
LocalRels & { [P in A]: Array<Projected<CC, CSel> & CRels> }
|
|
214
|
+
>;
|
|
99
215
|
/** Nest a child by a named {@link Relationship} (`rel(parent, child, {...})`) — the correlation comes
|
|
100
216
|
* from the relationship, so no `{ parent, child }` keys are restated. The relationship must belong to
|
|
101
217
|
* THIS table (its parent columns are checked against `C`). `alias` is still the result key. */
|
|
@@ -103,7 +219,13 @@ interface QueryBase<C extends AnyCols, Rels, One extends boolean, Sel extends st
|
|
|
103
219
|
alias: A,
|
|
104
220
|
relationship: Relationship<C, CC>,
|
|
105
221
|
build?: (q: Query<CC>) => Query<CC, CRels, boolean, CSel>,
|
|
106
|
-
): Query<
|
|
222
|
+
): Query<
|
|
223
|
+
C,
|
|
224
|
+
Rels & { [P in A]: Array<Projected<CC, CSel> & CRels> },
|
|
225
|
+
One,
|
|
226
|
+
Sel,
|
|
227
|
+
LocalRels & { [P in A]: Array<Projected<CC, CSel> & CRels> }
|
|
228
|
+
>;
|
|
107
229
|
/** Add a **relationship aggregate** — `issue.countAs("commentCount", comment, …)`
|
|
108
230
|
* (`REDUCE-DESIGN.md` §9). Like {@link sub} (explicit correlation, optional child
|
|
109
231
|
* `build` for a filtered `count(child WHERE …)`), but the relationship surfaces a single
|
|
@@ -114,28 +236,81 @@ interface QueryBase<C extends AnyCols, Rels, One extends boolean, Sel extends st
|
|
|
114
236
|
child: TableLike<CC>,
|
|
115
237
|
corr: { parent: Array<keyof C & string>; child: Array<keyof CC & string> },
|
|
116
238
|
build?: (q: Query<CC>) => Query<CC, unknown>,
|
|
117
|
-
): Query<C, Rels & { [P in A]: number }, One, Sel>;
|
|
239
|
+
): Query<C, Rels & { [P in A]: number }, One, Sel, LocalRels & { [P in A]: number }>;
|
|
118
240
|
/** `countAs` by a named {@link Relationship} — like the explicit form, but the correlation comes from
|
|
119
241
|
* `rel(...)` instead of being restated. */
|
|
120
242
|
countAs<A extends string, CC extends AnyCols>(
|
|
121
243
|
alias: A,
|
|
122
244
|
relationship: Relationship<C, CC>,
|
|
123
245
|
build?: (q: Query<CC>) => Query<CC, unknown>,
|
|
124
|
-
): Query<C, Rels & { [P in A]: number }, One, Sel>;
|
|
246
|
+
): Query<C, Rels & { [P in A]: number }, One, Sel, LocalRels & { [P in A]: number }>;
|
|
247
|
+
/** Reshape this query into a top-level `count(*)` aggregate (`REDUCE-DESIGN.md` §8) — the SQL
|
|
248
|
+
* `SELECT count(*) FROM table [GROUP BY …] [HAVING …]`. Without {@link groupBy} it is a GLOBAL
|
|
249
|
+
* count (one `{ count }` row, value `0` even on empty input); with it, one `{ …group, count }`
|
|
250
|
+
* row per distinct group. The result row becomes the aggregate's OUTPUT — the group-by columns
|
|
251
|
+
* plus a numeric `count` — so chain {@link having} to filter it. Distinct from {@link countAs}
|
|
252
|
+
* (a child-relationship scalar attached to the parent row); this reshapes the query ITSELF. The
|
|
253
|
+
* engine rejects pairing a root aggregate with `select`/`sub`/`countAs`/`orderBy`/`limit`/`one`. */
|
|
254
|
+
count(): Query<C, Rels, One, Sel, LocalRels, AggAcc<Agg> & { count: number }>;
|
|
255
|
+
/** Add a top-level `GROUP BY` column (chain for a compound key); only meaningful with
|
|
256
|
+
* {@link count}. Each grouped column joins the aggregate result row, keyed + sorted by the
|
|
257
|
+
* group key. */
|
|
258
|
+
groupBy<K extends keyof C & string>(
|
|
259
|
+
col: K,
|
|
260
|
+
): Query<C, Rels, One, Sel, LocalRels, AggAcc<Agg> & Pick<RowOf<C>, K>>;
|
|
261
|
+
/** `HAVING (…)` — filter the **post-aggregation** rows of a {@link count} query (`REDUCE-DESIGN.md`
|
|
262
|
+
* §4: a filter directly above the reduce). `build` receives a field binder over the aggregate's
|
|
263
|
+
* OUTPUT columns — the {@link groupBy} columns and the synthetic `count` — and returns a
|
|
264
|
+
* {@link Cond}; compose several with `and`/`or`. E.g.
|
|
265
|
+
* `.groupBy("status").count().having((h) => h.count(gt(3)))`. Distinct from {@link where}, which
|
|
266
|
+
* filters base rows BELOW the reduce. */
|
|
267
|
+
having(build: (h: HavingProxy<AggAcc<Agg>>) => Cond<AggAcc<Agg>>): Query<C, Rels, One, Sel, LocalRels, Agg>;
|
|
268
|
+
/** `HAVING count(child) <op> n` — filter THIS parent by a child relationship aggregate's count
|
|
269
|
+
* (`PARENT-AGGREGATE-FILTER-DESIGN.md`). `alias` must name a {@link countAs} relationship already
|
|
270
|
+
* on this query; this drops parents whose child count fails `<op> val`, maintained incrementally
|
|
271
|
+
* (a child add/remove crossing the threshold adds/removes the parent). The display `countAs` is
|
|
272
|
+
* untouched — a survivor still shows its real count. Distinct from the {@link having} overload
|
|
273
|
+
* above, which filters a top-level {@link count}'s own output rows.
|
|
274
|
+
*
|
|
275
|
+
* **v1: high-pass predicates only** — predicates *false* at count 0 (`>`, `>=`/`=`/`!=` for
|
|
276
|
+
* `n ≥ 1`). A childless parent forms no group, so the engine rejects (at build) a predicate *true*
|
|
277
|
+
* at count 0 (`<=`, `< n` for `n ≥ 1`, `= 0`, `>= 0`); those need row-widening (deferred). */
|
|
278
|
+
having<A extends AggregateAlias<Rels>>(
|
|
279
|
+
alias: A,
|
|
280
|
+
op: SimpleOp,
|
|
281
|
+
val: number,
|
|
282
|
+
): Query<C, Rels, One, Sel, LocalRels, Agg>;
|
|
125
283
|
/** The compiled Zero-wire AST (what a backend's `query` consumes). */
|
|
126
284
|
ast(): Ast;
|
|
127
285
|
/** Materialize into a live, typed view. Wired by the Store/backend. A top-level `.one()`
|
|
128
|
-
* yields a {@link SingularArrayView}; otherwise an {@link ArrayView}. The row is
|
|
129
|
-
*
|
|
130
|
-
|
|
286
|
+
* yields a {@link SingularArrayView}; otherwise an {@link ArrayView}. The row is the aggregate
|
|
287
|
+
* output once {@link count} reshaped the query, else masked to the projection ({@link Projected})
|
|
288
|
+
* — full {@link RowOf} until a column is `select`ed. */
|
|
289
|
+
materialize(): MaterializedView<ResultRow<C, Rels, Sel, Agg>, One>;
|
|
131
290
|
}
|
|
132
291
|
|
|
133
|
-
export type Query<
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
292
|
+
export type Query<
|
|
293
|
+
C extends AnyCols,
|
|
294
|
+
Rels = {},
|
|
295
|
+
One extends boolean = false,
|
|
296
|
+
Sel extends string = never,
|
|
297
|
+
LocalRels = Rels,
|
|
298
|
+
Agg = false,
|
|
299
|
+
> =
|
|
300
|
+
& Omit<QueryBase<C, Rels, One, Sel, LocalRels, Agg>, "where">
|
|
301
|
+
& { where: QueryBase<C, Rels, One, Sel, LocalRels, Agg>["where"] & WhereProxy<C, Rels, One, Sel, LocalRels> }
|
|
302
|
+
& WhereSugar<C, Rels, One, Sel, LocalRels>;
|
|
137
303
|
|
|
138
|
-
type AnyQuery = Query<any, any, any>;
|
|
304
|
+
export type AnyQuery = Query<any, any, any, any, any, any>;
|
|
305
|
+
|
|
306
|
+
export type QueryLocalData<Q extends AnyQuery> =
|
|
307
|
+
Q extends Query<infer C, any, infer One, infer Sel, infer LocalRels, infer Agg>
|
|
308
|
+
? [Agg] extends [false]
|
|
309
|
+
? One extends true
|
|
310
|
+
? (Projected<C, Sel> & LocalRels) | null
|
|
311
|
+
: readonly (Projected<C, Sel> & LocalRels)[]
|
|
312
|
+
: readonly Agg[]
|
|
313
|
+
: never;
|
|
139
314
|
const STAMP_NAMED_QUERY: unique symbol = Symbol("rindle.stampNamedQuery");
|
|
140
315
|
|
|
141
316
|
interface QueryInternals {
|
|
@@ -259,6 +434,8 @@ export function defineQuery(
|
|
|
259
434
|
// ----------------------------- fragments (FRAGMENT-COMPOSITION-DESIGN.md, Phase 0) -----------------------------
|
|
260
435
|
|
|
261
436
|
const FRAGMENT_BRAND: unique symbol = Symbol("rindle.fragment");
|
|
437
|
+
const LOCAL_FRAGMENT_REF_BRAND: unique symbol = Symbol("rindle.localFragmentRef");
|
|
438
|
+
const FRAGMENT_REL_BRAND: unique symbol = Symbol("rindle.fragmentRelationship");
|
|
262
439
|
|
|
263
440
|
/**
|
|
264
441
|
* A reusable, typed *selection over a table* — Relay's "fragment", promoted to a first-class
|
|
@@ -276,22 +453,38 @@ const FRAGMENT_BRAND: unique symbol = Symbol("rindle.fragment");
|
|
|
276
453
|
* fragments assembles ONE {@link Ast} → one materialization → one `/query` — the whole point
|
|
277
454
|
* (no request waterfall; design §1).
|
|
278
455
|
*/
|
|
279
|
-
export interface Fragment<C extends AnyCols, Rels = {}, Sel extends string = never> {
|
|
280
|
-
(q: Query<C>): Query<C, Rels, false, Sel>;
|
|
456
|
+
export interface Fragment<C extends AnyCols, Rels = {}, Sel extends string = never, LocalRels = Rels> {
|
|
457
|
+
(q: Query<C>): Query<C, Rels, false, Sel, LocalRels>;
|
|
281
458
|
readonly table: TableLike<C>;
|
|
282
459
|
readonly [FRAGMENT_BRAND]: true;
|
|
283
460
|
}
|
|
284
461
|
|
|
285
462
|
/**
|
|
286
|
-
* The
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
* columns it declared. A fragment with no `select` stays the full `RowOf<C> & Rels` (§10.6).
|
|
463
|
+
* The data returned by `useFragment(fragment, ref)`: the fragment's own selected columns plus
|
|
464
|
+
* immediate relationship values. Relationships whose builder is another fragment surface as
|
|
465
|
+
* opaque {@link FragmentRef}s, so child-owned payload is read only by the child fragment reader.
|
|
290
466
|
*/
|
|
291
|
-
export type
|
|
292
|
-
? Projected<C, Sel> &
|
|
467
|
+
export type FragmentData<F> = F extends Fragment<infer C, unknown, infer Sel, infer LocalRels>
|
|
468
|
+
? Projected<C, Sel> & LocalRels
|
|
293
469
|
: never;
|
|
294
470
|
|
|
471
|
+
export interface FragmentCoverage<Q extends AnyQuery = AnyQuery> {
|
|
472
|
+
readonly key: string;
|
|
473
|
+
readonly query: Q;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export interface LocalFragmentRef<F extends Fragment<any, any, any, any> = Fragment<any, any, any, any>> {
|
|
477
|
+
readonly [LOCAL_FRAGMENT_REF_BRAND]: true;
|
|
478
|
+
readonly source: "local";
|
|
479
|
+
readonly table: string;
|
|
480
|
+
readonly pk: Readonly<Record<string, LitValue>>;
|
|
481
|
+
readonly coverage: FragmentCoverage;
|
|
482
|
+
readonly __fragment?: F;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/** The opaque token a parent passes to a component that reads {@link FragmentData} for `F`. */
|
|
486
|
+
export type FragmentRef<F> = F extends Fragment<any, any, any, any> ? LocalFragmentRef<F> : never;
|
|
487
|
+
|
|
295
488
|
/**
|
|
296
489
|
* Define a co-located, composable {@link Fragment}: a named selection over `table`. The returned
|
|
297
490
|
* value is callable (the `build` transform), so it threads through the existing `sub`/`Rels`
|
|
@@ -304,17 +497,192 @@ export type FragmentRef<F> = F extends Fragment<infer C, infer Rels, infer Sel>
|
|
|
304
497
|
* .sub("author", schema.user, { parent: ["authorId"], child: ["id"] }, UserAvatar));
|
|
305
498
|
* ```
|
|
306
499
|
*/
|
|
307
|
-
export function defineFragment<C extends AnyCols, Rels = {}, Sel extends string = never>(
|
|
500
|
+
export function defineFragment<C extends AnyCols, Rels = {}, Sel extends string = never, LocalRels = Rels>(
|
|
308
501
|
table: TableLike<C>,
|
|
309
|
-
build: (q: Query<C>) => Query<C, Rels, boolean, Sel>,
|
|
310
|
-
): Fragment<C, Rels, Sel> {
|
|
311
|
-
const frag = (q: Query<C>): Query<C, Rels, false, Sel> => build(q) as Query<C, Rels, false, Sel>;
|
|
312
|
-
return Object.assign(frag, { table, [FRAGMENT_BRAND]: true as const }) as unknown as Fragment<C, Rels, Sel>;
|
|
502
|
+
build: (q: Query<C>) => Query<C, Rels, boolean, Sel, LocalRels>,
|
|
503
|
+
): Fragment<C, Rels, Sel, LocalRels> {
|
|
504
|
+
const frag = (q: Query<C>): Query<C, Rels, false, Sel, LocalRels> => build(q) as Query<C, Rels, false, Sel, LocalRels>;
|
|
505
|
+
return Object.assign(frag, { table, [FRAGMENT_BRAND]: true as const }) as unknown as Fragment<C, Rels, Sel, LocalRels>;
|
|
313
506
|
}
|
|
314
507
|
|
|
315
508
|
/** Runtime guard: is `v` a {@link Fragment} (a `defineFragment` value, not a plain build fn)? */
|
|
316
|
-
export function isFragment(v: unknown): v is Fragment<AnyCols, unknown> {
|
|
317
|
-
return typeof v === "function" && (v as Partial<Fragment<AnyCols>>)[FRAGMENT_BRAND] === true;
|
|
509
|
+
export function isFragment(v: unknown): v is Fragment<AnyCols, unknown, never, unknown> {
|
|
510
|
+
return typeof v === "function" && (v as Partial<Fragment<AnyCols, unknown, never, unknown>>)[FRAGMENT_BRAND] === true;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function isLocalFragmentRef(v: unknown): v is LocalFragmentRef {
|
|
514
|
+
return typeof v === "object" && v !== null && (v as Partial<LocalFragmentRef>)[LOCAL_FRAGMENT_REF_BRAND] === true;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
type FragmentRelationship = CorrelatedSubquery & { [FRAGMENT_REL_BRAND]?: true };
|
|
518
|
+
|
|
519
|
+
function markFragmentRelationship<T extends CorrelatedSubquery>(rel: T): T {
|
|
520
|
+
Object.defineProperty(rel, FRAGMENT_REL_BRAND, { value: true });
|
|
521
|
+
return rel;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
export function isFragmentRelationship(rel: CorrelatedSubquery): boolean {
|
|
525
|
+
return (rel as FragmentRelationship)[FRAGMENT_REL_BRAND] === true;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
export function fragmentKey(ref: FragmentRef<any>): string {
|
|
529
|
+
if (!isLocalFragmentRef(ref)) throw new Error("fragmentKey(): expected an opaque fragment ref.");
|
|
530
|
+
return stableKey({ table: ref.table, pk: ref.pk });
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
export function createLocalFragmentRef<F extends Fragment<any, any, any>>(
|
|
534
|
+
fragment: F,
|
|
535
|
+
pk: Record<string, LitValue>,
|
|
536
|
+
coverage: FragmentCoverage,
|
|
537
|
+
): LocalFragmentRef<F> {
|
|
538
|
+
return createLocalFragmentRefForTable(fragment.table[SCHEMA].name, pk, coverage);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
export function createLocalFragmentRefForTable<F extends Fragment<any, any, any>>(
|
|
542
|
+
table: string,
|
|
543
|
+
pk: Record<string, LitValue>,
|
|
544
|
+
coverage: FragmentCoverage,
|
|
545
|
+
): LocalFragmentRef<F> {
|
|
546
|
+
return {
|
|
547
|
+
[LOCAL_FRAGMENT_REF_BRAND]: true,
|
|
548
|
+
source: "local",
|
|
549
|
+
table,
|
|
550
|
+
pk: { ...pk },
|
|
551
|
+
coverage,
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
export function createRootFragmentRef<F extends Fragment<any, any, any>, Q extends AnyQuery>(
|
|
556
|
+
fragment: F,
|
|
557
|
+
query: Q,
|
|
558
|
+
coverageKey = stableKey({
|
|
559
|
+
ast: query.ast(),
|
|
560
|
+
remote: typeof query.name === "string" ? { name: query.name, args: query.args } : null,
|
|
561
|
+
}),
|
|
562
|
+
): LocalFragmentRef<F> {
|
|
563
|
+
const ast = query.ast();
|
|
564
|
+
const table = fragment.table[SCHEMA];
|
|
565
|
+
if (ast.table !== table.name) {
|
|
566
|
+
throw new Error(
|
|
567
|
+
`useRoot(): the coverage query is over "${ast.table}" but the fragment is over "${table.name}".`,
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
const pk = primaryKeyFromWhere(ast, table.primaryKey);
|
|
571
|
+
return createLocalFragmentRef(fragment, pk, { key: coverageKey, query });
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
export function fragmentAst<F extends Fragment<any, any, any>>(fragment: F): Ast {
|
|
575
|
+
return childAst(fragment.table as AnyTable, fragment as unknown as (q: unknown) => unknown);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
export function localFragmentReadAst<F extends Fragment<any, any, any>>(
|
|
579
|
+
fragment: F,
|
|
580
|
+
ref: LocalFragmentRef<F>,
|
|
581
|
+
primaryKeyFor: (table: string) => readonly string[],
|
|
582
|
+
): Ast {
|
|
583
|
+
const table = fragment.table[SCHEMA].name;
|
|
584
|
+
if (ref.table !== table) {
|
|
585
|
+
throw new Error(`useFragment(): the ref is for "${ref.table}" but the fragment is over "${table}".`);
|
|
586
|
+
}
|
|
587
|
+
const ast = localizeFragmentAst(fragmentAst(fragment), primaryKeyFor);
|
|
588
|
+
ast.where = andConditions(pkConditions(ref.pk), ast.where);
|
|
589
|
+
ast.one = true;
|
|
590
|
+
return ast;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
export function localRootFragmentRefsAst<F extends Fragment<any, any, any>>(
|
|
594
|
+
fragment: F,
|
|
595
|
+
query: AnyQuery,
|
|
596
|
+
primaryKeyFor: (table: string) => readonly string[],
|
|
597
|
+
): Ast {
|
|
598
|
+
const ast = query.ast();
|
|
599
|
+
const table = fragment.table[SCHEMA].name;
|
|
600
|
+
if (ast.table !== table) {
|
|
601
|
+
throw new Error(
|
|
602
|
+
`useRoot(): the coverage query is over "${ast.table}" but the fragment is over "${table}".`,
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
const out: Ast = { ...ast, select: uniqSort([...primaryKeyFor(table)]) };
|
|
606
|
+
delete out.related;
|
|
607
|
+
return out;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
export function localQueryReadAst(
|
|
611
|
+
query: AnyQuery,
|
|
612
|
+
primaryKeyFor: (table: string) => readonly string[],
|
|
613
|
+
): Ast {
|
|
614
|
+
return localizeFragmentAst(query.ast(), primaryKeyFor);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
export function queryFromAst(ast: Ast): AnyQuery {
|
|
618
|
+
return {
|
|
619
|
+
ast: () => ast,
|
|
620
|
+
materialize: () => {
|
|
621
|
+
throw new Error("queryFromAst(): materialize() requires a Store; pass this query through Store/React.");
|
|
622
|
+
},
|
|
623
|
+
} as unknown as AnyQuery;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function primaryKeyFromWhere(ast: Ast, primaryKey: readonly string[]): Record<string, LitValue> {
|
|
627
|
+
const found = new Map<string, LitValue>();
|
|
628
|
+
collectLiteralEquals(ast.where, found);
|
|
629
|
+
const pk: Record<string, LitValue> = {};
|
|
630
|
+
for (const col of primaryKey) {
|
|
631
|
+
if (!found.has(col)) {
|
|
632
|
+
throw new Error(
|
|
633
|
+
`useRoot(): the coverage query must constrain primary key column "${col}" with a literal equality.`,
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
pk[col] = found.get(col)!;
|
|
637
|
+
}
|
|
638
|
+
return pk;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function collectLiteralEquals(cond: Condition | undefined, out: Map<string, LitValue>): void {
|
|
642
|
+
if (!cond) return;
|
|
643
|
+
if (cond.type === "and") {
|
|
644
|
+
for (const c of cond.conditions) collectLiteralEquals(c, out);
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
if (cond.type !== "simple" || cond.op !== "=") return;
|
|
648
|
+
const leftCol = cond.left.type === "column" ? cond.left.name : undefined;
|
|
649
|
+
const rightCol = cond.right.type === "column" ? cond.right.name : undefined;
|
|
650
|
+
if (leftCol !== undefined && cond.right.type === "literal") out.set(leftCol, cond.right.value);
|
|
651
|
+
else if (rightCol !== undefined && cond.left.type === "literal") out.set(rightCol, cond.left.value);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function pkConditions(pk: Readonly<Record<string, LitValue>>): Condition[] {
|
|
655
|
+
return Object.keys(pk)
|
|
656
|
+
.sort()
|
|
657
|
+
.map((name) => fieldCondition(name, pk[name]));
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function andConditions(conditions: Condition[], tail: Condition | undefined): Condition | undefined {
|
|
661
|
+
const all = tail ? [...conditions, tail] : conditions;
|
|
662
|
+
if (all.length === 0) return undefined;
|
|
663
|
+
if (all.length === 1) return all[0];
|
|
664
|
+
return { type: "and", conditions: all };
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function localizeFragmentAst(ast: Ast, primaryKeyFor: (table: string) => readonly string[]): Ast {
|
|
668
|
+
const out: Ast = { ...ast };
|
|
669
|
+
if (ast.select !== undefined) out.select = uniqSort([...ast.select, ...primaryKeyFor(ast.table)]);
|
|
670
|
+
if (ast.related !== undefined) {
|
|
671
|
+
out.related = ast.related.map((rel) => {
|
|
672
|
+
if (rel.subquery.aggregate !== undefined) return rel;
|
|
673
|
+
if (!isFragmentRelationship(rel)) {
|
|
674
|
+
return { ...rel, subquery: localizeFragmentAst(rel.subquery, primaryKeyFor) };
|
|
675
|
+
}
|
|
676
|
+
const childPk = primaryKeyFor(rel.subquery.table);
|
|
677
|
+
const subquery: Ast = {
|
|
678
|
+
...rel.subquery,
|
|
679
|
+
select: uniqSort([...childPk]),
|
|
680
|
+
};
|
|
681
|
+
delete subquery.related;
|
|
682
|
+
return markFragmentRelationship({ ...rel, subquery });
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
return out;
|
|
318
686
|
}
|
|
319
687
|
|
|
320
688
|
// ----------------------------- the runtime builder -----------------------------
|
|
@@ -329,6 +697,12 @@ interface State {
|
|
|
329
697
|
limit?: number;
|
|
330
698
|
one: boolean;
|
|
331
699
|
select?: string[];
|
|
700
|
+
// Top-level aggregate (REDUCE-DESIGN.md §8): `aggregate` reshapes the query into a `count(*)`;
|
|
701
|
+
// `groupBy` partitions it; `having` filters the post-aggregation rows. All three are read by the
|
|
702
|
+
// engine only together (see the guard in `compile`).
|
|
703
|
+
aggregate?: "count";
|
|
704
|
+
groupBy: string[];
|
|
705
|
+
having?: Condition;
|
|
332
706
|
}
|
|
333
707
|
|
|
334
708
|
interface NamedQueryState {
|
|
@@ -337,7 +711,7 @@ interface NamedQueryState {
|
|
|
337
711
|
}
|
|
338
712
|
|
|
339
713
|
function emptyState(table: string): State {
|
|
340
|
-
return { table, wheres: [], orderBy: [], related: [], one: false };
|
|
714
|
+
return { table, wheres: [], orderBy: [], related: [], one: false, groupBy: [] };
|
|
341
715
|
}
|
|
342
716
|
|
|
343
717
|
function compile(s: State): Ast {
|
|
@@ -351,6 +725,18 @@ function compile(s: State): Ast {
|
|
|
351
725
|
if (s.limit !== undefined) ast.limit = s.limit;
|
|
352
726
|
if (s.one) ast.one = true;
|
|
353
727
|
if (s.select && s.select.length > 0) ast.select = s.select;
|
|
728
|
+
if (s.aggregate !== undefined) ast.aggregate = s.aggregate;
|
|
729
|
+
if (s.groupBy.length > 0) ast.groupBy = s.groupBy;
|
|
730
|
+
if (s.having !== undefined) ast.having = s.having;
|
|
731
|
+
// The engine takes the aggregate lowering ONLY when `aggregate` is set (REDUCE-DESIGN.md §8 /
|
|
732
|
+
// builder `build_pipeline`); `groupBy`/`having` on the row spine would be silently ignored. Fail
|
|
733
|
+
// loudly so a forgotten `.count()` is a clear error, not a query that quietly returns all rows.
|
|
734
|
+
if ((ast.groupBy !== undefined || ast.having !== undefined) && ast.aggregate === undefined) {
|
|
735
|
+
throw new Error(
|
|
736
|
+
"groupBy()/having() require count(): a top-level GROUP BY / HAVING is only honored on an " +
|
|
737
|
+
"aggregate query (REDUCE-DESIGN.md §8) — add .count().",
|
|
738
|
+
);
|
|
739
|
+
}
|
|
354
740
|
return ast;
|
|
355
741
|
}
|
|
356
742
|
|
|
@@ -401,22 +787,42 @@ interface ResolvedCorrelated {
|
|
|
401
787
|
child: AnyTable;
|
|
402
788
|
corr: { parent: string[]; child: string[] };
|
|
403
789
|
build: ((q: unknown) => unknown) | undefined;
|
|
790
|
+
fragment: boolean;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function composeCorrelatedBuild(
|
|
794
|
+
build: ((q: unknown) => unknown) | undefined,
|
|
795
|
+
edge: ((q: unknown) => unknown) | undefined,
|
|
796
|
+
): ((q: unknown) => unknown) | undefined {
|
|
797
|
+
if (edge === undefined) return build;
|
|
798
|
+
if (!isFragment(build)) {
|
|
799
|
+
throw new Error("sub(): an edge callback is only supported when the child build is a Fragment.");
|
|
800
|
+
}
|
|
801
|
+
return (q: unknown) => edge(build(q as Query<AnyCols>));
|
|
404
802
|
}
|
|
405
803
|
|
|
406
804
|
/** Resolve the two `sub`/`countAs`/`exists` call shapes to a common `{ child, corr, build }`:
|
|
407
805
|
* either a named {@link Relationship} (`(rel, build?)`) or an explicit `(child, corr, build?)`. */
|
|
408
|
-
function resolveCorrelated(a: AnyTable | AnyRelationship, b: unknown, c: unknown): ResolvedCorrelated {
|
|
806
|
+
function resolveCorrelated(a: AnyTable | AnyRelationship, b: unknown, c: unknown, d?: unknown): ResolvedCorrelated {
|
|
409
807
|
if (isRelationship(a)) {
|
|
410
808
|
return {
|
|
411
809
|
child: a.child as AnyTable,
|
|
412
810
|
corr: { parent: [...a.correlation.parent], child: [...a.correlation.child] },
|
|
413
|
-
build:
|
|
811
|
+
build: composeCorrelatedBuild(
|
|
812
|
+
b as ((q: unknown) => unknown) | undefined,
|
|
813
|
+
c as ((q: unknown) => unknown) | undefined,
|
|
814
|
+
),
|
|
815
|
+
fragment: isFragment(b),
|
|
414
816
|
};
|
|
415
817
|
}
|
|
416
818
|
return {
|
|
417
819
|
child: a,
|
|
418
820
|
corr: b as { parent: string[]; child: string[] },
|
|
419
|
-
build:
|
|
821
|
+
build: composeCorrelatedBuild(
|
|
822
|
+
c as ((q: unknown) => unknown) | undefined,
|
|
823
|
+
d as ((q: unknown) => unknown) | undefined,
|
|
824
|
+
),
|
|
825
|
+
fragment: isFragment(c),
|
|
420
826
|
};
|
|
421
827
|
}
|
|
422
828
|
|
|
@@ -507,7 +913,8 @@ function mergeRelatedLists(base: CorrelatedSubquery[], add: CorrelatedSubquery[]
|
|
|
507
913
|
`or align them (FRAGMENT-COMPOSITION-DESIGN §10.2).`,
|
|
508
914
|
);
|
|
509
915
|
}
|
|
510
|
-
|
|
916
|
+
const merged = { ...existing, subquery: mergeSubqueryAst(existing.subquery, csq.subquery) };
|
|
917
|
+
byAlias.set(alias, isFragmentRelationship(existing) || isFragmentRelationship(csq) ? markFragmentRelationship(merged) : merged);
|
|
511
918
|
}
|
|
512
919
|
return [...byAlias.values()].sort((a, b) => {
|
|
513
920
|
const x = aliasOf(a);
|
|
@@ -576,15 +983,15 @@ function makeQuery(
|
|
|
576
983
|
const fragAst = childAst(fragTable as AnyTable, fragment as unknown as (q: unknown) => unknown);
|
|
577
984
|
return makeQuery(meta, mergeIncludedFragment(s, fragAst), onMat, named, guardAst);
|
|
578
985
|
},
|
|
579
|
-
sub: (alias: string, childOrRel: AnyTable | AnyRelationship, b?: unknown, c?: unknown) => {
|
|
580
|
-
const { child, corr, build } = resolveCorrelated(childOrRel, b, c);
|
|
986
|
+
sub: (alias: string, childOrRel: AnyTable | AnyRelationship, b?: unknown, c?: unknown, d?: unknown) => {
|
|
987
|
+
const { child, corr, build, fragment } = resolveCorrelated(childOrRel, b, c, d);
|
|
581
988
|
const sub = childAst(child, build);
|
|
582
989
|
sub.alias = alias;
|
|
583
990
|
const csq: CorrelatedSubquery = {
|
|
584
991
|
correlation: { parentField: corr.parent, childField: corr.child },
|
|
585
992
|
subquery: sub,
|
|
586
993
|
};
|
|
587
|
-
return next({ related: [...s.related, csq] });
|
|
994
|
+
return next({ related: [...s.related, fragment ? markFragmentRelationship(csq) : csq] });
|
|
588
995
|
},
|
|
589
996
|
countAs: (alias: string, childOrRel: AnyTable | AnyRelationship, b?: unknown, c?: unknown) => {
|
|
590
997
|
// Same correlated-child mechanism as `sub`, but mark the child a `count` aggregate:
|
|
@@ -599,6 +1006,52 @@ function makeQuery(
|
|
|
599
1006
|
};
|
|
600
1007
|
return next({ related: [...s.related, csq] });
|
|
601
1008
|
},
|
|
1009
|
+
// Top-level aggregate (REDUCE-DESIGN §8): `count` reshapes this query into a `count(*)`;
|
|
1010
|
+
// `groupBy` partitions it; `having` filters the post-aggregation rows. `having`'s callback gets
|
|
1011
|
+
// a field binder over the aggregate's output columns (group cols + the synthetic `count`), so a
|
|
1012
|
+
// predicate can name `count` — which lives on no base table — exactly like `where.<field>(…)`.
|
|
1013
|
+
count: () => next({ aggregate: "count" }),
|
|
1014
|
+
groupBy: (col: string) => next({ groupBy: [...s.groupBy, col] }),
|
|
1015
|
+
having: (a: unknown, op?: SimpleOp, val?: number) => {
|
|
1016
|
+
// Overload 1 — top-level aggregate HAVING: `having((h) => h.count(gt(3)))`. The callback
|
|
1017
|
+
// binds the aggregate's output columns (group cols + the synthetic `count`).
|
|
1018
|
+
if (typeof a === "function") {
|
|
1019
|
+
const build = a as (h: Record<string, (arg: unknown) => Condition>) => Condition;
|
|
1020
|
+
const h = new Proxy({} as Record<string, (arg: unknown) => Condition>, {
|
|
1021
|
+
get: (_t, prop) => (typeof prop === "string" ? (arg: unknown) => fieldCondition(prop, arg) : undefined),
|
|
1022
|
+
});
|
|
1023
|
+
return next({ having: build(h) });
|
|
1024
|
+
}
|
|
1025
|
+
// Overload 2 — filter THIS parent by a child aggregate's count:
|
|
1026
|
+
// `.having("commentCount", ">", 10)` (PARENT-AGGREGATE-FILTER-DESIGN.md). Bind the existing
|
|
1027
|
+
// `countAs(alias, …)` relationship and push a hidden EXISTS whose subquery clones the same
|
|
1028
|
+
// correlation + child + `count` aggregate, plus a `HAVING count <op> val`. The engine lowers
|
|
1029
|
+
// it to an EXISTS over a HAVING-filtered reduce; the display `countAs` is untouched.
|
|
1030
|
+
const alias = a as string;
|
|
1031
|
+
const display = s.related.find(
|
|
1032
|
+
(r) => r.subquery.alias === alias && r.subquery.aggregate === "count",
|
|
1033
|
+
);
|
|
1034
|
+
if (!display) {
|
|
1035
|
+
throw new Error(
|
|
1036
|
+
`having("${alias}", …): this query has no countAs("${alias}", …) relationship to filter ` +
|
|
1037
|
+
`on — attach the child count aggregate first.`,
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
const gate: CorrelatedSubquery = {
|
|
1041
|
+
correlation: display.correlation,
|
|
1042
|
+
subquery: {
|
|
1043
|
+
...display.subquery,
|
|
1044
|
+
alias: `__having_${alias}`,
|
|
1045
|
+
having: {
|
|
1046
|
+
type: "simple",
|
|
1047
|
+
op: op as SimpleOp,
|
|
1048
|
+
left: { type: "column", name: "count" },
|
|
1049
|
+
right: { type: "literal", value: val as LitValue },
|
|
1050
|
+
},
|
|
1051
|
+
},
|
|
1052
|
+
};
|
|
1053
|
+
return next({ wheres: [...s.wheres, { type: "correlatedSubquery", op: "EXISTS", related: gate }] });
|
|
1054
|
+
},
|
|
602
1055
|
ast: () => {
|
|
603
1056
|
const a = compile(s);
|
|
604
1057
|
guardAst?.(a);
|