turbine-orm 0.78.0-next.4bd3c8f → 0.78.0-next.b792a6a
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/dist/cjs/powql.d.ts +32 -3
- package/dist/cjs/powql.js +96 -20
- package/dist/powql.d.ts +32 -3
- package/dist/powql.js +96 -20
- package/package.json +1 -1
package/dist/cjs/powql.d.ts
CHANGED
|
@@ -155,6 +155,34 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
|
|
|
155
155
|
* the two can never disagree about which limit is in force.
|
|
156
156
|
*/
|
|
157
157
|
private effectiveLimit;
|
|
158
|
+
/**
|
|
159
|
+
* Options for the child-side interfaces the relation loaders and the
|
|
160
|
+
* relation-filter resolvers build: the client's, with `defaultLimit` cleared
|
|
161
|
+
* and the unlimited warning off. `defaultLimit` bounds the caller's PAGE; on
|
|
162
|
+
* an internal fetch that serves that page (a relation's children, a filter's
|
|
163
|
+
* key set) it would cap the whole page's children, or the key set, at the
|
|
164
|
+
* page size: a silent wrong answer by another route than the one
|
|
165
|
+
* {@link pageOf} closes. The SQL batched loader clears it the same way
|
|
166
|
+
* (`batchedChildOptions` in query/builder.ts).
|
|
167
|
+
*/
|
|
168
|
+
private childOptions?;
|
|
169
|
+
private loaderChildOptions;
|
|
170
|
+
/**
|
|
171
|
+
* ONE parent's window of its stitched children: the relation's `offset` and
|
|
172
|
+
* `limit` applied to that parent's own list.
|
|
173
|
+
*
|
|
174
|
+
* A relation `limit` means "at most N on EACH parent". The loaders used to
|
|
175
|
+
* spread it onto the flat child fetch (`post filter .author_id in (...)
|
|
176
|
+
* limit 5`), which caps the TOTAL across every parent in the chunk, so ten
|
|
177
|
+
* parents shared five posts and most got none, with no error. It shipped
|
|
178
|
+
* for the loaders' whole life because the one test of the shape compared
|
|
179
|
+
* the join path against the loader path and both were wrong the same way;
|
|
180
|
+
* the cross-engine benchmark found it, where the wrong answer was the fast
|
|
181
|
+
* one. Every loader now fetches its children unbounded, WITH the relation
|
|
182
|
+
* `orderBy` (which decides which rows the window keeps), and applies this
|
|
183
|
+
* at stitch time: the rule the SQL engines' batched loader has always used.
|
|
184
|
+
*/
|
|
185
|
+
private static pageOf;
|
|
158
186
|
/**
|
|
159
187
|
* Reject a negative `limit` / `offset` before it reaches the engine. PowDB
|
|
160
188
|
* casts both with `as usize` at execution, so below engine 0.20 a negative
|
|
@@ -623,9 +651,10 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
|
|
|
623
651
|
* `referenceKey` on the TARGET table (the join's non-fetched side);
|
|
624
652
|
* - m2m keeps any `orderBy`/`limit`/`offset` on the loader (the junction-order
|
|
625
653
|
* stitch can't be reproduced by the 3-table join deterministically);
|
|
626
|
-
* - a to-one relation `limit`/`offset` (meaningless) stays on the loader
|
|
627
|
-
*
|
|
628
|
-
*
|
|
654
|
+
* - a to-one relation `limit`/`offset` (meaningless) stays on the loader. A
|
|
655
|
+
* to-many relation `limit`/`offset` is a PER-PARENT bound on both paths,
|
|
656
|
+
* fetched unbounded and sliced per parent at stitch time (`pageOf`), so
|
|
657
|
+
* the join statement carries neither clause.
|
|
629
658
|
*/
|
|
630
659
|
private joinEligible;
|
|
631
660
|
/**
|
package/dist/cjs/powql.js
CHANGED
|
@@ -393,6 +393,42 @@ class PowqlInterface {
|
|
|
393
393
|
effectiveLimit(args) {
|
|
394
394
|
return args.limit ?? this.defaultLimit;
|
|
395
395
|
}
|
|
396
|
+
/**
|
|
397
|
+
* Options for the child-side interfaces the relation loaders and the
|
|
398
|
+
* relation-filter resolvers build: the client's, with `defaultLimit` cleared
|
|
399
|
+
* and the unlimited warning off. `defaultLimit` bounds the caller's PAGE; on
|
|
400
|
+
* an internal fetch that serves that page (a relation's children, a filter's
|
|
401
|
+
* key set) it would cap the whole page's children, or the key set, at the
|
|
402
|
+
* page size: a silent wrong answer by another route than the one
|
|
403
|
+
* {@link pageOf} closes. The SQL batched loader clears it the same way
|
|
404
|
+
* (`batchedChildOptions` in query/builder.ts).
|
|
405
|
+
*/
|
|
406
|
+
childOptions;
|
|
407
|
+
loaderChildOptions() {
|
|
408
|
+
this.childOptions ??= { ...this.options, defaultLimit: undefined, warnOnUnlimited: false };
|
|
409
|
+
return this.childOptions;
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* ONE parent's window of its stitched children: the relation's `offset` and
|
|
413
|
+
* `limit` applied to that parent's own list.
|
|
414
|
+
*
|
|
415
|
+
* A relation `limit` means "at most N on EACH parent". The loaders used to
|
|
416
|
+
* spread it onto the flat child fetch (`post filter .author_id in (...)
|
|
417
|
+
* limit 5`), which caps the TOTAL across every parent in the chunk, so ten
|
|
418
|
+
* parents shared five posts and most got none, with no error. It shipped
|
|
419
|
+
* for the loaders' whole life because the one test of the shape compared
|
|
420
|
+
* the join path against the loader path and both were wrong the same way;
|
|
421
|
+
* the cross-engine benchmark found it, where the wrong answer was the fast
|
|
422
|
+
* one. Every loader now fetches its children unbounded, WITH the relation
|
|
423
|
+
* `orderBy` (which decides which rows the window keeps), and applies this
|
|
424
|
+
* at stitch time: the rule the SQL engines' batched loader has always used.
|
|
425
|
+
*/
|
|
426
|
+
static pageOf(rows, limit, offset) {
|
|
427
|
+
if (limit === undefined && !offset)
|
|
428
|
+
return rows;
|
|
429
|
+
const start = offset ?? 0;
|
|
430
|
+
return rows.slice(start, limit === undefined ? undefined : start + limit);
|
|
431
|
+
}
|
|
396
432
|
/**
|
|
397
433
|
* Reject a negative `limit` / `offset` before it reaches the engine. PowDB
|
|
398
434
|
* casts both with `as usize` at execution, so below engine 0.20 a negative
|
|
@@ -974,7 +1010,7 @@ class PowqlInterface {
|
|
|
974
1010
|
const childCol = rel.type === 'belongsTo' ? rk[0] : fk[0];
|
|
975
1011
|
const localField = this.meta.reverseColumnMap[localCol] ?? localCol;
|
|
976
1012
|
const childField = targetMeta.reverseColumnMap[childCol] ?? childCol;
|
|
977
|
-
const targetQi = new PowqlInterface(this.pool, rel.to, this.schema, [], this.
|
|
1013
|
+
const targetQi = new PowqlInterface(this.pool, rel.to, this.schema, [], this.loaderChildOptions());
|
|
978
1014
|
const collect = async (w) => {
|
|
979
1015
|
const rows = await targetQi.findMany({
|
|
980
1016
|
where: w,
|
|
@@ -1013,7 +1049,7 @@ class PowqlInterface {
|
|
|
1013
1049
|
const sourceRefField = this.meta.reverseColumnMap[sourceRefCol] ?? sourceRefCol;
|
|
1014
1050
|
const sourceRefColMeta = this.meta.columns.find((c) => c.name === sourceRefCol);
|
|
1015
1051
|
const targetPkField = targetMeta.reverseColumnMap[targetPkCol] ?? targetPkCol;
|
|
1016
|
-
const targetQi = new PowqlInterface(this.pool, rel.to, this.schema, [], this.
|
|
1052
|
+
const targetQi = new PowqlInterface(this.pool, rel.to, this.schema, [], this.loaderChildOptions());
|
|
1017
1053
|
const collectTargetPks = async (w) => {
|
|
1018
1054
|
const rows = await targetQi.findMany({
|
|
1019
1055
|
where: w,
|
|
@@ -1725,7 +1761,7 @@ class PowqlInterface {
|
|
|
1725
1761
|
const rel = this.meta.relations[relName];
|
|
1726
1762
|
if (!rel)
|
|
1727
1763
|
throw new errors_js_1.ValidationError(`Unknown relation "${relName}" on "${this.table}".`);
|
|
1728
|
-
if (strategyIsJoin && parent && this.joinEligible(rel, opt, parent.args
|
|
1764
|
+
if (strategyIsJoin && parent && this.joinEligible(rel, opt, parent.args)) {
|
|
1729
1765
|
if (this.capabilities.serverJoins) {
|
|
1730
1766
|
await this.loadRelationViaJoin(parents, rel, relName, opt, parent, timeout, includePii);
|
|
1731
1767
|
continue;
|
|
@@ -1748,7 +1784,19 @@ class PowqlInterface {
|
|
|
1748
1784
|
throw new errors_js_1.UnsupportedFeatureError('composite-key nested reads', 'PowDB', `relation "${relName}"`);
|
|
1749
1785
|
}
|
|
1750
1786
|
const options = (opt === true ? {} : opt);
|
|
1751
|
-
|
|
1787
|
+
// The relation's own page, applied PER PARENT at stitch time (see pageOf),
|
|
1788
|
+
// never on the flat fetch below.
|
|
1789
|
+
const relLimit = options.limit;
|
|
1790
|
+
const relOffset = options.offset;
|
|
1791
|
+
this.assertPagination(relLimit, relOffset, `relation "${relName}"`);
|
|
1792
|
+
const single = rel.type === 'belongsTo' || rel.type === 'hasOne';
|
|
1793
|
+
if (relLimit === 0) {
|
|
1794
|
+
// `[]` on every parent by construction: nothing to fetch.
|
|
1795
|
+
for (const parent of parents)
|
|
1796
|
+
parent[relName] = single ? null : [];
|
|
1797
|
+
continue;
|
|
1798
|
+
}
|
|
1799
|
+
const targetQi = new PowqlInterface(this.pool, rel.to, this.schema, [], this.loaderChildOptions());
|
|
1752
1800
|
const targetMeta = this.schema.tables[rel.to];
|
|
1753
1801
|
// Local key on the parent, remote key on the target child.
|
|
1754
1802
|
const parentKeyCol = rel.type === 'belongsTo' ? fk[0] : rk[0];
|
|
@@ -1806,6 +1854,9 @@ class PowqlInterface {
|
|
|
1806
1854
|
};
|
|
1807
1855
|
const children = (await targetQi.findMany({
|
|
1808
1856
|
...fetchOptions,
|
|
1857
|
+
// Unbounded: the relation limit/offset are per parent, applied below.
|
|
1858
|
+
limit: undefined,
|
|
1859
|
+
offset: undefined,
|
|
1809
1860
|
where: childWhere,
|
|
1810
1861
|
with: options.with,
|
|
1811
1862
|
timeout: options.timeout ?? timeout,
|
|
@@ -1832,11 +1883,12 @@ class PowqlInterface {
|
|
|
1832
1883
|
delete child[childKeyField];
|
|
1833
1884
|
}
|
|
1834
1885
|
}
|
|
1835
|
-
const single = rel.type === 'belongsTo' || rel.type === 'hasOne';
|
|
1836
1886
|
for (const parent of parents) {
|
|
1837
1887
|
const k = this.joinKey(parent[parentKeyField]);
|
|
1838
1888
|
const matches = (k == null ? undefined : childByKey.get(k)) ?? [];
|
|
1839
|
-
parent[relName] = single
|
|
1889
|
+
parent[relName] = single
|
|
1890
|
+
? (matches[0] ?? null)
|
|
1891
|
+
: PowqlInterface.pageOf(matches, relLimit, relOffset);
|
|
1840
1892
|
}
|
|
1841
1893
|
}
|
|
1842
1894
|
}
|
|
@@ -1902,7 +1954,13 @@ class PowqlInterface {
|
|
|
1902
1954
|
}
|
|
1903
1955
|
// (2) Target rows by PK, honouring the relation's own where/with/select/…
|
|
1904
1956
|
const options = (opt === true ? {} : opt);
|
|
1905
|
-
|
|
1957
|
+
this.assertPagination(options.limit, options.offset, `relation "${relName}"`);
|
|
1958
|
+
if (options.limit === 0) {
|
|
1959
|
+
for (const parent of parents)
|
|
1960
|
+
parent[relName] = [];
|
|
1961
|
+
return;
|
|
1962
|
+
}
|
|
1963
|
+
const targetQi = new PowqlInterface(this.pool, rel.to, this.schema, [], this.loaderChildOptions());
|
|
1906
1964
|
// This loader stitches on the TARGET's own primary key, so the PK has to be
|
|
1907
1965
|
// in the fetch even when the caller's select/omit excludes it, and has to
|
|
1908
1966
|
// come back off afterwards. Exactly the shape `loadRelation` uses for its
|
|
@@ -1939,6 +1997,11 @@ class PowqlInterface {
|
|
|
1939
1997
|
}
|
|
1940
1998
|
}
|
|
1941
1999
|
const targetByPk = new Map();
|
|
2000
|
+
// Position of each target in the fetch, i.e. its rank under the relation's
|
|
2001
|
+
// `orderBy`; the stitch below sorts a parent's children by it when an
|
|
2002
|
+
// orderBy was given, so `limit` keeps that parent's top-N and not its first
|
|
2003
|
+
// N junction rows. Across fetch CHUNKS the rank is fetch order.
|
|
2004
|
+
const targetRank = new Map();
|
|
1942
2005
|
const targetValList = [...allTargetVals].map((v) => targetPkColMeta ? coerceScalar(v, targetPkColMeta.tsType) : v);
|
|
1943
2006
|
const targetChunk = this.keyChunkSize(targetMeta, targetPkCol);
|
|
1944
2007
|
for (let i = 0; i < targetValList.length; i += targetChunk) {
|
|
@@ -1949,14 +2012,21 @@ class PowqlInterface {
|
|
|
1949
2012
|
};
|
|
1950
2013
|
const targets = (await targetQi.findMany({
|
|
1951
2014
|
...fetchOptions,
|
|
2015
|
+
// Unbounded: the relation limit/offset are per parent, applied in the stitch.
|
|
2016
|
+
limit: undefined,
|
|
2017
|
+
offset: undefined,
|
|
1952
2018
|
where,
|
|
1953
2019
|
with: options.with,
|
|
1954
2020
|
timeout: options.timeout ?? timeout,
|
|
1955
2021
|
// Public findMany, so the sentinel form. See loadRelation above.
|
|
1956
2022
|
includePii: includePii ? types_js_1.UNSAFE : undefined,
|
|
1957
2023
|
}));
|
|
1958
|
-
for (const t of targets)
|
|
1959
|
-
|
|
2024
|
+
for (const t of targets) {
|
|
2025
|
+
const pk = String(t[targetPkField]);
|
|
2026
|
+
targetByPk.set(pk, t);
|
|
2027
|
+
if (!targetRank.has(pk))
|
|
2028
|
+
targetRank.set(pk, targetRank.size);
|
|
2029
|
+
}
|
|
1960
2030
|
}
|
|
1961
2031
|
// (3) Stitch: each parent → its junction targets (m2m is always a list).
|
|
1962
2032
|
for (const parent of parents) {
|
|
@@ -1968,7 +2038,11 @@ class PowqlInterface {
|
|
|
1968
2038
|
if (child)
|
|
1969
2039
|
children.push(child);
|
|
1970
2040
|
}
|
|
1971
|
-
|
|
2041
|
+
if (options.orderBy) {
|
|
2042
|
+
const rank = (t) => targetRank.get(String(t[targetPkField])) ?? 0;
|
|
2043
|
+
children.sort((a, b) => rank(a) - rank(b));
|
|
2044
|
+
}
|
|
2045
|
+
parent[relName] = PowqlInterface.pageOf(children, options.limit, options.offset);
|
|
1972
2046
|
}
|
|
1973
2047
|
// Stitching is done: take the forced PK back off. Iterating the map rather
|
|
1974
2048
|
// than the stitched lists is deliberate, one target can be linked from many
|
|
@@ -2012,11 +2086,12 @@ class PowqlInterface {
|
|
|
2012
2086
|
* `referenceKey` on the TARGET table (the join's non-fetched side);
|
|
2013
2087
|
* - m2m keeps any `orderBy`/`limit`/`offset` on the loader (the junction-order
|
|
2014
2088
|
* stitch can't be reproduced by the 3-table join deterministically);
|
|
2015
|
-
* - a to-one relation `limit`/`offset` (meaningless) stays on the loader
|
|
2016
|
-
*
|
|
2017
|
-
*
|
|
2089
|
+
* - a to-one relation `limit`/`offset` (meaningless) stays on the loader. A
|
|
2090
|
+
* to-many relation `limit`/`offset` is a PER-PARENT bound on both paths,
|
|
2091
|
+
* fetched unbounded and sliced per parent at stitch time (`pageOf`), so
|
|
2092
|
+
* the join statement carries neither clause.
|
|
2018
2093
|
*/
|
|
2019
|
-
joinEligible(rel, opt, args
|
|
2094
|
+
joinEligible(rel, opt, args) {
|
|
2020
2095
|
const effLimit = args.limit ?? this.defaultLimit;
|
|
2021
2096
|
if (effLimit !== undefined || args.offset)
|
|
2022
2097
|
return false;
|
|
@@ -2057,9 +2132,8 @@ class PowqlInterface {
|
|
|
2057
2132
|
return false;
|
|
2058
2133
|
}
|
|
2059
2134
|
const single = rel.type === 'belongsTo' || rel.type === 'hasOne';
|
|
2060
|
-
if ((options.limit !== undefined || options.offset) &&
|
|
2135
|
+
if ((options.limit !== undefined || options.offset) && single)
|
|
2061
2136
|
return false;
|
|
2062
|
-
}
|
|
2063
2137
|
// A `limit 0` relation stays off the join statement for the same reason it
|
|
2064
2138
|
// stays off a nested projection: PowDB answered `limit 0` with one row below
|
|
2065
2139
|
// engine 0.20. The loader resolves it client-side, correctly on every version.
|
|
@@ -2106,13 +2180,13 @@ class PowqlInterface {
|
|
|
2106
2180
|
const { cols: childCols, forcedPk: childForcedPk } = this.joinChildCols(targetQi, options, includePii);
|
|
2107
2181
|
const filter = await this.joinFilter(targetQi, parent.resolvedWhere, options.where, 'c', params, options.timeout ?? timeout);
|
|
2108
2182
|
const order = targetQi.buildOrder(options.orderBy, params, 'c');
|
|
2183
|
+
// No limit/offset clause: on the join they would bound the TOTAL child
|
|
2184
|
+
// count across every parent. The relation's page is per parent (pageOf).
|
|
2109
2185
|
this.assertPagination(options.limit, options.offset, `relation "${relName}"`);
|
|
2110
|
-
const limitClause = options.limit !== undefined ? ` limit ${this.param(options.limit, params)}` : '';
|
|
2111
|
-
const offsetClause = options.offset ? ` offset ${this.param(options.offset, params)}` : '';
|
|
2112
2186
|
const proj = this.joinProjection(childCols, `p.${(0, powdb_shared_js_1.quotePowqlIdent)(parentKeyCol)}`, 'c');
|
|
2113
2187
|
const powql = `${targetQi.qt} as c join ${this.qt} as p ` +
|
|
2114
2188
|
`on c.${(0, powdb_shared_js_1.quotePowqlIdent)(childKeyCol)} = p.${(0, powdb_shared_js_1.quotePowqlIdent)(parentKeyCol)}` +
|
|
2115
|
-
`${filter}${order}
|
|
2189
|
+
`${filter}${order} ${proj}`;
|
|
2116
2190
|
// A READ: thread a read-shaped action through the exec seam.
|
|
2117
2191
|
const { rows, native } = await targetQi.exec(powql, params, timeout, 'findMany');
|
|
2118
2192
|
const single = rel.type === 'belongsTo' || rel.type === 'hasOne';
|
|
@@ -2120,7 +2194,9 @@ class PowqlInterface {
|
|
|
2120
2194
|
for (const p of parents) {
|
|
2121
2195
|
const key = this.joinKey(p[parentKeyField]);
|
|
2122
2196
|
const matches = (key == null ? undefined : byKey.get(key)) ?? [];
|
|
2123
|
-
p[relName] = single
|
|
2197
|
+
p[relName] = single
|
|
2198
|
+
? (matches[0] ?? null)
|
|
2199
|
+
: PowqlInterface.pageOf(matches, options.limit, options.offset);
|
|
2124
2200
|
}
|
|
2125
2201
|
}
|
|
2126
2202
|
/**
|
package/dist/powql.d.ts
CHANGED
|
@@ -155,6 +155,34 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
|
|
|
155
155
|
* the two can never disagree about which limit is in force.
|
|
156
156
|
*/
|
|
157
157
|
private effectiveLimit;
|
|
158
|
+
/**
|
|
159
|
+
* Options for the child-side interfaces the relation loaders and the
|
|
160
|
+
* relation-filter resolvers build: the client's, with `defaultLimit` cleared
|
|
161
|
+
* and the unlimited warning off. `defaultLimit` bounds the caller's PAGE; on
|
|
162
|
+
* an internal fetch that serves that page (a relation's children, a filter's
|
|
163
|
+
* key set) it would cap the whole page's children, or the key set, at the
|
|
164
|
+
* page size: a silent wrong answer by another route than the one
|
|
165
|
+
* {@link pageOf} closes. The SQL batched loader clears it the same way
|
|
166
|
+
* (`batchedChildOptions` in query/builder.ts).
|
|
167
|
+
*/
|
|
168
|
+
private childOptions?;
|
|
169
|
+
private loaderChildOptions;
|
|
170
|
+
/**
|
|
171
|
+
* ONE parent's window of its stitched children: the relation's `offset` and
|
|
172
|
+
* `limit` applied to that parent's own list.
|
|
173
|
+
*
|
|
174
|
+
* A relation `limit` means "at most N on EACH parent". The loaders used to
|
|
175
|
+
* spread it onto the flat child fetch (`post filter .author_id in (...)
|
|
176
|
+
* limit 5`), which caps the TOTAL across every parent in the chunk, so ten
|
|
177
|
+
* parents shared five posts and most got none, with no error. It shipped
|
|
178
|
+
* for the loaders' whole life because the one test of the shape compared
|
|
179
|
+
* the join path against the loader path and both were wrong the same way;
|
|
180
|
+
* the cross-engine benchmark found it, where the wrong answer was the fast
|
|
181
|
+
* one. Every loader now fetches its children unbounded, WITH the relation
|
|
182
|
+
* `orderBy` (which decides which rows the window keeps), and applies this
|
|
183
|
+
* at stitch time: the rule the SQL engines' batched loader has always used.
|
|
184
|
+
*/
|
|
185
|
+
private static pageOf;
|
|
158
186
|
/**
|
|
159
187
|
* Reject a negative `limit` / `offset` before it reaches the engine. PowDB
|
|
160
188
|
* casts both with `as usize` at execution, so below engine 0.20 a negative
|
|
@@ -623,9 +651,10 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
|
|
|
623
651
|
* `referenceKey` on the TARGET table (the join's non-fetched side);
|
|
624
652
|
* - m2m keeps any `orderBy`/`limit`/`offset` on the loader (the junction-order
|
|
625
653
|
* stitch can't be reproduced by the 3-table join deterministically);
|
|
626
|
-
* - a to-one relation `limit`/`offset` (meaningless) stays on the loader
|
|
627
|
-
*
|
|
628
|
-
*
|
|
654
|
+
* - a to-one relation `limit`/`offset` (meaningless) stays on the loader. A
|
|
655
|
+
* to-many relation `limit`/`offset` is a PER-PARENT bound on both paths,
|
|
656
|
+
* fetched unbounded and sliced per parent at stitch time (`pageOf`), so
|
|
657
|
+
* the join statement carries neither clause.
|
|
629
658
|
*/
|
|
630
659
|
private joinEligible;
|
|
631
660
|
/**
|
package/dist/powql.js
CHANGED
|
@@ -357,6 +357,42 @@ export class PowqlInterface {
|
|
|
357
357
|
effectiveLimit(args) {
|
|
358
358
|
return args.limit ?? this.defaultLimit;
|
|
359
359
|
}
|
|
360
|
+
/**
|
|
361
|
+
* Options for the child-side interfaces the relation loaders and the
|
|
362
|
+
* relation-filter resolvers build: the client's, with `defaultLimit` cleared
|
|
363
|
+
* and the unlimited warning off. `defaultLimit` bounds the caller's PAGE; on
|
|
364
|
+
* an internal fetch that serves that page (a relation's children, a filter's
|
|
365
|
+
* key set) it would cap the whole page's children, or the key set, at the
|
|
366
|
+
* page size: a silent wrong answer by another route than the one
|
|
367
|
+
* {@link pageOf} closes. The SQL batched loader clears it the same way
|
|
368
|
+
* (`batchedChildOptions` in query/builder.ts).
|
|
369
|
+
*/
|
|
370
|
+
childOptions;
|
|
371
|
+
loaderChildOptions() {
|
|
372
|
+
this.childOptions ??= { ...this.options, defaultLimit: undefined, warnOnUnlimited: false };
|
|
373
|
+
return this.childOptions;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* ONE parent's window of its stitched children: the relation's `offset` and
|
|
377
|
+
* `limit` applied to that parent's own list.
|
|
378
|
+
*
|
|
379
|
+
* A relation `limit` means "at most N on EACH parent". The loaders used to
|
|
380
|
+
* spread it onto the flat child fetch (`post filter .author_id in (...)
|
|
381
|
+
* limit 5`), which caps the TOTAL across every parent in the chunk, so ten
|
|
382
|
+
* parents shared five posts and most got none, with no error. It shipped
|
|
383
|
+
* for the loaders' whole life because the one test of the shape compared
|
|
384
|
+
* the join path against the loader path and both were wrong the same way;
|
|
385
|
+
* the cross-engine benchmark found it, where the wrong answer was the fast
|
|
386
|
+
* one. Every loader now fetches its children unbounded, WITH the relation
|
|
387
|
+
* `orderBy` (which decides which rows the window keeps), and applies this
|
|
388
|
+
* at stitch time: the rule the SQL engines' batched loader has always used.
|
|
389
|
+
*/
|
|
390
|
+
static pageOf(rows, limit, offset) {
|
|
391
|
+
if (limit === undefined && !offset)
|
|
392
|
+
return rows;
|
|
393
|
+
const start = offset ?? 0;
|
|
394
|
+
return rows.slice(start, limit === undefined ? undefined : start + limit);
|
|
395
|
+
}
|
|
360
396
|
/**
|
|
361
397
|
* Reject a negative `limit` / `offset` before it reaches the engine. PowDB
|
|
362
398
|
* casts both with `as usize` at execution, so below engine 0.20 a negative
|
|
@@ -938,7 +974,7 @@ export class PowqlInterface {
|
|
|
938
974
|
const childCol = rel.type === 'belongsTo' ? rk[0] : fk[0];
|
|
939
975
|
const localField = this.meta.reverseColumnMap[localCol] ?? localCol;
|
|
940
976
|
const childField = targetMeta.reverseColumnMap[childCol] ?? childCol;
|
|
941
|
-
const targetQi = new PowqlInterface(this.pool, rel.to, this.schema, [], this.
|
|
977
|
+
const targetQi = new PowqlInterface(this.pool, rel.to, this.schema, [], this.loaderChildOptions());
|
|
942
978
|
const collect = async (w) => {
|
|
943
979
|
const rows = await targetQi.findMany({
|
|
944
980
|
where: w,
|
|
@@ -977,7 +1013,7 @@ export class PowqlInterface {
|
|
|
977
1013
|
const sourceRefField = this.meta.reverseColumnMap[sourceRefCol] ?? sourceRefCol;
|
|
978
1014
|
const sourceRefColMeta = this.meta.columns.find((c) => c.name === sourceRefCol);
|
|
979
1015
|
const targetPkField = targetMeta.reverseColumnMap[targetPkCol] ?? targetPkCol;
|
|
980
|
-
const targetQi = new PowqlInterface(this.pool, rel.to, this.schema, [], this.
|
|
1016
|
+
const targetQi = new PowqlInterface(this.pool, rel.to, this.schema, [], this.loaderChildOptions());
|
|
981
1017
|
const collectTargetPks = async (w) => {
|
|
982
1018
|
const rows = await targetQi.findMany({
|
|
983
1019
|
where: w,
|
|
@@ -1689,7 +1725,7 @@ export class PowqlInterface {
|
|
|
1689
1725
|
const rel = this.meta.relations[relName];
|
|
1690
1726
|
if (!rel)
|
|
1691
1727
|
throw new ValidationError(`Unknown relation "${relName}" on "${this.table}".`);
|
|
1692
|
-
if (strategyIsJoin && parent && this.joinEligible(rel, opt, parent.args
|
|
1728
|
+
if (strategyIsJoin && parent && this.joinEligible(rel, opt, parent.args)) {
|
|
1693
1729
|
if (this.capabilities.serverJoins) {
|
|
1694
1730
|
await this.loadRelationViaJoin(parents, rel, relName, opt, parent, timeout, includePii);
|
|
1695
1731
|
continue;
|
|
@@ -1712,7 +1748,19 @@ export class PowqlInterface {
|
|
|
1712
1748
|
throw new UnsupportedFeatureError('composite-key nested reads', 'PowDB', `relation "${relName}"`);
|
|
1713
1749
|
}
|
|
1714
1750
|
const options = (opt === true ? {} : opt);
|
|
1715
|
-
|
|
1751
|
+
// The relation's own page, applied PER PARENT at stitch time (see pageOf),
|
|
1752
|
+
// never on the flat fetch below.
|
|
1753
|
+
const relLimit = options.limit;
|
|
1754
|
+
const relOffset = options.offset;
|
|
1755
|
+
this.assertPagination(relLimit, relOffset, `relation "${relName}"`);
|
|
1756
|
+
const single = rel.type === 'belongsTo' || rel.type === 'hasOne';
|
|
1757
|
+
if (relLimit === 0) {
|
|
1758
|
+
// `[]` on every parent by construction: nothing to fetch.
|
|
1759
|
+
for (const parent of parents)
|
|
1760
|
+
parent[relName] = single ? null : [];
|
|
1761
|
+
continue;
|
|
1762
|
+
}
|
|
1763
|
+
const targetQi = new PowqlInterface(this.pool, rel.to, this.schema, [], this.loaderChildOptions());
|
|
1716
1764
|
const targetMeta = this.schema.tables[rel.to];
|
|
1717
1765
|
// Local key on the parent, remote key on the target child.
|
|
1718
1766
|
const parentKeyCol = rel.type === 'belongsTo' ? fk[0] : rk[0];
|
|
@@ -1770,6 +1818,9 @@ export class PowqlInterface {
|
|
|
1770
1818
|
};
|
|
1771
1819
|
const children = (await targetQi.findMany({
|
|
1772
1820
|
...fetchOptions,
|
|
1821
|
+
// Unbounded: the relation limit/offset are per parent, applied below.
|
|
1822
|
+
limit: undefined,
|
|
1823
|
+
offset: undefined,
|
|
1773
1824
|
where: childWhere,
|
|
1774
1825
|
with: options.with,
|
|
1775
1826
|
timeout: options.timeout ?? timeout,
|
|
@@ -1796,11 +1847,12 @@ export class PowqlInterface {
|
|
|
1796
1847
|
delete child[childKeyField];
|
|
1797
1848
|
}
|
|
1798
1849
|
}
|
|
1799
|
-
const single = rel.type === 'belongsTo' || rel.type === 'hasOne';
|
|
1800
1850
|
for (const parent of parents) {
|
|
1801
1851
|
const k = this.joinKey(parent[parentKeyField]);
|
|
1802
1852
|
const matches = (k == null ? undefined : childByKey.get(k)) ?? [];
|
|
1803
|
-
parent[relName] = single
|
|
1853
|
+
parent[relName] = single
|
|
1854
|
+
? (matches[0] ?? null)
|
|
1855
|
+
: PowqlInterface.pageOf(matches, relLimit, relOffset);
|
|
1804
1856
|
}
|
|
1805
1857
|
}
|
|
1806
1858
|
}
|
|
@@ -1866,7 +1918,13 @@ export class PowqlInterface {
|
|
|
1866
1918
|
}
|
|
1867
1919
|
// (2) Target rows by PK, honouring the relation's own where/with/select/…
|
|
1868
1920
|
const options = (opt === true ? {} : opt);
|
|
1869
|
-
|
|
1921
|
+
this.assertPagination(options.limit, options.offset, `relation "${relName}"`);
|
|
1922
|
+
if (options.limit === 0) {
|
|
1923
|
+
for (const parent of parents)
|
|
1924
|
+
parent[relName] = [];
|
|
1925
|
+
return;
|
|
1926
|
+
}
|
|
1927
|
+
const targetQi = new PowqlInterface(this.pool, rel.to, this.schema, [], this.loaderChildOptions());
|
|
1870
1928
|
// This loader stitches on the TARGET's own primary key, so the PK has to be
|
|
1871
1929
|
// in the fetch even when the caller's select/omit excludes it, and has to
|
|
1872
1930
|
// come back off afterwards. Exactly the shape `loadRelation` uses for its
|
|
@@ -1903,6 +1961,11 @@ export class PowqlInterface {
|
|
|
1903
1961
|
}
|
|
1904
1962
|
}
|
|
1905
1963
|
const targetByPk = new Map();
|
|
1964
|
+
// Position of each target in the fetch, i.e. its rank under the relation's
|
|
1965
|
+
// `orderBy`; the stitch below sorts a parent's children by it when an
|
|
1966
|
+
// orderBy was given, so `limit` keeps that parent's top-N and not its first
|
|
1967
|
+
// N junction rows. Across fetch CHUNKS the rank is fetch order.
|
|
1968
|
+
const targetRank = new Map();
|
|
1906
1969
|
const targetValList = [...allTargetVals].map((v) => targetPkColMeta ? coerceScalar(v, targetPkColMeta.tsType) : v);
|
|
1907
1970
|
const targetChunk = this.keyChunkSize(targetMeta, targetPkCol);
|
|
1908
1971
|
for (let i = 0; i < targetValList.length; i += targetChunk) {
|
|
@@ -1913,14 +1976,21 @@ export class PowqlInterface {
|
|
|
1913
1976
|
};
|
|
1914
1977
|
const targets = (await targetQi.findMany({
|
|
1915
1978
|
...fetchOptions,
|
|
1979
|
+
// Unbounded: the relation limit/offset are per parent, applied in the stitch.
|
|
1980
|
+
limit: undefined,
|
|
1981
|
+
offset: undefined,
|
|
1916
1982
|
where,
|
|
1917
1983
|
with: options.with,
|
|
1918
1984
|
timeout: options.timeout ?? timeout,
|
|
1919
1985
|
// Public findMany, so the sentinel form. See loadRelation above.
|
|
1920
1986
|
includePii: includePii ? UNSAFE : undefined,
|
|
1921
1987
|
}));
|
|
1922
|
-
for (const t of targets)
|
|
1923
|
-
|
|
1988
|
+
for (const t of targets) {
|
|
1989
|
+
const pk = String(t[targetPkField]);
|
|
1990
|
+
targetByPk.set(pk, t);
|
|
1991
|
+
if (!targetRank.has(pk))
|
|
1992
|
+
targetRank.set(pk, targetRank.size);
|
|
1993
|
+
}
|
|
1924
1994
|
}
|
|
1925
1995
|
// (3) Stitch: each parent → its junction targets (m2m is always a list).
|
|
1926
1996
|
for (const parent of parents) {
|
|
@@ -1932,7 +2002,11 @@ export class PowqlInterface {
|
|
|
1932
2002
|
if (child)
|
|
1933
2003
|
children.push(child);
|
|
1934
2004
|
}
|
|
1935
|
-
|
|
2005
|
+
if (options.orderBy) {
|
|
2006
|
+
const rank = (t) => targetRank.get(String(t[targetPkField])) ?? 0;
|
|
2007
|
+
children.sort((a, b) => rank(a) - rank(b));
|
|
2008
|
+
}
|
|
2009
|
+
parent[relName] = PowqlInterface.pageOf(children, options.limit, options.offset);
|
|
1936
2010
|
}
|
|
1937
2011
|
// Stitching is done: take the forced PK back off. Iterating the map rather
|
|
1938
2012
|
// than the stitched lists is deliberate, one target can be linked from many
|
|
@@ -1976,11 +2050,12 @@ export class PowqlInterface {
|
|
|
1976
2050
|
* `referenceKey` on the TARGET table (the join's non-fetched side);
|
|
1977
2051
|
* - m2m keeps any `orderBy`/`limit`/`offset` on the loader (the junction-order
|
|
1978
2052
|
* stitch can't be reproduced by the 3-table join deterministically);
|
|
1979
|
-
* - a to-one relation `limit`/`offset` (meaningless) stays on the loader
|
|
1980
|
-
*
|
|
1981
|
-
*
|
|
2053
|
+
* - a to-one relation `limit`/`offset` (meaningless) stays on the loader. A
|
|
2054
|
+
* to-many relation `limit`/`offset` is a PER-PARENT bound on both paths,
|
|
2055
|
+
* fetched unbounded and sliced per parent at stitch time (`pageOf`), so
|
|
2056
|
+
* the join statement carries neither clause.
|
|
1982
2057
|
*/
|
|
1983
|
-
joinEligible(rel, opt, args
|
|
2058
|
+
joinEligible(rel, opt, args) {
|
|
1984
2059
|
const effLimit = args.limit ?? this.defaultLimit;
|
|
1985
2060
|
if (effLimit !== undefined || args.offset)
|
|
1986
2061
|
return false;
|
|
@@ -2021,9 +2096,8 @@ export class PowqlInterface {
|
|
|
2021
2096
|
return false;
|
|
2022
2097
|
}
|
|
2023
2098
|
const single = rel.type === 'belongsTo' || rel.type === 'hasOne';
|
|
2024
|
-
if ((options.limit !== undefined || options.offset) &&
|
|
2099
|
+
if ((options.limit !== undefined || options.offset) && single)
|
|
2025
2100
|
return false;
|
|
2026
|
-
}
|
|
2027
2101
|
// A `limit 0` relation stays off the join statement for the same reason it
|
|
2028
2102
|
// stays off a nested projection: PowDB answered `limit 0` with one row below
|
|
2029
2103
|
// engine 0.20. The loader resolves it client-side, correctly on every version.
|
|
@@ -2070,13 +2144,13 @@ export class PowqlInterface {
|
|
|
2070
2144
|
const { cols: childCols, forcedPk: childForcedPk } = this.joinChildCols(targetQi, options, includePii);
|
|
2071
2145
|
const filter = await this.joinFilter(targetQi, parent.resolvedWhere, options.where, 'c', params, options.timeout ?? timeout);
|
|
2072
2146
|
const order = targetQi.buildOrder(options.orderBy, params, 'c');
|
|
2147
|
+
// No limit/offset clause: on the join they would bound the TOTAL child
|
|
2148
|
+
// count across every parent. The relation's page is per parent (pageOf).
|
|
2073
2149
|
this.assertPagination(options.limit, options.offset, `relation "${relName}"`);
|
|
2074
|
-
const limitClause = options.limit !== undefined ? ` limit ${this.param(options.limit, params)}` : '';
|
|
2075
|
-
const offsetClause = options.offset ? ` offset ${this.param(options.offset, params)}` : '';
|
|
2076
2150
|
const proj = this.joinProjection(childCols, `p.${quotePowqlIdent(parentKeyCol)}`, 'c');
|
|
2077
2151
|
const powql = `${targetQi.qt} as c join ${this.qt} as p ` +
|
|
2078
2152
|
`on c.${quotePowqlIdent(childKeyCol)} = p.${quotePowqlIdent(parentKeyCol)}` +
|
|
2079
|
-
`${filter}${order}
|
|
2153
|
+
`${filter}${order} ${proj}`;
|
|
2080
2154
|
// A READ: thread a read-shaped action through the exec seam.
|
|
2081
2155
|
const { rows, native } = await targetQi.exec(powql, params, timeout, 'findMany');
|
|
2082
2156
|
const single = rel.type === 'belongsTo' || rel.type === 'hasOne';
|
|
@@ -2084,7 +2158,9 @@ export class PowqlInterface {
|
|
|
2084
2158
|
for (const p of parents) {
|
|
2085
2159
|
const key = this.joinKey(p[parentKeyField]);
|
|
2086
2160
|
const matches = (key == null ? undefined : byKey.get(key)) ?? [];
|
|
2087
|
-
p[relName] = single
|
|
2161
|
+
p[relName] = single
|
|
2162
|
+
? (matches[0] ?? null)
|
|
2163
|
+
: PowqlInterface.pageOf(matches, options.limit, options.offset);
|
|
2088
2164
|
}
|
|
2089
2165
|
}
|
|
2090
2166
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "turbine-orm",
|
|
3
|
-
"version": "0.78.0-next.
|
|
3
|
+
"version": "0.78.0-next.b792a6a",
|
|
4
4
|
"description": "Postgres-native TypeScript ORM, runs on Neon, Vercel Postgres, Cloudflare, Supabase. Streaming cursors, typed errors, single-query nested relations. One dependency, no WASM engine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"//exports": "Each subpath declares its types PER CONDITION. A single shared top-level \"types\" resolves to the ESM declarations for `require` too, which is TS1479 (\"is an ES module ... cannot be require()d\") for any CJS consumer on moduleResolution node16/nodenext. The require condition points at dist/cjs, which ships its own {\"type\":\"commonjs\"} package.json, so those declarations are CJS declarations. Gated in CI by publint + @arethetypeswrong/cli + a real .cts consumer typecheck (see the package-types job in ci.yml).",
|