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.
- package/dist/query/cte-builder.d.ts.map +1 -1
- package/dist/query/cte-builder.js +27 -6
- package/dist/query/cte-builder.js.map +1 -1
- package/dist/query/grouped-query.d.ts +23 -0
- package/dist/query/grouped-query.d.ts.map +1 -1
- package/dist/query/grouped-query.js +275 -103
- package/dist/query/grouped-query.js.map +1 -1
- package/dist/query/query-builder.d.ts.map +1 -1
- package/dist/query/query-builder.js +9 -7
- package/dist/query/query-builder.js.map +1 -1
- package/dist/query/strategies/cte-collection-strategy.d.ts +6 -5
- package/dist/query/strategies/cte-collection-strategy.d.ts.map +1 -1
- package/dist/query/strategies/cte-collection-strategy.js +13 -12
- package/dist/query/strategies/cte-collection-strategy.js.map +1 -1
- package/dist/query/strategies/lateral-collection-strategy.d.ts +13 -2
- package/dist/query/strategies/lateral-collection-strategy.d.ts.map +1 -1
- package/dist/query/strategies/lateral-collection-strategy.js +120 -7
- package/dist/query/strategies/lateral-collection-strategy.js.map +1 -1
- package/dist/query/strategies/temptable-collection-strategy.d.ts +3 -3
- package/dist/query/strategies/temptable-collection-strategy.js +11 -11
- package/dist/query/strategies/temptable-collection-strategy.js.map +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
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
|
-
//
|
|
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
|
-
|
|
585
|
-
|
|
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
|
-
|
|
720
|
+
innerSelectParts.push(`"${tableAlias}"."${field.__dbColumnName}" as "${key}"`);
|
|
721
|
+
groupByAliases.push(`"${key}"`);
|
|
591
722
|
}
|
|
592
723
|
}
|
|
593
|
-
//
|
|
594
|
-
const
|
|
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
|
-
//
|
|
767
|
+
// Aggregate function
|
|
598
768
|
const aggField = value;
|
|
599
769
|
const aggType = aggField.__aggregateType;
|
|
600
770
|
if (aggType === 'COUNT') {
|
|
601
|
-
|
|
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
|
|
612
|
-
//
|
|
777
|
+
const dbColName = fieldRef.__dbColumnName;
|
|
778
|
+
// Reference the alias from inner query
|
|
613
779
|
if (aggType === 'SUM' || aggType === 'AVG') {
|
|
614
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
792
|
+
// Backward compatibility
|
|
630
793
|
const agg = value;
|
|
631
794
|
if (agg.__aggregateType === 'COUNT') {
|
|
632
|
-
|
|
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
|
|
801
|
+
const dbColName = fieldRef.__dbColumnName;
|
|
640
802
|
if (agg.__aggregateType === 'SUM' || agg.__aggregateType === 'AVG') {
|
|
641
|
-
|
|
803
|
+
outerSelectParts.push(`CAST(${agg.__aggregateType}("${dbColName}") AS DOUBLE PRECISION) as "${alias}"`);
|
|
642
804
|
}
|
|
643
805
|
else {
|
|
644
|
-
|
|
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
|
-
//
|
|
657
|
-
const
|
|
658
|
-
if (
|
|
659
|
-
|
|
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
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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
|
|
717
|
-
|
|
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 ${
|
|
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
|
*/
|