Skip to content

Commit b3d0b6d

Browse files
queeliusclaude
andcommitted
fix(parser): pass modifier to ExceptOp/MinusOp constructors and reset between iterations
EXCEPT ALL/DISTINCT and MINUS ALL/DISTINCT modifiers were silently dropped during parsing because the grammar captured the modifier via SetOperationModifier() but constructed ExceptOp and MinusOp with their no-arg constructors (defaulting to empty string), unlike UnionOp and IntersectOp which correctly received the modifier. Additionally, the modifier variable was not reset between iterations of the set-operation loop, causing modifiers to leak from one operator to the next (e.g., UNION ALL ... EXCEPT would incorrectly make the EXCEPT inherit ALL). Fixes #2419 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent c0e1d05 commit b3d0b6d

File tree

2 files changed

+135
-3
lines changed

2 files changed

+135
-3
lines changed

src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5093,7 +5093,7 @@ Select SetOperationList(Select select) #SetOperationList: {
50935093
selects.add(select);
50945094
}
50955095

5096-
( LOOKAHEAD(2) (
5096+
( LOOKAHEAD(2) { modifier = null; } (
50975097
(
50985098
<K_UNION> [ modifier=SetOperationModifier() ] { UnionOp union = new UnionOp(modifier); linkAST(union,jjtThis); operations.add(union); }
50995099

@@ -5104,11 +5104,11 @@ Select SetOperationList(Select select) #SetOperationList: {
51045104
)
51055105
|
51065106
(
5107-
<K_MINUS> [ modifier=SetOperationModifier() ] { MinusOp minus = new MinusOp(); linkAST(minus,jjtThis); operations.add(minus); }
5107+
<K_MINUS> [ modifier=SetOperationModifier() ] { MinusOp minus = new MinusOp(modifier); linkAST(minus,jjtThis); operations.add(minus); }
51085108
)
51095109
|
51105110
(
5111-
<K_EXCEPT> [ modifier=SetOperationModifier() ] { ExceptOp except = new ExceptOp(); linkAST(except,jjtThis); operations.add(except); }
5111+
<K_EXCEPT> [ modifier=SetOperationModifier() ] { ExceptOp except = new ExceptOp(modifier); linkAST(except,jjtThis); operations.add(except); }
51125112
)
51135113

51145114
)
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2019 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.statement.select;
11+
12+
import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed;
13+
import static org.junit.jupiter.api.Assertions.*;
14+
15+
import net.sf.jsqlparser.JSQLParserException;
16+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
17+
import net.sf.jsqlparser.statement.Statement;
18+
import org.junit.jupiter.api.Test;
19+
import org.junit.jupiter.api.parallel.Execution;
20+
import org.junit.jupiter.api.parallel.ExecutionMode;
21+
22+
/**
23+
* Regression tests for EXCEPT/MINUS ALL/DISTINCT modifier handling.
24+
* <p>
25+
* Verifies that the ALL and DISTINCT modifiers are correctly preserved during
26+
* parse-toString round-trips for all set operation types: UNION, INTERSECT,
27+
* EXCEPT, and MINUS.
28+
*
29+
* @see <a href="https://github.com/JSQLParser/JSqlParser/issues/2080">#2080</a>
30+
*/
31+
@Execution(ExecutionMode.CONCURRENT)
32+
public class SetOperationModifierTest {
33+
34+
// ── EXCEPT modifier tests ─────────────────────────────────────
35+
36+
@Test
37+
public void testExceptAllRoundTrip() throws JSQLParserException {
38+
assertSqlCanBeParsedAndDeparsed("SELECT a FROM t1 EXCEPT ALL SELECT a FROM t2");
39+
}
40+
41+
@Test
42+
public void testExceptDistinctRoundTrip() throws JSQLParserException {
43+
assertSqlCanBeParsedAndDeparsed("SELECT a FROM t1 EXCEPT DISTINCT SELECT a FROM t2");
44+
}
45+
46+
@Test
47+
public void testPlainExceptRoundTrip() throws JSQLParserException {
48+
assertSqlCanBeParsedAndDeparsed("SELECT a FROM t1 EXCEPT SELECT a FROM t2");
49+
}
50+
51+
// ── MINUS modifier tests ─────────────────────────────────────
52+
53+
@Test
54+
public void testMinusAllRoundTrip() throws JSQLParserException {
55+
assertSqlCanBeParsedAndDeparsed("SELECT a FROM t1 MINUS ALL SELECT a FROM t2");
56+
}
57+
58+
@Test
59+
public void testMinusDistinctRoundTrip() throws JSQLParserException {
60+
assertSqlCanBeParsedAndDeparsed("SELECT a FROM t1 MINUS DISTINCT SELECT a FROM t2");
61+
}
62+
63+
@Test
64+
public void testPlainMinusRoundTrip() throws JSQLParserException {
65+
assertSqlCanBeParsedAndDeparsed("SELECT a FROM t1 MINUS SELECT a FROM t2");
66+
}
67+
68+
// ── Cross-check: UNION and INTERSECT still work ──────────────
69+
70+
@Test
71+
public void testUnionAllRoundTrip() throws JSQLParserException {
72+
assertSqlCanBeParsedAndDeparsed("SELECT a FROM t1 UNION ALL SELECT a FROM t2");
73+
}
74+
75+
@Test
76+
public void testIntersectAllRoundTrip() throws JSQLParserException {
77+
assertSqlCanBeParsedAndDeparsed("SELECT a FROM t1 INTERSECT ALL SELECT a FROM t2");
78+
}
79+
80+
// ── Modifier leak prevention: modifiers must not bleed across operators ──
81+
82+
@Test
83+
public void testModifierDoesNotLeakFromUnionAllToExcept() throws JSQLParserException {
84+
String sql = "SELECT a FROM t1 UNION ALL SELECT b FROM t2 EXCEPT SELECT c FROM t3";
85+
Statement stmt = CCJSqlParserUtil.parse(sql);
86+
String result = stmt.toString();
87+
// EXCEPT must NOT inherit ALL from the preceding UNION ALL
88+
assertFalse(result.contains("EXCEPT ALL"),
89+
"Modifier should not leak from UNION ALL to a subsequent plain EXCEPT: " + result);
90+
}
91+
92+
@Test
93+
public void testModifierDoesNotLeakFromIntersectAllToUnion() throws JSQLParserException {
94+
String sql = "SELECT a FROM t1 INTERSECT ALL SELECT b FROM t2 UNION SELECT c FROM t3";
95+
Statement stmt = CCJSqlParserUtil.parse(sql);
96+
String result = stmt.toString();
97+
assertFalse(result.contains("UNION ALL"),
98+
"Modifier should not leak from INTERSECT ALL to a subsequent plain UNION: " + result);
99+
}
100+
101+
@Test
102+
public void testMixedModifiersPreserved() throws JSQLParserException {
103+
// UNION ALL followed by EXCEPT DISTINCT
104+
assertSqlCanBeParsedAndDeparsed(
105+
"SELECT a FROM t1 UNION ALL SELECT b FROM t2 EXCEPT DISTINCT SELECT c FROM t3");
106+
}
107+
108+
// ── SetOperation object state verification ──────────────────
109+
110+
@Test
111+
public void testExceptAllSetOperationObject() throws JSQLParserException {
112+
String sql = "SELECT a FROM t1 EXCEPT ALL SELECT a FROM t2";
113+
Statement stmt = CCJSqlParserUtil.parse(sql);
114+
SetOperationList setOpList = (SetOperationList) stmt;
115+
SetOperation op = setOpList.getOperation(0);
116+
117+
assertInstanceOf(ExceptOp.class, op);
118+
assertTrue(op.isAll(), "ExceptOp should report isAll() == true");
119+
assertFalse(op.isDistinct(), "ExceptOp should report isDistinct() == false");
120+
}
121+
122+
@Test
123+
public void testMinusAllSetOperationObject() throws JSQLParserException {
124+
String sql = "SELECT a FROM t1 MINUS ALL SELECT a FROM t2";
125+
Statement stmt = CCJSqlParserUtil.parse(sql);
126+
SetOperationList setOpList = (SetOperationList) stmt;
127+
SetOperation op = setOpList.getOperation(0);
128+
129+
assertInstanceOf(MinusOp.class, op);
130+
assertTrue(op.isAll(), "MinusOp should report isAll() == true");
131+
}
132+
}

0 commit comments

Comments
 (0)