linkgress-orm 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/entity/db-context.d.ts +26 -0
- package/dist/entity/db-context.d.ts.map +1 -1
- package/dist/entity/db-context.js +31 -0
- package/dist/entity/db-context.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/query/collection-strategy.interface.d.ts +18 -0
- package/dist/query/collection-strategy.interface.d.ts.map +1 -1
- package/dist/query/conditions.d.ts +57 -13
- package/dist/query/conditions.d.ts.map +1 -1
- package/dist/query/conditions.js +94 -4
- package/dist/query/conditions.js.map +1 -1
- package/dist/query/prepared-query.d.ts +66 -0
- package/dist/query/prepared-query.d.ts.map +1 -0
- package/dist/query/prepared-query.js +86 -0
- package/dist/query/prepared-query.js.map +1 -0
- package/dist/query/query-builder.d.ts +63 -2
- package/dist/query/query-builder.d.ts.map +1 -1
- package/dist/query/query-builder.js +376 -30
- package/dist/query/query-builder.js.map +1 -1
- package/dist/query/strategies/cte-collection-strategy.d.ts.map +1 -1
- package/dist/query/strategies/cte-collection-strategy.js +86 -24
- package/dist/query/strategies/cte-collection-strategy.js.map +1 -1
- package/dist/query/strategies/lateral-collection-strategy.d.ts +9 -0
- package/dist/query/strategies/lateral-collection-strategy.d.ts.map +1 -1
- package/dist/query/strategies/lateral-collection-strategy.js +187 -70
- package/dist/query/strategies/lateral-collection-strategy.js.map +1 -1
- package/dist/query/strategies/temptable-collection-strategy.d.ts +5 -0
- package/dist/query/strategies/temptable-collection-strategy.d.ts.map +1 -1
- package/dist/query/strategies/temptable-collection-strategy.js +48 -18
- package/dist/query/strategies/temptable-collection-strategy.js.map +1 -1
- package/package.json +2 -3
|
@@ -6,6 +6,7 @@ exports.getRelationEntriesForSchema = getRelationEntriesForSchema;
|
|
|
6
6
|
exports.getTargetSchemaForRelation = getTargetSchemaForRelation;
|
|
7
7
|
exports.createNestedFieldRefProxy = createNestedFieldRefProxy;
|
|
8
8
|
const conditions_1 = require("./conditions");
|
|
9
|
+
const prepared_query_1 = require("./prepared-query");
|
|
9
10
|
const db_context_1 = require("../entity/db-context");
|
|
10
11
|
const query_utils_1 = require("./query-utils");
|
|
11
12
|
const subquery_1 = require("./subquery");
|
|
@@ -1043,14 +1044,30 @@ class SelectQueryBuilder {
|
|
|
1043
1044
|
allParams: [],
|
|
1044
1045
|
executor: this.executor,
|
|
1045
1046
|
};
|
|
1046
|
-
|
|
1047
|
-
const { sql, params } = this.buildCountQuery(context);
|
|
1048
|
-
// Execute
|
|
1047
|
+
const { sql, params } = this.buildAggregateQuery(context, 'count');
|
|
1049
1048
|
const result = this.executor
|
|
1050
1049
|
? await this.executor.query(sql, params)
|
|
1051
1050
|
: await this.client.query(sql, params);
|
|
1052
1051
|
return parseInt(result.rows[0]?.count ?? '0', 10);
|
|
1053
1052
|
}
|
|
1053
|
+
/**
|
|
1054
|
+
* Check if any rows match the query
|
|
1055
|
+
* More efficient than count() > 0 as it can stop after finding one row
|
|
1056
|
+
*/
|
|
1057
|
+
async exists() {
|
|
1058
|
+
const context = {
|
|
1059
|
+
ctes: new Map(),
|
|
1060
|
+
cteCounter: 0,
|
|
1061
|
+
paramCounter: 1,
|
|
1062
|
+
allParams: [],
|
|
1063
|
+
executor: this.executor,
|
|
1064
|
+
};
|
|
1065
|
+
const { sql, params } = this.buildAggregateQuery(context, 'exists');
|
|
1066
|
+
const result = this.executor
|
|
1067
|
+
? await this.executor.query(sql, params)
|
|
1068
|
+
: await this.client.query(sql, params);
|
|
1069
|
+
return result.rows[0]?.exists === true;
|
|
1070
|
+
}
|
|
1054
1071
|
/**
|
|
1055
1072
|
* Execute query and return results as array
|
|
1056
1073
|
* Collection results are automatically resolved to arrays
|
|
@@ -1200,7 +1217,28 @@ class SelectQueryBuilder {
|
|
|
1200
1217
|
for (const collection of collections) {
|
|
1201
1218
|
const resultMap = collectionResults.get(collection.name);
|
|
1202
1219
|
const parentId = baseRow.__pk_id;
|
|
1203
|
-
const
|
|
1220
|
+
const rawData = resultMap?.get(parentId);
|
|
1221
|
+
// Check if this is a single result (firstOrDefault) - return first item or null
|
|
1222
|
+
const isSingleResult = collection.builder.isSingleResult();
|
|
1223
|
+
let collectionData;
|
|
1224
|
+
if (isSingleResult) {
|
|
1225
|
+
// For single results, rawData might already be an object (from temp table strategy)
|
|
1226
|
+
// or might be an array (from other strategies) that we need to extract the first item from
|
|
1227
|
+
if (rawData === undefined || rawData === null) {
|
|
1228
|
+
collectionData = null;
|
|
1229
|
+
}
|
|
1230
|
+
else if (Array.isArray(rawData)) {
|
|
1231
|
+
collectionData = rawData.length > 0 ? rawData[0] : null;
|
|
1232
|
+
}
|
|
1233
|
+
else {
|
|
1234
|
+
// Already a single object
|
|
1235
|
+
collectionData = rawData;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
else {
|
|
1239
|
+
// For list results, ensure we return an array
|
|
1240
|
+
collectionData = rawData || this.getDefaultValueForCollection(collection.builder);
|
|
1241
|
+
}
|
|
1204
1242
|
// Handle nested paths
|
|
1205
1243
|
if (collection.path.length > 0) {
|
|
1206
1244
|
// Navigate to the nested object and set the collection there
|
|
@@ -1348,7 +1386,28 @@ class SelectQueryBuilder {
|
|
|
1348
1386
|
for (const collection of collections) {
|
|
1349
1387
|
const resultMap = collectionResults.get(collection.name);
|
|
1350
1388
|
const parentId = baseRow.__pk_id;
|
|
1351
|
-
const
|
|
1389
|
+
const rawData = resultMap?.get(parentId);
|
|
1390
|
+
// Check if this is a single result (firstOrDefault) - return first item or null
|
|
1391
|
+
const isSingleResult = collection.builder.isSingleResult();
|
|
1392
|
+
let collectionData;
|
|
1393
|
+
if (isSingleResult) {
|
|
1394
|
+
// For single results, rawData might already be an object (from temp table strategy)
|
|
1395
|
+
// or might be an array (from other strategies) that we need to extract the first item from
|
|
1396
|
+
if (rawData === undefined || rawData === null) {
|
|
1397
|
+
collectionData = null;
|
|
1398
|
+
}
|
|
1399
|
+
else if (Array.isArray(rawData)) {
|
|
1400
|
+
collectionData = rawData.length > 0 ? rawData[0] : null;
|
|
1401
|
+
}
|
|
1402
|
+
else {
|
|
1403
|
+
// Already a single object
|
|
1404
|
+
collectionData = rawData;
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
else {
|
|
1408
|
+
// For list results, ensure we return an array
|
|
1409
|
+
collectionData = rawData || [];
|
|
1410
|
+
}
|
|
1352
1411
|
// Handle nested paths
|
|
1353
1412
|
if (collection.path.length > 0) {
|
|
1354
1413
|
// Navigate to the nested object and set the collection there
|
|
@@ -1577,6 +1636,74 @@ class SelectQueryBuilder {
|
|
|
1577
1636
|
}
|
|
1578
1637
|
return result;
|
|
1579
1638
|
}
|
|
1639
|
+
/**
|
|
1640
|
+
* Create a prepared query for efficient reusable parameterized execution.
|
|
1641
|
+
*
|
|
1642
|
+
* Prepared queries build the SQL once and allow multiple executions
|
|
1643
|
+
* with different parameter values. This is useful for:
|
|
1644
|
+
* 1. Query building optimization - Build SQL once, execute many times
|
|
1645
|
+
* 2. Type-safe placeholders - Named parameters with validation
|
|
1646
|
+
* 3. Developer ergonomics - Cleaner API for reusable queries
|
|
1647
|
+
*
|
|
1648
|
+
* @param name - A name for the prepared query (for debugging)
|
|
1649
|
+
* @returns PreparedQuery that can be executed multiple times with different parameters
|
|
1650
|
+
*
|
|
1651
|
+
* @example
|
|
1652
|
+
* ```typescript
|
|
1653
|
+
* // Create a prepared query with a placeholder
|
|
1654
|
+
* const getUserById = db.users
|
|
1655
|
+
* .where(u => eq(u.id, sql.placeholder('userId')))
|
|
1656
|
+
* .prepare('getUserById');
|
|
1657
|
+
*
|
|
1658
|
+
* // Execute multiple times with different values
|
|
1659
|
+
* const user1 = await getUserById.execute({ userId: 10 });
|
|
1660
|
+
* const user2 = await getUserById.execute({ userId: 20 });
|
|
1661
|
+
* ```
|
|
1662
|
+
*
|
|
1663
|
+
* @example
|
|
1664
|
+
* ```typescript
|
|
1665
|
+
* // Multiple placeholders
|
|
1666
|
+
* const searchUsers = db.users
|
|
1667
|
+
* .where(u => and(
|
|
1668
|
+
* gt(u.age, sql.placeholder('minAge')),
|
|
1669
|
+
* like(u.name, sql.placeholder('namePattern'))
|
|
1670
|
+
* ))
|
|
1671
|
+
* .prepare('searchUsers');
|
|
1672
|
+
*
|
|
1673
|
+
* await searchUsers.execute({ minAge: 18, namePattern: '%john%' });
|
|
1674
|
+
* ```
|
|
1675
|
+
*/
|
|
1676
|
+
prepare(name) {
|
|
1677
|
+
// Build query with placeholder tracking
|
|
1678
|
+
const context = {
|
|
1679
|
+
ctes: new Map(),
|
|
1680
|
+
cteCounter: 0,
|
|
1681
|
+
paramCounter: 1,
|
|
1682
|
+
allParams: [],
|
|
1683
|
+
placeholders: new Map(),
|
|
1684
|
+
collectionStrategy: this.collectionStrategy,
|
|
1685
|
+
executor: this.executor,
|
|
1686
|
+
};
|
|
1687
|
+
// Analyze the selector to extract nested queries
|
|
1688
|
+
const mockRow = this._createMockRow();
|
|
1689
|
+
const selectionResult = this.selector(mockRow);
|
|
1690
|
+
// Build the query - this populates context.placeholders
|
|
1691
|
+
const { sql, nestedPaths } = this.buildQuery(selectionResult, context);
|
|
1692
|
+
// Create transform function (closure over schema, selection, nestedPaths)
|
|
1693
|
+
const transformFn = (rows) => {
|
|
1694
|
+
// If rawResult is enabled, return raw rows without any processing
|
|
1695
|
+
if (this.executor?.getOptions().rawResult) {
|
|
1696
|
+
return rows;
|
|
1697
|
+
}
|
|
1698
|
+
// Reconstruct nested objects from flat row data (if any)
|
|
1699
|
+
if (nestedPaths.size > 0) {
|
|
1700
|
+
rows = rows.map(row => this.reconstructNestedObjects(row, nestedPaths));
|
|
1701
|
+
}
|
|
1702
|
+
// Transform results
|
|
1703
|
+
return this.transformResults(rows, selectionResult);
|
|
1704
|
+
};
|
|
1705
|
+
return new prepared_query_1.PreparedQuery(sql, context.placeholders || new Map(), context.paramCounter - 1, this.client, transformFn, name);
|
|
1706
|
+
}
|
|
1580
1707
|
/**
|
|
1581
1708
|
* Delete records matching the current WHERE condition
|
|
1582
1709
|
* Returns a fluent builder that can be awaited directly or chained with .returning()
|
|
@@ -2374,6 +2501,15 @@ class SelectQueryBuilder {
|
|
|
2374
2501
|
allTableAliases.add(tableAlias);
|
|
2375
2502
|
}
|
|
2376
2503
|
}
|
|
2504
|
+
// Also collect intermediate navigation aliases for multi-level navigation (e.g., task.level.name)
|
|
2505
|
+
// The field ref may have __navigationAliases containing all aliases in the path
|
|
2506
|
+
if ('__navigationAliases' in fieldRef && Array.isArray(fieldRef.__navigationAliases)) {
|
|
2507
|
+
for (const navAlias of fieldRef.__navigationAliases) {
|
|
2508
|
+
if (navAlias && navAlias !== this.schema.name) {
|
|
2509
|
+
allTableAliases.add(navAlias);
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2377
2513
|
}
|
|
2378
2514
|
// Resolve all joins through the schema graph
|
|
2379
2515
|
this.resolveJoinsForTableAliases(allTableAliases, joins);
|
|
@@ -2702,10 +2838,14 @@ class SelectQueryBuilder {
|
|
|
2702
2838
|
let whereClause = '';
|
|
2703
2839
|
if (this.whereCond) {
|
|
2704
2840
|
const condBuilder = new conditions_1.ConditionBuilder();
|
|
2705
|
-
const { sql, params } = condBuilder.build(this.whereCond, context.paramCounter);
|
|
2841
|
+
const { sql, params, placeholders, paramCounter: newParamCounter } = condBuilder.build(this.whereCond, context.paramCounter, context.placeholders);
|
|
2706
2842
|
whereClause = `WHERE ${sql}`;
|
|
2707
|
-
context.paramCounter
|
|
2843
|
+
context.paramCounter = newParamCounter; // Use returned counter (handles both params and placeholders)
|
|
2708
2844
|
context.allParams.push(...params);
|
|
2845
|
+
// Update placeholders from the condition builder (for prepared statements)
|
|
2846
|
+
if (placeholders) {
|
|
2847
|
+
context.placeholders = placeholders;
|
|
2848
|
+
}
|
|
2709
2849
|
}
|
|
2710
2850
|
// Build ORDER BY clause
|
|
2711
2851
|
let orderByClause = '';
|
|
@@ -2761,9 +2901,12 @@ class SelectQueryBuilder {
|
|
|
2761
2901
|
const joinTypeStr = manualJoin.type === 'INNER' ? 'INNER JOIN' : 'LEFT JOIN';
|
|
2762
2902
|
// Build ON condition
|
|
2763
2903
|
const condBuilder = new conditions_1.ConditionBuilder();
|
|
2764
|
-
const { sql: condSql, params: condParams } = condBuilder.build(manualJoin.condition, context.paramCounter);
|
|
2765
|
-
context.paramCounter
|
|
2904
|
+
const { sql: condSql, params: condParams, placeholders: joinPlaceholders, paramCounter: newParamCounter } = condBuilder.build(manualJoin.condition, context.paramCounter, context.placeholders);
|
|
2905
|
+
context.paramCounter = newParamCounter; // Use returned counter (handles both params and placeholders)
|
|
2766
2906
|
context.allParams.push(...condParams);
|
|
2907
|
+
if (joinPlaceholders) {
|
|
2908
|
+
context.placeholders = joinPlaceholders;
|
|
2909
|
+
}
|
|
2767
2910
|
// Check if this is a CTE join
|
|
2768
2911
|
if (manualJoin.cte) {
|
|
2769
2912
|
// Join with CTE - use CTE name directly
|
|
@@ -3127,6 +3270,18 @@ class SelectQueryBuilder {
|
|
|
3127
3270
|
}
|
|
3128
3271
|
}
|
|
3129
3272
|
}
|
|
3273
|
+
// Build cache of nested collection info (fields that are themselves nested collections)
|
|
3274
|
+
// This is used for recursive transformation of nested collection results
|
|
3275
|
+
const nestedCollectionCache = new Map();
|
|
3276
|
+
if (selectedFieldConfigs) {
|
|
3277
|
+
for (const field of selectedFieldConfigs) {
|
|
3278
|
+
if (field.nestedCollectionInfo) {
|
|
3279
|
+
nestedCollectionCache.set(field.alias, field.nestedCollectionInfo);
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
// Get schema registry for nested collection transformation
|
|
3284
|
+
const schemaRegistry = collectionBuilder.getSchemaRegistry() || this.schemaRegistry;
|
|
3130
3285
|
// Transform items using pre-built mapper cache
|
|
3131
3286
|
const results = new Array(items.length);
|
|
3132
3287
|
let i = items.length;
|
|
@@ -3140,13 +3295,114 @@ class SelectQueryBuilder {
|
|
|
3140
3295
|
transformedItem[key] = mapper.fromDriver(value);
|
|
3141
3296
|
}
|
|
3142
3297
|
else {
|
|
3143
|
-
|
|
3298
|
+
// Check if this field is a nested collection that needs recursive transformation
|
|
3299
|
+
const nestedInfo = nestedCollectionCache.get(key);
|
|
3300
|
+
if (nestedInfo && value !== null && value !== undefined && schemaRegistry) {
|
|
3301
|
+
transformedItem[key] = this.transformNestedCollectionValue(value, nestedInfo, schemaRegistry);
|
|
3302
|
+
}
|
|
3303
|
+
else {
|
|
3304
|
+
transformedItem[key] = value;
|
|
3305
|
+
}
|
|
3144
3306
|
}
|
|
3145
3307
|
}
|
|
3146
3308
|
results[i] = transformedItem;
|
|
3147
3309
|
}
|
|
3148
3310
|
return results;
|
|
3149
3311
|
}
|
|
3312
|
+
/**
|
|
3313
|
+
* Transform a nested collection value (from firstOrDefault or toList inside another collection)
|
|
3314
|
+
* Applies custom mappers to fields within the nested collection result.
|
|
3315
|
+
*/
|
|
3316
|
+
transformNestedCollectionValue(value, nestedInfo, schemaRegistry) {
|
|
3317
|
+
const nestedSchema = schemaRegistry.get(nestedInfo.targetTable);
|
|
3318
|
+
if (!nestedSchema?.columnMetadataCache) {
|
|
3319
|
+
return value; // No schema info, return as-is
|
|
3320
|
+
}
|
|
3321
|
+
const columnCache = nestedSchema.columnMetadataCache;
|
|
3322
|
+
const selectedFieldConfigs = nestedInfo.selectedFieldConfigs;
|
|
3323
|
+
// Build mapper cache for nested collection fields
|
|
3324
|
+
const mapperCache = new Map();
|
|
3325
|
+
// First, add mappers from selected field configs (for aliased/navigation fields)
|
|
3326
|
+
if (selectedFieldConfigs) {
|
|
3327
|
+
for (const field of selectedFieldConfigs) {
|
|
3328
|
+
if (field.propertyName) {
|
|
3329
|
+
let mapper = null;
|
|
3330
|
+
if (field.sourceTable) {
|
|
3331
|
+
// Navigation field - look up from source table's schema
|
|
3332
|
+
const navSchema = schemaRegistry.get(field.sourceTable);
|
|
3333
|
+
if (navSchema?.columnMetadataCache) {
|
|
3334
|
+
const cached = navSchema.columnMetadataCache.get(field.propertyName);
|
|
3335
|
+
if (cached?.hasMapper) {
|
|
3336
|
+
mapper = cached.mapper;
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
}
|
|
3340
|
+
else {
|
|
3341
|
+
// Regular field from target schema
|
|
3342
|
+
const cached = columnCache.get(field.propertyName);
|
|
3343
|
+
if (cached?.hasMapper) {
|
|
3344
|
+
mapper = cached.mapper;
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
if (mapper) {
|
|
3348
|
+
mapperCache.set(field.alias, mapper);
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
// Also add direct property matches from target schema
|
|
3354
|
+
for (const [propertyName, cached] of columnCache) {
|
|
3355
|
+
if (!mapperCache.has(propertyName) && cached.hasMapper) {
|
|
3356
|
+
mapperCache.set(propertyName, cached.mapper);
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
3359
|
+
// Build cache for any deeply nested collections
|
|
3360
|
+
const deeplyNestedCache = new Map();
|
|
3361
|
+
if (selectedFieldConfigs) {
|
|
3362
|
+
for (const field of selectedFieldConfigs) {
|
|
3363
|
+
if (field.nestedCollectionInfo) {
|
|
3364
|
+
deeplyNestedCache.set(field.alias, field.nestedCollectionInfo);
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3367
|
+
}
|
|
3368
|
+
// Transform the value(s)
|
|
3369
|
+
const transformItem = (item) => {
|
|
3370
|
+
if (item === null || item === undefined) {
|
|
3371
|
+
return item;
|
|
3372
|
+
}
|
|
3373
|
+
const transformedItem = {};
|
|
3374
|
+
for (const key in item) {
|
|
3375
|
+
const fieldValue = item[key];
|
|
3376
|
+
const mapper = mapperCache.get(key);
|
|
3377
|
+
if (mapper) {
|
|
3378
|
+
transformedItem[key] = mapper.fromDriver(fieldValue);
|
|
3379
|
+
}
|
|
3380
|
+
else {
|
|
3381
|
+
// Check for deeply nested collections
|
|
3382
|
+
const deepNestedInfo = deeplyNestedCache.get(key);
|
|
3383
|
+
if (deepNestedInfo && fieldValue !== null && fieldValue !== undefined) {
|
|
3384
|
+
transformedItem[key] = this.transformNestedCollectionValue(fieldValue, deepNestedInfo, schemaRegistry);
|
|
3385
|
+
}
|
|
3386
|
+
else {
|
|
3387
|
+
transformedItem[key] = fieldValue;
|
|
3388
|
+
}
|
|
3389
|
+
}
|
|
3390
|
+
}
|
|
3391
|
+
return transformedItem;
|
|
3392
|
+
};
|
|
3393
|
+
if (nestedInfo.isSingleResult) {
|
|
3394
|
+
// Single item (firstOrDefault)
|
|
3395
|
+
return transformItem(value);
|
|
3396
|
+
}
|
|
3397
|
+
else if (Array.isArray(value)) {
|
|
3398
|
+
// Array of items (toList)
|
|
3399
|
+
return value.map(transformItem);
|
|
3400
|
+
}
|
|
3401
|
+
else {
|
|
3402
|
+
// Single object that should be treated as single result
|
|
3403
|
+
return transformItem(value);
|
|
3404
|
+
}
|
|
3405
|
+
}
|
|
3150
3406
|
/**
|
|
3151
3407
|
* Transform CTE aggregation items applying fromDriver mappers from selection metadata
|
|
3152
3408
|
*/
|
|
@@ -3237,10 +3493,13 @@ class SelectQueryBuilder {
|
|
|
3237
3493
|
let whereClause = '';
|
|
3238
3494
|
if (this.whereCond) {
|
|
3239
3495
|
const condBuilder = new conditions_1.ConditionBuilder();
|
|
3240
|
-
const { sql, params } = condBuilder.build(this.whereCond, context.paramCounter);
|
|
3496
|
+
const { sql, params, placeholders, paramCounter: newParamCounter } = condBuilder.build(this.whereCond, context.paramCounter, context.placeholders);
|
|
3241
3497
|
whereClause = `WHERE ${sql}`;
|
|
3242
|
-
context.paramCounter
|
|
3498
|
+
context.paramCounter = newParamCounter; // Use returned counter (handles both params and placeholders)
|
|
3243
3499
|
context.allParams.push(...params);
|
|
3500
|
+
if (placeholders) {
|
|
3501
|
+
context.placeholders = placeholders;
|
|
3502
|
+
}
|
|
3244
3503
|
}
|
|
3245
3504
|
// Build FROM clause with JOINs
|
|
3246
3505
|
let fromClause = `FROM "${this.schema.name}"`;
|
|
@@ -3248,9 +3507,12 @@ class SelectQueryBuilder {
|
|
|
3248
3507
|
for (const manualJoin of this.manualJoins) {
|
|
3249
3508
|
const joinTypeStr = manualJoin.type === 'INNER' ? 'INNER JOIN' : 'LEFT JOIN';
|
|
3250
3509
|
const condBuilder = new conditions_1.ConditionBuilder();
|
|
3251
|
-
const { sql: condSql, params: condParams } = condBuilder.build(manualJoin.condition, context.paramCounter);
|
|
3252
|
-
context.paramCounter
|
|
3510
|
+
const { sql: condSql, params: condParams, placeholders: joinPlaceholders, paramCounter: newJoinParamCounter } = condBuilder.build(manualJoin.condition, context.paramCounter, context.placeholders);
|
|
3511
|
+
context.paramCounter = newJoinParamCounter; // Use returned counter (handles both params and placeholders)
|
|
3253
3512
|
context.allParams.push(...condParams);
|
|
3513
|
+
if (joinPlaceholders) {
|
|
3514
|
+
context.placeholders = joinPlaceholders;
|
|
3515
|
+
}
|
|
3254
3516
|
// Check if this is a subquery join
|
|
3255
3517
|
if (manualJoin.isSubquery && manualJoin.subquery) {
|
|
3256
3518
|
const subqueryBuildContext = {
|
|
@@ -3272,27 +3534,37 @@ class SelectQueryBuilder {
|
|
|
3272
3534
|
};
|
|
3273
3535
|
}
|
|
3274
3536
|
/**
|
|
3275
|
-
* Build count
|
|
3537
|
+
* Build aggregate query (count or exists)
|
|
3276
3538
|
*/
|
|
3277
|
-
|
|
3539
|
+
buildAggregateQuery(context, type) {
|
|
3540
|
+
// Detect navigation property joins from WHERE condition
|
|
3541
|
+
const joins = [];
|
|
3542
|
+
this.detectAndAddJoinsFromCondition(this.whereCond, joins);
|
|
3278
3543
|
// Build WHERE clause
|
|
3279
3544
|
let whereClause = '';
|
|
3280
3545
|
if (this.whereCond) {
|
|
3281
3546
|
const condBuilder = new conditions_1.ConditionBuilder();
|
|
3282
|
-
const { sql, params } = condBuilder.build(this.whereCond, context.paramCounter);
|
|
3547
|
+
const { sql, params, placeholders, paramCounter: newParamCounter } = condBuilder.build(this.whereCond, context.paramCounter, context.placeholders);
|
|
3283
3548
|
whereClause = `WHERE ${sql}`;
|
|
3284
|
-
context.paramCounter
|
|
3549
|
+
context.paramCounter = newParamCounter; // Use returned counter (handles both params and placeholders)
|
|
3285
3550
|
context.allParams.push(...params);
|
|
3551
|
+
if (placeholders) {
|
|
3552
|
+
context.placeholders = placeholders;
|
|
3553
|
+
}
|
|
3286
3554
|
}
|
|
3287
3555
|
// Build FROM clause with JOINs
|
|
3288
|
-
|
|
3556
|
+
const qualifiedTableName = this.getQualifiedTableName(this.schema.name, this.schema.schema);
|
|
3557
|
+
let fromClause = `FROM ${qualifiedTableName}`;
|
|
3289
3558
|
// Add manual JOINs
|
|
3290
3559
|
for (const manualJoin of this.manualJoins) {
|
|
3291
3560
|
const joinTypeStr = manualJoin.type === 'INNER' ? 'INNER JOIN' : 'LEFT JOIN';
|
|
3292
3561
|
const condBuilder = new conditions_1.ConditionBuilder();
|
|
3293
|
-
const { sql: condSql, params: condParams } = condBuilder.build(manualJoin.condition, context.paramCounter);
|
|
3294
|
-
context.paramCounter
|
|
3562
|
+
const { sql: condSql, params: condParams, placeholders: joinPlaceholders, paramCounter: newJoinParamCounter } = condBuilder.build(manualJoin.condition, context.paramCounter, context.placeholders);
|
|
3563
|
+
context.paramCounter = newJoinParamCounter; // Use returned counter (handles both params and placeholders)
|
|
3295
3564
|
context.allParams.push(...condParams);
|
|
3565
|
+
if (joinPlaceholders) {
|
|
3566
|
+
context.placeholders = joinPlaceholders;
|
|
3567
|
+
}
|
|
3296
3568
|
// Check if this is a subquery join
|
|
3297
3569
|
if (manualJoin.isSubquery && manualJoin.subquery) {
|
|
3298
3570
|
const subqueryBuildContext = {
|
|
@@ -3307,7 +3579,30 @@ class SelectQueryBuilder {
|
|
|
3307
3579
|
fromClause += `\n${joinTypeStr} "${manualJoin.table}" AS "${manualJoin.alias}" ON ${condSql}`;
|
|
3308
3580
|
}
|
|
3309
3581
|
}
|
|
3310
|
-
|
|
3582
|
+
// Add JOINs for navigation properties referenced in WHERE clause
|
|
3583
|
+
for (const join of joins) {
|
|
3584
|
+
const joinType = join.isMandatory ? 'INNER JOIN' : 'LEFT JOIN';
|
|
3585
|
+
// Build ON clause for the join
|
|
3586
|
+
// For multi-level navigation, use the sourceAlias (the intermediate table)
|
|
3587
|
+
// For direct navigation, use the main table name
|
|
3588
|
+
const sourceTable = join.sourceAlias || this.schema.name;
|
|
3589
|
+
const onConditions = [];
|
|
3590
|
+
for (let i = 0; i < join.foreignKeys.length; i++) {
|
|
3591
|
+
const fk = join.foreignKeys[i];
|
|
3592
|
+
const match = join.matches[i];
|
|
3593
|
+
onConditions.push(`"${sourceTable}"."${fk}" = "${join.alias}"."${match}"`);
|
|
3594
|
+
}
|
|
3595
|
+
// Use schema-qualified table name if schema is specified
|
|
3596
|
+
const joinTableName = this.getQualifiedTableName(join.targetTable, join.targetSchema);
|
|
3597
|
+
fromClause += `\n${joinType} ${joinTableName} AS "${join.alias}" ON ${onConditions.join(' AND ')}`;
|
|
3598
|
+
}
|
|
3599
|
+
// Build SELECT clause based on type
|
|
3600
|
+
const selectClause = type === 'count'
|
|
3601
|
+
? 'SELECT COUNT(*) as count'
|
|
3602
|
+
: 'SELECT EXISTS(SELECT 1';
|
|
3603
|
+
const sql = type === 'count'
|
|
3604
|
+
? `${selectClause}\n${fromClause}\n${whereClause}`.trim()
|
|
3605
|
+
: `${selectClause}\n${fromClause}\n${whereClause})`.trim();
|
|
3311
3606
|
return {
|
|
3312
3607
|
sql,
|
|
3313
3608
|
params: context.allParams,
|
|
@@ -3338,12 +3633,13 @@ class SelectQueryBuilder {
|
|
|
3338
3633
|
asSubquery(mode = 'table') {
|
|
3339
3634
|
// Create a function that builds the subquery SQL when called
|
|
3340
3635
|
const sqlBuilder = (outerContext) => {
|
|
3341
|
-
// Create a fresh context for this subquery
|
|
3636
|
+
// Create a fresh context for this subquery, inheriting placeholders from outer context
|
|
3342
3637
|
const context = {
|
|
3343
3638
|
ctes: new Map(),
|
|
3344
3639
|
cteCounter: 0,
|
|
3345
3640
|
paramCounter: outerContext.paramCounter,
|
|
3346
3641
|
allParams: outerContext.params,
|
|
3642
|
+
placeholders: outerContext.placeholders, // Pass placeholders through for prepared statements
|
|
3347
3643
|
executor: this.executor,
|
|
3348
3644
|
};
|
|
3349
3645
|
// Analyze the selector to extract nested queries
|
|
@@ -3472,6 +3768,9 @@ class ReferenceQueryBuilder {
|
|
|
3472
3768
|
}
|
|
3473
3769
|
}
|
|
3474
3770
|
const sourceTable = this.targetTable; // Actual table name for schema lookup
|
|
3771
|
+
// Collect all navigation aliases from the path leading to this reference
|
|
3772
|
+
// This is needed for WHERE conditions that use multi-level navigation (e.g., task.level.name)
|
|
3773
|
+
const navigationAliases = this.navigationPath.map(nav => nav.alias);
|
|
3475
3774
|
for (const [colName, dbColumnName] of columnNameMap) {
|
|
3476
3775
|
const mapper = columnMappers[colName];
|
|
3477
3776
|
Object.defineProperty(mock, colName, {
|
|
@@ -3484,6 +3783,7 @@ class ReferenceQueryBuilder {
|
|
|
3484
3783
|
__tableAlias: tableAlias, // Alias for SQL generation
|
|
3485
3784
|
__sourceTable: sourceTable, // Actual table name for mapper lookup
|
|
3486
3785
|
__mapper: mapper, // Include mapper for toDriver transformation in conditions
|
|
3786
|
+
__navigationAliases: navigationAliases, // All intermediate navigation aliases for JOIN resolution
|
|
3487
3787
|
};
|
|
3488
3788
|
}
|
|
3489
3789
|
return cached;
|
|
@@ -3647,7 +3947,10 @@ class CollectionQueryBuilder {
|
|
|
3647
3947
|
// Performance: Lazy-cache FieldRef objects
|
|
3648
3948
|
const fieldRefCache = {};
|
|
3649
3949
|
// Add columns - include tableAlias for unambiguous column references in WHERE clauses
|
|
3650
|
-
|
|
3950
|
+
// Use a special marker alias for the collection's own table that can be rewritten later
|
|
3951
|
+
// This allows distinguishing between outer table references and inner collection references
|
|
3952
|
+
// when both target the same table (e.g., post.user.posts where both are "posts" table)
|
|
3953
|
+
const tableAlias = `__collection_${this.targetTable}__`;
|
|
3651
3954
|
for (const [colName, dbColumnName] of columnNameMap) {
|
|
3652
3955
|
Object.defineProperty(mock, colName, {
|
|
3653
3956
|
get() {
|
|
@@ -3833,6 +4136,12 @@ class CollectionQueryBuilder {
|
|
|
3833
4136
|
getTargetTableSchema() {
|
|
3834
4137
|
return this.targetTableSchema;
|
|
3835
4138
|
}
|
|
4139
|
+
/**
|
|
4140
|
+
* Get target table name
|
|
4141
|
+
*/
|
|
4142
|
+
getTargetTable() {
|
|
4143
|
+
return this.targetTable;
|
|
4144
|
+
}
|
|
3836
4145
|
/**
|
|
3837
4146
|
* Get selected field configs (for mapper lookup during transformation)
|
|
3838
4147
|
*/
|
|
@@ -4092,6 +4401,20 @@ class CollectionQueryBuilder {
|
|
|
4092
4401
|
// Determine strategy type - default to 'lateral' if not specified
|
|
4093
4402
|
const strategyType = context.collectionStrategy || 'lateral';
|
|
4094
4403
|
const strategy = collection_strategy_factory_1.CollectionStrategyFactory.getStrategy(strategyType);
|
|
4404
|
+
// For LATERAL strategy, reserve the counter early and register the table alias
|
|
4405
|
+
// This allows nested collections to reference this collection's aliased table
|
|
4406
|
+
let reservedCounter;
|
|
4407
|
+
if (strategyType === 'lateral') {
|
|
4408
|
+
reservedCounter = context.cteCounter++;
|
|
4409
|
+
const lateralAlias = `lateral_${reservedCounter}`;
|
|
4410
|
+
const innerTableAlias = `${lateralAlias}_${this.relationName}`;
|
|
4411
|
+
// Initialize the map if needed
|
|
4412
|
+
if (!context.lateralTableAliasMap) {
|
|
4413
|
+
context.lateralTableAliasMap = new Map();
|
|
4414
|
+
}
|
|
4415
|
+
// Register this collection's table alias for nested collections to reference
|
|
4416
|
+
context.lateralTableAliasMap.set(this.targetTable, innerTableAlias);
|
|
4417
|
+
}
|
|
4095
4418
|
// Build selected fields configuration (supports nested objects)
|
|
4096
4419
|
const selectedFieldConfigs = [];
|
|
4097
4420
|
const localParams = [];
|
|
@@ -4111,6 +4434,7 @@ class CollectionQueryBuilder {
|
|
|
4111
4434
|
const sqlBuildContext = {
|
|
4112
4435
|
paramCounter: context.paramCounter,
|
|
4113
4436
|
params: context.allParams,
|
|
4437
|
+
placeholders: context.placeholders, // Pass placeholders for prepared statements
|
|
4114
4438
|
};
|
|
4115
4439
|
const fragmentSql = field.buildSql(sqlBuildContext);
|
|
4116
4440
|
context.paramCounter = sqlBuildContext.paramCounter;
|
|
@@ -4157,10 +4481,25 @@ class CollectionQueryBuilder {
|
|
|
4157
4481
|
cteName: nestedResult.tableName,
|
|
4158
4482
|
joinClause: nestedJoinClause,
|
|
4159
4483
|
},
|
|
4484
|
+
// Store nested collection info for recursive mapper transformation
|
|
4485
|
+
nestedCollectionInfo: {
|
|
4486
|
+
targetTable: field.getTargetTable(),
|
|
4487
|
+
selectedFieldConfigs: field.getSelectedFieldConfigs(),
|
|
4488
|
+
isSingleResult: field.isSingleResult(),
|
|
4489
|
+
},
|
|
4160
4490
|
};
|
|
4161
4491
|
}
|
|
4162
4492
|
// The nested collection becomes a correlated subquery in SELECT
|
|
4163
|
-
return {
|
|
4493
|
+
return {
|
|
4494
|
+
alias,
|
|
4495
|
+
expression: nestedResult.selectExpression || nestedResult.sql,
|
|
4496
|
+
// Store nested collection info for recursive mapper transformation
|
|
4497
|
+
nestedCollectionInfo: {
|
|
4498
|
+
targetTable: field.getTargetTable(),
|
|
4499
|
+
selectedFieldConfigs: field.getSelectedFieldConfigs(),
|
|
4500
|
+
isSingleResult: field.isSingleResult(),
|
|
4501
|
+
},
|
|
4502
|
+
};
|
|
4164
4503
|
}
|
|
4165
4504
|
else if (typeof field === 'object' && field !== null && '__dbColumnName' in field) {
|
|
4166
4505
|
// FieldRef object - use database column name with optional table alias
|
|
@@ -4168,8 +4507,10 @@ class CollectionQueryBuilder {
|
|
|
4168
4507
|
const tableAlias = field.__tableAlias;
|
|
4169
4508
|
const fieldName = field.__fieldName; // Property name for mapper lookup
|
|
4170
4509
|
const sourceTable = field.__sourceTable; // Actual table name for schema lookup
|
|
4171
|
-
// If tableAlias differs from the target table, it's a navigation property reference
|
|
4172
|
-
|
|
4510
|
+
// If tableAlias differs from the target table (or its collection marker), it's a navigation property reference
|
|
4511
|
+
// The collection marker is `__collection_tableName__` and should be treated as the target table
|
|
4512
|
+
const collectionMarker = `__collection_${this.targetTable}__`;
|
|
4513
|
+
if (tableAlias && tableAlias !== this.targetTable && tableAlias !== collectionMarker) {
|
|
4173
4514
|
return { alias, expression: `"${tableAlias}"."${dbColumnName}"`, propertyName: fieldName, sourceTable };
|
|
4174
4515
|
}
|
|
4175
4516
|
return { alias, expression: `"${dbColumnName}"`, propertyName: fieldName };
|
|
@@ -4245,12 +4586,15 @@ class CollectionQueryBuilder {
|
|
|
4245
4586
|
let whereParams;
|
|
4246
4587
|
if (this.whereCond) {
|
|
4247
4588
|
const condBuilder = new conditions_1.ConditionBuilder();
|
|
4248
|
-
const { sql, params } = condBuilder.build(this.whereCond, context.paramCounter);
|
|
4589
|
+
const { sql, params, placeholders, paramCounter: newParamCounter } = condBuilder.build(this.whereCond, context.paramCounter, context.placeholders);
|
|
4249
4590
|
whereClause = sql;
|
|
4250
4591
|
whereParams = params;
|
|
4251
|
-
context.paramCounter
|
|
4592
|
+
context.paramCounter = newParamCounter; // Use returned counter (handles both params and placeholders)
|
|
4252
4593
|
localParams.push(...params);
|
|
4253
4594
|
context.allParams.push(...params);
|
|
4595
|
+
if (placeholders) {
|
|
4596
|
+
context.placeholders = placeholders;
|
|
4597
|
+
}
|
|
4254
4598
|
}
|
|
4255
4599
|
// Step 3: Build ORDER BY clauses SQL (without ORDER BY keyword)
|
|
4256
4600
|
// We need two versions:
|
|
@@ -4341,6 +4685,7 @@ class CollectionQueryBuilder {
|
|
|
4341
4685
|
whereParams, // Pass WHERE clause parameters
|
|
4342
4686
|
orderByClause,
|
|
4343
4687
|
orderByClauseAlias, // For json_agg ORDER BY which uses aliases
|
|
4688
|
+
orderByFields: this.orderByFields.length > 0 ? this.orderByFields : undefined, // For including ORDER BY columns in inner SELECT
|
|
4344
4689
|
limitValue: this.limitValue,
|
|
4345
4690
|
offsetValue: this.offsetValue,
|
|
4346
4691
|
isDistinct: this.isDistinct,
|
|
@@ -4349,7 +4694,8 @@ class CollectionQueryBuilder {
|
|
|
4349
4694
|
aggregateField,
|
|
4350
4695
|
arrayField,
|
|
4351
4696
|
defaultValue,
|
|
4352
|
-
counter
|
|
4697
|
+
// Use the reserved counter for LATERAL strategy, otherwise increment as before
|
|
4698
|
+
counter: reservedCounter !== undefined ? reservedCounter : context.cteCounter++,
|
|
4353
4699
|
navigationJoins: allNavigationJoins.length > 0 ? allNavigationJoins : undefined,
|
|
4354
4700
|
selectorNavigationJoins: navigationJoins.length > 0 ? navigationJoins : undefined,
|
|
4355
4701
|
};
|