Skip to content

Commit ce667b0

Browse files
authored
Merge pull request #183 from mk3008/feat_to_simple_query
feat: move ORDER BY from BinaryQuery right side to SimpleQuery level
2 parents eaa2247 + 96d298f commit ce667b0

File tree

2 files changed

+167
-7
lines changed

2 files changed

+167
-7
lines changed

packages/core/src/transformers/QueryBuilder.ts

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { SetClause, SetClauseItem, FromClause, WhereClause, SelectClause, SelectItem, SourceAliasExpression, SourceExpression, SubQuerySource, WithClause, TableSource, UpdateClause, InsertClause } from '../models/Clause';
1+
import { SetClause, SetClauseItem, FromClause, WhereClause, SelectClause, SelectItem, SourceAliasExpression, SourceExpression, SubQuerySource, WithClause, TableSource, UpdateClause, InsertClause, OrderByClause } from '../models/Clause';
22
import { UpdateQuery } from '../models/UpdateQuery';
33
import { BinaryExpression, ColumnReference } from '../models/ValueComponent';
44
import { SelectValueCollector } from './SelectValueCollector';
@@ -66,7 +66,10 @@ export class QueryBuilder {
6666
}
6767

6868
private static buildSimpleBinaryQuery(query: BinarySelectQuery): SimpleSelectQuery {
69-
// Create a subquery source from the binary query
69+
// Extract ORDER BY from the rightmost query in the binary tree and remove it
70+
const extractedOrderBy = QueryBuilder.extractAndRemoveOrderByFromBinaryQuery(query);
71+
72+
// Create a subquery source from the binary query (now without ORDER BY)
7073
const subQuerySource = new SubQuerySource(query);
7174

7275
// Create a source expression with alias
@@ -81,17 +84,65 @@ export class QueryBuilder {
8184
// Create SELECT clause with * (all columns)
8285
const selectClause = QueryBuilder.createSelectAllClause();
8386

84-
// Create the final simple select query
87+
// Create the final simple select query with extracted ORDER BY
8588
const q = new SimpleSelectQuery(
8689
{
8790
selectClause,
88-
fromClause
91+
fromClause,
92+
orderByClause: extractedOrderBy
8993
}
9094
);
9195

9296
return CTENormalizer.normalize(q) as SimpleSelectQuery;
9397
}
9498

99+
/**
100+
* Extracts ORDER BY clause from the rightmost query in a binary query tree and removes it.
101+
* This clarifies the semantics by moving the ORDER BY from the ambiguous position
102+
* in the UNION to the explicit outer SimpleSelectQuery level.
103+
*
104+
* NOTE: ORDER BY in UNION context applies to the entire result set, not individual subqueries.
105+
* Therefore, table prefixes (e.g., "a.column") in ORDER BY are invalid SQL and would cause
106+
* syntax errors. Valid ORDER BY clauses should only reference column names without prefixes
107+
* or use positional notation (ORDER BY 1, 2). Since we only process valid SQL, the current
108+
* implementation correctly handles legitimate cases without additional prefix processing.
109+
*
110+
* @param query BinarySelectQuery to process
111+
* @returns Extracted OrderByClause or null if none found
112+
*/
113+
private static extractAndRemoveOrderByFromBinaryQuery(query: BinarySelectQuery): OrderByClause | null {
114+
return QueryBuilder.findAndRemoveRightmostOrderBy(query);
115+
}
116+
117+
/**
118+
* Recursively finds and removes ORDER BY from the rightmost query in a binary tree.
119+
*
120+
* @param query Current query being processed
121+
* @returns Extracted OrderByClause or null
122+
*/
123+
private static findAndRemoveRightmostOrderBy(query: SelectQuery): OrderByClause | null {
124+
if (query instanceof BinarySelectQuery) {
125+
// For binary queries, check right side first (rightmost takes precedence)
126+
const rightOrderBy = QueryBuilder.findAndRemoveRightmostOrderBy(query.right);
127+
if (rightOrderBy) {
128+
return rightOrderBy;
129+
}
130+
131+
// If no ORDER BY on right side, check left side
132+
return QueryBuilder.findAndRemoveRightmostOrderBy(query.left);
133+
}
134+
else if (query instanceof SimpleSelectQuery) {
135+
// Extract ORDER BY from SimpleSelectQuery and remove it
136+
const orderBy = query.orderByClause;
137+
if (orderBy) {
138+
query.orderByClause = null;
139+
return orderBy;
140+
}
141+
}
142+
143+
return null;
144+
}
145+
95146
/**
96147
* Converts a ValuesQuery to a SimpleSelectQuery with sequentially numbered columns or user-specified columns
97148
*

packages/core/tests/models/SelectQuery.toSimpleQuery.test.ts

Lines changed: 112 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ describe('SelectQuery toSimpleQuery() conversion', () => {
4949

5050
test('should enable CTE management on converted BinarySelectQuery', () => {
5151
// Test that converted binary query supports CTE operations
52-
const query1 = SelectQueryParser.parse('SELECT id FROM users');
52+
const query1 = SelectQueryParser.parse('SELECT id FROM users').toSimpleQuery();
5353
const query2 = SelectQueryParser.parse('SELECT id FROM customers');
5454
const binaryQuery = query1.toUnion(query2);
5555
const cteQuery = SelectQueryParser.parse('SELECT id FROM accounts WHERE active = true');
@@ -65,7 +65,7 @@ describe('SelectQuery toSimpleQuery() conversion', () => {
6565

6666
test('should produce valid SQL with complex UNION and CTE', () => {
6767
// Test full workflow: UNION -> toSimpleQuery -> addCTE -> format
68-
const query1 = SelectQueryParser.parse('SELECT id, name FROM employees');
68+
const query1 = SelectQueryParser.parse('SELECT id, name FROM employees').toSimpleQuery();
6969
const query2 = SelectQueryParser.parse('SELECT id, name FROM contractors');
7070
const binaryQuery = query1.toUnionAll(query2);
7171
const cteQuery = SelectQueryParser.parse('SELECT id FROM departments WHERE active = true');
@@ -84,7 +84,7 @@ describe('SelectQuery toSimpleQuery() conversion', () => {
8484
describe('Method chaining patterns', () => {
8585
test('should support fluent API: binary -> toSimpleQuery -> CTE operations', () => {
8686
// Test the recommended usage pattern
87-
const query1 = SelectQueryParser.parse('SELECT id FROM table1');
87+
const query1 = SelectQueryParser.parse('SELECT id FROM table1').toSimpleQuery();
8888
const query2 = SelectQueryParser.parse('SELECT id FROM table2');
8989
const cte1 = SelectQueryParser.parse('SELECT id FROM temp1');
9090
const cte2 = SelectQueryParser.parse('SELECT id FROM temp2');
@@ -100,6 +100,115 @@ describe('SelectQuery toSimpleQuery() conversion', () => {
100100
});
101101
});
102102

103+
describe('BinarySelectQuery ORDER BY handling', () => {
104+
// NOTE: In SQL standard, ORDER BY in UNION context applies to the entire result set.
105+
// Table prefixes (e.g., "a.column") would be invalid SQL syntax in ORDER BY clauses
106+
// following UNION operations. Only column names without prefixes or positional notation
107+
// (ORDER BY 1, 2) are valid. This implementation correctly handles valid SQL cases.
108+
109+
test('should move ORDER BY from right query to SimpleQuery when converting', () => {
110+
// Test ORDER BY removal from right query and movement to SimpleQuery
111+
const query1 = SelectQueryParser.parse('SELECT id, name FROM users').toSimpleQuery();
112+
const query2 = SelectQueryParser.parse('SELECT id, name FROM customers ORDER BY name ASC');
113+
const binaryQuery = query1.toUnion(query2);
114+
115+
const result = binaryQuery.toSimpleQuery();
116+
const formatted = formatter.format(result);
117+
118+
// ORDER BY should be at SimpleQuery level, not in the subquery
119+
expect(result.orderByClause).not.toBeNull();
120+
121+
// Verify ORDER BY is at the outermost level (after the subquery)
122+
// Should be: SELECT * FROM (...) AS "bq" ORDER BY "name"
123+
const sql = formatted.formattedSql.trim();
124+
expect(sql).toMatch(/\)\s+as\s+"bq"\s+order\s+by\s+"name"/i);
125+
126+
// The binary query itself should no longer have ORDER BY on the right side
127+
const binaryFormatted = formatter.format(binaryQuery);
128+
// Should not contain ORDER BY before UNION
129+
expect(binaryFormatted.formattedSql).not.toMatch(/order\s+by\s+[^)]+\)\s*$/i);
130+
});
131+
132+
test('should handle multiple ORDER BY clauses correctly', () => {
133+
// Test when both queries have ORDER BY - should use right query's ORDER BY
134+
const query1 = SelectQueryParser.parse('SELECT id, name FROM users ORDER BY id DESC').toSimpleQuery();
135+
const query2 = SelectQueryParser.parse('SELECT id, name FROM customers ORDER BY name ASC, id DESC');
136+
const binaryQuery = query1.toUnion(query2);
137+
138+
const result = binaryQuery.toSimpleQuery();
139+
const formatted = formatter.format(result);
140+
141+
// Should have ORDER BY at SimpleQuery level from right query
142+
expect(result.orderByClause).not.toBeNull();
143+
144+
// Verify ORDER BY is moved to outermost level and contains columns from right query
145+
const sql = formatted.formattedSql.trim();
146+
expect(sql).toMatch(/\)\s+as\s+"bq"\s+order\s+by\s+"name"[^,]*,\s*"id"/i);
147+
148+
// Original binary query should have ORDER BY removed from right side
149+
const binaryFormatted = formatter.format(binaryQuery);
150+
// Left query still has its ORDER BY (not removed)
151+
expect(binaryFormatted.formattedSql).toContain('order by "id" desc union');
152+
// But right query's ORDER BY should be removed
153+
expect(binaryFormatted.formattedSql).not.toMatch(/customers.*order\s+by.*$/i);
154+
});
155+
156+
test('should work with nested binary queries', () => {
157+
// Test ORDER BY handling with nested binary operations
158+
const query1 = SelectQueryParser.parse('SELECT id FROM table1').toSimpleQuery();
159+
const query2 = SelectQueryParser.parse('SELECT id FROM table2 ORDER BY id ASC');
160+
const query3 = SelectQueryParser.parse('SELECT id FROM table3');
161+
162+
const binaryQuery1 = query1.toUnion(query2);
163+
const binaryQuery2 = binaryQuery1.toSimpleQuery().toUnion(query3);
164+
165+
const result = binaryQuery2.toSimpleQuery();
166+
const formatted = formatter.format(result);
167+
168+
// Should extract ORDER BY from the rightmost query that has it
169+
expect(result.orderByClause).not.toBeNull();
170+
171+
// ORDER BY should be at the outermost level
172+
const sql = formatted.formattedSql.trim();
173+
expect(sql).toMatch(/\)\s+as\s+"bq"\s+order\s+by\s+"id"/i);
174+
175+
// Verify the nested structure doesn't have ORDER BY in the middle
176+
expect(sql).not.toMatch(/table2.*order.*union/i);
177+
});
178+
179+
test('should handle case with no ORDER BY in any query', () => {
180+
// Test when no queries have ORDER BY
181+
const query1 = SelectQueryParser.parse('SELECT id FROM users').toSimpleQuery();
182+
const query2 = SelectQueryParser.parse('SELECT id FROM customers');
183+
const binaryQuery = query1.toUnion(query2);
184+
185+
const result = binaryQuery.toSimpleQuery();
186+
187+
// Should have no ORDER BY clause
188+
expect(result.orderByClause).toBeNull();
189+
});
190+
191+
test('should verify ORDER BY is actually removed from original query', () => {
192+
// Create queries and verify ORDER BY removal
193+
const query1 = SelectQueryParser.parse('SELECT id FROM users').toSimpleQuery();
194+
const query2 = SelectQueryParser.parse('SELECT id FROM customers ORDER BY id DESC').toSimpleQuery();
195+
196+
// Store original ORDER BY state
197+
expect(query2.orderByClause).not.toBeNull();
198+
const originalOrderBy = query2.orderByClause;
199+
200+
const binaryQuery = query1.toUnion(query2);
201+
const result = binaryQuery.toSimpleQuery();
202+
203+
// Original query should have ORDER BY removed
204+
expect(query2.orderByClause).toBeNull();
205+
206+
// Result should have the ORDER BY
207+
expect(result.orderByClause).not.toBeNull();
208+
expect(result.orderByClause).toBe(originalOrderBy);
209+
});
210+
});
211+
103212
describe('ValuesQuery.toSimpleQuery()', () => {
104213
test('should convert ValuesQuery to SimpleSelectQuery', () => {
105214
// Test conversion of VALUES query

0 commit comments

Comments
 (0)