Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
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>
  • Loading branch information
queelius and claude committed Mar 25, 2026
commit b3d0b6d74a60114b84883d515e53c71cf168d3f3
6 changes: 3 additions & 3 deletions src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -5093,7 +5093,7 @@ Select SetOperationList(Select select) #SetOperationList: {
selects.add(select);
}

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

Expand All @@ -5104,11 +5104,11 @@ Select SetOperationList(Select select) #SetOperationList: {
)
|
(
<K_MINUS> [ modifier=SetOperationModifier() ] { MinusOp minus = new MinusOp(); linkAST(minus,jjtThis); operations.add(minus); }
<K_MINUS> [ modifier=SetOperationModifier() ] { MinusOp minus = new MinusOp(modifier); linkAST(minus,jjtThis); operations.add(minus); }
)
|
(
<K_EXCEPT> [ modifier=SetOperationModifier() ] { ExceptOp except = new ExceptOp(); linkAST(except,jjtThis); operations.add(except); }
<K_EXCEPT> [ modifier=SetOperationModifier() ] { ExceptOp except = new ExceptOp(modifier); linkAST(except,jjtThis); operations.add(except); }
)

)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/*-
* #%L
* JSQLParser library
* %%
* Copyright (C) 2004 - 2019 JSQLParser
* %%
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
* #L%
*/
package net.sf.jsqlparser.statement.select;

import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed;
import static org.junit.jupiter.api.Assertions.*;

import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.Statement;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;

/**
* Regression tests for EXCEPT/MINUS ALL/DISTINCT modifier handling.
* <p>
* Verifies that the ALL and DISTINCT modifiers are correctly preserved during
* parse-toString round-trips for all set operation types: UNION, INTERSECT,
* EXCEPT, and MINUS.
*
* @see <a href="https://github.com/JSQLParser/JSqlParser/issues/2080">#2080</a>
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Javadoc references issue #2080, but this PR (and the described regression) is for #2419. Please update the link so future readers can trace the correct bug report.

Suggested change
* @see <a href="https://github.com/JSQLParser/JSqlParser/issues/2080">#2080</a>
* @see <a href="https://github.com/JSQLParser/JSqlParser/issues/2419">#2419</a>

Copilot uses AI. Check for mistakes.
*/
@Execution(ExecutionMode.CONCURRENT)
public class SetOperationModifierTest {

// ── EXCEPT modifier tests ─────────────────────────────────────

@Test
public void testExceptAllRoundTrip() throws JSQLParserException {
assertSqlCanBeParsedAndDeparsed("SELECT a FROM t1 EXCEPT ALL SELECT a FROM t2");
}

@Test
public void testExceptDistinctRoundTrip() throws JSQLParserException {
assertSqlCanBeParsedAndDeparsed("SELECT a FROM t1 EXCEPT DISTINCT SELECT a FROM t2");
}

@Test
public void testPlainExceptRoundTrip() throws JSQLParserException {
assertSqlCanBeParsedAndDeparsed("SELECT a FROM t1 EXCEPT SELECT a FROM t2");
}

// ── MINUS modifier tests ─────────────────────────────────────

@Test
public void testMinusAllRoundTrip() throws JSQLParserException {
assertSqlCanBeParsedAndDeparsed("SELECT a FROM t1 MINUS ALL SELECT a FROM t2");
}

@Test
public void testMinusDistinctRoundTrip() throws JSQLParserException {
assertSqlCanBeParsedAndDeparsed("SELECT a FROM t1 MINUS DISTINCT SELECT a FROM t2");
}

@Test
public void testPlainMinusRoundTrip() throws JSQLParserException {
assertSqlCanBeParsedAndDeparsed("SELECT a FROM t1 MINUS SELECT a FROM t2");
}

// ── Cross-check: UNION and INTERSECT still work ──────────────

@Test
public void testUnionAllRoundTrip() throws JSQLParserException {
assertSqlCanBeParsedAndDeparsed("SELECT a FROM t1 UNION ALL SELECT a FROM t2");
}

@Test
public void testIntersectAllRoundTrip() throws JSQLParserException {
assertSqlCanBeParsedAndDeparsed("SELECT a FROM t1 INTERSECT ALL SELECT a FROM t2");
}

// ── Modifier leak prevention: modifiers must not bleed across operators ──

@Test
public void testModifierDoesNotLeakFromUnionAllToExcept() throws JSQLParserException {
String sql = "SELECT a FROM t1 UNION ALL SELECT b FROM t2 EXCEPT SELECT c FROM t3";
Statement stmt = CCJSqlParserUtil.parse(sql);
String result = stmt.toString();
// EXCEPT must NOT inherit ALL from the preceding UNION ALL
assertFalse(result.contains("EXCEPT ALL"),
"Modifier should not leak from UNION ALL to a subsequent plain EXCEPT: " + result);
}

@Test
public void testModifierDoesNotLeakFromIntersectAllToUnion() throws JSQLParserException {
String sql = "SELECT a FROM t1 INTERSECT ALL SELECT b FROM t2 UNION SELECT c FROM t3";
Statement stmt = CCJSqlParserUtil.parse(sql);
String result = stmt.toString();
assertFalse(result.contains("UNION ALL"),
"Modifier should not leak from INTERSECT ALL to a subsequent plain UNION: " + result);
}

@Test
public void testMixedModifiersPreserved() throws JSQLParserException {
// UNION ALL followed by EXCEPT DISTINCT
assertSqlCanBeParsedAndDeparsed(
"SELECT a FROM t1 UNION ALL SELECT b FROM t2 EXCEPT DISTINCT SELECT c FROM t3");
}

// ── SetOperation object state verification ──────────────────

@Test
public void testExceptAllSetOperationObject() throws JSQLParserException {
String sql = "SELECT a FROM t1 EXCEPT ALL SELECT a FROM t2";
Statement stmt = CCJSqlParserUtil.parse(sql);
SetOperationList setOpList = (SetOperationList) stmt;
SetOperation op = setOpList.getOperation(0);

assertInstanceOf(ExceptOp.class, op);
assertTrue(op.isAll(), "ExceptOp should report isAll() == true");
assertFalse(op.isDistinct(), "ExceptOp should report isDistinct() == false");
}

@Test
public void testMinusAllSetOperationObject() throws JSQLParserException {
String sql = "SELECT a FROM t1 MINUS ALL SELECT a FROM t2";
Statement stmt = CCJSqlParserUtil.parse(sql);
SetOperationList setOpList = (SetOperationList) stmt;
SetOperation op = setOpList.getOperation(0);

assertInstanceOf(MinusOp.class, op);
assertTrue(op.isAll(), "MinusOp should report isAll() == true");
}
}
Loading