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.
- package/dist/entity/db-context.d.ts +79 -0
- package/dist/entity/db-context.d.ts.map +1 -1
- package/dist/entity/db-context.js +126 -1
- package/dist/entity/db-context.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -6
- package/dist/index.js.map +1 -1
- package/dist/query/collection-strategy.interface.d.ts +8 -0
- package/dist/query/collection-strategy.interface.d.ts.map +1 -1
- package/dist/query/conditions.js +2 -2
- package/dist/query/conditions.js.map +1 -1
- 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 +337 -129
- package/dist/query/grouped-query.js.map +1 -1
- package/dist/query/query-builder.d.ts +5 -0
- package/dist/query/query-builder.d.ts.map +1 -1
- package/dist/query/query-builder.js +579 -282
- package/dist/query/query-builder.js.map +1 -1
- package/dist/query/strategies/cte-collection-strategy.d.ts +11 -5
- package/dist/query/strategies/cte-collection-strategy.d.ts.map +1 -1
- package/dist/query/strategies/cte-collection-strategy.js +36 -14
- package/dist/query/strategies/cte-collection-strategy.js.map +1 -1
- package/dist/query/strategies/lateral-collection-strategy.d.ts +18 -2
- package/dist/query/strategies/lateral-collection-strategy.d.ts.map +1 -1
- package/dist/query/strategies/lateral-collection-strategy.js +140 -7
- package/dist/query/strategies/lateral-collection-strategy.js.map +1 -1
- package/dist/query/strategies/temptable-collection-strategy.d.ts +9 -3
- package/dist/query/strategies/temptable-collection-strategy.d.ts.map +1 -1
- package/dist/query/strategies/temptable-collection-strategy.js +53 -13
- package/dist/query/strategies/temptable-collection-strategy.js.map +1 -1
- package/dist/query/subquery.d.ts +19 -2
- package/dist/query/subquery.d.ts.map +1 -1
- package/dist/query/subquery.js +12 -0
- package/dist/query/subquery.js.map +1 -1
- package/dist/schema/table-builder.d.ts +20 -1
- package/dist/schema/table-builder.d.ts.map +1 -1
- package/dist/schema/table-builder.js +11 -2
- package/dist/schema/table-builder.js.map +1 -1
- package/dist/types/custom-types.d.ts +4 -2
- package/dist/types/custom-types.d.ts.map +1 -1
- package/dist/types/custom-types.js +6 -4
- package/dist/types/custom-types.js.map +1 -1
- 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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
585
|
-
|
|
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
|
-
|
|
738
|
+
innerSelectParts.push(`"${tableAlias}"."${field.__dbColumnName}" as "${key}"`);
|
|
739
|
+
groupByAliases.push(`"${key}"`);
|
|
591
740
|
}
|
|
592
741
|
}
|
|
593
|
-
//
|
|
594
|
-
const
|
|
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
|
-
//
|
|
785
|
+
// Aggregate function
|
|
598
786
|
const aggField = value;
|
|
599
787
|
const aggType = aggField.__aggregateType;
|
|
600
788
|
if (aggType === 'COUNT') {
|
|
601
|
-
|
|
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
|
|
612
|
-
//
|
|
795
|
+
const dbColName = fieldRef.__dbColumnName;
|
|
796
|
+
// Reference the alias from inner query
|
|
613
797
|
if (aggType === 'SUM' || aggType === 'AVG') {
|
|
614
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
810
|
+
// Backward compatibility
|
|
630
811
|
const agg = value;
|
|
631
812
|
if (agg.__aggregateType === 'COUNT') {
|
|
632
|
-
|
|
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
|
|
819
|
+
const dbColName = fieldRef.__dbColumnName;
|
|
640
820
|
if (agg.__aggregateType === 'SUM' || agg.__aggregateType === 'AVG') {
|
|
641
|
-
|
|
821
|
+
outerSelectParts.push(`CAST(${agg.__aggregateType}("${dbColName}") AS DOUBLE PRECISION) as "${alias}"`);
|
|
642
822
|
}
|
|
643
823
|
else {
|
|
644
|
-
|
|
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
|
-
//
|
|
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}"`);
|
|
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
|
-
|
|
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}`;
|
|
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
|
|
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(', ')}`;
|
|
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 ${
|
|
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
|
-
|
|
765
|
-
|
|
766
|
-
|
|
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
|
-
|
|
810
|
-
|
|
811
|
-
|
|
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
|
});
|