linkgress-orm 0.2.11 → 0.2.14
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 +117 -0
- package/dist/entity/db-context.d.ts.map +1 -1
- package/dist/entity/db-context.js +48 -0
- package/dist/entity/db-context.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -3
- package/dist/index.js.map +1 -1
- package/dist/query/future-query.d.ts +183 -0
- package/dist/query/future-query.d.ts.map +1 -0
- package/dist/query/future-query.js +251 -0
- package/dist/query/future-query.js.map +1 -0
- package/dist/query/query-builder.d.ts +96 -1
- package/dist/query/query-builder.d.ts.map +1 -1
- package/dist/query/query-builder.js +419 -6
- package/dist/query/query-builder.js.map +1 -1
- package/dist/query/strategies/lateral-collection-strategy.d.ts.map +1 -1
- package/dist/query/strategies/lateral-collection-strategy.js +4 -1
- package/dist/query/strategies/lateral-collection-strategy.js.map +1 -1
- package/dist/query/union-builder.d.ts +169 -0
- package/dist/query/union-builder.d.ts.map +1 -0
- package/dist/query/union-builder.js +244 -0
- package/dist/query/union-builder.js.map +1 -0
- package/package.json +1 -1
|
@@ -13,6 +13,8 @@ const subquery_1 = require("./subquery");
|
|
|
13
13
|
const grouped_query_1 = require("./grouped-query");
|
|
14
14
|
const cte_builder_1 = require("./cte-builder");
|
|
15
15
|
const collection_strategy_factory_1 = require("./collection-strategy.factory");
|
|
16
|
+
const union_builder_1 = require("./union-builder");
|
|
17
|
+
const future_query_1 = require("./future-query");
|
|
16
18
|
/**
|
|
17
19
|
* Field type categories for optimized result transformation
|
|
18
20
|
* const enum is inlined at compile time for zero runtime overhead
|
|
@@ -1068,6 +1070,177 @@ class SelectQueryBuilder {
|
|
|
1068
1070
|
: await this.client.query(sql, params);
|
|
1069
1071
|
return result.rows[0]?.exists === true;
|
|
1070
1072
|
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Combine this query with another using UNION (removes duplicates)
|
|
1075
|
+
*
|
|
1076
|
+
* @param query Another SelectQueryBuilder with compatible selection type
|
|
1077
|
+
* @returns A UnionQueryBuilder for further chaining
|
|
1078
|
+
*
|
|
1079
|
+
* @example
|
|
1080
|
+
* ```typescript
|
|
1081
|
+
* const result = await db.users
|
|
1082
|
+
* .select(u => ({ id: u.id, name: u.name }))
|
|
1083
|
+
* .union(db.customers.select(c => ({ id: c.id, name: c.name })))
|
|
1084
|
+
* .orderBy(r => r.name)
|
|
1085
|
+
* .toList();
|
|
1086
|
+
* ```
|
|
1087
|
+
*/
|
|
1088
|
+
union(query) {
|
|
1089
|
+
const unionBuilder = new union_builder_1.UnionQueryBuilder(this, this.client, this.executor);
|
|
1090
|
+
return unionBuilder.union(query);
|
|
1091
|
+
}
|
|
1092
|
+
/**
|
|
1093
|
+
* Combine this query with another using UNION ALL (keeps all rows including duplicates)
|
|
1094
|
+
*
|
|
1095
|
+
* @param query Another SelectQueryBuilder with compatible selection type
|
|
1096
|
+
* @returns A UnionQueryBuilder for further chaining
|
|
1097
|
+
*
|
|
1098
|
+
* @example
|
|
1099
|
+
* ```typescript
|
|
1100
|
+
* // UNION ALL is faster than UNION as it doesn't need to remove duplicates
|
|
1101
|
+
* const allLogs = await db.errorLogs
|
|
1102
|
+
* .select(l => ({ timestamp: l.createdAt, message: l.message }))
|
|
1103
|
+
* .unionAll(db.infoLogs.select(l => ({ timestamp: l.createdAt, message: l.message })))
|
|
1104
|
+
* .orderBy(r => r.timestamp)
|
|
1105
|
+
* .toList();
|
|
1106
|
+
* ```
|
|
1107
|
+
*/
|
|
1108
|
+
unionAll(query) {
|
|
1109
|
+
const unionBuilder = new union_builder_1.UnionQueryBuilder(this, this.client, this.executor);
|
|
1110
|
+
return unionBuilder.unionAll(query);
|
|
1111
|
+
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Build SQL for use in UNION queries (without ORDER BY, LIMIT, OFFSET)
|
|
1114
|
+
* @internal Used by UnionQueryBuilder
|
|
1115
|
+
*/
|
|
1116
|
+
buildUnionSql(context) {
|
|
1117
|
+
const queryContext = {
|
|
1118
|
+
ctes: new Map(),
|
|
1119
|
+
cteCounter: 0,
|
|
1120
|
+
paramCounter: context.paramCounter,
|
|
1121
|
+
allParams: context.params,
|
|
1122
|
+
collectionStrategy: this.collectionStrategy,
|
|
1123
|
+
executor: this.executor,
|
|
1124
|
+
};
|
|
1125
|
+
const mockRow = this._createMockRow();
|
|
1126
|
+
const selectionResult = this.selector(mockRow);
|
|
1127
|
+
// Build query without ORDER BY, LIMIT, OFFSET for union component
|
|
1128
|
+
const { sql } = this.buildQueryCore(selectionResult, queryContext, false);
|
|
1129
|
+
// Update context's param counter
|
|
1130
|
+
context.paramCounter = queryContext.paramCounter;
|
|
1131
|
+
return sql;
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* Create a future query that will be executed later.
|
|
1135
|
+
* Use with FutureQueryRunner.runAsync() for batch execution.
|
|
1136
|
+
*
|
|
1137
|
+
* @returns A FutureQuery that can be executed individually or in a batch
|
|
1138
|
+
*
|
|
1139
|
+
* @example
|
|
1140
|
+
* ```typescript
|
|
1141
|
+
* const q1 = db.users.select(u => ({ id: u.id, name: u.username })).future();
|
|
1142
|
+
* const q2 = db.posts.select(p => ({ title: p.title })).future();
|
|
1143
|
+
*
|
|
1144
|
+
* // Execute in a single roundtrip
|
|
1145
|
+
* const [users, posts] = await FutureQueryRunner.runAsync([q1, q2]);
|
|
1146
|
+
* ```
|
|
1147
|
+
*/
|
|
1148
|
+
future() {
|
|
1149
|
+
const context = {
|
|
1150
|
+
ctes: new Map(),
|
|
1151
|
+
cteCounter: 0,
|
|
1152
|
+
paramCounter: 1,
|
|
1153
|
+
allParams: [],
|
|
1154
|
+
collectionStrategy: this.collectionStrategy,
|
|
1155
|
+
executor: this.executor,
|
|
1156
|
+
};
|
|
1157
|
+
const mockRow = this._createMockRow();
|
|
1158
|
+
const selectionResult = this.selector(mockRow);
|
|
1159
|
+
const { sql, params, nestedPaths } = this.buildQuery(selectionResult, context);
|
|
1160
|
+
// Create transform function that captures current state
|
|
1161
|
+
const transformFn = (rows) => {
|
|
1162
|
+
if (rows.length === 0)
|
|
1163
|
+
return [];
|
|
1164
|
+
// Reconstruct nested objects if needed
|
|
1165
|
+
let processedRows = rows;
|
|
1166
|
+
if (nestedPaths.size > 0) {
|
|
1167
|
+
processedRows = rows.map(row => this.reconstructNestedObjects(row, nestedPaths));
|
|
1168
|
+
}
|
|
1169
|
+
return this.transformResults(processedRows, selectionResult);
|
|
1170
|
+
};
|
|
1171
|
+
return new future_query_1.FutureQuery(sql, params, transformFn, this.client, this.executor);
|
|
1172
|
+
}
|
|
1173
|
+
/**
|
|
1174
|
+
* Create a future query that returns a single result or null.
|
|
1175
|
+
* Use with FutureQueryRunner.runAsync() for batch execution.
|
|
1176
|
+
*
|
|
1177
|
+
* @returns A FutureSingleQuery that resolves to a single result or null
|
|
1178
|
+
*
|
|
1179
|
+
* @example
|
|
1180
|
+
* ```typescript
|
|
1181
|
+
* const q1 = db.users.where(u => eq(u.id, 1)).select(u => u).futureFirstOrDefault();
|
|
1182
|
+
* const q2 = db.posts.where(p => eq(p.id, 5)).select(p => p).futureFirstOrDefault();
|
|
1183
|
+
*
|
|
1184
|
+
* const [user, post] = await FutureQueryRunner.runAsync([q1, q2]);
|
|
1185
|
+
* // user: User | null
|
|
1186
|
+
* // post: Post | null
|
|
1187
|
+
* ```
|
|
1188
|
+
*/
|
|
1189
|
+
futureFirstOrDefault() {
|
|
1190
|
+
// Apply LIMIT 1 for efficiency
|
|
1191
|
+
const originalLimit = this.limitValue;
|
|
1192
|
+
this.limitValue = 1;
|
|
1193
|
+
const context = {
|
|
1194
|
+
ctes: new Map(),
|
|
1195
|
+
cteCounter: 0,
|
|
1196
|
+
paramCounter: 1,
|
|
1197
|
+
allParams: [],
|
|
1198
|
+
collectionStrategy: this.collectionStrategy,
|
|
1199
|
+
executor: this.executor,
|
|
1200
|
+
};
|
|
1201
|
+
const mockRow = this._createMockRow();
|
|
1202
|
+
const selectionResult = this.selector(mockRow);
|
|
1203
|
+
const { sql, params, nestedPaths } = this.buildQuery(selectionResult, context);
|
|
1204
|
+
// Restore original limit
|
|
1205
|
+
this.limitValue = originalLimit;
|
|
1206
|
+
// Create transform function
|
|
1207
|
+
const transformFn = (rows) => {
|
|
1208
|
+
if (rows.length === 0)
|
|
1209
|
+
return [];
|
|
1210
|
+
let processedRows = rows;
|
|
1211
|
+
if (nestedPaths.size > 0) {
|
|
1212
|
+
processedRows = rows.map(row => this.reconstructNestedObjects(row, nestedPaths));
|
|
1213
|
+
}
|
|
1214
|
+
return this.transformResults(processedRows, selectionResult);
|
|
1215
|
+
};
|
|
1216
|
+
return new future_query_1.FutureSingleQuery(sql, params, transformFn, this.client, this.executor);
|
|
1217
|
+
}
|
|
1218
|
+
/**
|
|
1219
|
+
* Create a future query that returns a count.
|
|
1220
|
+
* Use with FutureQueryRunner.runAsync() for batch execution.
|
|
1221
|
+
*
|
|
1222
|
+
* @returns A FutureCountQuery that resolves to a number
|
|
1223
|
+
*
|
|
1224
|
+
* @example
|
|
1225
|
+
* ```typescript
|
|
1226
|
+
* const q1 = db.users.futureCount();
|
|
1227
|
+
* const q2 = db.posts.futureCount();
|
|
1228
|
+
* const q3 = db.comments.where(c => eq(c.isPublished, true)).futureCount();
|
|
1229
|
+
*
|
|
1230
|
+
* const [userCount, postCount, commentCount] = await FutureQueryRunner.runAsync([q1, q2, q3]);
|
|
1231
|
+
* ```
|
|
1232
|
+
*/
|
|
1233
|
+
futureCount() {
|
|
1234
|
+
const context = {
|
|
1235
|
+
ctes: new Map(),
|
|
1236
|
+
cteCounter: 0,
|
|
1237
|
+
paramCounter: 1,
|
|
1238
|
+
allParams: [],
|
|
1239
|
+
executor: this.executor,
|
|
1240
|
+
};
|
|
1241
|
+
const { sql, params } = this.buildAggregateQuery(context, 'count');
|
|
1242
|
+
return new future_query_1.FutureCountQuery(sql, params, this.client, this.executor);
|
|
1243
|
+
}
|
|
1071
1244
|
/**
|
|
1072
1245
|
* Execute query and return results as array
|
|
1073
1246
|
* Collection results are automatically resolved to arrays
|
|
@@ -3412,6 +3585,237 @@ ${joinClauses.join('\n')}`;
|
|
|
3412
3585
|
nestedPaths,
|
|
3413
3586
|
};
|
|
3414
3587
|
}
|
|
3588
|
+
/**
|
|
3589
|
+
* Build the core SQL query, optionally excluding ORDER BY, LIMIT, and OFFSET
|
|
3590
|
+
* Used by buildUnionSql to build component queries for UNION
|
|
3591
|
+
* @internal
|
|
3592
|
+
*/
|
|
3593
|
+
buildQueryCore(selection, context, includeOrderLimitOffset = true) {
|
|
3594
|
+
// Handle user-defined CTEs first - their params need to come before main query params
|
|
3595
|
+
for (const cte of this.ctes) {
|
|
3596
|
+
context.allParams.push(...cte.params);
|
|
3597
|
+
context.paramCounter += cte.params.length;
|
|
3598
|
+
}
|
|
3599
|
+
const selectParts = [];
|
|
3600
|
+
const collectionFields = [];
|
|
3601
|
+
const joins = [];
|
|
3602
|
+
const nestedPaths = new Set();
|
|
3603
|
+
// Scan selection for navigation property references and add JOINs
|
|
3604
|
+
this.detectAndAddJoinsFromSelection(selection, joins);
|
|
3605
|
+
// Scan WHERE condition for navigation property references and add JOINs
|
|
3606
|
+
this.detectAndAddJoinsFromCondition(this.whereCond, joins);
|
|
3607
|
+
// Handle case where selection is a single value (not an object with properties)
|
|
3608
|
+
if (selection instanceof conditions_1.SqlFragment) {
|
|
3609
|
+
const sqlBuildContext = {
|
|
3610
|
+
paramCounter: context.paramCounter,
|
|
3611
|
+
params: context.allParams,
|
|
3612
|
+
};
|
|
3613
|
+
const fragmentSql = selection.buildSql(sqlBuildContext);
|
|
3614
|
+
context.paramCounter = sqlBuildContext.paramCounter;
|
|
3615
|
+
selectParts.push(fragmentSql);
|
|
3616
|
+
}
|
|
3617
|
+
else if (typeof selection === 'object' && selection !== null && '__dbColumnName' in selection) {
|
|
3618
|
+
const tableAlias = ('__tableAlias' in selection && selection.__tableAlias) ? selection.__tableAlias : this.schema.name;
|
|
3619
|
+
selectParts.push(`"${tableAlias}"."${selection.__dbColumnName}"`);
|
|
3620
|
+
}
|
|
3621
|
+
else if (selection instanceof CollectionQueryBuilder) {
|
|
3622
|
+
throw new Error('Cannot use CollectionQueryBuilder directly as selection');
|
|
3623
|
+
}
|
|
3624
|
+
else {
|
|
3625
|
+
// Process selection object properties
|
|
3626
|
+
for (const [key, value] of Object.entries(selection)) {
|
|
3627
|
+
if (value instanceof CollectionQueryBuilder || (value && typeof value === 'object' && '__collectionResult' in value)) {
|
|
3628
|
+
// Collection fields are not supported in UNION queries for simplicity
|
|
3629
|
+
// Skip them - they would need complex handling
|
|
3630
|
+
continue;
|
|
3631
|
+
}
|
|
3632
|
+
else if (value instanceof subquery_1.Subquery || (value && typeof value === 'object' && 'buildSql' in value && typeof value.buildSql === 'function' && '__mode' in value)) {
|
|
3633
|
+
const sqlBuildContext = {
|
|
3634
|
+
paramCounter: context.paramCounter,
|
|
3635
|
+
params: context.allParams,
|
|
3636
|
+
};
|
|
3637
|
+
const subquerySql = value.buildSql(sqlBuildContext);
|
|
3638
|
+
context.paramCounter = sqlBuildContext.paramCounter;
|
|
3639
|
+
selectParts.push(`(${subquerySql}) as "${key}"`);
|
|
3640
|
+
}
|
|
3641
|
+
else if (value instanceof conditions_1.SqlFragment) {
|
|
3642
|
+
const sqlBuildContext = {
|
|
3643
|
+
paramCounter: context.paramCounter,
|
|
3644
|
+
params: context.allParams,
|
|
3645
|
+
};
|
|
3646
|
+
const fragmentSql = value.buildSql(sqlBuildContext);
|
|
3647
|
+
context.paramCounter = sqlBuildContext.paramCounter;
|
|
3648
|
+
selectParts.push(`${fragmentSql} as "${key}"`);
|
|
3649
|
+
}
|
|
3650
|
+
else if (typeof value === 'object' && value !== null && '__dbColumnName' in value) {
|
|
3651
|
+
if ('__tableAlias' in value && value.__tableAlias && typeof value.__tableAlias === 'string') {
|
|
3652
|
+
const tableAlias = value.__tableAlias;
|
|
3653
|
+
const columnName = value.__dbColumnName;
|
|
3654
|
+
const relConfig = this.schema.relations[tableAlias];
|
|
3655
|
+
if (relConfig && !joins.find(j => j.alias === tableAlias)) {
|
|
3656
|
+
let targetSchema;
|
|
3657
|
+
if (relConfig.targetTableBuilder) {
|
|
3658
|
+
const targetTableSchema = relConfig.targetTableBuilder.build();
|
|
3659
|
+
targetSchema = targetTableSchema.schema;
|
|
3660
|
+
}
|
|
3661
|
+
joins.push({
|
|
3662
|
+
alias: tableAlias,
|
|
3663
|
+
targetTable: relConfig.targetTable,
|
|
3664
|
+
targetSchema,
|
|
3665
|
+
foreignKeys: relConfig.foreignKeys || [relConfig.foreignKey || ''],
|
|
3666
|
+
matches: relConfig.matches || [],
|
|
3667
|
+
isMandatory: relConfig.isMandatory ?? false,
|
|
3668
|
+
});
|
|
3669
|
+
}
|
|
3670
|
+
const cteJoin = this.manualJoins.find(j => j.cte && j.cte.name === tableAlias);
|
|
3671
|
+
if (cteJoin && cteJoin.cte && cteJoin.cte.isAggregationColumn(columnName)) {
|
|
3672
|
+
selectParts.push(`COALESCE("${tableAlias}"."${columnName}", '[]'::json) as "${key}"`);
|
|
3673
|
+
}
|
|
3674
|
+
else {
|
|
3675
|
+
selectParts.push(`"${tableAlias}"."${columnName}" as "${key}"`);
|
|
3676
|
+
}
|
|
3677
|
+
}
|
|
3678
|
+
else {
|
|
3679
|
+
selectParts.push(`"${this.schema.name}"."${value.__dbColumnName}" as "${key}"`);
|
|
3680
|
+
}
|
|
3681
|
+
}
|
|
3682
|
+
else if (typeof value === 'string') {
|
|
3683
|
+
selectParts.push(`"${this.schema.name}"."${value}" as "${key}"`);
|
|
3684
|
+
}
|
|
3685
|
+
else if (typeof value === 'object' && value !== null) {
|
|
3686
|
+
if (!('__dbColumnName' in value)) {
|
|
3687
|
+
if (Array.isArray(value)) {
|
|
3688
|
+
continue;
|
|
3689
|
+
}
|
|
3690
|
+
if (value instanceof CollectionQueryBuilder) {
|
|
3691
|
+
continue;
|
|
3692
|
+
}
|
|
3693
|
+
else if (value instanceof ReferenceQueryBuilder) {
|
|
3694
|
+
continue; // Skip ReferenceQueryBuilder in union queries
|
|
3695
|
+
}
|
|
3696
|
+
}
|
|
3697
|
+
selectParts.push(`$${context.paramCounter++} as "${key}"`);
|
|
3698
|
+
context.allParams.push(value);
|
|
3699
|
+
}
|
|
3700
|
+
else if (value === undefined) {
|
|
3701
|
+
continue;
|
|
3702
|
+
}
|
|
3703
|
+
else {
|
|
3704
|
+
selectParts.push(`$${context.paramCounter++} as "${key}"`);
|
|
3705
|
+
context.allParams.push(value);
|
|
3706
|
+
}
|
|
3707
|
+
}
|
|
3708
|
+
}
|
|
3709
|
+
// Build WHERE clause
|
|
3710
|
+
let whereClause = '';
|
|
3711
|
+
if (this.whereCond) {
|
|
3712
|
+
const condBuilder = new conditions_1.ConditionBuilder();
|
|
3713
|
+
const { sql, params, placeholders, paramCounter: newParamCounter } = condBuilder.build(this.whereCond, context.paramCounter, context.placeholders);
|
|
3714
|
+
whereClause = `WHERE ${sql}`;
|
|
3715
|
+
context.paramCounter = newParamCounter;
|
|
3716
|
+
context.allParams.push(...params);
|
|
3717
|
+
if (placeholders) {
|
|
3718
|
+
context.placeholders = placeholders;
|
|
3719
|
+
}
|
|
3720
|
+
}
|
|
3721
|
+
// Build ORDER BY clause (only if includeOrderLimitOffset is true)
|
|
3722
|
+
let orderByClause = '';
|
|
3723
|
+
if (includeOrderLimitOffset && this.orderByFields.length > 0) {
|
|
3724
|
+
const colNameMap = getColumnNameMapForSchema(this.schema);
|
|
3725
|
+
const orderParts = this.orderByFields.map(({ field, direction }) => {
|
|
3726
|
+
if (selection && typeof selection === 'object' && !Array.isArray(selection) && field in selection) {
|
|
3727
|
+
return `"${field}" ${direction}`;
|
|
3728
|
+
}
|
|
3729
|
+
else {
|
|
3730
|
+
const dbColumnName = colNameMap.get(field) ?? field;
|
|
3731
|
+
return `"${this.schema.name}"."${dbColumnName}" ${direction}`;
|
|
3732
|
+
}
|
|
3733
|
+
});
|
|
3734
|
+
orderByClause = `ORDER BY ${orderParts.join(', ')}`;
|
|
3735
|
+
}
|
|
3736
|
+
// Build LIMIT/OFFSET (only if includeOrderLimitOffset is true)
|
|
3737
|
+
let limitClause = '';
|
|
3738
|
+
if (includeOrderLimitOffset) {
|
|
3739
|
+
if (this.limitValue !== undefined) {
|
|
3740
|
+
limitClause = `LIMIT ${this.limitValue}`;
|
|
3741
|
+
}
|
|
3742
|
+
if (this.offsetValue !== undefined) {
|
|
3743
|
+
limitClause += ` OFFSET ${this.offsetValue}`;
|
|
3744
|
+
}
|
|
3745
|
+
}
|
|
3746
|
+
// Build final query with CTEs
|
|
3747
|
+
let finalQuery = '';
|
|
3748
|
+
const allCtes = [];
|
|
3749
|
+
for (const cte of this.ctes) {
|
|
3750
|
+
allCtes.push(`"${cte.name}" AS (${cte.query})`);
|
|
3751
|
+
}
|
|
3752
|
+
if (context.ctes.size > 0) {
|
|
3753
|
+
for (const [cteName, { sql }] of context.ctes.entries()) {
|
|
3754
|
+
allCtes.push(`"${cteName}" AS (${sql})`);
|
|
3755
|
+
}
|
|
3756
|
+
}
|
|
3757
|
+
if (allCtes.length > 0) {
|
|
3758
|
+
finalQuery = `WITH ${allCtes.join(', ')}\n`;
|
|
3759
|
+
}
|
|
3760
|
+
// Build main query
|
|
3761
|
+
const qualifiedTableName = this.getQualifiedTableName(this.schema.name, this.schema.schema);
|
|
3762
|
+
let fromClause = `FROM ${qualifiedTableName}`;
|
|
3763
|
+
// Add manual JOINs
|
|
3764
|
+
for (const manualJoin of this.manualJoins) {
|
|
3765
|
+
const joinTypeStr = manualJoin.type === 'INNER' ? 'INNER JOIN' : 'LEFT JOIN';
|
|
3766
|
+
const condBuilder = new conditions_1.ConditionBuilder();
|
|
3767
|
+
const { sql: condSql, params: condParams, placeholders: joinPlaceholders, paramCounter: newParamCounter } = condBuilder.build(manualJoin.condition, context.paramCounter, context.placeholders);
|
|
3768
|
+
context.paramCounter = newParamCounter;
|
|
3769
|
+
context.allParams.push(...condParams);
|
|
3770
|
+
if (joinPlaceholders) {
|
|
3771
|
+
context.placeholders = joinPlaceholders;
|
|
3772
|
+
}
|
|
3773
|
+
if (manualJoin.cte) {
|
|
3774
|
+
fromClause += `\n${joinTypeStr} "${manualJoin.cte.name}" ON ${condSql}`;
|
|
3775
|
+
}
|
|
3776
|
+
else if (manualJoin.isSubquery && manualJoin.subquery) {
|
|
3777
|
+
const subqueryBuildContext = {
|
|
3778
|
+
paramCounter: context.paramCounter,
|
|
3779
|
+
params: context.allParams,
|
|
3780
|
+
};
|
|
3781
|
+
const subquerySql = manualJoin.subquery.buildSql(subqueryBuildContext);
|
|
3782
|
+
context.paramCounter = subqueryBuildContext.paramCounter;
|
|
3783
|
+
fromClause += `\n${joinTypeStr} (${subquerySql}) AS "${manualJoin.alias}" ON ${condSql}`;
|
|
3784
|
+
}
|
|
3785
|
+
else {
|
|
3786
|
+
fromClause += `\n${joinTypeStr} "${manualJoin.table}" AS "${manualJoin.alias}" ON ${condSql}`;
|
|
3787
|
+
}
|
|
3788
|
+
}
|
|
3789
|
+
// Add JOINs for single navigation
|
|
3790
|
+
for (const join of joins) {
|
|
3791
|
+
const joinType = join.isMandatory ? 'INNER JOIN' : 'LEFT JOIN';
|
|
3792
|
+
const sourceTable = join.sourceAlias || this.schema.name;
|
|
3793
|
+
const onConditions = [];
|
|
3794
|
+
for (let i = 0; i < join.foreignKeys.length; i++) {
|
|
3795
|
+
const fk = join.foreignKeys[i];
|
|
3796
|
+
const match = join.matches[i];
|
|
3797
|
+
onConditions.push(`"${sourceTable}"."${fk}" = "${join.alias}"."${match}"`);
|
|
3798
|
+
}
|
|
3799
|
+
const joinTableName = this.getQualifiedTableName(join.targetTable, join.targetSchema);
|
|
3800
|
+
fromClause += `\n${joinType} ${joinTableName} AS "${join.alias}" ON ${onConditions.join(' AND ')}`;
|
|
3801
|
+
}
|
|
3802
|
+
// Add DISTINCT if needed
|
|
3803
|
+
const distinctClause = this.isDistinct ? 'DISTINCT ' : '';
|
|
3804
|
+
// Build final SQL
|
|
3805
|
+
const queryParts = [`SELECT ${distinctClause}${selectParts.join(', ')}`, fromClause];
|
|
3806
|
+
if (whereClause)
|
|
3807
|
+
queryParts.push(whereClause);
|
|
3808
|
+
if (orderByClause)
|
|
3809
|
+
queryParts.push(orderByClause);
|
|
3810
|
+
if (limitClause)
|
|
3811
|
+
queryParts.push(limitClause);
|
|
3812
|
+
finalQuery += queryParts.join('\n').trim();
|
|
3813
|
+
return {
|
|
3814
|
+
sql: finalQuery,
|
|
3815
|
+
params: context.allParams,
|
|
3816
|
+
nestedPaths,
|
|
3817
|
+
};
|
|
3818
|
+
}
|
|
3415
3819
|
/**
|
|
3416
3820
|
* Transform database results
|
|
3417
3821
|
*/
|
|
@@ -4907,21 +5311,19 @@ class CollectionQueryBuilder {
|
|
|
4907
5311
|
context.paramCounter = nestedCtx.paramCounter;
|
|
4908
5312
|
// For CTE/LATERAL strategy, we need to track the nested join
|
|
4909
5313
|
// The nested aggregation needs to be joined in the outer collection's subquery
|
|
4910
|
-
|
|
5314
|
+
// However, correlated subqueries (used for toNumberList, etc.) don't need joins -
|
|
5315
|
+
// they are embedded directly in the SELECT expression
|
|
5316
|
+
if (nestedResult.tableName && (nestedResult.isCTE || nestedResult.joinClause)) {
|
|
4911
5317
|
let nestedJoinClause;
|
|
4912
5318
|
if (nestedResult.isCTE) {
|
|
4913
5319
|
// CTE strategy: join by parent_id
|
|
4914
5320
|
// The join should be: this.targetTable.id = nestedCte.parent_id
|
|
4915
5321
|
nestedJoinClause = `LEFT JOIN "${nestedResult.tableName}" ON "${this.targetTable}"."id" = "${nestedResult.tableName}".parent_id`;
|
|
4916
5322
|
}
|
|
4917
|
-
else
|
|
5323
|
+
else {
|
|
4918
5324
|
// LATERAL strategy: use the provided join clause (contains full LATERAL subquery)
|
|
4919
5325
|
nestedJoinClause = nestedResult.joinClause;
|
|
4920
5326
|
}
|
|
4921
|
-
else {
|
|
4922
|
-
// Fallback for other strategies
|
|
4923
|
-
nestedJoinClause = `LEFT JOIN "${nestedResult.tableName}" ON "${this.targetTable}"."id" = "${nestedResult.tableName}".parent_id`;
|
|
4924
|
-
}
|
|
4925
5327
|
return {
|
|
4926
5328
|
alias,
|
|
4927
5329
|
expression: nestedResult.selectExpression || nestedResult.sql,
|
|
@@ -4938,6 +5340,17 @@ class CollectionQueryBuilder {
|
|
|
4938
5340
|
};
|
|
4939
5341
|
}
|
|
4940
5342
|
// The nested collection becomes a correlated subquery in SELECT
|
|
5343
|
+
// For scalar aggregations (count, min, max, sum), don't include nestedCollectionInfo
|
|
5344
|
+
// because the result is a scalar value, not a structured object that needs transformation
|
|
5345
|
+
const aggregationType = field.getAggregationType();
|
|
5346
|
+
const isScalarAggregation = aggregationType && ['COUNT', 'MIN', 'MAX', 'SUM'].includes(aggregationType);
|
|
5347
|
+
if (isScalarAggregation) {
|
|
5348
|
+
// Scalar aggregation - just return the expression, no nested transformation needed
|
|
5349
|
+
return {
|
|
5350
|
+
alias,
|
|
5351
|
+
expression: nestedResult.selectExpression || nestedResult.sql,
|
|
5352
|
+
};
|
|
5353
|
+
}
|
|
4941
5354
|
return {
|
|
4942
5355
|
alias,
|
|
4943
5356
|
expression: nestedResult.selectExpression || nestedResult.sql,
|