Skip to content

[BUG] EXCEPT ALL/DISTINCT and MINUS ALL/DISTINCT modifiers lost during parse-toString round-trip #2419

@queelius

Description

@queelius

Description

EXCEPT ALL, EXCEPT DISTINCT, MINUS ALL, and MINUS DISTINCT modifiers are silently dropped during parsing. The parser accepts the modifier syntax but does not pass the captured modifier string to the ExceptOp and MinusOp constructors.

Reproducer

String sql = "SELECT a FROM t1 EXCEPT ALL SELECT a FROM t2";
Statement stmt = CCJSqlParserUtil.parse(sql);
System.out.println(stmt.toString());
// Expected: SELECT a FROM t1 EXCEPT ALL SELECT a FROM t2
// Actual:   SELECT a FROM t1 EXCEPT SELECT a FROM t2

The same issue affects MINUS ALL:

String sql = "SELECT a FROM t1 MINUS ALL SELECT a FROM t2";
Statement stmt = CCJSqlParserUtil.parse(sql);
System.out.println(stmt.toString());
// Expected: SELECT a FROM t1 MINUS ALL SELECT a FROM t2
// Actual:   SELECT a FROM t1 MINUS SELECT a FROM t2

Root Cause

In JSqlParserCC.jjt, the SetOperationList rule captures the modifier via modifier=SetOperationModifier() but constructs MinusOp and ExceptOp with their no-arg constructors (which default to empty string), discarding the modifier:

<K_MINUS> [ modifier=SetOperationModifier() ] { MinusOp minus = new MinusOp(); ... }
<K_EXCEPT> [ modifier=SetOperationModifier() ] { ExceptOp except = new ExceptOp(); ... }

Compare with UnionOp and IntersectOp which correctly pass the modifier:

<K_UNION> [ modifier=SetOperationModifier() ] { UnionOp union = new UnionOp(modifier); ... }
<K_INTERSECT> [ modifier=SetOperationModifier() ] { IntersectOp intersect = new IntersectOp(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).

Impact

  • Semantic correctness: EXCEPT ALL preserves duplicates while EXCEPT removes them. Silently dropping the ALL modifier changes query semantics.
  • Round-trip fidelity: parse(sql).toString() does not reproduce the original SQL.

Affected versions

Current master (regression introduced in commit 5fe938bc which refactored SetOperationModifier handling for the CORRESPONDING feature).

Fix

  1. Pass modifier to MinusOp and ExceptOp constructors
  2. Reset modifier = null at the start of each set-operation loop iteration

A PR with the fix and regression tests will follow.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions