linkgress-orm 0.4.25 → 0.4.27
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/entity/db-context.d.ts.map +1 -1
- package/dist/entity/db-context.js +8 -2
- package/dist/entity/db-context.js.map +1 -1
- package/dist/query/query-builder.d.ts +28 -0
- package/dist/query/query-builder.d.ts.map +1 -1
- package/dist/query/query-builder.js +142 -4
- package/dist/query/query-builder.js.map +1 -1
- package/dist/query/union-builder.d.ts +23 -9
- package/dist/query/union-builder.d.ts.map +1 -1
- package/dist/query/union-builder.js +37 -0
- package/dist/query/union-builder.js.map +1 -1
- package/package.json +1 -1
|
@@ -1181,11 +1181,46 @@ class SelectQueryBuilder {
|
|
|
1181
1181
|
const mockRow = this._createMockRow();
|
|
1182
1182
|
const selectionResult = this.selector(mockRow);
|
|
1183
1183
|
// Build query without ORDER BY, LIMIT, OFFSET for union component
|
|
1184
|
-
const { sql } = this.buildQueryCore(selectionResult, queryContext, false);
|
|
1184
|
+
const { sql, nestedPaths } = this.buildQueryCore(selectionResult, queryContext, false);
|
|
1185
1185
|
// Update context's param counter
|
|
1186
1186
|
context.paramCounter = queryContext.paramCounter;
|
|
1187
|
+
// Stash the rich metadata (nestedPaths + selection) so UnionQueryBuilder
|
|
1188
|
+
// can post-process the result rows (reconstructNestedObjects +
|
|
1189
|
+
// transformResults for collection projections). buildUnionSql is always
|
|
1190
|
+
// called by UnionQueryBuilder synchronously immediately before the union
|
|
1191
|
+
// SQL is executed, so this is safe — no concurrency. The fields are
|
|
1192
|
+
// refreshed on every call; `_lastUnionMetadata` is the contract.
|
|
1193
|
+
this._lastUnionMetadata = {
|
|
1194
|
+
nestedPaths,
|
|
1195
|
+
selectionResult,
|
|
1196
|
+
};
|
|
1187
1197
|
return sql;
|
|
1188
1198
|
}
|
|
1199
|
+
/**
|
|
1200
|
+
* Read (and consume) the metadata captured by the most recent
|
|
1201
|
+
* `buildUnionSql` call. Internal contract with `UnionQueryBuilder`.
|
|
1202
|
+
* @internal
|
|
1203
|
+
*/
|
|
1204
|
+
_consumeUnionMetadata() {
|
|
1205
|
+
const meta = this._lastUnionMetadata;
|
|
1206
|
+
this._lastUnionMetadata = undefined;
|
|
1207
|
+
return meta;
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* Apply the standard post-fetch result transformation to the raw rows
|
|
1211
|
+
* returned by a UNION execution. Combines `reconstructNestedObjects` (for
|
|
1212
|
+
* `__nested__<path>` columns produced by nested-object projections) with
|
|
1213
|
+
* `transformResults` (for collection projections, scalar mappers, FieldRef
|
|
1214
|
+
* mappers, etc.). Used exclusively by `UnionQueryBuilder.toList()`.
|
|
1215
|
+
* @internal
|
|
1216
|
+
*/
|
|
1217
|
+
_applyUnionPostProcessing(rows, meta) {
|
|
1218
|
+
let processed = rows;
|
|
1219
|
+
if (meta.nestedPaths.size > 0) {
|
|
1220
|
+
processed = rows.map(row => this.reconstructNestedObjects(row, meta.nestedPaths));
|
|
1221
|
+
}
|
|
1222
|
+
return this.transformResults(processed, meta.selectionResult);
|
|
1223
|
+
}
|
|
1189
1224
|
/**
|
|
1190
1225
|
* Create a future query that will be executed later.
|
|
1191
1226
|
* Use with FutureQueryRunner.runAsync() for batch execution.
|
|
@@ -3747,11 +3782,34 @@ ${joinClauses.join('\n')}`;
|
|
|
3747
3782
|
}
|
|
3748
3783
|
else {
|
|
3749
3784
|
// Process selection object properties
|
|
3785
|
+
//
|
|
3786
|
+
// Parity with buildQuery (the non-UNION path): UNION legs must support
|
|
3787
|
+
// nested-object projections (`{ address: { street, city } }`) AND
|
|
3788
|
+
// collection projections (`{ cards: u.cards.toList(...) }`) so that the
|
|
3789
|
+
// same selector shape can be reused across UNION legs and stand-alone
|
|
3790
|
+
// queries. The legacy buildQueryCore silently dropped both:
|
|
3791
|
+
// - nested objects fell through to the literal-param branch and
|
|
3792
|
+
// arrived as a stringified blob,
|
|
3793
|
+
// - collections hit an explicit `continue` and were never emitted.
|
|
3794
|
+
// Both regressions are now covered by union-nested-select.test.ts and
|
|
3795
|
+
// union-collection-nav.test.ts.
|
|
3750
3796
|
for (const [key, value] of Object.entries(selection)) {
|
|
3751
3797
|
if (value instanceof CollectionQueryBuilder || (value && typeof value === 'object' && '__collectionResult' in value)) {
|
|
3752
|
-
// Collection
|
|
3753
|
-
//
|
|
3754
|
-
|
|
3798
|
+
// Collection projection inside a UNION leg — delegate to the
|
|
3799
|
+
// collection strategy (LATERAL by default) and emit per-row
|
|
3800
|
+
// correlated subquery. Same flow as buildQuery (line ~4266):
|
|
3801
|
+
// buildCTE() registers the CTE / produces the joinClause +
|
|
3802
|
+
// selectExpression. We push the selectExpression as the SELECT
|
|
3803
|
+
// slot for this leg and add the joinClause to FROM later.
|
|
3804
|
+
const cteData = value.buildCTE ? value.buildCTE(context) : value.buildCTE(context);
|
|
3805
|
+
const isCTE = cteData.isCTE !== false;
|
|
3806
|
+
collectionFields.push({
|
|
3807
|
+
name: key,
|
|
3808
|
+
cteName: cteData.tableName || `cte_${context.cteCounter - 1}`,
|
|
3809
|
+
isCTE,
|
|
3810
|
+
joinClause: cteData.joinClause,
|
|
3811
|
+
selectExpression: cteData.selectExpression,
|
|
3812
|
+
});
|
|
3755
3813
|
}
|
|
3756
3814
|
else if (value instanceof subquery_1.Subquery || (value && typeof value === 'object' && 'buildSql' in value && typeof value.buildSql === 'function' && '__mode' in value)) {
|
|
3757
3815
|
const sqlBuildContext = {
|
|
@@ -3817,6 +3875,24 @@ ${joinClauses.join('\n')}`;
|
|
|
3817
3875
|
else if (value instanceof ReferenceQueryBuilder) {
|
|
3818
3876
|
continue; // Skip ReferenceQueryBuilder in union queries
|
|
3819
3877
|
}
|
|
3878
|
+
// Plain nested object containing FieldRefs / SqlFragments /
|
|
3879
|
+
// primitives (e.g. `address: { street: p.street, city: p.city }`).
|
|
3880
|
+
// Same flat-flattening pass as buildQuery: produces flat
|
|
3881
|
+
// `__nested__<path>__<leaf>` columns with deterministic ordering
|
|
3882
|
+
// across UNION legs. The UnionQueryBuilder reconstructs them
|
|
3883
|
+
// post-fetch via reconstructNestedObjects().
|
|
3884
|
+
const handled = this.tryBuildFlatNestedSelect(value, context, joins, selectParts, `__nested__${key}`, nestedPaths);
|
|
3885
|
+
if (handled) {
|
|
3886
|
+
continue;
|
|
3887
|
+
}
|
|
3888
|
+
// Nested object that ALSO contains a collection inside it
|
|
3889
|
+
// (e.g. `content: { posts: u.posts.toList() }`). Build the
|
|
3890
|
+
// non-collection leaves flat, register the collection in
|
|
3891
|
+
// collectionFields so it gets the LATERAL / CTE treatment.
|
|
3892
|
+
if (this.hasNestedCollections(value)) {
|
|
3893
|
+
this.tryBuildFlatNestedSelectExcludingCollections(value, context, joins, selectParts, `__nested__${key}`, nestedPaths, collectionFields);
|
|
3894
|
+
continue;
|
|
3895
|
+
}
|
|
3820
3896
|
}
|
|
3821
3897
|
selectParts.push(`$${context.paramCounter++} as "${key}"`);
|
|
3822
3898
|
context.allParams.push(value);
|
|
@@ -3829,6 +3905,57 @@ ${joinClauses.join('\n')}`;
|
|
|
3829
3905
|
context.allParams.push(value);
|
|
3830
3906
|
}
|
|
3831
3907
|
}
|
|
3908
|
+
// Add collection fields as JSON / array aggregations joined from CTEs or
|
|
3909
|
+
// LATERAL joins. Mirrors the buildQuery flow at line ~4498 — collections
|
|
3910
|
+
// must contribute a SELECT slot (or every UNION leg's column count
|
|
3911
|
+
// diverges) and a FROM-side join clause.
|
|
3912
|
+
for (const { name, cteName, selectExpression } of collectionFields) {
|
|
3913
|
+
if (selectExpression) {
|
|
3914
|
+
selectParts.push(`${selectExpression} as "${name}"`);
|
|
3915
|
+
continue;
|
|
3916
|
+
}
|
|
3917
|
+
// Fallback path mirrors buildQuery for the CTE strategy (when no
|
|
3918
|
+
// pre-built selectExpression was produced by buildCTE).
|
|
3919
|
+
let collectionValue;
|
|
3920
|
+
if (name.startsWith('__nested__')) {
|
|
3921
|
+
const pathParts = name.substring('__nested__'.length).split('__');
|
|
3922
|
+
collectionValue = selection;
|
|
3923
|
+
for (const part of pathParts) {
|
|
3924
|
+
if (collectionValue && typeof collectionValue === 'object') {
|
|
3925
|
+
collectionValue = collectionValue[part];
|
|
3926
|
+
}
|
|
3927
|
+
else {
|
|
3928
|
+
collectionValue = undefined;
|
|
3929
|
+
break;
|
|
3930
|
+
}
|
|
3931
|
+
}
|
|
3932
|
+
}
|
|
3933
|
+
else {
|
|
3934
|
+
collectionValue = selection[name];
|
|
3935
|
+
}
|
|
3936
|
+
const isArrayAgg = collectionValue && typeof collectionValue === 'object' && 'isArrayAggregation' in collectionValue && collectionValue.isArrayAggregation();
|
|
3937
|
+
const isScalarAgg = collectionValue instanceof CollectionQueryBuilder && collectionValue.isScalarAggregation();
|
|
3938
|
+
if (isScalarAgg) {
|
|
3939
|
+
const aggregationType = collectionValue.getAggregationType();
|
|
3940
|
+
if (aggregationType === 'COUNT') {
|
|
3941
|
+
selectParts.push(`COALESCE("${cteName}".data, 0) as "${name}"`);
|
|
3942
|
+
}
|
|
3943
|
+
else if (aggregationType === 'EXISTS') {
|
|
3944
|
+
selectParts.push(`COALESCE("${cteName}".data, false) as "${name}"`);
|
|
3945
|
+
}
|
|
3946
|
+
else {
|
|
3947
|
+
selectParts.push(`"${cteName}".data as "${name}"`);
|
|
3948
|
+
}
|
|
3949
|
+
}
|
|
3950
|
+
else if (isArrayAgg) {
|
|
3951
|
+
const flattenType = collectionValue.getFlattenResultType?.() || 'string';
|
|
3952
|
+
const arrayType = flattenType === 'number' ? 'integer[]' : 'text[]';
|
|
3953
|
+
selectParts.push(`COALESCE("${cteName}".data, ARRAY[]::${arrayType}) as "${name}"`);
|
|
3954
|
+
}
|
|
3955
|
+
else {
|
|
3956
|
+
selectParts.push(`COALESCE("${cteName}".data, '[]'::json) as "${name}"`);
|
|
3957
|
+
}
|
|
3958
|
+
}
|
|
3832
3959
|
}
|
|
3833
3960
|
// Build WHERE clause
|
|
3834
3961
|
let whereClause = '';
|
|
@@ -3923,6 +4050,17 @@ ${joinClauses.join('\n')}`;
|
|
|
3923
4050
|
const joinTableName = this.getQualifiedTableName(join.targetTable, join.targetSchema);
|
|
3924
4051
|
fromClause += `\n${joinType} ${joinTableName} AS "${join.alias}" ON ${onConditions.join(' AND ')}`;
|
|
3925
4052
|
}
|
|
4053
|
+
// Join CTEs / LATERAL subqueries for collection projections inside the UNION
|
|
4054
|
+
// leg. Mirror the buildQuery flow at line ~4678. CTE legs join by parent_id;
|
|
4055
|
+
// LATERAL legs splice in the pre-built `LEFT JOIN LATERAL (...)` clause.
|
|
4056
|
+
for (const { cteName, isCTE, joinClause } of collectionFields) {
|
|
4057
|
+
if (isCTE) {
|
|
4058
|
+
fromClause += `\nLEFT JOIN "${cteName}" ON "${cteName}".parent_id = ${qualifiedTableName}.id`;
|
|
4059
|
+
}
|
|
4060
|
+
else if (joinClause) {
|
|
4061
|
+
fromClause += `\n${joinClause}`;
|
|
4062
|
+
}
|
|
4063
|
+
}
|
|
3926
4064
|
// Add DISTINCT if needed
|
|
3927
4065
|
const distinctClause = this.isDistinct ? 'DISTINCT ' : '';
|
|
3928
4066
|
// Build final SQL
|