linkgress-orm 0.4.75 → 0.4.78
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/config/linkgress-config.d.ts +23 -0
- package/dist/config/linkgress-config.d.ts.map +1 -1
- package/dist/config/linkgress-config.js +29 -0
- package/dist/config/linkgress-config.js.map +1 -1
- package/dist/entity/db-context.d.ts +9 -0
- package/dist/entity/db-context.d.ts.map +1 -1
- package/dist/entity/db-context.js +76 -65
- package/dist/entity/db-context.js.map +1 -1
- package/dist/entity/entity-builder.d.ts.map +1 -1
- package/dist/entity/entity-builder.js +12 -1
- package/dist/entity/entity-builder.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -4
- package/dist/index.js.map +1 -1
- package/dist/query/collection-strategy.interface.d.ts +14 -0
- package/dist/query/collection-strategy.interface.d.ts.map +1 -1
- package/dist/query/conditions.d.ts +36 -0
- package/dist/query/conditions.d.ts.map +1 -1
- package/dist/query/conditions.js +78 -10
- package/dist/query/conditions.js.map +1 -1
- package/dist/query/lateral-sql-cache.d.ts +53 -0
- package/dist/query/lateral-sql-cache.d.ts.map +1 -0
- package/dist/query/lateral-sql-cache.js +63 -0
- package/dist/query/lateral-sql-cache.js.map +1 -0
- package/dist/query/query-builder.d.ts +18 -1
- package/dist/query/query-builder.d.ts.map +1 -1
- package/dist/query/query-builder.js +209 -64
- package/dist/query/query-builder.js.map +1 -1
- package/dist/query/query-utils.d.ts +1 -0
- package/dist/query/query-utils.d.ts.map +1 -1
- package/dist/query/query-utils.js +20 -0
- package/dist/query/query-utils.js.map +1 -1
- package/dist/query/strategies/cte-collection-strategy.d.ts.map +1 -1
- package/dist/query/strategies/cte-collection-strategy.js +105 -104
- package/dist/query/strategies/cte-collection-strategy.js.map +1 -1
- package/dist/query/strategies/lateral-collection-strategy.d.ts +28 -3
- package/dist/query/strategies/lateral-collection-strategy.d.ts.map +1 -1
- package/dist/query/strategies/lateral-collection-strategy.js +227 -230
- package/dist/query/strategies/lateral-collection-strategy.js.map +1 -1
- package/dist/query/strategies/temptable-collection-strategy.d.ts.map +1 -1
- package/dist/query/strategies/temptable-collection-strategy.js +85 -84
- package/dist/query/strategies/temptable-collection-strategy.js.map +1 -1
- package/package.json +80 -80
|
@@ -1,7 +1,61 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.LateralCollectionStrategy = void 0;
|
|
3
|
+
exports.LateralCollectionStrategy = exports.lateralShapeKey = void 0;
|
|
4
|
+
const query_utils_1 = require("../query-utils");
|
|
4
5
|
const join_utils_1 = require("../join-utils");
|
|
6
|
+
const lateral_sql_cache_1 = require("../lateral-sql-cache");
|
|
7
|
+
/** Key-part separator: a control character no alias, column name or SQL text contains. */
|
|
8
|
+
const KEY_SEP = String.fromCharCode(1);
|
|
9
|
+
/** A column list as one key part; the one-column case (nearly every FK) needs no join allocation. */
|
|
10
|
+
const listKey = (list) => (list === undefined ? '' : list.length === 1 ? list[0] : list.join(','));
|
|
11
|
+
/** Appends what `buildNavigationJoinsWithAlias` reads from each join, including the alias-map resolution of its source. */
|
|
12
|
+
const appendNavigationJoinsKey = (key, joins, aliasMap) => {
|
|
13
|
+
if (!joins) {
|
|
14
|
+
return key + '-' + KEY_SEP;
|
|
15
|
+
}
|
|
16
|
+
key += joins.length + KEY_SEP;
|
|
17
|
+
for (const join of joins) {
|
|
18
|
+
key += join.alias + KEY_SEP + join.targetTable + KEY_SEP + (join.targetSchema ?? '') + KEY_SEP
|
|
19
|
+
+ listKey(join.foreignKeys) + KEY_SEP + listKey(join.matches) + KEY_SEP + (join.isMandatory ? 'I' : 'L') + KEY_SEP
|
|
20
|
+
+ join.sourceAlias + KEY_SEP + (aliasMap?.get(join.sourceAlias) ?? '') + KEY_SEP;
|
|
21
|
+
}
|
|
22
|
+
return key;
|
|
23
|
+
};
|
|
24
|
+
/** Appends the projection: aliases, expressions, nesting, and nested laterals by memo id (or by text when not memoised). */
|
|
25
|
+
const appendFieldsKey = (key, fields) => {
|
|
26
|
+
key += '[' + fields.length + KEY_SEP;
|
|
27
|
+
for (const field of fields) {
|
|
28
|
+
key += field.alias + KEY_SEP + (field.expression ?? '') + KEY_SEP + (field.isColumn === true ? 'c' : 'e') + KEY_SEP;
|
|
29
|
+
if (field.nestedCteJoin) {
|
|
30
|
+
key += field.nestedCteJoin.cteName + KEY_SEP + (field.nestedCteJoin.memoId ? '#' + field.nestedCteJoin.memoId : field.nestedCteJoin.joinClause) + KEY_SEP;
|
|
31
|
+
}
|
|
32
|
+
key = field.nested ? appendFieldsKey(key, field.nested) : key + '-' + KEY_SEP;
|
|
33
|
+
}
|
|
34
|
+
return key + ']' + KEY_SEP;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* The LateralSqlCache key of an aggregation: every input `LateralCollectionStrategy.render` and
|
|
38
|
+
* its helpers read, in order — the alias inputs (counter, relation, tables), the correlation
|
|
39
|
+
* columns, the clause TEXT (which already carries the `$n` placeholder numbering), the scalar
|
|
40
|
+
* flags and limits, both navigation-join lists with their alias-map resolution, and the
|
|
41
|
+
* projection. Parameter VALUES never enter the strategy and never enter the key. Built by plain
|
|
42
|
+
* concatenation (a rope V8 flattens once, on the lookup) — cheaper than an array join.
|
|
43
|
+
*/
|
|
44
|
+
const lateralShapeKey = (config, context) => {
|
|
45
|
+
const aliasMap = context.lateralTableAliasMap;
|
|
46
|
+
let key = config.counter + KEY_SEP + config.relationName + KEY_SEP + config.targetTable + KEY_SEP + config.foreignKey + KEY_SEP
|
|
47
|
+
+ listKey(config.foreignKeys) + KEY_SEP + listKey(config.matches) + KEY_SEP + (config.foreignKeyTableAlias ?? '') + KEY_SEP
|
|
48
|
+
+ config.sourceTable + KEY_SEP + (aliasMap?.get(config.sourceTable) ?? '') + KEY_SEP
|
|
49
|
+
+ (config.whereClause ?? '') + KEY_SEP + (config.orderByClause ?? '') + KEY_SEP + (config.orderByClauseAlias ?? '') + KEY_SEP
|
|
50
|
+
+ (config.limitValue ?? '') + KEY_SEP + (config.offsetValue ?? '') + KEY_SEP
|
|
51
|
+
+ (config.isDistinct === true ? 'D' : '') + (config.isSingleResult === true ? 'S' : '') + (config.useJsonArrayAggregation === true ? 'J' : '') + KEY_SEP
|
|
52
|
+
+ config.aggregationType + KEY_SEP + (config.aggregateField ?? '') + KEY_SEP + (config.aggregateExpression ?? '') + KEY_SEP
|
|
53
|
+
+ (config.arrayField ?? '') + KEY_SEP + config.defaultValue + KEY_SEP;
|
|
54
|
+
key = appendNavigationJoinsKey(key, config.navigationJoins, aliasMap);
|
|
55
|
+
key = appendNavigationJoinsKey(key, config.selectorNavigationJoins, aliasMap);
|
|
56
|
+
return appendFieldsKey(key, config.selectedFields);
|
|
57
|
+
};
|
|
58
|
+
exports.lateralShapeKey = lateralShapeKey;
|
|
5
59
|
/**
|
|
6
60
|
* LATERAL JOIN-based collection strategy
|
|
7
61
|
*
|
|
@@ -70,6 +124,38 @@ class LateralCollectionStrategy {
|
|
|
70
124
|
return `"${fkTableAlias}"."${foreignKey}" = "${sourceAlias}"."id"`;
|
|
71
125
|
}
|
|
72
126
|
buildAggregation(config, context) {
|
|
127
|
+
// Memoised per shape (see LateralSqlCache): the rendering below reads nothing but the
|
|
128
|
+
// config and the enclosing alias map, and mutates neither, so a hit is a pure lookup.
|
|
129
|
+
if (!lateral_sql_cache_1.LateralSqlCache.isEnabled()) {
|
|
130
|
+
return this.render(config, context);
|
|
131
|
+
}
|
|
132
|
+
const key = (0, exports.lateralShapeKey)(config, context);
|
|
133
|
+
const hit = lateral_sql_cache_1.LateralSqlCache.get(key);
|
|
134
|
+
if (hit) {
|
|
135
|
+
return {
|
|
136
|
+
sql: hit.sql,
|
|
137
|
+
params: context.allParams,
|
|
138
|
+
tableName: hit.tableName,
|
|
139
|
+
joinClause: hit.joinClause,
|
|
140
|
+
selectExpression: hit.selectExpression,
|
|
141
|
+
isCTE: false,
|
|
142
|
+
memoId: hit.id,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
const rendered = this.render(config, context);
|
|
146
|
+
const entry = lateral_sql_cache_1.LateralSqlCache.store(key, {
|
|
147
|
+
sql: rendered.sql,
|
|
148
|
+
joinClause: rendered.joinClause,
|
|
149
|
+
selectExpression: rendered.selectExpression,
|
|
150
|
+
tableName: rendered.tableName,
|
|
151
|
+
});
|
|
152
|
+
if (entry.id > 0) {
|
|
153
|
+
rendered.memoId = entry.id;
|
|
154
|
+
}
|
|
155
|
+
return rendered;
|
|
156
|
+
}
|
|
157
|
+
/** The uncached rendering behind {@link buildAggregation}. */
|
|
158
|
+
render(config, context) {
|
|
73
159
|
const lateralAlias = `lateral_${config.counter}`;
|
|
74
160
|
// Optimization: For simple aggregations without LIMIT/OFFSET/ORDER BY,
|
|
75
161
|
// use a correlated subquery in SELECT instead of LATERAL JOIN.
|
|
@@ -164,7 +250,10 @@ class LateralCollectionStrategy {
|
|
|
164
250
|
// Helper to rewrite expressions that reference the collection's table to use inner alias
|
|
165
251
|
const rewriteTableReference = (expression) => {
|
|
166
252
|
// Replace the special marker alias `"__collection_tableName__".` with `"innerTableAlias".`
|
|
167
|
-
|
|
253
|
+
if (!expression.includes('"__collection_')) {
|
|
254
|
+
return expression; // nothing to rewrite — skip the regex pass entirely
|
|
255
|
+
}
|
|
256
|
+
const markerPattern = (0, query_utils_1.collectionMarkerPattern)(targetTable, true);
|
|
168
257
|
return expression.replace(markerPattern, `"${innerTableAlias}".`);
|
|
169
258
|
};
|
|
170
259
|
// Build navigation JOINs for multi-level navigation (selector joins only)
|
|
@@ -198,24 +287,24 @@ class LateralCollectionStrategy {
|
|
|
198
287
|
// Pattern: (SELECT array_agg(x) FROM (SELECT DISTINCT x FROM ...) sub)
|
|
199
288
|
// This is more efficient than array_agg(DISTINCT x) which forces a sort
|
|
200
289
|
if (isDistinct) {
|
|
201
|
-
subquerySQL = `(SELECT COALESCE(${arrayAggFn}("${arrayField}"), ${defaultValue})
|
|
202
|
-
FROM (SELECT DISTINCT ${fieldExpression} as "${arrayField}"
|
|
203
|
-
FROM "${targetTable}" "${innerTableAlias}"
|
|
204
|
-
${navJoinsSQL}
|
|
290
|
+
subquerySQL = `(SELECT COALESCE(${arrayAggFn}("${arrayField}"), ${defaultValue})
|
|
291
|
+
FROM (SELECT DISTINCT ${fieldExpression} as "${arrayField}"
|
|
292
|
+
FROM "${targetTable}" "${innerTableAlias}"
|
|
293
|
+
${navJoinsSQL}
|
|
205
294
|
WHERE ${whereSQL}) "sq")`;
|
|
206
295
|
}
|
|
207
296
|
else {
|
|
208
|
-
subquerySQL = `(SELECT COALESCE(${arrayAggFn}(${fieldExpression}), ${defaultValue})
|
|
209
|
-
FROM "${targetTable}" "${innerTableAlias}"
|
|
210
|
-
${navJoinsSQL}
|
|
297
|
+
subquerySQL = `(SELECT COALESCE(${arrayAggFn}(${fieldExpression}), ${defaultValue})
|
|
298
|
+
FROM "${targetTable}" "${innerTableAlias}"
|
|
299
|
+
${navJoinsSQL}
|
|
211
300
|
WHERE ${whereSQL})`;
|
|
212
301
|
}
|
|
213
302
|
}
|
|
214
303
|
else if (aggregationType === 'exists') {
|
|
215
304
|
// EXISTS as correlated subquery: (SELECT EXISTS(SELECT 1 FROM ... WHERE ...))
|
|
216
|
-
subquerySQL = `(SELECT EXISTS(SELECT 1
|
|
217
|
-
FROM "${targetTable}" "${innerTableAlias}"
|
|
218
|
-
${navJoinsSQL}
|
|
305
|
+
subquerySQL = `(SELECT EXISTS(SELECT 1
|
|
306
|
+
FROM "${targetTable}" "${innerTableAlias}"
|
|
307
|
+
${navJoinsSQL}
|
|
219
308
|
WHERE ${whereSQL}))`;
|
|
220
309
|
}
|
|
221
310
|
else {
|
|
@@ -243,9 +332,9 @@ WHERE ${whereSQL}))`;
|
|
|
243
332
|
throw new Error(`Unknown aggregation type: ${aggregationType}`);
|
|
244
333
|
}
|
|
245
334
|
// Build correlated subquery for scalar aggregation
|
|
246
|
-
subquerySQL = `(SELECT COALESCE(${aggregateSql}, ${defaultValue})
|
|
247
|
-
FROM "${targetTable}" "${innerTableAlias}"
|
|
248
|
-
${navJoinsSQL}
|
|
335
|
+
subquerySQL = `(SELECT COALESCE(${aggregateSql}, ${defaultValue})
|
|
336
|
+
FROM "${targetTable}" "${innerTableAlias}"
|
|
337
|
+
${navJoinsSQL}
|
|
249
338
|
WHERE ${whereSQL})`;
|
|
250
339
|
}
|
|
251
340
|
// For correlated subquery, the select expression IS the subquery
|
|
@@ -260,20 +349,60 @@ WHERE ${whereSQL})`;
|
|
|
260
349
|
};
|
|
261
350
|
}
|
|
262
351
|
/**
|
|
263
|
-
*
|
|
264
|
-
*
|
|
352
|
+
* Rewrites the collection marker alias (`"__collection_<table>__".`) to the lateral's inner
|
|
353
|
+
* alias. The regex pass runs only when the marker is present at all.
|
|
354
|
+
*/
|
|
355
|
+
rewriteMarker(expression, targetTable, innerTableAlias) {
|
|
356
|
+
if (!expression.includes('"__collection_')) {
|
|
357
|
+
return expression;
|
|
358
|
+
}
|
|
359
|
+
return expression.replace((0, query_utils_1.collectionMarkerPattern)(targetTable, true), '"' + innerTableAlias + '".');
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* One select-list entry for a leaf field. A bare quoted column (`"col"`, no dot, no inner
|
|
363
|
+
* quote) is qualified with the inner alias; anything else has the marker rewritten and is
|
|
364
|
+
* aliased unless it already renders as exactly `"<alias>"`.
|
|
265
365
|
*/
|
|
266
|
-
|
|
267
|
-
const
|
|
366
|
+
renderLeafSelect(expression, fullAlias, targetTable, innerTableAlias) {
|
|
367
|
+
const length = expression.length;
|
|
368
|
+
const isSimpleColumn = length > 2
|
|
369
|
+
&& expression.charCodeAt(0) === 34
|
|
370
|
+
&& expression.charCodeAt(length - 1) === 34
|
|
371
|
+
&& expression.indexOf('"', 1) === length - 1
|
|
372
|
+
&& expression.indexOf('.') === -1;
|
|
373
|
+
if (isSimpleColumn) {
|
|
374
|
+
return '"' + innerTableAlias + '".' + expression + ' as "' + fullAlias + '"';
|
|
375
|
+
}
|
|
376
|
+
const rewritten = this.rewriteMarker(expression, targetTable, innerTableAlias);
|
|
377
|
+
return rewritten === '"' + fullAlias + '"' ? rewritten : rewritten + ' as "' + fullAlias + '"';
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Renders the selected fields in ONE recursive pass: returns the `json_build_object(...)` text
|
|
381
|
+
* of this level and appends the flattened inner select list and the nested lateral joins to
|
|
382
|
+
* `out` (same order as the three separate walks this replaces — depth first, own join before
|
|
383
|
+
* the nested ones). No intermediate arrays: the select list of a wide collection projection was
|
|
384
|
+
* three allocations per field and three joins per statement.
|
|
385
|
+
*/
|
|
386
|
+
renderFields(fields, prefix, targetTable, innerTableAlias, out) {
|
|
387
|
+
let json = '';
|
|
268
388
|
for (const field of fields) {
|
|
389
|
+
const alias = field.alias;
|
|
390
|
+
const fullAlias = prefix ? prefix + '__' + alias : alias;
|
|
269
391
|
if (field.nestedCteJoin) {
|
|
270
|
-
joins.
|
|
392
|
+
out.joins += (out.joins ? '\n ' : '') + field.nestedCteJoin.joinClause;
|
|
271
393
|
}
|
|
272
394
|
if (field.nested) {
|
|
273
|
-
|
|
395
|
+
const nestedJson = this.renderFields(field.nested, fullAlias, targetTable, innerTableAlias, out);
|
|
396
|
+
json += (json ? ', ' : '') + "'" + alias + "', " + nestedJson;
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
json += (json ? ', ' : '') + "'" + alias + "', \"" + fullAlias + '"';
|
|
400
|
+
if (field.expression) {
|
|
401
|
+
out.select += (out.select ? ', ' : '') + this.renderLeafSelect(field.expression, fullAlias, targetTable, innerTableAlias);
|
|
402
|
+
}
|
|
274
403
|
}
|
|
275
404
|
}
|
|
276
|
-
return
|
|
405
|
+
return 'json_build_object(' + json + ')';
|
|
277
406
|
}
|
|
278
407
|
/**
|
|
279
408
|
* Build navigation JOINs SQL for multi-level navigation in collection queries
|
|
@@ -309,14 +438,15 @@ WHERE ${whereSQL})`;
|
|
|
309
438
|
* @param targetTable - Optional: the original target table name (e.g., "posts") to map to innerTableAlias
|
|
310
439
|
* @param context - Optional: QueryContext containing lateralTableAliasMap for nested lateral references
|
|
311
440
|
*/
|
|
312
|
-
buildNavigationJoinsWithAlias(navigationJoins, innerTableAlias, targetTable, context) {
|
|
441
|
+
buildNavigationJoinsWithAlias(navigationJoins, innerTableAlias, targetTable, context, relationName) {
|
|
313
442
|
if (!navigationJoins || navigationJoins.length === 0) {
|
|
314
443
|
return '';
|
|
315
444
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
445
|
+
if (relationName === undefined) {
|
|
446
|
+
// `lateral_<n>_<relation>` — the relation name is everything after the second `_`
|
|
447
|
+
const parts = innerTableAlias.split('_');
|
|
448
|
+
relationName = parts.length >= 3 ? parts.slice(2).join('_') : innerTableAlias;
|
|
449
|
+
}
|
|
320
450
|
// Get the lateral table alias map from context (for nested lateral references)
|
|
321
451
|
// This is used when a nested collection's selector navigation references a parent collection's table
|
|
322
452
|
const lateralAliasMap = context?.lateralTableAliasMap;
|
|
@@ -359,98 +489,24 @@ WHERE ${whereSQL})`;
|
|
|
359
489
|
*/
|
|
360
490
|
buildJsonbAggregation(config, lateralAlias, context) {
|
|
361
491
|
const { selectedFields, targetTable, foreignKey, sourceTable, whereClause, orderByClause, limitValue, offsetValue, isDistinct, navigationJoins, relationName } = config;
|
|
362
|
-
// Use a unique table alias to avoid conflicts with outer query tables
|
|
363
|
-
// This is important when the collection targets the same table as the outer query
|
|
364
|
-
// (e.g., post.user.posts where both outer and inner are "posts" table)
|
|
365
492
|
const innerTableAlias = `${lateralAlias}_${relationName}`;
|
|
366
|
-
//
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
if (field.nested) {
|
|
372
|
-
result.push(...collectLeafFields(field.nested, fullAlias));
|
|
373
|
-
}
|
|
374
|
-
else if (field.expression) {
|
|
375
|
-
result.push({ alias: fullAlias, expression: field.expression });
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
return result;
|
|
379
|
-
};
|
|
380
|
-
// Helper to build json_build_object expression (handles nested structures)
|
|
381
|
-
const buildJsonbObject = (fields, prefix = '') => {
|
|
382
|
-
const parts = [];
|
|
383
|
-
for (const field of fields) {
|
|
384
|
-
if (field.nested) {
|
|
385
|
-
const nestedJsonb = buildJsonbObject(field.nested, prefix ? `${prefix}__${field.alias}` : field.alias);
|
|
386
|
-
parts.push(`'${field.alias}', ${nestedJsonb}`);
|
|
387
|
-
}
|
|
388
|
-
else {
|
|
389
|
-
const fullAlias = prefix ? `${prefix}__${field.alias}` : field.alias;
|
|
390
|
-
parts.push(`'${field.alias}', "${fullAlias}"`);
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
return `json_build_object(${parts.join(', ')})`;
|
|
394
|
-
};
|
|
395
|
-
// Collect all leaf fields for the SELECT clause
|
|
396
|
-
const leafFields = collectLeafFields(selectedFields);
|
|
397
|
-
// When there are navigation joins, we need to qualify unqualified field expressions
|
|
398
|
-
// with the inner table alias to avoid ambiguous column references
|
|
399
|
-
const hasNavigationJoins = navigationJoins && navigationJoins.length > 0;
|
|
400
|
-
// Helper to rewrite expressions that reference the collection's table to use inner alias
|
|
401
|
-
const rewriteTableReference = (expression) => {
|
|
402
|
-
// Replace the special marker alias `"__collection_tableName__".` with `"innerTableAlias".`
|
|
403
|
-
// This marker is set in CollectionQueryBuilder.createMockItem() to distinguish
|
|
404
|
-
// collection references from outer table references when both target the same table
|
|
405
|
-
const markerPattern = new RegExp(`"__collection_${targetTable}__"\\.`, 'g');
|
|
406
|
-
return expression.replace(markerPattern, `"${innerTableAlias}".`);
|
|
407
|
-
};
|
|
408
|
-
// Build the subquery SELECT fields (no foreign key needed since we correlate with parent)
|
|
409
|
-
const allSelectFields = leafFields.map(f => {
|
|
410
|
-
// If expression is just a quoted column name (e.g., `"id"`), qualify it with inner table alias
|
|
411
|
-
// But if it's already qualified (e.g., `"user"."username"`), rewrite if it references target table
|
|
412
|
-
const isSimpleColumn = /^"[^".]+"$/.test(f.expression);
|
|
413
|
-
if (isSimpleColumn) {
|
|
414
|
-
// Unqualified column - qualify with inner table alias
|
|
415
|
-
const columnName = f.expression.slice(1, -1); // Remove quotes
|
|
416
|
-
return `"${innerTableAlias}"."${columnName}" as "${f.alias}"`;
|
|
417
|
-
}
|
|
418
|
-
// Already qualified - rewrite target table references
|
|
419
|
-
const rewritten = rewriteTableReference(f.expression);
|
|
420
|
-
if (rewritten !== `"${f.alias}"`) {
|
|
421
|
-
return `${rewritten} as "${f.alias}"`;
|
|
422
|
-
}
|
|
423
|
-
return rewritten;
|
|
424
|
-
});
|
|
425
|
-
// Build the JSONB fields for json_build_object
|
|
426
|
-
const jsonbObjectExpr = buildJsonbObject(selectedFields);
|
|
427
|
-
// Build navigation JOINs for multi-level navigation
|
|
428
|
-
// Pass innerTableAlias so navigation joins can reference it properly
|
|
429
|
-
const navJoinsSQL = this.buildNavigationJoinsWithAlias(navigationJoins, innerTableAlias, targetTable, context);
|
|
430
|
-
// Collect nested CTE/LATERAL joins (for collections within collections)
|
|
431
|
-
const nestedCteJoins = this.collectNestedCteJoins(selectedFields);
|
|
432
|
-
const nestedCteJoinsSQL = nestedCteJoins.length > 0 ? nestedCteJoins.join('\n ') : '';
|
|
433
|
-
// For nested collections, the source table may be aliased in a parent LATERAL
|
|
434
|
-
// Check the lateralTableAliasMap to get the correct alias
|
|
493
|
+
// Inner select list + json_build_object + nested lateral joins in one pass
|
|
494
|
+
const rendered = { select: '', joins: '' };
|
|
495
|
+
const jsonbObjectExpr = this.renderFields(selectedFields, '', targetTable, innerTableAlias, rendered);
|
|
496
|
+
const navJoinsSQL = this.buildNavigationJoinsWithAlias(navigationJoins, innerTableAlias, targetTable, context, relationName);
|
|
497
|
+
// Check if the source table has been aliased by a parent LATERAL (for nested collections)
|
|
435
498
|
const effectiveSourceTable = context.lateralTableAliasMap?.get(sourceTable) || sourceTable;
|
|
436
|
-
// Build WHERE clause
|
|
437
|
-
//
|
|
438
|
-
// predicates carried by `config.foreignKeys`/`matches`).
|
|
439
|
-
// For selectMany, the FK is on the intermediate table (joined via navJoins)
|
|
499
|
+
// Build WHERE clause with correlation to parent
|
|
500
|
+
// For selectMany, the FK is on the intermediate table (joined via navJoins), not the target table
|
|
440
501
|
const fkTableAlias = config.foreignKeyTableAlias || innerTableAlias;
|
|
441
502
|
let whereSQL = `WHERE ${this.buildParentCorrelation(config, fkTableAlias, effectiveSourceTable, foreignKey)}`;
|
|
442
503
|
if (whereClause) {
|
|
443
|
-
|
|
444
|
-
const rewrittenWhereClause = rewriteTableReference(whereClause);
|
|
445
|
-
whereSQL += ` AND ${rewrittenWhereClause}`;
|
|
504
|
+
whereSQL += ` AND ${this.rewriteMarker(whereClause, targetTable, innerTableAlias)}`;
|
|
446
505
|
}
|
|
447
|
-
// Build ORDER BY clause - also rewrite table references
|
|
448
506
|
let orderBySQL = '';
|
|
449
507
|
if (orderByClause) {
|
|
450
|
-
|
|
451
|
-
orderBySQL = `ORDER BY ${rewrittenOrderBy}`;
|
|
508
|
+
orderBySQL = `ORDER BY ${this.rewriteMarker(orderByClause, targetTable, innerTableAlias)}`;
|
|
452
509
|
}
|
|
453
|
-
// Build LIMIT/OFFSET
|
|
454
510
|
let limitOffsetClause = '';
|
|
455
511
|
if (limitValue !== undefined) {
|
|
456
512
|
limitOffsetClause = `LIMIT ${limitValue}`;
|
|
@@ -458,25 +514,22 @@ WHERE ${whereSQL})`;
|
|
|
458
514
|
if (offsetValue !== undefined) {
|
|
459
515
|
limitOffsetClause += ` OFFSET ${offsetValue}`;
|
|
460
516
|
}
|
|
461
|
-
// Build DISTINCT clause
|
|
462
517
|
const distinctClause = isDistinct ? 'DISTINCT ' : '';
|
|
463
|
-
//
|
|
464
|
-
//
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
FROM
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
${
|
|
475
|
-
${
|
|
476
|
-
${
|
|
477
|
-
|
|
478
|
-
${limitOffsetClause}
|
|
479
|
-
) sub
|
|
518
|
+
// Build LATERAL subquery
|
|
519
|
+
// Structure: SELECT json_agg(json_build_object(...)) FROM (SELECT ... LIMIT/OFFSET) sub
|
|
520
|
+
const lateralSQL = `
|
|
521
|
+
SELECT json_agg(
|
|
522
|
+
${jsonbObjectExpr}
|
|
523
|
+
) as data
|
|
524
|
+
FROM (
|
|
525
|
+
SELECT ${distinctClause}${rendered.select}
|
|
526
|
+
FROM "${targetTable}" "${innerTableAlias}"
|
|
527
|
+
${navJoinsSQL}
|
|
528
|
+
${rendered.joins}
|
|
529
|
+
${whereSQL}
|
|
530
|
+
${orderBySQL}
|
|
531
|
+
${limitOffsetClause}
|
|
532
|
+
) sub
|
|
480
533
|
`.trim();
|
|
481
534
|
return lateralSQL;
|
|
482
535
|
}
|
|
@@ -486,96 +539,34 @@ FROM (
|
|
|
486
539
|
*/
|
|
487
540
|
buildSingleJsonAggregation(config, lateralAlias, context) {
|
|
488
541
|
const { selectedFields, targetTable, foreignKey, sourceTable, whereClause, orderByClause, isDistinct, navigationJoins, relationName } = config;
|
|
489
|
-
// Use a unique table alias to avoid conflicts with outer query tables
|
|
490
542
|
const innerTableAlias = `${lateralAlias}_${relationName}`;
|
|
491
|
-
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
for (const field of fields) {
|
|
495
|
-
const fullAlias = prefix ? `${prefix}__${field.alias}` : field.alias;
|
|
496
|
-
if (field.nested) {
|
|
497
|
-
result.push(...collectLeafFields(field.nested, fullAlias));
|
|
498
|
-
}
|
|
499
|
-
else if (field.expression) {
|
|
500
|
-
result.push({ alias: fullAlias, expression: field.expression });
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
return result;
|
|
504
|
-
};
|
|
505
|
-
// Helper to build json_build_object expression (handles nested structures)
|
|
506
|
-
const buildJsonbObject = (fields, prefix = '') => {
|
|
507
|
-
const parts = [];
|
|
508
|
-
for (const field of fields) {
|
|
509
|
-
if (field.nested) {
|
|
510
|
-
const nestedJsonb = buildJsonbObject(field.nested, prefix ? `${prefix}__${field.alias}` : field.alias);
|
|
511
|
-
parts.push(`'${field.alias}', ${nestedJsonb}`);
|
|
512
|
-
}
|
|
513
|
-
else {
|
|
514
|
-
const fullAlias = prefix ? `${prefix}__${field.alias}` : field.alias;
|
|
515
|
-
parts.push(`'${field.alias}', "${fullAlias}"`);
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
return `json_build_object(${parts.join(', ')})`;
|
|
519
|
-
};
|
|
520
|
-
// Collect all leaf fields for the SELECT clause
|
|
521
|
-
const leafFields = collectLeafFields(selectedFields);
|
|
522
|
-
// Helper to rewrite expressions that reference the collection's table to use inner alias
|
|
523
|
-
const rewriteTableReference = (expression) => {
|
|
524
|
-
// Replace the special marker alias `"__collection_tableName__".` with `"innerTableAlias".`
|
|
525
|
-
const markerPattern = new RegExp(`"__collection_${targetTable}__"\\.`, 'g');
|
|
526
|
-
return expression.replace(markerPattern, `"${innerTableAlias}".`);
|
|
527
|
-
};
|
|
528
|
-
// Build the subquery SELECT fields using inner table alias
|
|
529
|
-
const allSelectFields = leafFields.map(f => {
|
|
530
|
-
const isSimpleColumn = /^"[^".]+"$/.test(f.expression);
|
|
531
|
-
if (isSimpleColumn) {
|
|
532
|
-
const columnName = f.expression.slice(1, -1);
|
|
533
|
-
return `"${innerTableAlias}"."${columnName}" as "${f.alias}"`;
|
|
534
|
-
}
|
|
535
|
-
const rewritten = rewriteTableReference(f.expression);
|
|
536
|
-
if (rewritten !== `"${f.alias}"`) {
|
|
537
|
-
return `${rewritten} as "${f.alias}"`;
|
|
538
|
-
}
|
|
539
|
-
return rewritten;
|
|
540
|
-
});
|
|
541
|
-
// Build the JSONB fields for json_build_object
|
|
542
|
-
const jsonbObjectExpr = buildJsonbObject(selectedFields);
|
|
543
|
-
// Build navigation JOINs for multi-level navigation
|
|
544
|
-
const navJoinsSQL = this.buildNavigationJoinsWithAlias(navigationJoins, innerTableAlias, targetTable, context);
|
|
545
|
-
// Collect nested CTE/LATERAL joins (for collections within collections)
|
|
546
|
-
const nestedCteJoins = this.collectNestedCteJoins(selectedFields);
|
|
547
|
-
const nestedCteJoinsSQL = nestedCteJoins.length > 0 ? nestedCteJoins.join('\n ') : '';
|
|
548
|
-
// For nested collections, the source table may be aliased in a parent LATERAL
|
|
543
|
+
const rendered = { select: '', joins: '' };
|
|
544
|
+
const jsonbObjectExpr = this.renderFields(selectedFields, '', targetTable, innerTableAlias, rendered);
|
|
545
|
+
const navJoinsSQL = this.buildNavigationJoinsWithAlias(navigationJoins, innerTableAlias, targetTable, context, relationName);
|
|
549
546
|
const effectiveSourceTable = context.lateralTableAliasMap?.get(sourceTable) || sourceTable;
|
|
550
|
-
// Build WHERE clause - LATERAL correlates with parent via foreign key
|
|
551
|
-
// For selectMany, the FK is on the intermediate table (joined via navJoins)
|
|
552
547
|
const fkTableAlias2 = config.foreignKeyTableAlias || innerTableAlias;
|
|
553
548
|
let whereSQL = `WHERE ${this.buildParentCorrelation(config, fkTableAlias2, effectiveSourceTable, foreignKey)}`;
|
|
554
549
|
if (whereClause) {
|
|
555
|
-
|
|
556
|
-
whereSQL += ` AND ${rewrittenWhereClause}`;
|
|
550
|
+
whereSQL += ` AND ${this.rewriteMarker(whereClause, targetTable, innerTableAlias)}`;
|
|
557
551
|
}
|
|
558
|
-
// Build ORDER BY clause
|
|
559
552
|
let orderBySQL = '';
|
|
560
553
|
if (orderByClause) {
|
|
561
|
-
|
|
562
|
-
orderBySQL = `ORDER BY ${rewrittenOrderBy}`;
|
|
554
|
+
orderBySQL = `ORDER BY ${this.rewriteMarker(orderByClause, targetTable, innerTableAlias)}`;
|
|
563
555
|
}
|
|
564
|
-
// Build DISTINCT clause
|
|
565
556
|
const distinctClause = isDistinct ? 'DISTINCT ' : '';
|
|
566
|
-
//
|
|
567
|
-
//
|
|
568
|
-
const lateralSQL = `
|
|
569
|
-
SELECT ${jsonbObjectExpr} as data
|
|
570
|
-
FROM (
|
|
571
|
-
SELECT ${distinctClause}${
|
|
572
|
-
FROM "${targetTable}" "${innerTableAlias}"
|
|
573
|
-
${navJoinsSQL}
|
|
574
|
-
${
|
|
575
|
-
${whereSQL}
|
|
576
|
-
${orderBySQL}
|
|
577
|
-
LIMIT 1
|
|
578
|
-
) sub
|
|
557
|
+
// Structure: SELECT json_build_object(...) FROM (SELECT ... LIMIT 1) sub
|
|
558
|
+
// Returns null if no rows (LEFT JOIN LATERAL handles this)
|
|
559
|
+
const lateralSQL = `
|
|
560
|
+
SELECT ${jsonbObjectExpr} as data
|
|
561
|
+
FROM (
|
|
562
|
+
SELECT ${distinctClause}${rendered.select}
|
|
563
|
+
FROM "${targetTable}" "${innerTableAlias}"
|
|
564
|
+
${navJoinsSQL}
|
|
565
|
+
${rendered.joins}
|
|
566
|
+
${whereSQL}
|
|
567
|
+
${orderBySQL}
|
|
568
|
+
LIMIT 1
|
|
569
|
+
) sub
|
|
579
570
|
`.trim();
|
|
580
571
|
return lateralSQL;
|
|
581
572
|
}
|
|
@@ -592,7 +583,10 @@ FROM (
|
|
|
592
583
|
// Helper to rewrite expressions that reference the collection's table to use inner alias
|
|
593
584
|
const rewriteTableReference = (expression) => {
|
|
594
585
|
// Replace the special marker alias `"__collection_tableName__".` with `"innerTableAlias".`
|
|
595
|
-
|
|
586
|
+
if (!expression.includes('"__collection_')) {
|
|
587
|
+
return expression; // nothing to rewrite — skip the regex pass entirely
|
|
588
|
+
}
|
|
589
|
+
const markerPattern = (0, query_utils_1.collectionMarkerPattern)(targetTable, true);
|
|
596
590
|
return expression.replace(markerPattern, `"${innerTableAlias}".`);
|
|
597
591
|
};
|
|
598
592
|
// Get the actual field expression from selectedFields (if available)
|
|
@@ -634,18 +628,18 @@ FROM (
|
|
|
634
628
|
// Build DISTINCT clause
|
|
635
629
|
const distinctClause = isDistinct ? 'DISTINCT ' : '';
|
|
636
630
|
// Note: We don't add ORDER BY inside array_agg because the inner subquery already sorts
|
|
637
|
-
const lateralSQL = `
|
|
638
|
-
SELECT ${config.useJsonArrayAggregation ? 'json_agg' : 'array_agg'}(
|
|
639
|
-
"${arrayField}"
|
|
640
|
-
) as data
|
|
641
|
-
FROM (
|
|
642
|
-
SELECT ${distinctClause}${fieldExpression} as "${arrayField}"
|
|
643
|
-
FROM "${targetTable}" "${innerTableAlias}"
|
|
644
|
-
${navJoinsSQL}
|
|
645
|
-
${whereSQL}
|
|
646
|
-
${orderBySQL}
|
|
647
|
-
${limitOffsetClause}
|
|
648
|
-
) sub
|
|
631
|
+
const lateralSQL = `
|
|
632
|
+
SELECT ${config.useJsonArrayAggregation ? 'json_agg' : 'array_agg'}(
|
|
633
|
+
"${arrayField}"
|
|
634
|
+
) as data
|
|
635
|
+
FROM (
|
|
636
|
+
SELECT ${distinctClause}${fieldExpression} as "${arrayField}"
|
|
637
|
+
FROM "${targetTable}" "${innerTableAlias}"
|
|
638
|
+
${navJoinsSQL}
|
|
639
|
+
${whereSQL}
|
|
640
|
+
${orderBySQL}
|
|
641
|
+
${limitOffsetClause}
|
|
642
|
+
) sub
|
|
649
643
|
`.trim();
|
|
650
644
|
return lateralSQL;
|
|
651
645
|
}
|
|
@@ -659,7 +653,10 @@ FROM (
|
|
|
659
653
|
// Helper to rewrite expressions that reference the collection's table to use inner alias
|
|
660
654
|
const rewriteTableReference = (expression) => {
|
|
661
655
|
// Replace the special marker alias `"__collection_tableName__".` with `"innerTableAlias".`
|
|
662
|
-
|
|
656
|
+
if (!expression.includes('"__collection_')) {
|
|
657
|
+
return expression; // nothing to rewrite — skip the regex pass entirely
|
|
658
|
+
}
|
|
659
|
+
const markerPattern = (0, query_utils_1.collectionMarkerPattern)(targetTable, true);
|
|
663
660
|
return expression.replace(markerPattern, `"${innerTableAlias}".`);
|
|
664
661
|
};
|
|
665
662
|
// For nested collections, the source table may be aliased in a parent LATERAL
|
|
@@ -694,20 +691,20 @@ FROM (
|
|
|
694
691
|
break;
|
|
695
692
|
case 'exists': {
|
|
696
693
|
// EXISTS as LATERAL: SELECT EXISTS(SELECT 1 FROM ... WHERE ...)
|
|
697
|
-
const lateralSQL = `
|
|
698
|
-
SELECT EXISTS(SELECT 1
|
|
699
|
-
FROM "${targetTable}" "${innerTableAlias}"
|
|
700
|
-
${whereSQL}) as data
|
|
694
|
+
const lateralSQL = `
|
|
695
|
+
SELECT EXISTS(SELECT 1
|
|
696
|
+
FROM "${targetTable}" "${innerTableAlias}"
|
|
697
|
+
${whereSQL}) as data
|
|
701
698
|
`.trim();
|
|
702
699
|
return lateralSQL;
|
|
703
700
|
}
|
|
704
701
|
default:
|
|
705
702
|
throw new Error(`Unknown aggregation type: ${aggregationType}`);
|
|
706
703
|
}
|
|
707
|
-
const lateralSQL = `
|
|
708
|
-
SELECT ${aggregateExpression} as data
|
|
709
|
-
FROM "${targetTable}" "${innerTableAlias}"
|
|
710
|
-
${whereSQL}
|
|
704
|
+
const lateralSQL = `
|
|
705
|
+
SELECT ${aggregateExpression} as data
|
|
706
|
+
FROM "${targetTable}" "${innerTableAlias}"
|
|
707
|
+
${whereSQL}
|
|
711
708
|
`.trim();
|
|
712
709
|
return lateralSQL;
|
|
713
710
|
}
|