Skip to content

Commit 5001ffd

Browse files
Copilotmathiasrw
andauthored
Fix UNION (ALL) for parenthesis SELECT to support ORDER BY and LIMIT and close #1965 (#2395)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: mathiasrw <1063454+mathiasrw@users.noreply.github.com> Co-authored-by: M. Wulff <m@rawu.dk>
1 parent 8dea3a9 commit 5001ffd

6 files changed

Lines changed: 836 additions & 537 deletions

File tree

src/38query.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,53 @@ function queryfn3(query) {
177177
// Remove distinct values
178178
doDistinct(query);
179179

180+
// If we have UNION/UNION ALL/EXCEPT/INTERSECT with ORDER BY/LIMIT before it,
181+
// apply ORDER BY and LIMIT to the first SELECT before combining.
182+
// This handles the pattern: SELECT ... ORDER BY ... LIMIT ... UNION ALL SELECT ... ORDER BY ... LIMIT ...
183+
// We only do this if the UNION branch also has ORDER BY/LIMIT (pattern 2), not if ORDER BY is at the end (pattern 1).
184+
var unionBranchHasOrder = ['unionallfn', 'unionfn', 'exceptfn', 'intersectfn'].some(
185+
function (fnName) {
186+
var fn = query[fnName];
187+
return fn && fn.query && (fn.query.orderfn || fn.query.limit);
188+
}
189+
);
190+
191+
if (unionBranchHasOrder && (query.orderfn || query.limit)) {
192+
// Apply ordering to first SELECT's data
193+
if (query.orderfn) {
194+
// Populate order keys before sorting (needed for UNION queries)
195+
if (query.orderColumns) {
196+
for (var i = 0, ilen = query.data.length; i < ilen; i++) {
197+
for (var idx = 0; idx < query.orderColumns.length; idx++) {
198+
var v = query.orderColumns[idx];
199+
var key = '$$$' + idx;
200+
var r = query.data[i];
201+
if (v instanceof yy.Column && r[v.columnid] !== undefined) {
202+
r[key] = r[v.columnid];
203+
} else if (v instanceof yy.Column) {
204+
r[key] = undefined;
205+
} else {
206+
r[key] = undefined;
207+
}
208+
if (i === 0 && query.removeKeys.indexOf(key) === -1) {
209+
query.removeKeys.push(key);
210+
}
211+
}
212+
}
213+
}
214+
query.data = query.data.sort(query.orderfn);
215+
// Clear orderfn so it doesn't get applied again after UNION
216+
query.orderfn = null;
217+
}
218+
// Apply limit to first SELECT's data
219+
if (query.limit) {
220+
doLimit(query);
221+
// Clear limit so it doesn't get applied again after UNION
222+
query.limit = null;
223+
query.offset = null;
224+
}
225+
}
226+
180227
// UNION / UNION ALL
181228
if (query.unionallfn) {
182229
// TODO Simplify this part of program

src/alasqlparser.jison

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,7 @@ Statement
439439
| Reindex
440440
| RenameTable
441441
| Select
442+
| ParenthesizedSelect
442443
| ShowCreateTable
443444
| ShowColumns
444445
| ShowDatabases
@@ -541,6 +542,15 @@ Select
541542
if(yy.exists) $$.exists = yy.exists.slice();
542543
/* if(yy.queries) $$.queries = yy.queries;
543544
delete yy.queries;
545+
*/ }
546+
| LPAR Select RPAR UnionClause OrderClause LimitClause
547+
{
548+
$$ = $2;
549+
yy.extend($$,$4);
550+
yy.extend($$,$5); yy.extend($$,$6);
551+
if(yy.exists) $$.exists = yy.exists.slice();
552+
/* if(yy.queries) $$.queries = yy.queries;
553+
delete yy.queries;
544554
*/ }
545555
| ParenthesizedSelect UnionClause OrderClause LimitClause
546556
{
@@ -556,6 +566,11 @@ Select
556566
}
557567
;
558568

569+
ParenthesizedSelect
570+
: LPAR Select RPAR
571+
{ $$ = $2; }
572+
;
573+
559574
SelectWithoutOrderOrLimit
560575
: SelectClause RemoveClause? IntoClause FromClause PivotClause? WhereClause GroupClause UnionClause
561576
{

src/alasqlparser.js

Lines changed: 544 additions & 534 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,6 @@ console.log(
2020
isWebWorker: alasql.utils.isWebWorker,
2121
},
2222
null,
23-
4
23+
2
2424
)
2525
);

test/test1965.js

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
if (typeof exports === 'object') {
2+
var assert = require('assert');
3+
var alasql = require('..');
4+
}
5+
6+
describe('Test UNION ALL with ORDER BY and LIMIT on each SELECT', function () {
7+
const test = 'union_order_limit';
8+
9+
before(function () {
10+
alasql('create database test' + test);
11+
alasql('use test' + test);
12+
});
13+
14+
after(function () {
15+
alasql('drop database test' + test);
16+
});
17+
18+
it('A) UNION ALL with ORDER BY and LIMIT using parentheses in UNION branch', function () {
19+
// Create test data
20+
alasql('CREATE TABLE temptable (subcategoryname STRING, totalamount FLOAT)');
21+
alasql(`INSERT INTO temptable VALUES
22+
('Bikes', 1000.5),
23+
('Components', 2000.75),
24+
('Clothing', 1500.25),
25+
('Accessories', 800.10),
26+
('Socks', 9556.37),
27+
('Helmets', 3000.00),
28+
('Gloves', 1200.50)`);
29+
30+
// SQL-99 compliant: Use parentheses on second SELECT for ORDER BY/LIMIT
31+
// This works: plain SELECT, then UNION ALL with parenthesized SELECT
32+
var sql = `
33+
SELECT subcategoryname, SUM(totalamount) AS sales
34+
FROM temptable
35+
WHERE subcategoryname IN ('Socks', 'Helmets', 'Components')
36+
GROUP BY subcategoryname
37+
UNION ALL
38+
(SELECT subcategoryname, SUM(totalamount) AS sales
39+
FROM temptable
40+
WHERE subcategoryname IN ('Accessories', 'Bikes', 'Gloves')
41+
GROUP BY subcategoryname
42+
ORDER BY sales ASC
43+
LIMIT 3)
44+
`;
45+
46+
var res = alasql(sql);
47+
48+
// Expected: 6 rows (3 from first + 3 from second with LIMIT 3)
49+
var expected = [
50+
{subcategoryname: 'Components', sales: 2000.75},
51+
{subcategoryname: 'Socks', sales: 9556.37},
52+
{subcategoryname: 'Helmets', sales: 3000},
53+
{subcategoryname: 'Accessories', sales: 800.1},
54+
{subcategoryname: 'Bikes', sales: 1000.5},
55+
{subcategoryname: 'Gloves', sales: 1200.5},
56+
];
57+
assert.deepStrictEqual(
58+
res,
59+
expected,
60+
'Should return correct rows with ORDER BY/LIMIT on second SELECT'
61+
);
62+
63+
alasql('DROP TABLE temptable');
64+
});
65+
66+
it('B) UNION with ORDER BY and LIMIT using parentheses', function () {
67+
// Create test data
68+
alasql('CREATE TABLE test2 (val INT)');
69+
alasql('INSERT INTO test2 VALUES (1),(2),(3),(4),(5),(6),(7),(8),(9),(10)');
70+
71+
// SQL-99 compliant: Use parentheses on second SELECT for ORDER BY/LIMIT
72+
var sql = `
73+
SELECT val FROM test2
74+
WHERE val <= 5
75+
UNION
76+
(SELECT val FROM test2
77+
WHERE val >= 6
78+
ORDER BY val ASC
79+
LIMIT 2)
80+
`;
81+
82+
var res = alasql(sql);
83+
84+
// Expected: 7 rows (5 from first + 2 from second with LIMIT, UNION removes duplicates)
85+
// Note: UNION doesn't guarantee order, so we sort the result for comparison
86+
res.sort((a, b) => a.val - b.val);
87+
var expected = [{val: 1}, {val: 2}, {val: 3}, {val: 4}, {val: 5}, {val: 6}, {val: 7}];
88+
assert.deepStrictEqual(
89+
res,
90+
expected,
91+
'Should return correct rows with UNION and LIMIT on second SELECT'
92+
);
93+
94+
alasql('DROP TABLE test2');
95+
});
96+
97+
it('C) Both SELECTs parenthesized with ORDER BY/LIMIT', function () {
98+
// Test with both branches having parentheses
99+
alasql('CREATE TABLE test3 (id INT, name STRING)');
100+
alasql("INSERT INTO test3 VALUES (1,'Alice'),(2,'Bob'),(3,'Charlie'),(4,'David'),(5,'Eve')");
101+
102+
var sql = `
103+
(SELECT id, name FROM test3 ORDER BY id DESC LIMIT 2)
104+
UNION ALL
105+
(SELECT id, name FROM test3 ORDER BY id ASC LIMIT 2)
106+
`;
107+
108+
var res = alasql(sql);
109+
110+
var expected = [
111+
{id: 5, name: 'Eve'},
112+
{id: 4, name: 'David'},
113+
{id: 1, name: 'Alice'},
114+
{id: 2, name: 'Bob'},
115+
];
116+
assert.deepStrictEqual(res, expected, 'Both parenthesized SELECTs with ORDER BY/LIMIT');
117+
118+
alasql('DROP TABLE test3');
119+
});
120+
121+
it('D) Parenthesized SELECT with ORDER BY DESC and LIMIT in UNION', function () {
122+
// Test ORDER BY DESC
123+
alasql('CREATE TABLE test4 (num INT)');
124+
alasql('INSERT INTO test4 VALUES (10),(20),(30),(40),(50)');
125+
126+
var sql = `
127+
SELECT num FROM test4 WHERE num <= 30
128+
UNION ALL
129+
(SELECT num FROM test4 WHERE num > 30 ORDER BY num DESC LIMIT 1)
130+
`;
131+
132+
var res = alasql(sql);
133+
134+
var expected = [{num: 10}, {num: 20}, {num: 30}, {num: 50}];
135+
assert.deepStrictEqual(res, expected, 'Parenthesized SELECT with ORDER BY DESC');
136+
137+
alasql('DROP TABLE test4');
138+
});
139+
140+
it('E) UNION ALL with ORDER BY on both branches', function () {
141+
// Test UNION ALL with ORDER BY on both first and second SELECT
142+
alasql('CREATE TABLE test5 (letter STRING)');
143+
alasql("INSERT INTO test5 VALUES ('a'),('b'),('c'),('d'),('e'),('f')");
144+
145+
var sql = `
146+
SELECT letter FROM test5 WHERE letter < 'c'
147+
UNION ALL
148+
(SELECT letter FROM test5 WHERE letter >= 'c' ORDER BY letter DESC LIMIT 2)
149+
`;
150+
151+
var res = alasql(sql);
152+
153+
var expected = [{letter: 'a'}, {letter: 'b'}, {letter: 'f'}, {letter: 'e'}];
154+
assert.deepStrictEqual(
155+
res,
156+
expected,
157+
'UNION ALL with ORDER BY DESC and LIMIT on second branch'
158+
);
159+
160+
alasql('DROP TABLE test5');
161+
});
162+
163+
it('F) EXCEPT with parenthesized ORDER BY/LIMIT', function () {
164+
// Test EXCEPT operation with parentheses
165+
alasql('CREATE TABLE test6a (num INT)');
166+
alasql('CREATE TABLE test6b (num INT)');
167+
alasql('INSERT INTO test6a VALUES (1),(2),(3),(4),(5)');
168+
alasql('INSERT INTO test6b VALUES (3),(4)');
169+
170+
var sql = `
171+
SELECT num FROM test6a
172+
EXCEPT
173+
(SELECT num FROM test6b ORDER BY num ASC LIMIT 1)
174+
`;
175+
176+
var res = alasql(sql);
177+
178+
var expected = [{num: 1}, {num: 2}, {num: 4}, {num: 5}];
179+
assert.deepStrictEqual(res, expected, 'EXCEPT with parenthesized ORDER BY/LIMIT');
180+
181+
alasql('DROP TABLE test6a');
182+
alasql('DROP TABLE test6b');
183+
});
184+
185+
it('G) INTERSECT with parenthesized ORDER BY/LIMIT', function () {
186+
// Test INTERSECT operation with parentheses
187+
alasql('CREATE TABLE test7a (num INT)');
188+
alasql('CREATE TABLE test7b (num INT)');
189+
alasql('INSERT INTO test7a VALUES (1),(2),(3),(4),(5)');
190+
alasql('INSERT INTO test7b VALUES (3),(4),(5),(6)');
191+
192+
var sql = `
193+
(SELECT num FROM test7a WHERE num >= 3 ORDER BY num ASC LIMIT 2)
194+
INTERSECT
195+
SELECT num FROM test7b
196+
`;
197+
198+
var res = alasql(sql);
199+
200+
var expected = [{num: 3}, {num: 4}];
201+
assert.deepStrictEqual(res, expected, 'INTERSECT with parenthesized ORDER BY/LIMIT');
202+
203+
alasql('DROP TABLE test7a');
204+
alasql('DROP TABLE test7b');
205+
});
206+
207+
it('H) ORDER BY after UNION applies to entire result (backwards compatibility)', function () {
208+
// Test that ORDER BY after UNION still applies to the entire result
209+
alasql('CREATE TABLE test8 (val INT)');
210+
alasql('INSERT INTO test8 VALUES (30),(10),(20)');
211+
212+
var sql = `
213+
SELECT val FROM test8
214+
UNION ALL
215+
(SELECT val + 100 AS val FROM test8 ORDER BY val DESC LIMIT 2)
216+
ORDER BY val ASC
217+
`;
218+
219+
var res = alasql(sql);
220+
221+
// ORDER BY val ASC should apply to the combined result
222+
var expected = [{val: 10}, {val: 20}, {val: 30}, {val: 130}, {val: 120}];
223+
assert.deepStrictEqual(res, expected, 'ORDER BY after UNION applies to entire result');
224+
225+
alasql('DROP TABLE test8');
226+
});
227+
});

test/test2414.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,11 +129,11 @@ describe('Test 2414 - UNION with parenthesized SELECT and ORDER BY', function ()
129129
];
130130

131131
var res = alasql(
132-
`(SELECT a FROM ? ORDER BY a LIMIT 2) UNION ALL (SELECT a FROM ? ORDER BY a DESC LIMIT 1)`,
132+
`(SELECT b FROM ? ORDER BY a LIMIT 2) UNION ALL (SELECT b FROM ? ORDER BY a DESC LIMIT 1)`,
133133
[data, data]
134134
);
135135

136-
assert.deepEqual(res, [{a: 1}, {a: 2}]);
136+
assert.deepEqual(res, [{b: 'x'}, {b: 'y'}, {b: 'w'}]);
137137
});
138138

139139
it('H) UNION ALL with parenthesized ORDER BY', function () {

0 commit comments

Comments
 (0)