linkgress-orm 0.1.4 → 0.1.5

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.
@@ -551,6 +551,16 @@ class GroupedSelectQueryBuilder {
551
551
  }
552
552
  /**
553
553
  * Build the SQL query for grouped results
554
+ *
555
+ * Optimization: When grouping by SqlFragment expressions, we wrap the base query
556
+ * in a subquery to avoid repeating complex expressions in both SELECT and GROUP BY.
557
+ * This matches the pattern used by Drizzle and improves query performance.
558
+ *
559
+ * Without optimization:
560
+ * SELECT complex_expr as "alias" FROM table GROUP BY complex_expr
561
+ *
562
+ * With optimization:
563
+ * SELECT "alias" FROM (SELECT complex_expr as "alias" FROM table) q1 GROUP BY "alias"
554
564
  */
555
565
  buildQuery(context) {
556
566
  // Create mocks for evaluation
@@ -568,166 +578,262 @@ class GroupedSelectQueryBuilder {
568
578
  avg: (selector) => createAggregateFieldRef('AVG', selector),
569
579
  };
570
580
  const mockResult = this.resultSelector(mockGroup);
581
+ // Check if we have SqlFragment expressions in the grouping key
582
+ // If so, we'll use the subquery wrapping optimization
583
+ const hasSqlFragmentInGroupBy = Object.values(mockGroupingKey).some(value => value instanceof conditions_1.SqlFragment);
584
+ // Detect navigation property references in WHERE and add JOINs
585
+ const navigationJoins = [];
586
+ // Detect joins from the original selection (navigation properties used in select)
587
+ this.detectAndAddJoinsFromSelection(mockOriginalSelection, navigationJoins);
588
+ // Detect joins from WHERE condition
589
+ this.detectAndAddJoinsFromCondition(this.whereCond, navigationJoins);
590
+ // Build base FROM clause with JOINs
591
+ let baseFromClause = `"${this.schema.name}"`;
592
+ // Add navigation property JOINs first
593
+ for (const navJoin of navigationJoins) {
594
+ const joinType = navJoin.isMandatory ? 'INNER JOIN' : 'LEFT JOIN';
595
+ const targetTableName = navJoin.targetSchema
596
+ ? `"${navJoin.targetSchema}"."${navJoin.targetTable}"`
597
+ : `"${navJoin.targetTable}"`;
598
+ // Build join condition: source.foreignKey = target.match
599
+ const joinConditions = navJoin.foreignKeys.map((fk, i) => {
600
+ const targetCol = navJoin.matches[i] || 'id';
601
+ return `"${this.schema.name}"."${fk}" = "${navJoin.alias}"."${targetCol}"`;
602
+ });
603
+ baseFromClause += `\n${joinType} ${targetTableName} AS "${navJoin.alias}" ON ${joinConditions.join(' AND ')}`;
604
+ }
605
+ // Add manual JOINs
606
+ for (const manualJoin of this.manualJoins) {
607
+ const joinTypeStr = manualJoin.type === 'INNER' ? 'INNER JOIN' : 'LEFT JOIN';
608
+ const condBuilder = new conditions_1.ConditionBuilder();
609
+ const { sql: condSql, params: condParams } = condBuilder.build(manualJoin.condition, context.paramCounter);
610
+ context.paramCounter += condParams.length;
611
+ context.allParams.push(...condParams);
612
+ // Check if this is a subquery join
613
+ if (manualJoin.isSubquery && manualJoin.subquery) {
614
+ const subqueryBuildContext = {
615
+ paramCounter: context.paramCounter,
616
+ params: context.allParams,
617
+ };
618
+ const subquerySql = manualJoin.subquery.buildSql(subqueryBuildContext);
619
+ context.paramCounter = subqueryBuildContext.paramCounter;
620
+ baseFromClause += `\n${joinTypeStr} (${subquerySql}) AS "${manualJoin.alias}" ON ${condSql}`;
621
+ }
622
+ else {
623
+ baseFromClause += `\n${joinTypeStr} "${manualJoin.table}" AS "${manualJoin.alias}" ON ${condSql}`;
624
+ }
625
+ }
626
+ // Build WHERE clause
627
+ let whereClause = '';
628
+ if (this.whereCond) {
629
+ const condBuilder = new conditions_1.ConditionBuilder();
630
+ const { sql, params } = condBuilder.build(this.whereCond, context.paramCounter);
631
+ whereClause = `WHERE ${sql}`;
632
+ context.paramCounter += params.length;
633
+ context.allParams.push(...params);
634
+ }
635
+ if (hasSqlFragmentInGroupBy) {
636
+ // Use subquery wrapping optimization for complex GROUP BY expressions
637
+ return this.buildQueryWithSubqueryWrapping(context, mockOriginalSelection, mockGroupingKey, mockResult, baseFromClause, whereClause);
638
+ }
639
+ else {
640
+ // Use simple query for basic GROUP BY (column references only)
641
+ return this.buildSimpleGroupedQuery(context, mockOriginalSelection, mockGroupingKey, mockResult, baseFromClause, whereClause);
642
+ }
643
+ }
644
+ /**
645
+ * Build a simple grouped query when GROUP BY only contains column references
646
+ */
647
+ buildSimpleGroupedQuery(context, mockOriginalSelection, mockGroupingKey, mockResult, baseFromClause, whereClause) {
571
648
  // 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
649
  const groupByFields = [];
574
- const sqlFragmentCache = new Map(); // Cache built SQL for reuse in SELECT
650
+ for (const [key, value] of Object.entries(mockGroupingKey)) {
651
+ if (typeof value === 'object' && value !== null && '__dbColumnName' in value) {
652
+ const field = value;
653
+ const tableAlias = field.__tableAlias || this.schema.name;
654
+ groupByFields.push(`"${tableAlias}"."${field.__dbColumnName}"`);
655
+ }
656
+ }
657
+ // Build SELECT clause from result selector
658
+ const selectParts = [];
659
+ for (const [alias, value] of Object.entries(mockResult)) {
660
+ const selectPart = this.buildSelectPart(alias, value, mockOriginalSelection, context, this.schema.name);
661
+ if (selectPart) {
662
+ selectParts.push(selectPart);
663
+ }
664
+ }
665
+ // Build GROUP BY clause
666
+ const groupByClause = `GROUP BY ${groupByFields.join(', ')}`;
667
+ // Build HAVING clause
668
+ let havingClause = '';
669
+ if (this.havingCond) {
670
+ const havingSql = this.buildHavingCondition(this.havingCond, context);
671
+ havingClause = `HAVING ${havingSql}`;
672
+ }
673
+ // Build ORDER BY clause
674
+ let orderByClause = '';
675
+ if (this.orderByFields.length > 0) {
676
+ const orderParts = this.orderByFields.map(({ field, direction }) => `"${field}" ${direction}`);
677
+ orderByClause = `ORDER BY ${orderParts.join(', ')}`;
678
+ }
679
+ // Build LIMIT/OFFSET
680
+ let limitClause = '';
681
+ if (this.limitValue !== undefined) {
682
+ limitClause = `LIMIT ${this.limitValue}`;
683
+ }
684
+ if (this.offsetValue !== undefined) {
685
+ limitClause += ` OFFSET ${this.offsetValue}`;
686
+ }
687
+ const finalQuery = `SELECT ${selectParts.join(', ')}\nFROM ${baseFromClause}\n${whereClause}\n${groupByClause}\n${havingClause}\n${orderByClause}\n${limitClause}`.trim();
688
+ return {
689
+ sql: finalQuery,
690
+ params: context.allParams,
691
+ };
692
+ }
693
+ /**
694
+ * Build a grouped query with subquery wrapping for complex GROUP BY expressions
695
+ * This avoids repeating SqlFragment expressions in both SELECT and GROUP BY
696
+ */
697
+ buildQueryWithSubqueryWrapping(context, mockOriginalSelection, mockGroupingKey, mockResult, baseFromClause, whereClause) {
698
+ // Step 1: Build the inner subquery that computes all expressions
699
+ // This includes: grouping key fields, fields needed for aggregates
700
+ const innerSelectParts = [];
701
+ const groupByAliases = []; // Aliases to use in outer GROUP BY
702
+ const sqlFragmentAliasMap = new Map(); // Map SqlFragment to its alias
703
+ // Add grouping key fields to inner select
575
704
  for (const [key, value] of Object.entries(mockGroupingKey)) {
576
705
  if (value instanceof conditions_1.SqlFragment) {
577
- // SqlFragment in GROUP BY - build the SQL expression and cache it
706
+ // Build the SqlFragment expression
578
707
  const sqlBuildContext = {
579
708
  paramCounter: context.paramCounter,
580
709
  params: context.allParams,
581
710
  };
582
711
  const fragmentSql = value.buildSql(sqlBuildContext);
583
712
  context.paramCounter = sqlBuildContext.paramCounter;
584
- groupByFields.push(fragmentSql);
585
- sqlFragmentCache.set(value, fragmentSql);
713
+ innerSelectParts.push(`${fragmentSql} as "${key}"`);
714
+ groupByAliases.push(`"${key}"`);
715
+ sqlFragmentAliasMap.set(value, key);
586
716
  }
587
717
  else if (typeof value === 'object' && value !== null && '__dbColumnName' in value) {
588
718
  const field = value;
589
719
  const tableAlias = field.__tableAlias || this.schema.name;
590
- groupByFields.push(`"${tableAlias}"."${field.__dbColumnName}"`);
720
+ innerSelectParts.push(`"${tableAlias}"."${field.__dbColumnName}" as "${key}"`);
721
+ groupByAliases.push(`"${key}"`);
591
722
  }
592
723
  }
593
- // Build SELECT clause from result selector
594
- const selectParts = [];
724
+ // Add fields needed for aggregates to inner select
725
+ const aggregateFields = new Set(); // Track fields we've already added
726
+ for (const [, value] of Object.entries(mockResult)) {
727
+ if (typeof value === 'object' && value !== null && '__isAggregate' in value && value.__isAggregate) {
728
+ const aggField = value;
729
+ if (aggField.__aggregateSelector) {
730
+ const field = aggField.__aggregateSelector(mockOriginalSelection);
731
+ if (typeof field === 'object' && field !== null && '__dbColumnName' in field) {
732
+ const fieldRef = field;
733
+ const tableAlias = fieldRef.__tableAlias || this.schema.name;
734
+ const dbColName = fieldRef.__dbColumnName;
735
+ const fieldKey = `${tableAlias}.${dbColName}`;
736
+ if (!aggregateFields.has(fieldKey)) {
737
+ aggregateFields.add(fieldKey);
738
+ innerSelectParts.push(`"${tableAlias}"."${dbColName}" as "${dbColName}"`);
739
+ }
740
+ }
741
+ }
742
+ }
743
+ else if (typeof value === 'object' && value !== null && '__aggregateType' in value) {
744
+ // Backward compatibility
745
+ const agg = value;
746
+ if (agg.__selector) {
747
+ const field = agg.__selector(mockOriginalSelection);
748
+ if (typeof field === 'object' && field !== null && '__dbColumnName' in field) {
749
+ const fieldRef = field;
750
+ const tableAlias = fieldRef.__tableAlias || this.schema.name;
751
+ const dbColName = fieldRef.__dbColumnName;
752
+ const fieldKey = `${tableAlias}.${dbColName}`;
753
+ if (!aggregateFields.has(fieldKey)) {
754
+ aggregateFields.add(fieldKey);
755
+ innerSelectParts.push(`"${tableAlias}"."${dbColName}" as "${dbColName}"`);
756
+ }
757
+ }
758
+ }
759
+ }
760
+ }
761
+ // Build the inner subquery
762
+ const innerQuery = `SELECT ${innerSelectParts.join(', ')}\nFROM ${baseFromClause}\n${whereClause}`.trim();
763
+ // Step 2: Build the outer query that groups by aliases
764
+ const outerSelectParts = [];
595
765
  for (const [alias, value] of Object.entries(mockResult)) {
596
766
  if (typeof value === 'object' && value !== null && '__isAggregate' in value && value.__isAggregate) {
597
- // This is an AggregateFieldRef (from our mock GroupedItem)
767
+ // Aggregate function
598
768
  const aggField = value;
599
769
  const aggType = aggField.__aggregateType;
600
770
  if (aggType === 'COUNT') {
601
- // COUNT always returns bigint, cast to integer for cleaner results
602
- selectParts.push(`CAST(COUNT(*) AS INTEGER) as "${alias}"`);
771
+ outerSelectParts.push(`CAST(COUNT(*) AS INTEGER) as "${alias}"`);
603
772
  }
604
773
  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
774
  const field = aggField.__aggregateSelector(mockOriginalSelection);
609
775
  if (typeof field === 'object' && field !== null && '__dbColumnName' in field) {
610
776
  const fieldRef = field;
611
- const tableAlias = fieldRef.__tableAlias || this.schema.name;
612
- // Cast numeric aggregates to appropriate types
777
+ const dbColName = fieldRef.__dbColumnName;
778
+ // Reference the alias from inner query
613
779
  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}"`);
780
+ outerSelectParts.push(`CAST(${aggType}("${dbColName}") AS DOUBLE PRECISION) as "${alias}"`);
616
781
  }
617
782
  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}"`);
783
+ outerSelectParts.push(`${aggType}("${dbColName}") as "${alias}"`);
620
784
  }
621
785
  }
622
786
  }
623
787
  else {
624
- // Aggregate without selector (shouldn't happen for SUM/MIN/MAX/AVG, but handle it)
625
- selectParts.push(`${aggType}(*) as "${alias}"`);
788
+ outerSelectParts.push(`${aggType}(*) as "${alias}"`);
626
789
  }
627
790
  }
628
791
  else if (typeof value === 'object' && value !== null && '__aggregateType' in value) {
629
- // Backward compatibility: Old AggregateMarker style (if any still exist)
792
+ // Backward compatibility
630
793
  const agg = value;
631
794
  if (agg.__aggregateType === 'COUNT') {
632
- selectParts.push(`CAST(COUNT(*) AS INTEGER) as "${alias}"`);
795
+ outerSelectParts.push(`CAST(COUNT(*) AS INTEGER) as "${alias}"`);
633
796
  }
634
797
  else if (agg.__selector) {
635
- // Use mockOriginalSelection - the selector references fields from the first .select()
636
798
  const field = agg.__selector(mockOriginalSelection);
637
799
  if (typeof field === 'object' && field !== null && '__dbColumnName' in field) {
638
800
  const fieldRef = field;
639
- const tableAlias = fieldRef.__tableAlias || this.schema.name;
801
+ const dbColName = fieldRef.__dbColumnName;
640
802
  if (agg.__aggregateType === 'SUM' || agg.__aggregateType === 'AVG') {
641
- selectParts.push(`CAST(${agg.__aggregateType}("${tableAlias}"."${fieldRef.__dbColumnName}") AS DOUBLE PRECISION) as "${alias}"`);
803
+ outerSelectParts.push(`CAST(${agg.__aggregateType}("${dbColName}") AS DOUBLE PRECISION) as "${alias}"`);
642
804
  }
643
805
  else {
644
- selectParts.push(`${agg.__aggregateType}("${tableAlias}"."${fieldRef.__dbColumnName}") as "${alias}"`);
806
+ outerSelectParts.push(`${agg.__aggregateType}("${dbColName}") as "${alias}"`);
645
807
  }
646
808
  }
647
809
  }
648
810
  }
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
811
  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}"`);
812
+ // SqlFragment - reference the alias from inner query
813
+ const innerAlias = sqlFragmentAliasMap.get(value);
814
+ if (innerAlias) {
815
+ outerSelectParts.push(`"${innerAlias}" as "${alias}"`);
671
816
  }
672
817
  }
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}`;
818
+ else if (typeof value === 'object' && value !== null && '__dbColumnName' in value) {
819
+ // Direct field reference - find the matching key in grouping key
820
+ const field = value;
821
+ // Look for matching alias in groupByAliases
822
+ for (const [key, gkValue] of Object.entries(mockGroupingKey)) {
823
+ if (gkValue === value ||
824
+ (typeof gkValue === 'object' && gkValue !== null && '__dbColumnName' in gkValue &&
825
+ gkValue.__dbColumnName === field.__dbColumnName)) {
826
+ outerSelectParts.push(`"${key}" as "${alias}"`);
827
+ break;
828
+ }
829
+ }
714
830
  }
715
831
  }
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(', ')}`;
832
+ // Build GROUP BY clause using aliases
833
+ const groupByClause = `GROUP BY ${groupByAliases.join(', ')}`;
727
834
  // Build HAVING clause
728
835
  let havingClause = '';
729
836
  if (this.havingCond) {
730
- // Build the HAVING condition, but we need to substitute aggregate markers with actual SQL
731
837
  const havingSql = this.buildHavingCondition(this.havingCond, context);
732
838
  havingClause = `HAVING ${havingSql}`;
733
839
  }
@@ -745,12 +851,78 @@ class GroupedSelectQueryBuilder {
745
851
  if (this.offsetValue !== undefined) {
746
852
  limitClause += ` OFFSET ${this.offsetValue}`;
747
853
  }
748
- const finalQuery = `SELECT ${selectParts.join(', ')}\n${fromClause}\n${whereClause}\n${groupByClause}\n${havingClause}\n${orderByClause}\n${limitClause}`.trim();
854
+ const finalQuery = `SELECT ${outerSelectParts.join(', ')}\nFROM (${innerQuery}) "q1"\n${groupByClause}\n${havingClause}\n${orderByClause}\n${limitClause}`.trim();
749
855
  return {
750
856
  sql: finalQuery,
751
857
  params: context.allParams,
752
858
  };
753
859
  }
860
+ /**
861
+ * Build a single SELECT part for a result field
862
+ */
863
+ buildSelectPart(alias, value, mockOriginalSelection, context, defaultTableAlias) {
864
+ if (typeof value === 'object' && value !== null && '__isAggregate' in value && value.__isAggregate) {
865
+ // This is an AggregateFieldRef (from our mock GroupedItem)
866
+ const aggField = value;
867
+ const aggType = aggField.__aggregateType;
868
+ if (aggType === 'COUNT') {
869
+ return `CAST(COUNT(*) AS INTEGER) as "${alias}"`;
870
+ }
871
+ else if (aggField.__aggregateSelector) {
872
+ const field = aggField.__aggregateSelector(mockOriginalSelection);
873
+ if (typeof field === 'object' && field !== null && '__dbColumnName' in field) {
874
+ const fieldRef = field;
875
+ const tableAlias = fieldRef.__tableAlias || defaultTableAlias;
876
+ if (aggType === 'SUM' || aggType === 'AVG') {
877
+ return `CAST(${aggType}("${tableAlias}"."${fieldRef.__dbColumnName}") AS DOUBLE PRECISION) as "${alias}"`;
878
+ }
879
+ else {
880
+ return `${aggType}("${tableAlias}"."${fieldRef.__dbColumnName}") as "${alias}"`;
881
+ }
882
+ }
883
+ }
884
+ else {
885
+ return `${aggType}(*) as "${alias}"`;
886
+ }
887
+ }
888
+ else if (typeof value === 'object' && value !== null && '__aggregateType' in value) {
889
+ // Backward compatibility: Old AggregateMarker style
890
+ const agg = value;
891
+ if (agg.__aggregateType === 'COUNT') {
892
+ return `CAST(COUNT(*) AS INTEGER) as "${alias}"`;
893
+ }
894
+ else if (agg.__selector) {
895
+ const field = agg.__selector(mockOriginalSelection);
896
+ if (typeof field === 'object' && field !== null && '__dbColumnName' in field) {
897
+ const fieldRef = field;
898
+ const tableAlias = fieldRef.__tableAlias || defaultTableAlias;
899
+ if (agg.__aggregateType === 'SUM' || agg.__aggregateType === 'AVG') {
900
+ return `CAST(${agg.__aggregateType}("${tableAlias}"."${fieldRef.__dbColumnName}") AS DOUBLE PRECISION) as "${alias}"`;
901
+ }
902
+ else {
903
+ return `${agg.__aggregateType}("${tableAlias}"."${fieldRef.__dbColumnName}") as "${alias}"`;
904
+ }
905
+ }
906
+ }
907
+ }
908
+ else if (typeof value === 'object' && value !== null && '__dbColumnName' in value) {
909
+ // Direct field reference from the grouping key
910
+ const field = value;
911
+ const tableAlias = field.__tableAlias || defaultTableAlias;
912
+ return `"${tableAlias}"."${field.__dbColumnName}" as "${alias}"`;
913
+ }
914
+ else if (value instanceof conditions_1.SqlFragment) {
915
+ // SQL fragment - build the expression
916
+ const sqlBuildContext = {
917
+ paramCounter: context.paramCounter,
918
+ params: context.allParams,
919
+ };
920
+ const fragmentSql = value.buildSql(sqlBuildContext);
921
+ context.paramCounter = sqlBuildContext.paramCounter;
922
+ return `${fragmentSql} as "${alias}"`;
923
+ }
924
+ return null;
925
+ }
754
926
  /**
755
927
  * Create mock row for the original table
756
928
  */