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.
Files changed (35) hide show
  1. package/README.md +1 -0
  2. package/dist/entity/db-context.d.ts +26 -0
  3. package/dist/entity/db-context.d.ts.map +1 -1
  4. package/dist/entity/db-context.js +31 -0
  5. package/dist/entity/db-context.js.map +1 -1
  6. package/dist/index.d.ts +2 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +7 -3
  9. package/dist/index.js.map +1 -1
  10. package/dist/query/collection-strategy.interface.d.ts +18 -0
  11. package/dist/query/collection-strategy.interface.d.ts.map +1 -1
  12. package/dist/query/conditions.d.ts +57 -13
  13. package/dist/query/conditions.d.ts.map +1 -1
  14. package/dist/query/conditions.js +94 -4
  15. package/dist/query/conditions.js.map +1 -1
  16. package/dist/query/prepared-query.d.ts +66 -0
  17. package/dist/query/prepared-query.d.ts.map +1 -0
  18. package/dist/query/prepared-query.js +86 -0
  19. package/dist/query/prepared-query.js.map +1 -0
  20. package/dist/query/query-builder.d.ts +63 -2
  21. package/dist/query/query-builder.d.ts.map +1 -1
  22. package/dist/query/query-builder.js +376 -30
  23. package/dist/query/query-builder.js.map +1 -1
  24. package/dist/query/strategies/cte-collection-strategy.d.ts.map +1 -1
  25. package/dist/query/strategies/cte-collection-strategy.js +86 -24
  26. package/dist/query/strategies/cte-collection-strategy.js.map +1 -1
  27. package/dist/query/strategies/lateral-collection-strategy.d.ts +9 -0
  28. package/dist/query/strategies/lateral-collection-strategy.d.ts.map +1 -1
  29. package/dist/query/strategies/lateral-collection-strategy.js +187 -70
  30. package/dist/query/strategies/lateral-collection-strategy.js.map +1 -1
  31. package/dist/query/strategies/temptable-collection-strategy.d.ts +5 -0
  32. package/dist/query/strategies/temptable-collection-strategy.d.ts.map +1 -1
  33. package/dist/query/strategies/temptable-collection-strategy.js +48 -18
  34. package/dist/query/strategies/temptable-collection-strategy.js.map +1 -1
  35. 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
- // Build count query
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 collectionData = resultMap?.get(parentId) || this.getDefaultValueForCollection(collection.builder);
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 collectionData = resultMap?.get(parentId) || [];
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 += params.length;
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 += condParams.length;
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
- transformedItem[key] = value;
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 += params.length;
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 += condParams.length;
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 query
3537
+ * Build aggregate query (count or exists)
3276
3538
  */
3277
- buildCountQuery(context) {
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 += params.length;
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
- let fromClause = `FROM "${this.schema.name}"`;
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 += condParams.length;
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
- const sql = `SELECT COUNT(*) as count\n${fromClause}\n${whereClause}`.trim();
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
- const tableAlias = this.targetTable;
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 { alias, expression: nestedResult.selectExpression || nestedResult.sql };
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
- if (tableAlias && tableAlias !== this.targetTable) {
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 += params.length;
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: context.cteCounter++,
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
  };