linkgress-orm 0.1.0 → 0.1.2

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 (41) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +196 -196
  3. package/dist/entity/db-context.d.ts.map +1 -1
  4. package/dist/entity/db-context.js +7 -5
  5. package/dist/entity/db-context.js.map +1 -1
  6. package/dist/migration/db-schema-manager.d.ts +5 -0
  7. package/dist/migration/db-schema-manager.d.ts.map +1 -1
  8. package/dist/migration/db-schema-manager.js +147 -79
  9. package/dist/migration/db-schema-manager.js.map +1 -1
  10. package/dist/migration/enum-migrator.js +6 -6
  11. package/dist/query/collection-strategy.interface.d.ts +38 -0
  12. package/dist/query/collection-strategy.interface.d.ts.map +1 -1
  13. package/dist/query/cte-builder.d.ts +18 -1
  14. package/dist/query/cte-builder.d.ts.map +1 -1
  15. package/dist/query/cte-builder.js +102 -11
  16. package/dist/query/cte-builder.js.map +1 -1
  17. package/dist/query/grouped-query.d.ts +24 -1
  18. package/dist/query/grouped-query.d.ts.map +1 -1
  19. package/dist/query/grouped-query.js +260 -71
  20. package/dist/query/grouped-query.js.map +1 -1
  21. package/dist/query/join-builder.d.ts.map +1 -1
  22. package/dist/query/join-builder.js +10 -14
  23. package/dist/query/join-builder.js.map +1 -1
  24. package/dist/query/query-builder.d.ts +65 -1
  25. package/dist/query/query-builder.d.ts.map +1 -1
  26. package/dist/query/query-builder.js +566 -167
  27. package/dist/query/query-builder.js.map +1 -1
  28. package/dist/query/strategies/cte-collection-strategy.d.ts +4 -0
  29. package/dist/query/strategies/cte-collection-strategy.d.ts.map +1 -1
  30. package/dist/query/strategies/cte-collection-strategy.js +169 -78
  31. package/dist/query/strategies/cte-collection-strategy.js.map +1 -1
  32. package/dist/query/strategies/lateral-collection-strategy.d.ts +4 -0
  33. package/dist/query/strategies/lateral-collection-strategy.d.ts.map +1 -1
  34. package/dist/query/strategies/lateral-collection-strategy.js +86 -28
  35. package/dist/query/strategies/lateral-collection-strategy.js.map +1 -1
  36. package/dist/query/strategies/temptable-collection-strategy.js +97 -97
  37. package/dist/schema/table-builder.d.ts +16 -0
  38. package/dist/schema/table-builder.d.ts.map +1 -1
  39. package/dist/schema/table-builder.js +23 -1
  40. package/dist/schema/table-builder.js.map +1 -1
  41. package/package.json +1 -1
@@ -83,9 +83,9 @@ class GroupedQueryBuilder {
83
83
  */
84
84
  createMockRow() {
85
85
  const mock = {};
86
- // Add columns as FieldRef objects
87
- for (const [colName, colBuilder] of Object.entries(this.schema.columns)) {
88
- const dbColumnName = colBuilder.build().name;
86
+ // Add columns as FieldRef objects - use pre-computed column name map if available
87
+ const columnNameMap = (0, query_builder_1.getColumnNameMapForSchema)(this.schema);
88
+ for (const [colName, dbColumnName] of columnNameMap) {
89
89
  Object.defineProperty(mock, colName, {
90
90
  get: () => ({
91
91
  __fieldName: colName,
@@ -97,15 +97,11 @@ class GroupedQueryBuilder {
97
97
  });
98
98
  }
99
99
  // Add navigation properties (collections and single references)
100
- const relationSchemas = new Map();
101
- for (const [relName, relConfig] of Object.entries(this.schema.relations)) {
102
- if (relConfig.targetTableBuilder) {
103
- relationSchemas.set(relName, relConfig.targetTableBuilder.build());
104
- }
105
- }
106
- for (const [relName, relConfig] of Object.entries(this.schema.relations)) {
100
+ // Performance: Use pre-computed relation entries and cached schemas
101
+ const relationEntries = (0, query_builder_1.getRelationEntriesForSchema)(this.schema);
102
+ for (const [relName, relConfig] of relationEntries) {
103
+ const targetSchema = (0, query_builder_1.getTargetSchemaForRelation)(this.schema, relName, relConfig);
107
104
  if (relConfig.type === 'many') {
108
- const targetSchema = relationSchemas.get(relName);
109
105
  Object.defineProperty(mock, relName, {
110
106
  get: () => {
111
107
  return new query_builder_1.CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', this.schema.name, targetSchema);
@@ -115,7 +111,6 @@ class GroupedQueryBuilder {
115
111
  });
116
112
  }
117
113
  else {
118
- const targetSchema = relationSchemas.get(relName);
119
114
  Object.defineProperty(mock, relName, {
120
115
  get: () => {
121
116
  const refBuilder = new query_builder_1.ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema);
@@ -131,8 +126,8 @@ class GroupedQueryBuilder {
131
126
  if (join.isSubquery || !join.schema) {
132
127
  continue;
133
128
  }
134
- for (const [colName, colBuilder] of Object.entries(join.schema.columns)) {
135
- const dbColumnName = colBuilder.build().name;
129
+ const joinColumnNameMap = (0, query_builder_1.getColumnNameMapForSchema)(join.schema);
130
+ for (const [colName, dbColumnName] of joinColumnNameMap) {
136
131
  if (!mock[join.alias]) {
137
132
  mock[join.alias] = {};
138
133
  }
@@ -221,12 +216,52 @@ class GroupedSelectQueryBuilder {
221
216
  return this.transformResults(result.rows);
222
217
  }
223
218
  /**
224
- * Transform database results - convert aggregate values from strings to numbers
219
+ * Transform database results - convert aggregate values and apply mappers
225
220
  */
226
221
  transformResults(rows) {
227
- // Get the mock result to identify which fields are aggregates
222
+ // Get the mock result to identify which fields are aggregates and have mappers
228
223
  const mockGroup = this.createMockGroupedItem();
229
224
  const mockResult = this.resultSelector(mockGroup);
225
+ // Also get the original selection to track field origins for mapper lookup
226
+ const mockRow = this.createMockRow();
227
+ const mockOriginalSelection = this.originalSelector(mockRow);
228
+ // Build column metadata cache from schema for mapper lookup
229
+ const columnMetadataCache = {};
230
+ for (const [key, mockValue] of Object.entries(mockResult)) {
231
+ // Check if mockValue has getMapper (SqlFragment or aliased field with mapper)
232
+ if (typeof mockValue === 'object' && mockValue !== null && typeof mockValue.getMapper === 'function') {
233
+ const mapper = mockValue.getMapper();
234
+ if (mapper) {
235
+ columnMetadataCache[key] = { hasMapper: true, mapper };
236
+ }
237
+ }
238
+ // Check if this is a FieldRef from schema column
239
+ else if (typeof mockValue === 'object' && mockValue !== null && '__fieldName' in mockValue) {
240
+ const fieldName = mockValue.__fieldName;
241
+ // Look up in schema
242
+ const column = this.schema.columns[fieldName];
243
+ if (column) {
244
+ const config = column.build();
245
+ if (config.mapper) {
246
+ columnMetadataCache[key] = { hasMapper: true, mapper: config.mapper };
247
+ }
248
+ }
249
+ // Also check original selection for mapper (for aliased fields like p.key.distinctDay)
250
+ else if (mockOriginalSelection && fieldName in mockOriginalSelection) {
251
+ const origValue = mockOriginalSelection[fieldName];
252
+ if (typeof origValue === 'object' && origValue !== null && '__fieldName' in origValue) {
253
+ const origFieldName = origValue.__fieldName;
254
+ const origColumn = this.schema.columns[origFieldName];
255
+ if (origColumn) {
256
+ const config = origColumn.build();
257
+ if (config.mapper) {
258
+ columnMetadataCache[key] = { hasMapper: true, mapper: config.mapper };
259
+ }
260
+ }
261
+ }
262
+ }
263
+ }
264
+ }
230
265
  return rows.map(row => {
231
266
  const transformed = {};
232
267
  for (const [key, value] of Object.entries(row)) {
@@ -242,14 +277,81 @@ class GroupedSelectQueryBuilder {
242
277
  transformed[key] = value;
243
278
  }
244
279
  }
280
+ // Check if this field has a mapper
281
+ else if (columnMetadataCache[key]?.hasMapper) {
282
+ const mapper = columnMetadataCache[key].mapper;
283
+ if (value === null || value === undefined) {
284
+ transformed[key] = null;
285
+ }
286
+ else if (typeof mapper.fromDriver === 'function') {
287
+ transformed[key] = mapper.fromDriver(value);
288
+ }
289
+ else {
290
+ transformed[key] = value;
291
+ }
292
+ }
245
293
  else {
246
- // Non-aggregate field - keep as is
294
+ // Non-aggregate field without mapper - keep as is
247
295
  transformed[key] = value;
248
296
  }
249
297
  }
250
298
  return transformed;
251
299
  });
252
300
  }
301
+ /**
302
+ * Get selection metadata for mapper preservation in CTEs
303
+ * Enhances the selection result with mapper info from original schema columns
304
+ * @internal
305
+ */
306
+ getSelectionMetadata() {
307
+ const mockGroup = this.createMockGroupedItem();
308
+ const mockResult = this.resultSelector(mockGroup);
309
+ // Get original selection to map field names back to schema columns
310
+ const mockRow = this.createMockRow();
311
+ const mockOriginalSelection = this.originalSelector(mockRow);
312
+ // Build enhanced metadata with mappers
313
+ const enhancedMetadata = {};
314
+ for (const [key, value] of Object.entries(mockResult)) {
315
+ // Check if it's a FieldRef
316
+ if (typeof value === 'object' && value !== null && '__fieldName' in value) {
317
+ const fieldName = value.__fieldName;
318
+ // First check if schema has mapper for this field
319
+ const column = this.schema.columns[fieldName];
320
+ if (column) {
321
+ const config = column.build();
322
+ if (config.mapper) {
323
+ // Add mapper info to the metadata
324
+ enhancedMetadata[key] = {
325
+ ...value,
326
+ getMapper: () => config.mapper,
327
+ };
328
+ continue;
329
+ }
330
+ }
331
+ // Check original selection for mapper (for aliased fields like p.key.distinctDay)
332
+ if (mockOriginalSelection && fieldName in mockOriginalSelection) {
333
+ const origValue = mockOriginalSelection[fieldName];
334
+ if (typeof origValue === 'object' && origValue !== null && '__fieldName' in origValue) {
335
+ const origFieldName = origValue.__fieldName;
336
+ const origColumn = this.schema.columns[origFieldName];
337
+ if (origColumn) {
338
+ const config = origColumn.build();
339
+ if (config.mapper) {
340
+ enhancedMetadata[key] = {
341
+ ...value,
342
+ getMapper: () => config.mapper,
343
+ };
344
+ continue;
345
+ }
346
+ }
347
+ }
348
+ }
349
+ }
350
+ // No mapper found, use original value
351
+ enhancedMetadata[key] = value;
352
+ }
353
+ return enhancedMetadata;
354
+ }
253
355
  /**
254
356
  * Execute query and return first result or null
255
357
  */
@@ -282,7 +384,9 @@ class GroupedSelectQueryBuilder {
282
384
  outerContext.paramCounter = context.paramCounter;
283
385
  return sql;
284
386
  };
285
- return new subquery_1.Subquery(sqlBuilder, mode);
387
+ // Get selection metadata with mappers for table subqueries
388
+ const selectionMetadata = mode === 'table' ? this.getSelectionMetadata() : undefined;
389
+ return new subquery_1.Subquery(sqlBuilder, mode, selectionMetadata);
286
390
  }
287
391
  /**
288
392
  * Build SQL for use in CTEs - public interface for CTE builder
@@ -453,12 +557,34 @@ class GroupedSelectQueryBuilder {
453
557
  const mockRow = this.createMockRow();
454
558
  const mockOriginalSelection = this.originalSelector(mockRow);
455
559
  const mockGroupingKey = this.groupingKeySelector(mockOriginalSelection);
456
- const mockGroup = this.createMockGroupedItem();
560
+ // Create mock grouped item using the SAME grouping key (not a fresh one)
561
+ // This ensures SqlFragment instances are shared between GROUP BY and SELECT
562
+ const mockGroup = {
563
+ key: mockGroupingKey,
564
+ count: () => createAggregateFieldRef('COUNT'),
565
+ sum: (selector) => createAggregateFieldRef('SUM', selector),
566
+ min: (selector) => createAggregateFieldRef('MIN', selector),
567
+ max: (selector) => createAggregateFieldRef('MAX', selector),
568
+ avg: (selector) => createAggregateFieldRef('AVG', selector),
569
+ };
457
570
  const mockResult = this.resultSelector(mockGroup);
458
571
  // Extract GROUP BY fields from the grouping key
572
+ // We build these first and cache the SQL for SqlFragments so they can be reused in SELECT
459
573
  const groupByFields = [];
574
+ const sqlFragmentCache = new Map(); // Cache built SQL for reuse in SELECT
460
575
  for (const [key, value] of Object.entries(mockGroupingKey)) {
461
- if (typeof value === 'object' && value !== null && '__dbColumnName' in value) {
576
+ if (value instanceof conditions_1.SqlFragment) {
577
+ // SqlFragment in GROUP BY - build the SQL expression and cache it
578
+ const sqlBuildContext = {
579
+ paramCounter: context.paramCounter,
580
+ params: context.allParams,
581
+ };
582
+ const fragmentSql = value.buildSql(sqlBuildContext);
583
+ context.paramCounter = sqlBuildContext.paramCounter;
584
+ groupByFields.push(fragmentSql);
585
+ sqlFragmentCache.set(value, fragmentSql);
586
+ }
587
+ else if (typeof value === 'object' && value !== null && '__dbColumnName' in value) {
462
588
  const field = value;
463
589
  const tableAlias = field.__tableAlias || this.schema.name;
464
590
  groupByFields.push(`"${tableAlias}"."${field.__dbColumnName}"`);
@@ -477,8 +603,9 @@ class GroupedSelectQueryBuilder {
477
603
  }
478
604
  else if (aggField.__aggregateSelector) {
479
605
  // SUM, MIN, MAX, AVG with selector
480
- const mockOriginalRow = this.createMockRow();
481
- const field = aggField.__aggregateSelector(mockOriginalRow);
606
+ // Note: The selector references fields from the ORIGINAL SELECTION (after first .select()),
607
+ // not the raw table row. So we need to use mockOriginalSelection, not a fresh mock row.
608
+ const field = aggField.__aggregateSelector(mockOriginalSelection);
482
609
  if (typeof field === 'object' && field !== null && '__dbColumnName' in field) {
483
610
  const fieldRef = field;
484
611
  const tableAlias = fieldRef.__tableAlias || this.schema.name;
@@ -505,8 +632,8 @@ class GroupedSelectQueryBuilder {
505
632
  selectParts.push(`CAST(COUNT(*) AS INTEGER) as "${alias}"`);
506
633
  }
507
634
  else if (agg.__selector) {
508
- const mockOriginalRow = this.createMockRow();
509
- const field = agg.__selector(mockOriginalRow);
635
+ // Use mockOriginalSelection - the selector references fields from the first .select()
636
+ const field = agg.__selector(mockOriginalSelection);
510
637
  if (typeof field === 'object' && field !== null && '__dbColumnName' in field) {
511
638
  const fieldRef = field;
512
639
  const tableAlias = fieldRef.__tableAlias || this.schema.name;
@@ -526,14 +653,22 @@ class GroupedSelectQueryBuilder {
526
653
  selectParts.push(`"${tableAlias}"."${field.__dbColumnName}" as "${alias}"`);
527
654
  }
528
655
  else if (value instanceof conditions_1.SqlFragment) {
529
- // SQL fragment
530
- const sqlBuildContext = {
531
- paramCounter: context.paramCounter,
532
- params: context.allParams,
533
- };
534
- const fragmentSql = value.buildSql(sqlBuildContext);
535
- context.paramCounter = sqlBuildContext.paramCounter;
536
- selectParts.push(`${fragmentSql} as "${alias}"`);
656
+ // SQL fragment - check if we already built this for GROUP BY
657
+ const cachedSql = sqlFragmentCache.get(value);
658
+ if (cachedSql) {
659
+ // Reuse the cached SQL to ensure same parameter numbers
660
+ selectParts.push(`${cachedSql} as "${alias}"`);
661
+ }
662
+ else {
663
+ // Build new SQL for this fragment
664
+ const sqlBuildContext = {
665
+ paramCounter: context.paramCounter,
666
+ params: context.allParams,
667
+ };
668
+ const fragmentSql = value.buildSql(sqlBuildContext);
669
+ context.paramCounter = sqlBuildContext.paramCounter;
670
+ selectParts.push(`${fragmentSql} as "${alias}"`);
671
+ }
537
672
  }
538
673
  }
539
674
  // Detect navigation property references in WHERE and add JOINs
@@ -621,9 +756,9 @@ class GroupedSelectQueryBuilder {
621
756
  */
622
757
  createMockRow() {
623
758
  const mock = {};
624
- // Add columns as FieldRef objects
625
- for (const [colName, colBuilder] of Object.entries(this.schema.columns)) {
626
- const dbColumnName = colBuilder.build().name;
759
+ // Add columns as FieldRef objects - use pre-computed column name map if available
760
+ const columnNameMap = (0, query_builder_1.getColumnNameMapForSchema)(this.schema);
761
+ for (const [colName, dbColumnName] of columnNameMap) {
627
762
  Object.defineProperty(mock, colName, {
628
763
  get: () => ({
629
764
  __fieldName: colName,
@@ -635,15 +770,11 @@ class GroupedSelectQueryBuilder {
635
770
  });
636
771
  }
637
772
  // Add navigation properties (collections and single references)
638
- const relationSchemas = new Map();
639
- for (const [relName, relConfig] of Object.entries(this.schema.relations)) {
640
- if (relConfig.targetTableBuilder) {
641
- relationSchemas.set(relName, relConfig.targetTableBuilder.build());
642
- }
643
- }
644
- for (const [relName, relConfig] of Object.entries(this.schema.relations)) {
773
+ // Performance: Use pre-computed relation entries and cached schemas
774
+ const relationEntries = (0, query_builder_1.getRelationEntriesForSchema)(this.schema);
775
+ for (const [relName, relConfig] of relationEntries) {
776
+ const targetSchema = (0, query_builder_1.getTargetSchemaForRelation)(this.schema, relName, relConfig);
645
777
  if (relConfig.type === 'many') {
646
- const targetSchema = relationSchemas.get(relName);
647
778
  Object.defineProperty(mock, relName, {
648
779
  get: () => {
649
780
  return new query_builder_1.CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', this.schema.name, targetSchema);
@@ -653,7 +784,6 @@ class GroupedSelectQueryBuilder {
653
784
  });
654
785
  }
655
786
  else {
656
- const targetSchema = relationSchemas.get(relName);
657
787
  Object.defineProperty(mock, relName, {
658
788
  get: () => {
659
789
  const refBuilder = new query_builder_1.ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema);
@@ -669,8 +799,8 @@ class GroupedSelectQueryBuilder {
669
799
  if (join.isSubquery || !join.schema) {
670
800
  continue;
671
801
  }
672
- for (const [colName, colBuilder] of Object.entries(join.schema.columns)) {
673
- const dbColumnName = colBuilder.build().name;
802
+ const joinColumnNameMap = (0, query_builder_1.getColumnNameMapForSchema)(join.schema);
803
+ for (const [colName, dbColumnName] of joinColumnNameMap) {
674
804
  if (!mock[join.alias]) {
675
805
  mock[join.alias] = {};
676
806
  }
@@ -760,27 +890,13 @@ class GroupedSelectQueryBuilder {
760
890
  for (const [, value] of Object.entries(selection)) {
761
891
  if (value && typeof value === 'object' && '__tableAlias' in value && '__dbColumnName' in value) {
762
892
  // This is a FieldRef with a table alias - check if it's from a related table
763
- const tableAlias = value.__tableAlias;
764
- if (tableAlias !== this.schema.name && !joins.some(j => j.alias === tableAlias)) {
765
- // This references a related table - find the relation and add a JOIN
766
- const relation = this.schema.relations[tableAlias];
767
- if (relation && relation.type === 'one') {
768
- // Get target schema from targetTableBuilder if available
769
- let targetSchema;
770
- if (relation.targetTableBuilder) {
771
- const targetTableSchema = relation.targetTableBuilder.build();
772
- targetSchema = targetTableSchema.schema;
773
- }
774
- // Add a JOIN for this reference
775
- joins.push({
776
- alias: tableAlias,
777
- targetTable: relation.targetTable,
778
- targetSchema,
779
- foreignKeys: relation.foreignKeys || [relation.foreignKey || ''],
780
- matches: relation.matches || [],
781
- isMandatory: relation.isMandatory ?? false,
782
- });
783
- }
893
+ this.addJoinForFieldRef(value, joins);
894
+ }
895
+ else if (value instanceof conditions_1.SqlFragment) {
896
+ // SqlFragment may contain navigation property references - extract them
897
+ const fieldRefs = value.getFieldRefs();
898
+ for (const fieldRef of fieldRefs) {
899
+ this.addJoinForFieldRef(fieldRef, joins);
784
900
  }
785
901
  }
786
902
  else if (value && typeof value === 'object' && !Array.isArray(value)) {
@@ -789,6 +905,36 @@ class GroupedSelectQueryBuilder {
789
905
  }
790
906
  }
791
907
  }
908
+ /**
909
+ * Add a JOIN for a FieldRef if it references a related table
910
+ */
911
+ addJoinForFieldRef(fieldRef, joins) {
912
+ if (!fieldRef || typeof fieldRef !== 'object' || !('__tableAlias' in fieldRef) || !('__dbColumnName' in fieldRef)) {
913
+ return;
914
+ }
915
+ const tableAlias = fieldRef.__tableAlias;
916
+ if (tableAlias && tableAlias !== this.schema.name && !joins.some(j => j.alias === tableAlias)) {
917
+ // This references a related table - find the relation and add a JOIN
918
+ const relation = this.schema.relations[tableAlias];
919
+ if (relation && relation.type === 'one') {
920
+ // Get target schema from targetTableBuilder if available
921
+ let targetSchema;
922
+ if (relation.targetTableBuilder) {
923
+ const targetTableSchema = relation.targetTableBuilder.build();
924
+ targetSchema = targetTableSchema.schema;
925
+ }
926
+ // Add a JOIN for this reference
927
+ joins.push({
928
+ alias: tableAlias,
929
+ targetTable: relation.targetTable,
930
+ targetSchema,
931
+ foreignKeys: relation.foreignKeys || [relation.foreignKey || ''],
932
+ matches: relation.matches || [],
933
+ isMandatory: relation.isMandatory ?? false,
934
+ });
935
+ }
936
+ }
937
+ }
792
938
  /**
793
939
  * Build HAVING condition SQL - handles aggregate field refs specially
794
940
  */
@@ -974,20 +1120,63 @@ class GroupedJoinedQueryBuilder {
974
1120
  const selectionMetadata = this.resultSelector(mockLeft, mockRight);
975
1121
  return new subquery_1.Subquery(sqlBuilder, mode, selectionMetadata);
976
1122
  }
1123
+ /**
1124
+ * Get CTEs used by this query builder
1125
+ * @internal
1126
+ */
1127
+ getReferencedCtes() {
1128
+ return this.cte ? [this.cte] : [];
1129
+ }
1130
+ /**
1131
+ * Get selection metadata for mapper preservation in CTEs
1132
+ * Enhances the selection result with mapper info from the left subquery
1133
+ * @internal
1134
+ */
1135
+ getSelectionMetadata() {
1136
+ const mockLeft = this.createLeftMock();
1137
+ const mockRight = this.createRightMock();
1138
+ const mockResult = this.resultSelector(mockLeft, mockRight);
1139
+ // Get mapper metadata from the left subquery
1140
+ const leftMetadata = this.leftSubquery.getSelectionMetadata();
1141
+ // Build enhanced metadata with mappers
1142
+ const enhancedMetadata = {};
1143
+ for (const [key, value] of Object.entries(mockResult)) {
1144
+ // Check if it's a FieldRef from the left side
1145
+ if (typeof value === 'object' && value !== null && '__fieldName' in value) {
1146
+ const fieldName = value.__fieldName;
1147
+ // Check if left metadata has mapper for this field
1148
+ if (leftMetadata && fieldName in leftMetadata) {
1149
+ const leftValue = leftMetadata[fieldName];
1150
+ if (typeof leftValue === 'object' && leftValue !== null && typeof leftValue.getMapper === 'function') {
1151
+ enhancedMetadata[key] = {
1152
+ ...value,
1153
+ getMapper: leftValue.getMapper,
1154
+ };
1155
+ continue;
1156
+ }
1157
+ }
1158
+ }
1159
+ // No mapper found, use original value
1160
+ enhancedMetadata[key] = value;
1161
+ }
1162
+ return enhancedMetadata;
1163
+ }
977
1164
  /**
978
1165
  * Build SQL for use in CTEs - public interface for CTE builder
1166
+ * This returns SQL WITHOUT the WITH clause - CTEs should be extracted separately via getReferencedCtes()
979
1167
  * @internal
980
1168
  */
981
1169
  buildCteQuery(queryContext) {
982
- return this.buildQuery(queryContext);
1170
+ return this.buildQuery(queryContext, true);
983
1171
  }
984
1172
  /**
985
1173
  * Build the SQL query
1174
+ * @param skipCteClause If true, don't include WITH clause (for embedding in outer CTEs)
986
1175
  */
987
- buildQuery(context) {
988
- // Build CTE clause if needed
1176
+ buildQuery(context, skipCteClause = false) {
1177
+ // Build CTE clause if needed (unless we're being embedded in another CTE)
989
1178
  let cteClause = '';
990
- if (this.cte) {
1179
+ if (this.cte && !skipCteClause) {
991
1180
  cteClause = `WITH "${this.cte.name}" AS (${this.cte.query})\n`;
992
1181
  context.allParams.push(...this.cte.params);
993
1182
  context.paramCounter += this.cte.params.length;