linkgress-orm 0.1.4 → 0.1.6

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 (48) hide show
  1. package/dist/entity/db-context.d.ts +79 -0
  2. package/dist/entity/db-context.d.ts.map +1 -1
  3. package/dist/entity/db-context.js +126 -1
  4. package/dist/entity/db-context.js.map +1 -1
  5. package/dist/index.d.ts +1 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +9 -6
  8. package/dist/index.js.map +1 -1
  9. package/dist/query/collection-strategy.interface.d.ts +8 -0
  10. package/dist/query/collection-strategy.interface.d.ts.map +1 -1
  11. package/dist/query/conditions.js +2 -2
  12. package/dist/query/conditions.js.map +1 -1
  13. package/dist/query/cte-builder.d.ts.map +1 -1
  14. package/dist/query/cte-builder.js +27 -6
  15. package/dist/query/cte-builder.js.map +1 -1
  16. package/dist/query/grouped-query.d.ts +23 -0
  17. package/dist/query/grouped-query.d.ts.map +1 -1
  18. package/dist/query/grouped-query.js +337 -129
  19. package/dist/query/grouped-query.js.map +1 -1
  20. package/dist/query/query-builder.d.ts +5 -0
  21. package/dist/query/query-builder.d.ts.map +1 -1
  22. package/dist/query/query-builder.js +579 -282
  23. package/dist/query/query-builder.js.map +1 -1
  24. package/dist/query/strategies/cte-collection-strategy.d.ts +11 -5
  25. package/dist/query/strategies/cte-collection-strategy.d.ts.map +1 -1
  26. package/dist/query/strategies/cte-collection-strategy.js +36 -14
  27. package/dist/query/strategies/cte-collection-strategy.js.map +1 -1
  28. package/dist/query/strategies/lateral-collection-strategy.d.ts +18 -2
  29. package/dist/query/strategies/lateral-collection-strategy.d.ts.map +1 -1
  30. package/dist/query/strategies/lateral-collection-strategy.js +140 -7
  31. package/dist/query/strategies/lateral-collection-strategy.js.map +1 -1
  32. package/dist/query/strategies/temptable-collection-strategy.d.ts +9 -3
  33. package/dist/query/strategies/temptable-collection-strategy.d.ts.map +1 -1
  34. package/dist/query/strategies/temptable-collection-strategy.js +53 -13
  35. package/dist/query/strategies/temptable-collection-strategy.js.map +1 -1
  36. package/dist/query/subquery.d.ts +19 -2
  37. package/dist/query/subquery.d.ts.map +1 -1
  38. package/dist/query/subquery.js +12 -0
  39. package/dist/query/subquery.js.map +1 -1
  40. package/dist/schema/table-builder.d.ts +20 -1
  41. package/dist/schema/table-builder.d.ts.map +1 -1
  42. package/dist/schema/table-builder.js +11 -2
  43. package/dist/schema/table-builder.js.map +1 -1
  44. package/dist/types/custom-types.d.ts +4 -2
  45. package/dist/types/custom-types.d.ts.map +1 -1
  46. package/dist/types/custom-types.js +6 -4
  47. package/dist/types/custom-types.js.map +1 -1
  48. package/package.json +1 -1
@@ -83,15 +83,24 @@ class GroupedQueryBuilder {
83
83
  */
84
84
  createMockRow() {
85
85
  const mock = {};
86
+ const tableAlias = this.schema.name;
86
87
  // Add columns as FieldRef objects - use pre-computed column name map if available
87
88
  const columnNameMap = (0, query_builder_1.getColumnNameMapForSchema)(this.schema);
89
+ // Performance: Lazy-cache FieldRef objects
90
+ const fieldRefCache = {};
88
91
  for (const [colName, dbColumnName] of columnNameMap) {
89
92
  Object.defineProperty(mock, colName, {
90
- get: () => ({
91
- __fieldName: colName,
92
- __dbColumnName: dbColumnName,
93
- __tableAlias: this.schema.name,
94
- }),
93
+ get() {
94
+ let cached = fieldRefCache[colName];
95
+ if (!cached) {
96
+ cached = fieldRefCache[colName] = {
97
+ __fieldName: colName,
98
+ __dbColumnName: dbColumnName,
99
+ __tableAlias: tableAlias,
100
+ };
101
+ }
102
+ return cached;
103
+ },
95
104
  enumerable: true,
96
105
  configurable: true,
97
106
  });
@@ -127,16 +136,25 @@ class GroupedQueryBuilder {
127
136
  continue;
128
137
  }
129
138
  const joinColumnNameMap = (0, query_builder_1.getColumnNameMapForSchema)(join.schema);
139
+ if (!mock[join.alias]) {
140
+ mock[join.alias] = {};
141
+ }
142
+ // Lazy-cache for joined table
143
+ const joinFieldRefCache = {};
144
+ const joinAlias = join.alias;
130
145
  for (const [colName, dbColumnName] of joinColumnNameMap) {
131
- if (!mock[join.alias]) {
132
- mock[join.alias] = {};
133
- }
134
146
  Object.defineProperty(mock[join.alias], colName, {
135
- get: () => ({
136
- __fieldName: colName,
137
- __dbColumnName: dbColumnName,
138
- __tableAlias: join.alias,
139
- }),
147
+ get() {
148
+ let cached = joinFieldRefCache[colName];
149
+ if (!cached) {
150
+ cached = joinFieldRefCache[colName] = {
151
+ __fieldName: colName,
152
+ __dbColumnName: dbColumnName,
153
+ __tableAlias: joinAlias,
154
+ };
155
+ }
156
+ return cached;
157
+ },
140
158
  enumerable: true,
141
159
  configurable: true,
142
160
  });
@@ -551,6 +569,16 @@ class GroupedSelectQueryBuilder {
551
569
  }
552
570
  /**
553
571
  * Build the SQL query for grouped results
572
+ *
573
+ * Optimization: When grouping by SqlFragment expressions, we wrap the base query
574
+ * in a subquery to avoid repeating complex expressions in both SELECT and GROUP BY.
575
+ * This matches the pattern used by Drizzle and improves query performance.
576
+ *
577
+ * Without optimization:
578
+ * SELECT complex_expr as "alias" FROM table GROUP BY complex_expr
579
+ *
580
+ * With optimization:
581
+ * SELECT "alias" FROM (SELECT complex_expr as "alias" FROM table) q1 GROUP BY "alias"
554
582
  */
555
583
  buildQuery(context) {
556
584
  // Create mocks for evaluation
@@ -568,166 +596,262 @@ class GroupedSelectQueryBuilder {
568
596
  avg: (selector) => createAggregateFieldRef('AVG', selector),
569
597
  };
570
598
  const mockResult = this.resultSelector(mockGroup);
599
+ // Check if we have SqlFragment expressions in the grouping key
600
+ // If so, we'll use the subquery wrapping optimization
601
+ const hasSqlFragmentInGroupBy = Object.values(mockGroupingKey).some(value => value instanceof conditions_1.SqlFragment);
602
+ // Detect navigation property references in WHERE and add JOINs
603
+ const navigationJoins = [];
604
+ // Detect joins from the original selection (navigation properties used in select)
605
+ this.detectAndAddJoinsFromSelection(mockOriginalSelection, navigationJoins);
606
+ // Detect joins from WHERE condition
607
+ this.detectAndAddJoinsFromCondition(this.whereCond, navigationJoins);
608
+ // Build base FROM clause with JOINs
609
+ let baseFromClause = `"${this.schema.name}"`;
610
+ // Add navigation property JOINs first
611
+ for (const navJoin of navigationJoins) {
612
+ const joinType = navJoin.isMandatory ? 'INNER JOIN' : 'LEFT JOIN';
613
+ const targetTableName = navJoin.targetSchema
614
+ ? `"${navJoin.targetSchema}"."${navJoin.targetTable}"`
615
+ : `"${navJoin.targetTable}"`;
616
+ // Build join condition: source.foreignKey = target.match
617
+ const joinConditions = navJoin.foreignKeys.map((fk, i) => {
618
+ const targetCol = navJoin.matches[i] || 'id';
619
+ return `"${this.schema.name}"."${fk}" = "${navJoin.alias}"."${targetCol}"`;
620
+ });
621
+ baseFromClause += `\n${joinType} ${targetTableName} AS "${navJoin.alias}" ON ${joinConditions.join(' AND ')}`;
622
+ }
623
+ // Add manual JOINs
624
+ for (const manualJoin of this.manualJoins) {
625
+ const joinTypeStr = manualJoin.type === 'INNER' ? 'INNER JOIN' : 'LEFT JOIN';
626
+ const condBuilder = new conditions_1.ConditionBuilder();
627
+ const { sql: condSql, params: condParams } = condBuilder.build(manualJoin.condition, context.paramCounter);
628
+ context.paramCounter += condParams.length;
629
+ context.allParams.push(...condParams);
630
+ // Check if this is a subquery join
631
+ if (manualJoin.isSubquery && manualJoin.subquery) {
632
+ const subqueryBuildContext = {
633
+ paramCounter: context.paramCounter,
634
+ params: context.allParams,
635
+ };
636
+ const subquerySql = manualJoin.subquery.buildSql(subqueryBuildContext);
637
+ context.paramCounter = subqueryBuildContext.paramCounter;
638
+ baseFromClause += `\n${joinTypeStr} (${subquerySql}) AS "${manualJoin.alias}" ON ${condSql}`;
639
+ }
640
+ else {
641
+ baseFromClause += `\n${joinTypeStr} "${manualJoin.table}" AS "${manualJoin.alias}" ON ${condSql}`;
642
+ }
643
+ }
644
+ // Build WHERE clause
645
+ let whereClause = '';
646
+ if (this.whereCond) {
647
+ const condBuilder = new conditions_1.ConditionBuilder();
648
+ const { sql, params } = condBuilder.build(this.whereCond, context.paramCounter);
649
+ whereClause = `WHERE ${sql}`;
650
+ context.paramCounter += params.length;
651
+ context.allParams.push(...params);
652
+ }
653
+ if (hasSqlFragmentInGroupBy) {
654
+ // Use subquery wrapping optimization for complex GROUP BY expressions
655
+ return this.buildQueryWithSubqueryWrapping(context, mockOriginalSelection, mockGroupingKey, mockResult, baseFromClause, whereClause);
656
+ }
657
+ else {
658
+ // Use simple query for basic GROUP BY (column references only)
659
+ return this.buildSimpleGroupedQuery(context, mockOriginalSelection, mockGroupingKey, mockResult, baseFromClause, whereClause);
660
+ }
661
+ }
662
+ /**
663
+ * Build a simple grouped query when GROUP BY only contains column references
664
+ */
665
+ buildSimpleGroupedQuery(context, mockOriginalSelection, mockGroupingKey, mockResult, baseFromClause, whereClause) {
571
666
  // 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
573
667
  const groupByFields = [];
574
- const sqlFragmentCache = new Map(); // Cache built SQL for reuse in SELECT
668
+ for (const [key, value] of Object.entries(mockGroupingKey)) {
669
+ if (typeof value === 'object' && value !== null && '__dbColumnName' in value) {
670
+ const field = value;
671
+ const tableAlias = field.__tableAlias || this.schema.name;
672
+ groupByFields.push(`"${tableAlias}"."${field.__dbColumnName}"`);
673
+ }
674
+ }
675
+ // Build SELECT clause from result selector
676
+ const selectParts = [];
677
+ for (const [alias, value] of Object.entries(mockResult)) {
678
+ const selectPart = this.buildSelectPart(alias, value, mockOriginalSelection, context, this.schema.name);
679
+ if (selectPart) {
680
+ selectParts.push(selectPart);
681
+ }
682
+ }
683
+ // Build GROUP BY clause
684
+ const groupByClause = `GROUP BY ${groupByFields.join(', ')}`;
685
+ // Build HAVING clause
686
+ let havingClause = '';
687
+ if (this.havingCond) {
688
+ const havingSql = this.buildHavingCondition(this.havingCond, context);
689
+ havingClause = `HAVING ${havingSql}`;
690
+ }
691
+ // Build ORDER BY clause
692
+ let orderByClause = '';
693
+ if (this.orderByFields.length > 0) {
694
+ const orderParts = this.orderByFields.map(({ field, direction }) => `"${field}" ${direction}`);
695
+ orderByClause = `ORDER BY ${orderParts.join(', ')}`;
696
+ }
697
+ // Build LIMIT/OFFSET
698
+ let limitClause = '';
699
+ if (this.limitValue !== undefined) {
700
+ limitClause = `LIMIT ${this.limitValue}`;
701
+ }
702
+ if (this.offsetValue !== undefined) {
703
+ limitClause += ` OFFSET ${this.offsetValue}`;
704
+ }
705
+ const finalQuery = `SELECT ${selectParts.join(', ')}\nFROM ${baseFromClause}\n${whereClause}\n${groupByClause}\n${havingClause}\n${orderByClause}\n${limitClause}`.trim();
706
+ return {
707
+ sql: finalQuery,
708
+ params: context.allParams,
709
+ };
710
+ }
711
+ /**
712
+ * Build a grouped query with subquery wrapping for complex GROUP BY expressions
713
+ * This avoids repeating SqlFragment expressions in both SELECT and GROUP BY
714
+ */
715
+ buildQueryWithSubqueryWrapping(context, mockOriginalSelection, mockGroupingKey, mockResult, baseFromClause, whereClause) {
716
+ // Step 1: Build the inner subquery that computes all expressions
717
+ // This includes: grouping key fields, fields needed for aggregates
718
+ const innerSelectParts = [];
719
+ const groupByAliases = []; // Aliases to use in outer GROUP BY
720
+ const sqlFragmentAliasMap = new Map(); // Map SqlFragment to its alias
721
+ // Add grouping key fields to inner select
575
722
  for (const [key, value] of Object.entries(mockGroupingKey)) {
576
723
  if (value instanceof conditions_1.SqlFragment) {
577
- // SqlFragment in GROUP BY - build the SQL expression and cache it
724
+ // Build the SqlFragment expression
578
725
  const sqlBuildContext = {
579
726
  paramCounter: context.paramCounter,
580
727
  params: context.allParams,
581
728
  };
582
729
  const fragmentSql = value.buildSql(sqlBuildContext);
583
730
  context.paramCounter = sqlBuildContext.paramCounter;
584
- groupByFields.push(fragmentSql);
585
- sqlFragmentCache.set(value, fragmentSql);
731
+ innerSelectParts.push(`${fragmentSql} as "${key}"`);
732
+ groupByAliases.push(`"${key}"`);
733
+ sqlFragmentAliasMap.set(value, key);
586
734
  }
587
735
  else if (typeof value === 'object' && value !== null && '__dbColumnName' in value) {
588
736
  const field = value;
589
737
  const tableAlias = field.__tableAlias || this.schema.name;
590
- groupByFields.push(`"${tableAlias}"."${field.__dbColumnName}"`);
738
+ innerSelectParts.push(`"${tableAlias}"."${field.__dbColumnName}" as "${key}"`);
739
+ groupByAliases.push(`"${key}"`);
591
740
  }
592
741
  }
593
- // Build SELECT clause from result selector
594
- const selectParts = [];
742
+ // Add fields needed for aggregates to inner select
743
+ const aggregateFields = new Set(); // Track fields we've already added
744
+ for (const [, value] of Object.entries(mockResult)) {
745
+ if (typeof value === 'object' && value !== null && '__isAggregate' in value && value.__isAggregate) {
746
+ const aggField = value;
747
+ if (aggField.__aggregateSelector) {
748
+ const field = aggField.__aggregateSelector(mockOriginalSelection);
749
+ if (typeof field === 'object' && field !== null && '__dbColumnName' in field) {
750
+ const fieldRef = field;
751
+ const tableAlias = fieldRef.__tableAlias || this.schema.name;
752
+ const dbColName = fieldRef.__dbColumnName;
753
+ const fieldKey = `${tableAlias}.${dbColName}`;
754
+ if (!aggregateFields.has(fieldKey)) {
755
+ aggregateFields.add(fieldKey);
756
+ innerSelectParts.push(`"${tableAlias}"."${dbColName}" as "${dbColName}"`);
757
+ }
758
+ }
759
+ }
760
+ }
761
+ else if (typeof value === 'object' && value !== null && '__aggregateType' in value) {
762
+ // Backward compatibility
763
+ const agg = value;
764
+ if (agg.__selector) {
765
+ const field = agg.__selector(mockOriginalSelection);
766
+ if (typeof field === 'object' && field !== null && '__dbColumnName' in field) {
767
+ const fieldRef = field;
768
+ const tableAlias = fieldRef.__tableAlias || this.schema.name;
769
+ const dbColName = fieldRef.__dbColumnName;
770
+ const fieldKey = `${tableAlias}.${dbColName}`;
771
+ if (!aggregateFields.has(fieldKey)) {
772
+ aggregateFields.add(fieldKey);
773
+ innerSelectParts.push(`"${tableAlias}"."${dbColName}" as "${dbColName}"`);
774
+ }
775
+ }
776
+ }
777
+ }
778
+ }
779
+ // Build the inner subquery
780
+ const innerQuery = `SELECT ${innerSelectParts.join(', ')}\nFROM ${baseFromClause}\n${whereClause}`.trim();
781
+ // Step 2: Build the outer query that groups by aliases
782
+ const outerSelectParts = [];
595
783
  for (const [alias, value] of Object.entries(mockResult)) {
596
784
  if (typeof value === 'object' && value !== null && '__isAggregate' in value && value.__isAggregate) {
597
- // This is an AggregateFieldRef (from our mock GroupedItem)
785
+ // Aggregate function
598
786
  const aggField = value;
599
787
  const aggType = aggField.__aggregateType;
600
788
  if (aggType === 'COUNT') {
601
- // COUNT always returns bigint, cast to integer for cleaner results
602
- selectParts.push(`CAST(COUNT(*) AS INTEGER) as "${alias}"`);
789
+ outerSelectParts.push(`CAST(COUNT(*) AS INTEGER) as "${alias}"`);
603
790
  }
604
791
  else if (aggField.__aggregateSelector) {
605
- // SUM, MIN, MAX, AVG with selector
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
792
  const field = aggField.__aggregateSelector(mockOriginalSelection);
609
793
  if (typeof field === 'object' && field !== null && '__dbColumnName' in field) {
610
794
  const fieldRef = field;
611
- const tableAlias = fieldRef.__tableAlias || this.schema.name;
612
- // Cast numeric aggregates to appropriate types
795
+ const dbColName = fieldRef.__dbColumnName;
796
+ // Reference the alias from inner query
613
797
  if (aggType === 'SUM' || aggType === 'AVG') {
614
- // SUM and AVG return numeric - cast to double precision for JavaScript number
615
- selectParts.push(`CAST(${aggType}("${tableAlias}"."${fieldRef.__dbColumnName}") AS DOUBLE PRECISION) as "${alias}"`);
798
+ outerSelectParts.push(`CAST(${aggType}("${dbColName}") AS DOUBLE PRECISION) as "${alias}"`);
616
799
  }
617
800
  else {
618
- // MIN/MAX preserve the field's type - no cast needed usually, but could be added if needed
619
- selectParts.push(`${aggType}("${tableAlias}"."${fieldRef.__dbColumnName}") as "${alias}"`);
801
+ outerSelectParts.push(`${aggType}("${dbColName}") as "${alias}"`);
620
802
  }
621
803
  }
622
804
  }
623
805
  else {
624
- // Aggregate without selector (shouldn't happen for SUM/MIN/MAX/AVG, but handle it)
625
- selectParts.push(`${aggType}(*) as "${alias}"`);
806
+ outerSelectParts.push(`${aggType}(*) as "${alias}"`);
626
807
  }
627
808
  }
628
809
  else if (typeof value === 'object' && value !== null && '__aggregateType' in value) {
629
- // Backward compatibility: Old AggregateMarker style (if any still exist)
810
+ // Backward compatibility
630
811
  const agg = value;
631
812
  if (agg.__aggregateType === 'COUNT') {
632
- selectParts.push(`CAST(COUNT(*) AS INTEGER) as "${alias}"`);
813
+ outerSelectParts.push(`CAST(COUNT(*) AS INTEGER) as "${alias}"`);
633
814
  }
634
815
  else if (agg.__selector) {
635
- // Use mockOriginalSelection - the selector references fields from the first .select()
636
816
  const field = agg.__selector(mockOriginalSelection);
637
817
  if (typeof field === 'object' && field !== null && '__dbColumnName' in field) {
638
818
  const fieldRef = field;
639
- const tableAlias = fieldRef.__tableAlias || this.schema.name;
819
+ const dbColName = fieldRef.__dbColumnName;
640
820
  if (agg.__aggregateType === 'SUM' || agg.__aggregateType === 'AVG') {
641
- selectParts.push(`CAST(${agg.__aggregateType}("${tableAlias}"."${fieldRef.__dbColumnName}") AS DOUBLE PRECISION) as "${alias}"`);
821
+ outerSelectParts.push(`CAST(${agg.__aggregateType}("${dbColName}") AS DOUBLE PRECISION) as "${alias}"`);
642
822
  }
643
823
  else {
644
- selectParts.push(`${agg.__aggregateType}("${tableAlias}"."${fieldRef.__dbColumnName}") as "${alias}"`);
824
+ outerSelectParts.push(`${agg.__aggregateType}("${dbColName}") as "${alias}"`);
645
825
  }
646
826
  }
647
827
  }
648
828
  }
649
- else if (typeof value === 'object' && value !== null && '__dbColumnName' in value) {
650
- // Direct field reference from the grouping key
651
- const field = value;
652
- const tableAlias = field.__tableAlias || this.schema.name;
653
- selectParts.push(`"${tableAlias}"."${field.__dbColumnName}" as "${alias}"`);
654
- }
655
829
  else if (value instanceof conditions_1.SqlFragment) {
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}"`);
830
+ // SqlFragment - reference the alias from inner query
831
+ const innerAlias = sqlFragmentAliasMap.get(value);
832
+ if (innerAlias) {
833
+ outerSelectParts.push(`"${innerAlias}" as "${alias}"`);
671
834
  }
672
835
  }
673
- }
674
- // Detect navigation property references in WHERE and add JOINs
675
- const navigationJoins = [];
676
- // Detect joins from the original selection (navigation properties used in select)
677
- this.detectAndAddJoinsFromSelection(mockOriginalSelection, navigationJoins);
678
- // Detect joins from WHERE condition
679
- this.detectAndAddJoinsFromCondition(this.whereCond, navigationJoins);
680
- // Build FROM clause with JOINs
681
- let fromClause = `FROM "${this.schema.name}"`;
682
- // Add navigation property JOINs first
683
- for (const navJoin of navigationJoins) {
684
- const joinType = navJoin.isMandatory ? 'INNER JOIN' : 'LEFT JOIN';
685
- const targetTableName = navJoin.targetSchema
686
- ? `"${navJoin.targetSchema}"."${navJoin.targetTable}"`
687
- : `"${navJoin.targetTable}"`;
688
- // Build join condition: source.foreignKey = target.match
689
- const joinConditions = navJoin.foreignKeys.map((fk, i) => {
690
- const targetCol = navJoin.matches[i] || 'id';
691
- return `"${this.schema.name}"."${fk}" = "${navJoin.alias}"."${targetCol}"`;
692
- });
693
- fromClause += `\n${joinType} ${targetTableName} AS "${navJoin.alias}" ON ${joinConditions.join(' AND ')}`;
694
- }
695
- // Add manual JOINs
696
- for (const manualJoin of this.manualJoins) {
697
- const joinTypeStr = manualJoin.type === 'INNER' ? 'INNER JOIN' : 'LEFT JOIN';
698
- const condBuilder = new conditions_1.ConditionBuilder();
699
- const { sql: condSql, params: condParams } = condBuilder.build(manualJoin.condition, context.paramCounter);
700
- context.paramCounter += condParams.length;
701
- context.allParams.push(...condParams);
702
- // Check if this is a subquery join
703
- if (manualJoin.isSubquery && manualJoin.subquery) {
704
- const subqueryBuildContext = {
705
- paramCounter: context.paramCounter,
706
- params: context.allParams,
707
- };
708
- const subquerySql = manualJoin.subquery.buildSql(subqueryBuildContext);
709
- context.paramCounter = subqueryBuildContext.paramCounter;
710
- fromClause += `\n${joinTypeStr} (${subquerySql}) AS "${manualJoin.alias}" ON ${condSql}`;
711
- }
712
- else {
713
- fromClause += `\n${joinTypeStr} "${manualJoin.table}" AS "${manualJoin.alias}" ON ${condSql}`;
836
+ else if (typeof value === 'object' && value !== null && '__dbColumnName' in value) {
837
+ // Direct field reference - find the matching key in grouping key
838
+ const field = value;
839
+ // Look for matching alias in groupByAliases
840
+ for (const [key, gkValue] of Object.entries(mockGroupingKey)) {
841
+ if (gkValue === value ||
842
+ (typeof gkValue === 'object' && gkValue !== null && '__dbColumnName' in gkValue &&
843
+ gkValue.__dbColumnName === field.__dbColumnName)) {
844
+ outerSelectParts.push(`"${key}" as "${alias}"`);
845
+ break;
846
+ }
847
+ }
714
848
  }
715
849
  }
716
- // Build WHERE clause
717
- let whereClause = '';
718
- if (this.whereCond) {
719
- const condBuilder = new conditions_1.ConditionBuilder();
720
- const { sql, params } = condBuilder.build(this.whereCond, context.paramCounter);
721
- whereClause = `WHERE ${sql}`;
722
- context.paramCounter += params.length;
723
- context.allParams.push(...params);
724
- }
725
- // Build GROUP BY clause
726
- const groupByClause = `GROUP BY ${groupByFields.join(', ')}`;
850
+ // Build GROUP BY clause using aliases
851
+ const groupByClause = `GROUP BY ${groupByAliases.join(', ')}`;
727
852
  // Build HAVING clause
728
853
  let havingClause = '';
729
854
  if (this.havingCond) {
730
- // Build the HAVING condition, but we need to substitute aggregate markers with actual SQL
731
855
  const havingSql = this.buildHavingCondition(this.havingCond, context);
732
856
  havingClause = `HAVING ${havingSql}`;
733
857
  }
@@ -745,26 +869,101 @@ class GroupedSelectQueryBuilder {
745
869
  if (this.offsetValue !== undefined) {
746
870
  limitClause += ` OFFSET ${this.offsetValue}`;
747
871
  }
748
- const finalQuery = `SELECT ${selectParts.join(', ')}\n${fromClause}\n${whereClause}\n${groupByClause}\n${havingClause}\n${orderByClause}\n${limitClause}`.trim();
872
+ const finalQuery = `SELECT ${outerSelectParts.join(', ')}\nFROM (${innerQuery}) "q1"\n${groupByClause}\n${havingClause}\n${orderByClause}\n${limitClause}`.trim();
749
873
  return {
750
874
  sql: finalQuery,
751
875
  params: context.allParams,
752
876
  };
753
877
  }
878
+ /**
879
+ * Build a single SELECT part for a result field
880
+ */
881
+ buildSelectPart(alias, value, mockOriginalSelection, context, defaultTableAlias) {
882
+ if (typeof value === 'object' && value !== null && '__isAggregate' in value && value.__isAggregate) {
883
+ // This is an AggregateFieldRef (from our mock GroupedItem)
884
+ const aggField = value;
885
+ const aggType = aggField.__aggregateType;
886
+ if (aggType === 'COUNT') {
887
+ return `CAST(COUNT(*) AS INTEGER) as "${alias}"`;
888
+ }
889
+ else if (aggField.__aggregateSelector) {
890
+ const field = aggField.__aggregateSelector(mockOriginalSelection);
891
+ if (typeof field === 'object' && field !== null && '__dbColumnName' in field) {
892
+ const fieldRef = field;
893
+ const tableAlias = fieldRef.__tableAlias || defaultTableAlias;
894
+ if (aggType === 'SUM' || aggType === 'AVG') {
895
+ return `CAST(${aggType}("${tableAlias}"."${fieldRef.__dbColumnName}") AS DOUBLE PRECISION) as "${alias}"`;
896
+ }
897
+ else {
898
+ return `${aggType}("${tableAlias}"."${fieldRef.__dbColumnName}") as "${alias}"`;
899
+ }
900
+ }
901
+ }
902
+ else {
903
+ return `${aggType}(*) as "${alias}"`;
904
+ }
905
+ }
906
+ else if (typeof value === 'object' && value !== null && '__aggregateType' in value) {
907
+ // Backward compatibility: Old AggregateMarker style
908
+ const agg = value;
909
+ if (agg.__aggregateType === 'COUNT') {
910
+ return `CAST(COUNT(*) AS INTEGER) as "${alias}"`;
911
+ }
912
+ else if (agg.__selector) {
913
+ const field = agg.__selector(mockOriginalSelection);
914
+ if (typeof field === 'object' && field !== null && '__dbColumnName' in field) {
915
+ const fieldRef = field;
916
+ const tableAlias = fieldRef.__tableAlias || defaultTableAlias;
917
+ if (agg.__aggregateType === 'SUM' || agg.__aggregateType === 'AVG') {
918
+ return `CAST(${agg.__aggregateType}("${tableAlias}"."${fieldRef.__dbColumnName}") AS DOUBLE PRECISION) as "${alias}"`;
919
+ }
920
+ else {
921
+ return `${agg.__aggregateType}("${tableAlias}"."${fieldRef.__dbColumnName}") as "${alias}"`;
922
+ }
923
+ }
924
+ }
925
+ }
926
+ else if (typeof value === 'object' && value !== null && '__dbColumnName' in value) {
927
+ // Direct field reference from the grouping key
928
+ const field = value;
929
+ const tableAlias = field.__tableAlias || defaultTableAlias;
930
+ return `"${tableAlias}"."${field.__dbColumnName}" as "${alias}"`;
931
+ }
932
+ else if (value instanceof conditions_1.SqlFragment) {
933
+ // SQL fragment - build the expression
934
+ const sqlBuildContext = {
935
+ paramCounter: context.paramCounter,
936
+ params: context.allParams,
937
+ };
938
+ const fragmentSql = value.buildSql(sqlBuildContext);
939
+ context.paramCounter = sqlBuildContext.paramCounter;
940
+ return `${fragmentSql} as "${alias}"`;
941
+ }
942
+ return null;
943
+ }
754
944
  /**
755
945
  * Create mock row for the original table
756
946
  */
757
947
  createMockRow() {
758
948
  const mock = {};
949
+ const tableAlias = this.schema.name;
759
950
  // Add columns as FieldRef objects - use pre-computed column name map if available
760
951
  const columnNameMap = (0, query_builder_1.getColumnNameMapForSchema)(this.schema);
952
+ // Performance: Lazy-cache FieldRef objects
953
+ const fieldRefCache = {};
761
954
  for (const [colName, dbColumnName] of columnNameMap) {
762
955
  Object.defineProperty(mock, colName, {
763
- get: () => ({
764
- __fieldName: colName,
765
- __dbColumnName: dbColumnName,
766
- __tableAlias: this.schema.name,
767
- }),
956
+ get() {
957
+ let cached = fieldRefCache[colName];
958
+ if (!cached) {
959
+ cached = fieldRefCache[colName] = {
960
+ __fieldName: colName,
961
+ __dbColumnName: dbColumnName,
962
+ __tableAlias: tableAlias,
963
+ };
964
+ }
965
+ return cached;
966
+ },
768
967
  enumerable: true,
769
968
  configurable: true,
770
969
  });
@@ -800,16 +999,25 @@ class GroupedSelectQueryBuilder {
800
999
  continue;
801
1000
  }
802
1001
  const joinColumnNameMap = (0, query_builder_1.getColumnNameMapForSchema)(join.schema);
1002
+ if (!mock[join.alias]) {
1003
+ mock[join.alias] = {};
1004
+ }
1005
+ // Lazy-cache for joined table
1006
+ const joinFieldRefCache = {};
1007
+ const joinAlias = join.alias;
803
1008
  for (const [colName, dbColumnName] of joinColumnNameMap) {
804
- if (!mock[join.alias]) {
805
- mock[join.alias] = {};
806
- }
807
1009
  Object.defineProperty(mock[join.alias], colName, {
808
- get: () => ({
809
- __fieldName: colName,
810
- __dbColumnName: dbColumnName,
811
- __tableAlias: join.alias,
812
- }),
1010
+ get() {
1011
+ let cached = joinFieldRefCache[colName];
1012
+ if (!cached) {
1013
+ cached = joinFieldRefCache[colName] = {
1014
+ __fieldName: colName,
1015
+ __dbColumnName: dbColumnName,
1016
+ __tableAlias: joinAlias,
1017
+ };
1018
+ }
1019
+ return cached;
1020
+ },
813
1021
  enumerable: true,
814
1022
  configurable: true,
815
1023
  });