Skip to content

fix: recognise set operation quantifiers written in any case - #2614

Merged
manticore-projects merged 1 commit into
JSQLParser:masterfrom
knutwannheden:fix/set-operation-modifier-case
Sep 12, 2026
Merged

fix: recognise set operation quantifiers written in any case#2614
manticore-projects merged 1 commit into
JSQLParser:masterfrom
knutwannheden:fix/set-operation-modifier-case

Conversation

@knutwannheden

Copy link
Copy Markdown
Contributor

union all parses to a UnionOp that says it is not an ALL union:

Statement stmt = CCJSqlParserUtil.parse("select id from x union all select id from y");
SetOperation op = ((SetOperationList) stmt).getOperations().get(0);
op.getModifier();  // "all"
op.isAll();        // false

Write the same statement in upper case and it answers true. Every set operation is affected, and DISTINCT the same way:

SQL modifier isAll() isDistinct()
... union all ... "all" false false
... UNION ALL ... "ALL" true false
... Union All ... "All" false false
... union distinct ... "distinct" false false
... except all ... "all" false false
... intersect all ... "all" false false

On 5.3 all of these answered correctly, so a consumer reading isAll() to tell UNION from UNION ALL silently reclassifies half its input after upgrading. I hit this through a static-analysis rule that flags UNION where UNION ALL would do: it started flagging statements that already said all.

Where it comes from

5fe938b replaced the boolean all and boolean distinct fields with a single modifier string, so that CORRESPONDING BY NAME MATCHING(...) could be carried alongside the quantifier. The grammar records that string verbatim:

[ ( tk=<K_ALL> | tk="DISTINCT")  { modifier+=tk.image; } ]

tk.image keeps the source's own casing, while the predicates ask a case-sensitive question:

public boolean isAll() {
    return modifier != null && modifier.contains("ALL");
}

Nothing in the suite spells a set operation in lower case — SetOperationModifierTest, added for #2419, covers all four operators but every fixture is upper case — so the change read as a pure refactor.

The fix

Match the leading keyword case-insensitively. The match is anchored at the start and has to end on a word boundary, which makes it stricter than the contains it replaces: a column list such as CORRESPONDING BY NAME MATCHING(all) can no longer be mistaken for the quantifier, which contains("ALL") would have got wrong too had it been written in upper case.

public boolean isAll() {
    return leadsWith("ALL");
}

While in there, the setters ignored their argument and wrote the keyword they are named after regardless:

public void setAll(boolean all) {
    this.modifier = "ALL";   // setAll(false) left an ALL modifier behind
}

They now honour it.

Round-tripping is left alone

toString() still emits the modifier as written, so select ... union all ... deparses to SELECT ... UNION all ... where 5.3 produced UNION ALL. That is a visible change from 5.3 and arguably wants normalising too, but it changes output for everyone rather than fixing a wrong answer, so I left it out. Happy to add it here if you would rather the two land together.

Testing

14 cases added to SetOperationModifierTest covering each quantifier in mixed case, each operator, the unqualified operation, and the setters. Full suite: 6618 tests, 0 failures.

The grammar records a set operation's modifier verbatim from the source, so
`union all` arrives as "all" while `isAll()` asked whether the modifier
contained "ALL". Every spelling but upper case therefore read as an unqualified
UNION, silently dropping ALL semantics — and the same for DISTINCT, across
UNION, EXCEPT, INTERSECT and MINUS alike.

A regression from 5fe938b, which replaced the boolean `all` and `distinct`
fields with a single verbatim modifier string. The round-trip tests all spell
their SQL in upper case, so none of them noticed.

Match the leading keyword case-insensitively instead. The match is anchored at
the start of the modifier and must end on a word boundary, so a
`CORRESPONDING BY NAME MATCHING(all)` column list is no longer mistaken for the
ALL quantifier the way a bare `contains` would.

`setAll` and `setDistinct` wrote their keyword whatever they were passed, so
`setAll(false)` left an ALL modifier behind. They now honour the argument.
@manticore-projects
manticore-projects merged commit 537c8c1 into JSQLParser:master Sep 12, 2026
10 checks passed
@manticore-projects

Copy link
Copy Markdown
Contributor

Thank you much for cleaning this up!

@knutwannheden
knutwannheden deleted the fix/set-operation-modifier-case branch September 12, 2026 07:26
@knutwannheden

Copy link
Copy Markdown
Contributor Author

I did some testing with 5.4 and noticed this regression as well as another one that I've been working on and am about to wrap up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants