linkgress-orm 0.2.1 → 0.2.3

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 +15 -0
  3. package/dist/entity/db-context.d.ts.map +1 -1
  4. package/dist/entity/db-context.js +18 -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 +24 -0
  11. package/dist/query/collection-strategy.interface.d.ts.map +1 -1
  12. package/dist/query/conditions.d.ts +45 -13
  13. package/dist/query/conditions.d.ts.map +1 -1
  14. package/dist/query/conditions.js +68 -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 +50 -0
  21. package/dist/query/query-builder.d.ts.map +1 -1
  22. package/dist/query/query-builder.js +299 -24
  23. package/dist/query/query-builder.js.map +1 -1
  24. package/dist/query/strategies/cte-collection-strategy.d.ts +18 -0
  25. package/dist/query/strategies/cte-collection-strategy.d.ts.map +1 -1
  26. package/dist/query/strategies/cte-collection-strategy.js +98 -2
  27. package/dist/query/strategies/cte-collection-strategy.js.map +1 -1
  28. package/dist/query/strategies/lateral-collection-strategy.d.ts +5 -0
  29. package/dist/query/strategies/lateral-collection-strategy.d.ts.map +1 -1
  30. package/dist/query/strategies/lateral-collection-strategy.js +96 -2
  31. package/dist/query/strategies/lateral-collection-strategy.js.map +1 -1
  32. package/dist/query/strategies/temptable-collection-strategy.d.ts.map +1 -1
  33. package/dist/query/strategies/temptable-collection-strategy.js +21 -8
  34. package/dist/query/strategies/temptable-collection-strategy.js.map +1 -1
  35. package/package.json +3 -2
@@ -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");
@@ -1200,7 +1201,28 @@ class SelectQueryBuilder {
1200
1201
  for (const collection of collections) {
1201
1202
  const resultMap = collectionResults.get(collection.name);
1202
1203
  const parentId = baseRow.__pk_id;
1203
- const collectionData = resultMap?.get(parentId) || this.getDefaultValueForCollection(collection.builder);
1204
+ const rawData = resultMap?.get(parentId);
1205
+ // Check if this is a single result (firstOrDefault) - return first item or null
1206
+ const isSingleResult = collection.builder.isSingleResult();
1207
+ let collectionData;
1208
+ if (isSingleResult) {
1209
+ // For single results, rawData might already be an object (from temp table strategy)
1210
+ // or might be an array (from other strategies) that we need to extract the first item from
1211
+ if (rawData === undefined || rawData === null) {
1212
+ collectionData = null;
1213
+ }
1214
+ else if (Array.isArray(rawData)) {
1215
+ collectionData = rawData.length > 0 ? rawData[0] : null;
1216
+ }
1217
+ else {
1218
+ // Already a single object
1219
+ collectionData = rawData;
1220
+ }
1221
+ }
1222
+ else {
1223
+ // For list results, ensure we return an array
1224
+ collectionData = rawData || this.getDefaultValueForCollection(collection.builder);
1225
+ }
1204
1226
  // Handle nested paths
1205
1227
  if (collection.path.length > 0) {
1206
1228
  // Navigate to the nested object and set the collection there
@@ -1348,7 +1370,28 @@ class SelectQueryBuilder {
1348
1370
  for (const collection of collections) {
1349
1371
  const resultMap = collectionResults.get(collection.name);
1350
1372
  const parentId = baseRow.__pk_id;
1351
- const collectionData = resultMap?.get(parentId) || [];
1373
+ const rawData = resultMap?.get(parentId);
1374
+ // Check if this is a single result (firstOrDefault) - return first item or null
1375
+ const isSingleResult = collection.builder.isSingleResult();
1376
+ let collectionData;
1377
+ if (isSingleResult) {
1378
+ // For single results, rawData might already be an object (from temp table strategy)
1379
+ // or might be an array (from other strategies) that we need to extract the first item from
1380
+ if (rawData === undefined || rawData === null) {
1381
+ collectionData = null;
1382
+ }
1383
+ else if (Array.isArray(rawData)) {
1384
+ collectionData = rawData.length > 0 ? rawData[0] : null;
1385
+ }
1386
+ else {
1387
+ // Already a single object
1388
+ collectionData = rawData;
1389
+ }
1390
+ }
1391
+ else {
1392
+ // For list results, ensure we return an array
1393
+ collectionData = rawData || [];
1394
+ }
1352
1395
  // Handle nested paths
1353
1396
  if (collection.path.length > 0) {
1354
1397
  // Navigate to the nested object and set the collection there
@@ -1577,6 +1620,74 @@ class SelectQueryBuilder {
1577
1620
  }
1578
1621
  return result;
1579
1622
  }
1623
+ /**
1624
+ * Create a prepared query for efficient reusable parameterized execution.
1625
+ *
1626
+ * Prepared queries build the SQL once and allow multiple executions
1627
+ * with different parameter values. This is useful for:
1628
+ * 1. Query building optimization - Build SQL once, execute many times
1629
+ * 2. Type-safe placeholders - Named parameters with validation
1630
+ * 3. Developer ergonomics - Cleaner API for reusable queries
1631
+ *
1632
+ * @param name - A name for the prepared query (for debugging)
1633
+ * @returns PreparedQuery that can be executed multiple times with different parameters
1634
+ *
1635
+ * @example
1636
+ * ```typescript
1637
+ * // Create a prepared query with a placeholder
1638
+ * const getUserById = db.users
1639
+ * .where(u => eq(u.id, sql.placeholder('userId')))
1640
+ * .prepare('getUserById');
1641
+ *
1642
+ * // Execute multiple times with different values
1643
+ * const user1 = await getUserById.execute({ userId: 10 });
1644
+ * const user2 = await getUserById.execute({ userId: 20 });
1645
+ * ```
1646
+ *
1647
+ * @example
1648
+ * ```typescript
1649
+ * // Multiple placeholders
1650
+ * const searchUsers = db.users
1651
+ * .where(u => and(
1652
+ * gt(u.age, sql.placeholder('minAge')),
1653
+ * like(u.name, sql.placeholder('namePattern'))
1654
+ * ))
1655
+ * .prepare('searchUsers');
1656
+ *
1657
+ * await searchUsers.execute({ minAge: 18, namePattern: '%john%' });
1658
+ * ```
1659
+ */
1660
+ prepare(name) {
1661
+ // Build query with placeholder tracking
1662
+ const context = {
1663
+ ctes: new Map(),
1664
+ cteCounter: 0,
1665
+ paramCounter: 1,
1666
+ allParams: [],
1667
+ placeholders: new Map(),
1668
+ collectionStrategy: this.collectionStrategy,
1669
+ executor: this.executor,
1670
+ };
1671
+ // Analyze the selector to extract nested queries
1672
+ const mockRow = this._createMockRow();
1673
+ const selectionResult = this.selector(mockRow);
1674
+ // Build the query - this populates context.placeholders
1675
+ const { sql, nestedPaths } = this.buildQuery(selectionResult, context);
1676
+ // Create transform function (closure over schema, selection, nestedPaths)
1677
+ const transformFn = (rows) => {
1678
+ // If rawResult is enabled, return raw rows without any processing
1679
+ if (this.executor?.getOptions().rawResult) {
1680
+ return rows;
1681
+ }
1682
+ // Reconstruct nested objects from flat row data (if any)
1683
+ if (nestedPaths.size > 0) {
1684
+ rows = rows.map(row => this.reconstructNestedObjects(row, nestedPaths));
1685
+ }
1686
+ // Transform results
1687
+ return this.transformResults(rows, selectionResult);
1688
+ };
1689
+ return new prepared_query_1.PreparedQuery(sql, context.placeholders || new Map(), context.paramCounter - 1, this.client, transformFn, name);
1690
+ }
1580
1691
  /**
1581
1692
  * Delete records matching the current WHERE condition
1582
1693
  * Returns a fluent builder that can be awaited directly or chained with .returning()
@@ -2702,10 +2813,14 @@ class SelectQueryBuilder {
2702
2813
  let whereClause = '';
2703
2814
  if (this.whereCond) {
2704
2815
  const condBuilder = new conditions_1.ConditionBuilder();
2705
- const { sql, params } = condBuilder.build(this.whereCond, context.paramCounter);
2816
+ const { sql, params, placeholders, paramCounter: newParamCounter } = condBuilder.build(this.whereCond, context.paramCounter, context.placeholders);
2706
2817
  whereClause = `WHERE ${sql}`;
2707
- context.paramCounter += params.length;
2818
+ context.paramCounter = newParamCounter; // Use returned counter (handles both params and placeholders)
2708
2819
  context.allParams.push(...params);
2820
+ // Update placeholders from the condition builder (for prepared statements)
2821
+ if (placeholders) {
2822
+ context.placeholders = placeholders;
2823
+ }
2709
2824
  }
2710
2825
  // Build ORDER BY clause
2711
2826
  let orderByClause = '';
@@ -2761,9 +2876,12 @@ class SelectQueryBuilder {
2761
2876
  const joinTypeStr = manualJoin.type === 'INNER' ? 'INNER JOIN' : 'LEFT JOIN';
2762
2877
  // Build ON condition
2763
2878
  const condBuilder = new conditions_1.ConditionBuilder();
2764
- const { sql: condSql, params: condParams } = condBuilder.build(manualJoin.condition, context.paramCounter);
2765
- context.paramCounter += condParams.length;
2879
+ const { sql: condSql, params: condParams, placeholders: joinPlaceholders, paramCounter: newParamCounter } = condBuilder.build(manualJoin.condition, context.paramCounter, context.placeholders);
2880
+ context.paramCounter = newParamCounter; // Use returned counter (handles both params and placeholders)
2766
2881
  context.allParams.push(...condParams);
2882
+ if (joinPlaceholders) {
2883
+ context.placeholders = joinPlaceholders;
2884
+ }
2767
2885
  // Check if this is a CTE join
2768
2886
  if (manualJoin.cte) {
2769
2887
  // Join with CTE - use CTE name directly
@@ -2998,14 +3116,18 @@ class SelectQueryBuilder {
2998
3116
  break;
2999
3117
  }
3000
3118
  case 9 /* FieldType.COLLECTION_SINGLE */: {
3001
- // firstOrDefault() - return first item or null
3002
- const items = rawValue || [];
3003
- if (config.collectionBuilder && items.length > 0) {
3004
- const transformedItems = this.transformCollectionItems(items, config.collectionBuilder);
3119
+ // firstOrDefault() - return single object or null
3120
+ // With CTE/LATERAL single result, rawValue is already a single object (not array)
3121
+ if (rawValue === null || rawValue === undefined) {
3122
+ result[key] = null;
3123
+ }
3124
+ else if (config.collectionBuilder) {
3125
+ // Transform the single item using collection mapper if available
3126
+ const transformedItems = this.transformCollectionItems([rawValue], config.collectionBuilder);
3005
3127
  result[key] = transformedItems[0] ?? null;
3006
3128
  }
3007
3129
  else {
3008
- result[key] = items[0] ?? null;
3130
+ result[key] = rawValue;
3009
3131
  }
3010
3132
  break;
3011
3133
  }
@@ -3123,6 +3245,18 @@ class SelectQueryBuilder {
3123
3245
  }
3124
3246
  }
3125
3247
  }
3248
+ // Build cache of nested collection info (fields that are themselves nested collections)
3249
+ // This is used for recursive transformation of nested collection results
3250
+ const nestedCollectionCache = new Map();
3251
+ if (selectedFieldConfigs) {
3252
+ for (const field of selectedFieldConfigs) {
3253
+ if (field.nestedCollectionInfo) {
3254
+ nestedCollectionCache.set(field.alias, field.nestedCollectionInfo);
3255
+ }
3256
+ }
3257
+ }
3258
+ // Get schema registry for nested collection transformation
3259
+ const schemaRegistry = collectionBuilder.getSchemaRegistry() || this.schemaRegistry;
3126
3260
  // Transform items using pre-built mapper cache
3127
3261
  const results = new Array(items.length);
3128
3262
  let i = items.length;
@@ -3136,13 +3270,114 @@ class SelectQueryBuilder {
3136
3270
  transformedItem[key] = mapper.fromDriver(value);
3137
3271
  }
3138
3272
  else {
3139
- transformedItem[key] = value;
3273
+ // Check if this field is a nested collection that needs recursive transformation
3274
+ const nestedInfo = nestedCollectionCache.get(key);
3275
+ if (nestedInfo && value !== null && value !== undefined && schemaRegistry) {
3276
+ transformedItem[key] = this.transformNestedCollectionValue(value, nestedInfo, schemaRegistry);
3277
+ }
3278
+ else {
3279
+ transformedItem[key] = value;
3280
+ }
3140
3281
  }
3141
3282
  }
3142
3283
  results[i] = transformedItem;
3143
3284
  }
3144
3285
  return results;
3145
3286
  }
3287
+ /**
3288
+ * Transform a nested collection value (from firstOrDefault or toList inside another collection)
3289
+ * Applies custom mappers to fields within the nested collection result.
3290
+ */
3291
+ transformNestedCollectionValue(value, nestedInfo, schemaRegistry) {
3292
+ const nestedSchema = schemaRegistry.get(nestedInfo.targetTable);
3293
+ if (!nestedSchema?.columnMetadataCache) {
3294
+ return value; // No schema info, return as-is
3295
+ }
3296
+ const columnCache = nestedSchema.columnMetadataCache;
3297
+ const selectedFieldConfigs = nestedInfo.selectedFieldConfigs;
3298
+ // Build mapper cache for nested collection fields
3299
+ const mapperCache = new Map();
3300
+ // First, add mappers from selected field configs (for aliased/navigation fields)
3301
+ if (selectedFieldConfigs) {
3302
+ for (const field of selectedFieldConfigs) {
3303
+ if (field.propertyName) {
3304
+ let mapper = null;
3305
+ if (field.sourceTable) {
3306
+ // Navigation field - look up from source table's schema
3307
+ const navSchema = schemaRegistry.get(field.sourceTable);
3308
+ if (navSchema?.columnMetadataCache) {
3309
+ const cached = navSchema.columnMetadataCache.get(field.propertyName);
3310
+ if (cached?.hasMapper) {
3311
+ mapper = cached.mapper;
3312
+ }
3313
+ }
3314
+ }
3315
+ else {
3316
+ // Regular field from target schema
3317
+ const cached = columnCache.get(field.propertyName);
3318
+ if (cached?.hasMapper) {
3319
+ mapper = cached.mapper;
3320
+ }
3321
+ }
3322
+ if (mapper) {
3323
+ mapperCache.set(field.alias, mapper);
3324
+ }
3325
+ }
3326
+ }
3327
+ }
3328
+ // Also add direct property matches from target schema
3329
+ for (const [propertyName, cached] of columnCache) {
3330
+ if (!mapperCache.has(propertyName) && cached.hasMapper) {
3331
+ mapperCache.set(propertyName, cached.mapper);
3332
+ }
3333
+ }
3334
+ // Build cache for any deeply nested collections
3335
+ const deeplyNestedCache = new Map();
3336
+ if (selectedFieldConfigs) {
3337
+ for (const field of selectedFieldConfigs) {
3338
+ if (field.nestedCollectionInfo) {
3339
+ deeplyNestedCache.set(field.alias, field.nestedCollectionInfo);
3340
+ }
3341
+ }
3342
+ }
3343
+ // Transform the value(s)
3344
+ const transformItem = (item) => {
3345
+ if (item === null || item === undefined) {
3346
+ return item;
3347
+ }
3348
+ const transformedItem = {};
3349
+ for (const key in item) {
3350
+ const fieldValue = item[key];
3351
+ const mapper = mapperCache.get(key);
3352
+ if (mapper) {
3353
+ transformedItem[key] = mapper.fromDriver(fieldValue);
3354
+ }
3355
+ else {
3356
+ // Check for deeply nested collections
3357
+ const deepNestedInfo = deeplyNestedCache.get(key);
3358
+ if (deepNestedInfo && fieldValue !== null && fieldValue !== undefined) {
3359
+ transformedItem[key] = this.transformNestedCollectionValue(fieldValue, deepNestedInfo, schemaRegistry);
3360
+ }
3361
+ else {
3362
+ transformedItem[key] = fieldValue;
3363
+ }
3364
+ }
3365
+ }
3366
+ return transformedItem;
3367
+ };
3368
+ if (nestedInfo.isSingleResult) {
3369
+ // Single item (firstOrDefault)
3370
+ return transformItem(value);
3371
+ }
3372
+ else if (Array.isArray(value)) {
3373
+ // Array of items (toList)
3374
+ return value.map(transformItem);
3375
+ }
3376
+ else {
3377
+ // Single object that should be treated as single result
3378
+ return transformItem(value);
3379
+ }
3380
+ }
3146
3381
  /**
3147
3382
  * Transform CTE aggregation items applying fromDriver mappers from selection metadata
3148
3383
  */
@@ -3233,10 +3468,13 @@ class SelectQueryBuilder {
3233
3468
  let whereClause = '';
3234
3469
  if (this.whereCond) {
3235
3470
  const condBuilder = new conditions_1.ConditionBuilder();
3236
- const { sql, params } = condBuilder.build(this.whereCond, context.paramCounter);
3471
+ const { sql, params, placeholders, paramCounter: newParamCounter } = condBuilder.build(this.whereCond, context.paramCounter, context.placeholders);
3237
3472
  whereClause = `WHERE ${sql}`;
3238
- context.paramCounter += params.length;
3473
+ context.paramCounter = newParamCounter; // Use returned counter (handles both params and placeholders)
3239
3474
  context.allParams.push(...params);
3475
+ if (placeholders) {
3476
+ context.placeholders = placeholders;
3477
+ }
3240
3478
  }
3241
3479
  // Build FROM clause with JOINs
3242
3480
  let fromClause = `FROM "${this.schema.name}"`;
@@ -3244,9 +3482,12 @@ class SelectQueryBuilder {
3244
3482
  for (const manualJoin of this.manualJoins) {
3245
3483
  const joinTypeStr = manualJoin.type === 'INNER' ? 'INNER JOIN' : 'LEFT JOIN';
3246
3484
  const condBuilder = new conditions_1.ConditionBuilder();
3247
- const { sql: condSql, params: condParams } = condBuilder.build(manualJoin.condition, context.paramCounter);
3248
- context.paramCounter += condParams.length;
3485
+ const { sql: condSql, params: condParams, placeholders: joinPlaceholders, paramCounter: newJoinParamCounter } = condBuilder.build(manualJoin.condition, context.paramCounter, context.placeholders);
3486
+ context.paramCounter = newJoinParamCounter; // Use returned counter (handles both params and placeholders)
3249
3487
  context.allParams.push(...condParams);
3488
+ if (joinPlaceholders) {
3489
+ context.placeholders = joinPlaceholders;
3490
+ }
3250
3491
  // Check if this is a subquery join
3251
3492
  if (manualJoin.isSubquery && manualJoin.subquery) {
3252
3493
  const subqueryBuildContext = {
@@ -3275,10 +3516,13 @@ class SelectQueryBuilder {
3275
3516
  let whereClause = '';
3276
3517
  if (this.whereCond) {
3277
3518
  const condBuilder = new conditions_1.ConditionBuilder();
3278
- const { sql, params } = condBuilder.build(this.whereCond, context.paramCounter);
3519
+ const { sql, params, placeholders, paramCounter: newParamCounter } = condBuilder.build(this.whereCond, context.paramCounter, context.placeholders);
3279
3520
  whereClause = `WHERE ${sql}`;
3280
- context.paramCounter += params.length;
3521
+ context.paramCounter = newParamCounter; // Use returned counter (handles both params and placeholders)
3281
3522
  context.allParams.push(...params);
3523
+ if (placeholders) {
3524
+ context.placeholders = placeholders;
3525
+ }
3282
3526
  }
3283
3527
  // Build FROM clause with JOINs
3284
3528
  let fromClause = `FROM "${this.schema.name}"`;
@@ -3286,9 +3530,12 @@ class SelectQueryBuilder {
3286
3530
  for (const manualJoin of this.manualJoins) {
3287
3531
  const joinTypeStr = manualJoin.type === 'INNER' ? 'INNER JOIN' : 'LEFT JOIN';
3288
3532
  const condBuilder = new conditions_1.ConditionBuilder();
3289
- const { sql: condSql, params: condParams } = condBuilder.build(manualJoin.condition, context.paramCounter);
3290
- context.paramCounter += condParams.length;
3533
+ const { sql: condSql, params: condParams, placeholders: joinPlaceholders, paramCounter: newJoinParamCounter } = condBuilder.build(manualJoin.condition, context.paramCounter, context.placeholders);
3534
+ context.paramCounter = newJoinParamCounter; // Use returned counter (handles both params and placeholders)
3291
3535
  context.allParams.push(...condParams);
3536
+ if (joinPlaceholders) {
3537
+ context.placeholders = joinPlaceholders;
3538
+ }
3292
3539
  // Check if this is a subquery join
3293
3540
  if (manualJoin.isSubquery && manualJoin.subquery) {
3294
3541
  const subqueryBuildContext = {
@@ -3334,12 +3581,13 @@ class SelectQueryBuilder {
3334
3581
  asSubquery(mode = 'table') {
3335
3582
  // Create a function that builds the subquery SQL when called
3336
3583
  const sqlBuilder = (outerContext) => {
3337
- // Create a fresh context for this subquery
3584
+ // Create a fresh context for this subquery, inheriting placeholders from outer context
3338
3585
  const context = {
3339
3586
  ctes: new Map(),
3340
3587
  cteCounter: 0,
3341
3588
  paramCounter: outerContext.paramCounter,
3342
3589
  allParams: outerContext.params,
3590
+ placeholders: outerContext.placeholders, // Pass placeholders through for prepared statements
3343
3591
  executor: this.executor,
3344
3592
  };
3345
3593
  // Analyze the selector to extract nested queries
@@ -3829,6 +4077,12 @@ class CollectionQueryBuilder {
3829
4077
  getTargetTableSchema() {
3830
4078
  return this.targetTableSchema;
3831
4079
  }
4080
+ /**
4081
+ * Get target table name
4082
+ */
4083
+ getTargetTable() {
4084
+ return this.targetTable;
4085
+ }
3832
4086
  /**
3833
4087
  * Get selected field configs (for mapper lookup during transformation)
3834
4088
  */
@@ -4107,6 +4361,7 @@ class CollectionQueryBuilder {
4107
4361
  const sqlBuildContext = {
4108
4362
  paramCounter: context.paramCounter,
4109
4363
  params: context.allParams,
4364
+ placeholders: context.placeholders, // Pass placeholders for prepared statements
4110
4365
  };
4111
4366
  const fragmentSql = field.buildSql(sqlBuildContext);
4112
4367
  context.paramCounter = sqlBuildContext.paramCounter;
@@ -4153,10 +4408,25 @@ class CollectionQueryBuilder {
4153
4408
  cteName: nestedResult.tableName,
4154
4409
  joinClause: nestedJoinClause,
4155
4410
  },
4411
+ // Store nested collection info for recursive mapper transformation
4412
+ nestedCollectionInfo: {
4413
+ targetTable: field.getTargetTable(),
4414
+ selectedFieldConfigs: field.getSelectedFieldConfigs(),
4415
+ isSingleResult: field.isSingleResult(),
4416
+ },
4156
4417
  };
4157
4418
  }
4158
4419
  // The nested collection becomes a correlated subquery in SELECT
4159
- return { alias, expression: nestedResult.selectExpression || nestedResult.sql };
4420
+ return {
4421
+ alias,
4422
+ expression: nestedResult.selectExpression || nestedResult.sql,
4423
+ // Store nested collection info for recursive mapper transformation
4424
+ nestedCollectionInfo: {
4425
+ targetTable: field.getTargetTable(),
4426
+ selectedFieldConfigs: field.getSelectedFieldConfigs(),
4427
+ isSingleResult: field.isSingleResult(),
4428
+ },
4429
+ };
4160
4430
  }
4161
4431
  else if (typeof field === 'object' && field !== null && '__dbColumnName' in field) {
4162
4432
  // FieldRef object - use database column name with optional table alias
@@ -4241,12 +4511,15 @@ class CollectionQueryBuilder {
4241
4511
  let whereParams;
4242
4512
  if (this.whereCond) {
4243
4513
  const condBuilder = new conditions_1.ConditionBuilder();
4244
- const { sql, params } = condBuilder.build(this.whereCond, context.paramCounter);
4514
+ const { sql, params, placeholders, paramCounter: newParamCounter } = condBuilder.build(this.whereCond, context.paramCounter, context.placeholders);
4245
4515
  whereClause = sql;
4246
4516
  whereParams = params;
4247
- context.paramCounter += params.length;
4517
+ context.paramCounter = newParamCounter; // Use returned counter (handles both params and placeholders)
4248
4518
  localParams.push(...params);
4249
4519
  context.allParams.push(...params);
4520
+ if (placeholders) {
4521
+ context.placeholders = placeholders;
4522
+ }
4250
4523
  }
4251
4524
  // Step 3: Build ORDER BY clauses SQL (without ORDER BY keyword)
4252
4525
  // We need two versions:
@@ -4337,9 +4610,11 @@ class CollectionQueryBuilder {
4337
4610
  whereParams, // Pass WHERE clause parameters
4338
4611
  orderByClause,
4339
4612
  orderByClauseAlias, // For json_agg ORDER BY which uses aliases
4613
+ orderByFields: this.orderByFields.length > 0 ? this.orderByFields : undefined, // For including ORDER BY columns in inner SELECT
4340
4614
  limitValue: this.limitValue,
4341
4615
  offsetValue: this.offsetValue,
4342
4616
  isDistinct: this.isDistinct,
4617
+ isSingleResult: this.isSingleResult(), // For firstOrDefault() - returns single object instead of array
4343
4618
  aggregationType,
4344
4619
  aggregateField,
4345
4620
  arrayField,