linkgress-orm 0.1.18 → 0.1.20

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 (36) hide show
  1. package/dist/database/database-client.interface.d.ts +29 -0
  2. package/dist/database/database-client.interface.d.ts.map +1 -1
  3. package/dist/database/database-client.interface.js +45 -1
  4. package/dist/database/database-client.interface.js.map +1 -1
  5. package/dist/database/pg-client.d.ts +5 -0
  6. package/dist/database/pg-client.d.ts.map +1 -1
  7. package/dist/database/pg-client.js +27 -0
  8. package/dist/database/pg-client.js.map +1 -1
  9. package/dist/database/postgres-client.d.ts +6 -0
  10. package/dist/database/postgres-client.d.ts.map +1 -1
  11. package/dist/database/postgres-client.js +17 -0
  12. package/dist/database/postgres-client.js.map +1 -1
  13. package/dist/entity/db-column.d.ts +17 -0
  14. package/dist/entity/db-column.d.ts.map +1 -1
  15. package/dist/entity/db-column.js.map +1 -1
  16. package/dist/entity/db-context.d.ts +56 -6
  17. package/dist/entity/db-context.d.ts.map +1 -1
  18. package/dist/entity/db-context.js +180 -74
  19. package/dist/entity/db-context.js.map +1 -1
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/query/collection-strategy.interface.d.ts +12 -0
  24. package/dist/query/collection-strategy.interface.d.ts.map +1 -1
  25. package/dist/query/query-builder.d.ts +15 -2
  26. package/dist/query/query-builder.d.ts.map +1 -1
  27. package/dist/query/query-builder.js +179 -48
  28. package/dist/query/query-builder.js.map +1 -1
  29. package/dist/query/sql-utils.d.ts +2 -0
  30. package/dist/query/sql-utils.d.ts.map +1 -1
  31. package/dist/query/sql-utils.js +24 -7
  32. package/dist/query/sql-utils.js.map +1 -1
  33. package/dist/query/strategies/temptable-collection-strategy.d.ts.map +1 -1
  34. package/dist/query/strategies/temptable-collection-strategy.js +4 -3
  35. package/dist/query/strategies/temptable-collection-strategy.js.map +1 -1
  36. package/package.json +2 -3
@@ -247,24 +247,27 @@ class QueryBuilder {
247
247
  targetSchema = getTargetSchemaForRelation(this.schema, relName, relConfig);
248
248
  }
249
249
  if (relConfig.type === 'many') {
250
+ // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
250
251
  Object.defineProperty(mock, relName, {
251
252
  get: () => {
252
253
  return new CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', this.schema.name, targetSchema, this.schemaRegistry // Pass schema registry for nested navigation resolution
253
254
  );
254
255
  },
255
- enumerable: true,
256
+ enumerable: false,
256
257
  configurable: true,
257
258
  });
258
259
  }
259
260
  else {
260
261
  // Single reference navigation (many-to-one, one-to-one)
262
+ // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow
263
+ // with circular relations like User->Posts->User)
261
264
  Object.defineProperty(mock, relName, {
262
265
  get: () => {
263
266
  const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema, this.schemaRegistry // Pass schema registry for nested navigation resolution
264
267
  );
265
268
  return refBuilder.createMockTargetRow();
266
269
  },
267
- enumerable: true,
270
+ enumerable: false,
268
271
  configurable: true,
269
272
  });
270
273
  }
@@ -406,22 +409,24 @@ class QueryBuilder {
406
409
  const targetSchema = getTargetSchemaForRelation(schema, relName, relConfig);
407
410
  if (relConfig.type === 'many') {
408
411
  // Collection navigation
412
+ // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
409
413
  Object.defineProperty(mock, relName, {
410
414
  get: () => {
411
415
  return new CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', schema.name, targetSchema);
412
416
  },
413
- enumerable: true,
417
+ enumerable: false,
414
418
  configurable: true,
415
419
  });
416
420
  }
417
421
  else {
418
422
  // Single reference navigation
423
+ // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
419
424
  Object.defineProperty(mock, relName, {
420
425
  get: () => {
421
426
  const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema);
422
427
  return refBuilder.createMockTargetRow();
423
428
  },
424
- enumerable: true,
429
+ enumerable: false,
425
430
  configurable: true,
426
431
  });
427
432
  }
@@ -786,22 +791,24 @@ class SelectQueryBuilder {
786
791
  const targetSchema = getTargetSchemaForRelation(schema, relName, relConfig);
787
792
  if (relConfig.type === 'many') {
788
793
  // Collection navigation
794
+ // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
789
795
  Object.defineProperty(mock, relName, {
790
796
  get: () => {
791
797
  return new CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', schema.name, targetSchema);
792
798
  },
793
- enumerable: true,
799
+ enumerable: false,
794
800
  configurable: true,
795
801
  });
796
802
  }
797
803
  else {
798
804
  // Single reference navigation
805
+ // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
799
806
  Object.defineProperty(mock, relName, {
800
807
  get: () => {
801
808
  const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema);
802
809
  return refBuilder.createMockTargetRow();
803
810
  },
804
- enumerable: true,
811
+ enumerable: false,
805
812
  configurable: true,
806
813
  });
807
814
  }
@@ -1473,8 +1480,10 @@ class SelectQueryBuilder {
1473
1480
  }
1474
1481
  const condBuilder = new conditions_1.ConditionBuilder();
1475
1482
  const { sql: whereSql, params: whereParams } = condBuilder.build(queryBuilder.whereCond, 1);
1476
- // Build RETURNING clause
1477
- const returningClause = queryBuilder.buildDeleteReturningClause(returning);
1483
+ // Build RETURNING clause (not needed for count-only)
1484
+ const returningClause = returning !== 'count'
1485
+ ? queryBuilder.buildDeleteReturningClause(returning)
1486
+ : undefined;
1478
1487
  const qualifiedTableName = queryBuilder.getQualifiedTableName(queryBuilder.schema.name, queryBuilder.schema.schema);
1479
1488
  let sql = `DELETE FROM ${qualifiedTableName} WHERE ${whereSql}`;
1480
1489
  if (returningClause) {
@@ -1483,6 +1492,10 @@ class SelectQueryBuilder {
1483
1492
  const result = queryBuilder.executor
1484
1493
  ? await queryBuilder.executor.query(sql, whereParams)
1485
1494
  : await queryBuilder.client.query(sql, whereParams);
1495
+ // Return affected count
1496
+ if (returning === 'count') {
1497
+ return result.rowCount ?? 0;
1498
+ }
1486
1499
  if (!returningClause) {
1487
1500
  return undefined;
1488
1501
  }
@@ -1492,6 +1505,13 @@ class SelectQueryBuilder {
1492
1505
  then(onfulfilled, onrejected) {
1493
1506
  return executeDelete(undefined).then(onfulfilled, onrejected);
1494
1507
  },
1508
+ affectedCount() {
1509
+ return {
1510
+ then(onfulfilled, onrejected) {
1511
+ return executeDelete('count').then(onfulfilled, onrejected);
1512
+ }
1513
+ };
1514
+ },
1495
1515
  returning(selector) {
1496
1516
  const returningConfig = selector ?? true;
1497
1517
  return {
@@ -1550,8 +1570,10 @@ class SelectQueryBuilder {
1550
1570
  const condBuilder = new conditions_1.ConditionBuilder();
1551
1571
  const { sql: whereSql, params: whereParams } = condBuilder.build(queryBuilder.whereCond, paramIndex);
1552
1572
  values.push(...whereParams);
1553
- // Build RETURNING clause
1554
- const returningClause = queryBuilder.buildDeleteReturningClause(returning);
1573
+ // Build RETURNING clause (not needed for count-only)
1574
+ const returningClause = returning !== 'count'
1575
+ ? queryBuilder.buildDeleteReturningClause(returning)
1576
+ : undefined;
1555
1577
  const qualifiedTableName = queryBuilder.getQualifiedTableName(queryBuilder.schema.name, queryBuilder.schema.schema);
1556
1578
  let sql = `UPDATE ${qualifiedTableName} SET ${setClauses.join(', ')} WHERE ${whereSql}`;
1557
1579
  if (returningClause) {
@@ -1560,6 +1582,10 @@ class SelectQueryBuilder {
1560
1582
  const result = queryBuilder.executor
1561
1583
  ? await queryBuilder.executor.query(sql, values)
1562
1584
  : await queryBuilder.client.query(sql, values);
1585
+ // Return affected count
1586
+ if (returning === 'count') {
1587
+ return result.rowCount ?? 0;
1588
+ }
1563
1589
  if (!returningClause) {
1564
1590
  return undefined;
1565
1591
  }
@@ -1569,6 +1595,13 @@ class SelectQueryBuilder {
1569
1595
  then(onfulfilled, onrejected) {
1570
1596
  return executeUpdate(undefined).then(onfulfilled, onrejected);
1571
1597
  },
1598
+ affectedCount() {
1599
+ return {
1600
+ then(onfulfilled, onrejected) {
1601
+ return executeUpdate('count').then(onfulfilled, onrejected);
1602
+ }
1603
+ };
1604
+ },
1572
1605
  returning(selector) {
1573
1606
  const returningConfig = selector ?? true;
1574
1607
  return {
@@ -1737,18 +1770,20 @@ class SelectQueryBuilder {
1737
1770
  targetSchema = getTargetSchemaForRelation(this.schema, relName, relConfig);
1738
1771
  }
1739
1772
  if (relConfig.type === 'many') {
1773
+ // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
1740
1774
  Object.defineProperty(mock, relName, {
1741
1775
  get: () => {
1742
1776
  return new CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', this.schema.name, targetSchema, // Pass the target schema directly
1743
1777
  this.schemaRegistry // Pass schema registry for nested resolution
1744
1778
  );
1745
1779
  },
1746
- enumerable: true,
1780
+ enumerable: false,
1747
1781
  configurable: true,
1748
1782
  });
1749
1783
  }
1750
1784
  else {
1751
1785
  // For single reference (many-to-one), create a ReferenceQueryBuilder
1786
+ // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
1752
1787
  Object.defineProperty(mock, relName, {
1753
1788
  get: () => {
1754
1789
  const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema, // Pass the target schema directly
@@ -1757,7 +1792,7 @@ class SelectQueryBuilder {
1757
1792
  // Return a mock object that exposes the target table's columns
1758
1793
  return refBuilder.createMockTargetRow();
1759
1794
  },
1760
- enumerable: true,
1795
+ enumerable: false,
1761
1796
  configurable: true,
1762
1797
  });
1763
1798
  }
@@ -2640,26 +2675,58 @@ class SelectQueryBuilder {
2640
2675
  // Use pre-cached column metadata from target schema
2641
2676
  // This avoids repeated column.build() calls for each item
2642
2677
  const columnCache = targetSchema.columnMetadataCache;
2643
- if (!columnCache || columnCache.size === 0) {
2644
- // Fallback for schemas without cache (shouldn't happen, but be safe)
2645
- return items.map(item => {
2646
- const transformedItem = {};
2647
- for (const [key, value] of Object.entries(item)) {
2648
- const column = targetSchema.columns[key];
2649
- if (column) {
2650
- const config = column.build();
2651
- transformedItem[key] = config.mapper
2652
- ? config.mapper.fromDriver(value)
2653
- : value;
2654
- }
2655
- else {
2656
- transformedItem[key] = value;
2678
+ // Build alias-to-field-info mapping from selected field configs
2679
+ // This allows us to find the correct mapper when:
2680
+ // 1. alias differs from property name (e.g., reservationExpiry: i.expiresAt)
2681
+ // 2. field comes from navigation (e.g., customerBirthdate: i.userEshop.birthdate)
2682
+ const selectedFieldConfigs = collectionBuilder.getSelectedFieldConfigs();
2683
+ const aliasToFieldInfo = new Map();
2684
+ if (selectedFieldConfigs) {
2685
+ for (const field of selectedFieldConfigs) {
2686
+ if (field.propertyName) {
2687
+ aliasToFieldInfo.set(field.alias, {
2688
+ propertyName: field.propertyName,
2689
+ sourceTable: field.sourceTable,
2690
+ });
2691
+ }
2692
+ }
2693
+ }
2694
+ // Pre-build mapper cache for all fields (including navigation fields)
2695
+ // This avoids repeated schema lookups per item
2696
+ const mapperCache = new Map(); // alias -> mapper
2697
+ for (const [alias, fieldInfo] of aliasToFieldInfo) {
2698
+ let mapper = null;
2699
+ const schemaRegistry = collectionBuilder.getSchemaRegistry() || this.schemaRegistry;
2700
+ if (fieldInfo.sourceTable && schemaRegistry) {
2701
+ // Navigation field - look up mapper from the source table's schema
2702
+ const navSchema = schemaRegistry.get(fieldInfo.sourceTable);
2703
+ if (navSchema?.columnMetadataCache) {
2704
+ const cached = navSchema.columnMetadataCache.get(fieldInfo.propertyName);
2705
+ if (cached?.hasMapper) {
2706
+ mapper = cached.mapper;
2657
2707
  }
2658
2708
  }
2659
- return transformedItem;
2660
- });
2709
+ }
2710
+ else {
2711
+ // Regular field from target schema
2712
+ const cached = columnCache?.get(fieldInfo.propertyName);
2713
+ if (cached?.hasMapper) {
2714
+ mapper = cached.mapper;
2715
+ }
2716
+ }
2717
+ if (mapper) {
2718
+ mapperCache.set(alias, mapper);
2719
+ }
2720
+ }
2721
+ // Also add direct property matches from target schema (when no alias mapping)
2722
+ if (columnCache) {
2723
+ for (const [propertyName, cached] of columnCache) {
2724
+ if (!mapperCache.has(propertyName) && cached.hasMapper) {
2725
+ mapperCache.set(propertyName, cached.mapper);
2726
+ }
2727
+ }
2661
2728
  }
2662
- // Optimized path using cached metadata and while(i--) loop
2729
+ // Transform items using pre-built mapper cache
2663
2730
  const results = new Array(items.length);
2664
2731
  let i = items.length;
2665
2732
  while (i--) {
@@ -2667,9 +2734,9 @@ class SelectQueryBuilder {
2667
2734
  const transformedItem = {};
2668
2735
  for (const key in item) {
2669
2736
  const value = item[key];
2670
- const cached = columnCache.get(key);
2671
- if (cached && cached.hasMapper) {
2672
- transformedItem[key] = cached.mapper.fromDriver(value);
2737
+ const mapper = mapperCache.get(key);
2738
+ if (mapper) {
2739
+ transformedItem[key] = mapper.fromDriver(value);
2673
2740
  }
2674
2741
  else {
2675
2742
  transformedItem[key] = value;
@@ -2929,13 +2996,15 @@ exports.SelectQueryBuilder = SelectQueryBuilder;
2929
2996
  * Reference query builder for single navigation (many-to-one, one-to-one)
2930
2997
  */
2931
2998
  class ReferenceQueryBuilder {
2932
- constructor(relationName, targetTable, foreignKeys, matches, isMandatory, targetTableSchema, schemaRegistry) {
2999
+ constructor(relationName, targetTable, foreignKeys, matches, isMandatory, targetTableSchema, schemaRegistry, navigationPath, sourceAlias) {
2933
3000
  this.relationName = relationName;
2934
3001
  this.targetTable = targetTable;
2935
3002
  this.foreignKeys = foreignKeys;
2936
3003
  this.matches = matches;
2937
3004
  this.isMandatory = isMandatory;
2938
3005
  this.schemaRegistry = schemaRegistry;
3006
+ this.navigationPath = navigationPath || [];
3007
+ this.sourceAlias = sourceAlias || '';
2939
3008
  // Prefer registry lookup (has full relations) over passed schema
2940
3009
  if (this.schemaRegistry) {
2941
3010
  this.targetTableSchema = this.schemaRegistry.get(targetTable);
@@ -3001,6 +3070,7 @@ class ReferenceQueryBuilder {
3001
3070
  columnMappers[colName] = config.mapper;
3002
3071
  }
3003
3072
  }
3073
+ const sourceTable = this.targetTable; // Actual table name for schema lookup
3004
3074
  for (const [colName, dbColumnName] of columnNameMap) {
3005
3075
  const mapper = columnMappers[colName];
3006
3076
  Object.defineProperty(mock, colName, {
@@ -3010,7 +3080,8 @@ class ReferenceQueryBuilder {
3010
3080
  cached = fieldRefCache[colName] = {
3011
3081
  __fieldName: colName,
3012
3082
  __dbColumnName: dbColumnName,
3013
- __tableAlias: tableAlias, // Mark which table this belongs to
3083
+ __tableAlias: tableAlias, // Alias for SQL generation
3084
+ __sourceTable: sourceTable, // Actual table name for mapper lookup
3014
3085
  __mapper: mapper, // Include mapper for toDriver transformation in conditions
3015
3086
  };
3016
3087
  }
@@ -3020,6 +3091,23 @@ class ReferenceQueryBuilder {
3020
3091
  configurable: true,
3021
3092
  });
3022
3093
  }
3094
+ // Build extended navigation path for nested collections
3095
+ // Only build navigation path if we have a sourceAlias (meaning we're inside a collection's selector)
3096
+ // If sourceAlias is empty, we're in the main query and references are joined in the FROM clause
3097
+ let extendedNavPath = [];
3098
+ if (this.sourceAlias) {
3099
+ // Build the current navigation step to include in path for nested collections
3100
+ // This represents the join from sourceAlias to this.relationName (this.targetTable)
3101
+ const currentNavStep = {
3102
+ alias: this.relationName,
3103
+ targetTable: this.targetTable,
3104
+ foreignKeys: this.foreignKeys,
3105
+ matches: this.matches.length > 0 ? this.matches : ['id'], // Default to 'id' if not specified
3106
+ isMandatory: this.isMandatory,
3107
+ sourceAlias: this.sourceAlias,
3108
+ };
3109
+ extendedNavPath = [...this.navigationPath, currentNavStep];
3110
+ }
3023
3111
  // Add navigation properties (both collections and references)
3024
3112
  if (this.targetTableSchema.relations) {
3025
3113
  for (const [relName, relConfig] of Object.entries(this.targetTableSchema.relations)) {
@@ -3033,27 +3121,34 @@ class ReferenceQueryBuilder {
3033
3121
  }
3034
3122
  if (relConfig.type === 'many') {
3035
3123
  // Collection navigation
3124
+ // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
3036
3125
  Object.defineProperty(mock, relName, {
3037
3126
  get: () => {
3038
3127
  const fk = relConfig.foreignKey || relConfig.foreignKeys?.[0] || '';
3039
- return new CollectionQueryBuilder(relName, relConfig.targetTable, fk, this.targetTable, nestedTargetSchema, // Pass the target schema directly
3040
- this.schemaRegistry // Pass schema registry for nested resolution
3128
+ return new CollectionQueryBuilder(relName, relConfig.targetTable, fk, this.relationName, // Use alias (relationName) for correlation in lateral joins
3129
+ nestedTargetSchema, // Pass the target schema directly
3130
+ this.schemaRegistry, // Pass schema registry for nested resolution
3131
+ extendedNavPath // Pass navigation path for intermediate joins (empty if main query)
3041
3132
  );
3042
3133
  },
3043
- enumerable: true,
3134
+ enumerable: false,
3044
3135
  configurable: true,
3045
3136
  });
3046
3137
  }
3047
3138
  else {
3048
3139
  // Reference navigation
3140
+ // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow
3141
+ // with circular relations like User->Posts->User)
3049
3142
  Object.defineProperty(mock, relName, {
3050
3143
  get: () => {
3051
3144
  const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, nestedTargetSchema, // Pass the target schema directly
3052
- this.schemaRegistry // Pass schema registry for nested resolution
3145
+ this.schemaRegistry, // Pass schema registry for nested resolution
3146
+ extendedNavPath, // Pass navigation path for nested collections
3147
+ this.sourceAlias ? this.relationName : '' // Only set source if tracking path
3053
3148
  );
3054
3149
  return refBuilder.createMockTargetRow();
3055
3150
  },
3056
- enumerable: true,
3151
+ enumerable: false,
3057
3152
  configurable: true,
3058
3153
  });
3059
3154
  }
@@ -3072,7 +3167,7 @@ exports.ReferenceQueryBuilder = ReferenceQueryBuilder;
3072
3167
  * Collection query builder for nested queries
3073
3168
  */
3074
3169
  class CollectionQueryBuilder {
3075
- constructor(relationName, targetTable, foreignKey, sourceTable, targetTableSchema, schemaRegistry) {
3170
+ constructor(relationName, targetTable, foreignKey, sourceTable, targetTableSchema, schemaRegistry, navigationPath) {
3076
3171
  this.orderByFields = [];
3077
3172
  this.isMarkedAsList = false;
3078
3173
  this.isDistinct = false;
@@ -3082,6 +3177,7 @@ class CollectionQueryBuilder {
3082
3177
  this.foreignKey = foreignKey;
3083
3178
  this.sourceTable = sourceTable;
3084
3179
  this.schemaRegistry = schemaRegistry;
3180
+ this.navigationPath = navigationPath || [];
3085
3181
  // Prefer registry lookup (has full relations) over passed schema
3086
3182
  if (this.schemaRegistry) {
3087
3183
  const registrySchema = this.schemaRegistry.get(targetTable);
@@ -3098,7 +3194,8 @@ class CollectionQueryBuilder {
3098
3194
  * Select specific fields from collection items
3099
3195
  */
3100
3196
  select(selector) {
3101
- const newBuilder = new CollectionQueryBuilder(this.relationName, this.targetTable, this.foreignKey, this.sourceTable, this.targetTableSchema, this.schemaRegistry // Pass schema registry for nested navigation resolution
3197
+ const newBuilder = new CollectionQueryBuilder(this.relationName, this.targetTable, this.foreignKey, this.sourceTable, this.targetTableSchema, this.schemaRegistry, // Pass schema registry for nested navigation resolution
3198
+ this.navigationPath // Pass navigation path for intermediate joins
3102
3199
  );
3103
3200
  newBuilder.selector = selector;
3104
3201
  newBuilder.whereCond = this.whereCond;
@@ -3148,7 +3245,8 @@ class CollectionQueryBuilder {
3148
3245
  const columnNameMap = getColumnNameMapForSchema(this.targetTableSchema);
3149
3246
  // Performance: Lazy-cache FieldRef objects
3150
3247
  const fieldRefCache = {};
3151
- // Add columns
3248
+ // Add columns - include tableAlias for unambiguous column references in WHERE clauses
3249
+ const tableAlias = this.targetTable;
3152
3250
  for (const [colName, dbColumnName] of columnNameMap) {
3153
3251
  Object.defineProperty(mock, colName, {
3154
3252
  get() {
@@ -3157,6 +3255,7 @@ class CollectionQueryBuilder {
3157
3255
  cached = fieldRefCache[colName] = {
3158
3256
  __fieldName: colName,
3159
3257
  __dbColumnName: dbColumnName,
3258
+ __tableAlias: tableAlias, // Include table alias for unambiguous references
3160
3259
  };
3161
3260
  }
3162
3261
  return cached;
@@ -3170,30 +3269,35 @@ class CollectionQueryBuilder {
3170
3269
  for (const [relName, relConfig] of Object.entries(this.targetTableSchema.relations)) {
3171
3270
  if (relConfig.type === 'many') {
3172
3271
  // Collection navigation
3272
+ // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
3173
3273
  Object.defineProperty(mock, relName, {
3174
3274
  get: () => {
3175
3275
  // Don't call build() - it returns schema without relations
3176
3276
  const fk = relConfig.foreignKey || relConfig.foreignKeys?.[0] || '';
3177
3277
  return new CollectionQueryBuilder(relName, relConfig.targetTable, fk, this.targetTable, undefined, // Don't pass schema, force registry lookup
3178
3278
  this.schemaRegistry // Pass schema registry for nested resolution
3279
+ // No navigation path needed here - direct collection access from parent
3179
3280
  );
3180
3281
  },
3181
- enumerable: true,
3282
+ enumerable: false,
3182
3283
  configurable: true,
3183
3284
  });
3184
3285
  }
3185
3286
  else {
3186
3287
  // Reference navigation
3288
+ // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
3187
3289
  Object.defineProperty(mock, relName, {
3188
3290
  get: () => {
3189
3291
  // Don't call build() - it returns schema without relations
3190
3292
  // Instead, pass undefined and let ReferenceQueryBuilder look it up from registry
3191
3293
  const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, undefined, // Don't pass schema, force registry lookup
3192
- this.schemaRegistry // Pass schema registry for nested resolution
3294
+ this.schemaRegistry, // Pass schema registry for nested resolution
3295
+ [], // Empty navigation path - this is the first reference in the chain
3296
+ this.targetTable // Source alias is this collection's target table
3193
3297
  );
3194
3298
  return refBuilder.createMockTargetRow();
3195
3299
  },
3196
- enumerable: true,
3300
+ enumerable: false,
3197
3301
  configurable: true,
3198
3302
  });
3199
3303
  }
@@ -3328,6 +3432,18 @@ class CollectionQueryBuilder {
3328
3432
  getTargetTableSchema() {
3329
3433
  return this.targetTableSchema;
3330
3434
  }
3435
+ /**
3436
+ * Get selected field configs (for mapper lookup during transformation)
3437
+ */
3438
+ getSelectedFieldConfigs() {
3439
+ return this._selectedFieldConfigs;
3440
+ }
3441
+ /**
3442
+ * Get schema registry (for mapper lookup during transformation of navigation fields)
3443
+ */
3444
+ getSchemaRegistry() {
3445
+ return this.schemaRegistry;
3446
+ }
3331
3447
  /**
3332
3448
  * Check if this collection uses array aggregation (for flattened results)
3333
3449
  */
@@ -3613,7 +3729,9 @@ class CollectionQueryBuilder {
3613
3729
  cteCounter: context.cteCounter,
3614
3730
  };
3615
3731
  const nestedResult = field.buildCTE(nestedCtx, client);
3732
+ // Sync both counters back - cteCounter for CTE naming, paramCounter for parameter numbering
3616
3733
  context.cteCounter = nestedCtx.cteCounter;
3734
+ context.paramCounter = nestedCtx.paramCounter;
3617
3735
  // For CTE/LATERAL strategy, we need to track the nested join
3618
3736
  // The nested aggregation needs to be joined in the outer collection's subquery
3619
3737
  if (nestedResult.tableName) {
@@ -3647,11 +3765,13 @@ class CollectionQueryBuilder {
3647
3765
  // FieldRef object - use database column name with optional table alias
3648
3766
  const dbColumnName = field.__dbColumnName;
3649
3767
  const tableAlias = field.__tableAlias;
3768
+ const fieldName = field.__fieldName; // Property name for mapper lookup
3769
+ const sourceTable = field.__sourceTable; // Actual table name for schema lookup
3650
3770
  // If tableAlias differs from the target table, it's a navigation property reference
3651
3771
  if (tableAlias && tableAlias !== this.targetTable) {
3652
- return { alias, expression: `"${tableAlias}"."${dbColumnName}"` };
3772
+ return { alias, expression: `"${tableAlias}"."${dbColumnName}"`, propertyName: fieldName, sourceTable };
3653
3773
  }
3654
- return { alias, expression: `"${dbColumnName}"` };
3774
+ return { alias, expression: `"${dbColumnName}"`, propertyName: fieldName };
3655
3775
  }
3656
3776
  else if (typeof field === 'string') {
3657
3777
  // Simple string reference (for backward compatibility)
@@ -3682,9 +3802,11 @@ class CollectionQueryBuilder {
3682
3802
  // Single field selection - use the field name as both alias and expression
3683
3803
  const field = selectedFields;
3684
3804
  const dbColumnName = field.__dbColumnName;
3805
+ const fieldName = field.__fieldName; // Property name for mapper lookup
3685
3806
  selectedFieldConfigs.push({
3686
3807
  alias: dbColumnName,
3687
3808
  expression: `"${dbColumnName}"`,
3809
+ propertyName: fieldName,
3688
3810
  });
3689
3811
  }
3690
3812
  else {
@@ -3703,6 +3825,7 @@ class CollectionQueryBuilder {
3703
3825
  selectedFieldConfigs.push({
3704
3826
  alias: colName,
3705
3827
  expression: `"${dbColumnName}"`,
3828
+ propertyName: colName, // Same as alias when selecting all fields
3706
3829
  });
3707
3830
  }
3708
3831
  }
@@ -3714,6 +3837,8 @@ class CollectionQueryBuilder {
3714
3837
  });
3715
3838
  }
3716
3839
  }
3840
+ // Cache selected field configs for mapper lookup during transformation
3841
+ this._selectedFieldConfigs = selectedFieldConfigs;
3717
3842
  // Step 2: Build WHERE clause SQL (without WHERE keyword)
3718
3843
  let whereClause;
3719
3844
  let whereParams;
@@ -3780,6 +3905,12 @@ class CollectionQueryBuilder {
3780
3905
  const selectedFields = this.selector(mockItem);
3781
3906
  this.detectNavigationJoins(selectedFields, navigationJoins, this.targetTable, this.targetTableSchema);
3782
3907
  }
3908
+ // Step 5b: Merge navigation path joins (for intermediate tables in navigation chains)
3909
+ // These joins are needed when accessing a collection through a chain like:
3910
+ // oi.productPrice.product.resort.productIntegrationDefinitions
3911
+ // The navigation path contains joins for productPrice, product, resort
3912
+ // which must be included in the lateral subquery for correlation
3913
+ const allNavigationJoins = [...this.navigationPath, ...navigationJoins];
3783
3914
  // Step 6: Build CollectionAggregationConfig object
3784
3915
  const config = {
3785
3916
  relationName: this.relationName,
@@ -3799,7 +3930,7 @@ class CollectionQueryBuilder {
3799
3930
  arrayField,
3800
3931
  defaultValue,
3801
3932
  counter: context.cteCounter++,
3802
- navigationJoins: navigationJoins.length > 0 ? navigationJoins : undefined,
3933
+ navigationJoins: allNavigationJoins.length > 0 ? allNavigationJoins : undefined,
3803
3934
  };
3804
3935
  // Step 6: Call the strategy
3805
3936
  const result = strategy.buildAggregation(config, context, client);