[FIX] SPARQL Parsing of sub-SELECT as a Set-Operation Operand#1436
Merged
Conversation
A `{ SELECT ... }` sub-SELECT used as an operand of UNION, MINUS, OPTIONAL,
GRAPH, SERVICE, or EXISTS/NOT EXISTS was mis-parsed, silently producing wrong
results (fluree/azure-chat#42: UNION returned only one arm or 0 rows;
fluree/azure-chat#43: MINUS subtracted nothing and returned the full left set).
Root cause: `parse_group_graph_pattern` only detected a `{ SELECT ... }`
sub-SELECT inside its own nested-`{` branch. Every other caller (OPTIONAL,
GRAPH, SERVICE, the MINUS right arm, each UNION arm, and the free
`parse_group_graph_pattern` used by EXISTS/NOT EXISTS) consumes the `{` and then
calls `parse_group_graph_pattern` positioned at the `SELECT`, where the leading
`SELECT` was treated as an unexpected token. So the sub-SELECT was parsed as a
bare BGP with its `(expr AS ?v)` projection dropped (MINUS/OPTIONAL lose the
projected var), or — for UNION — the sub-SELECT left arm was pushed without a
trailing-UNION check, the `UNION` token was discarded as "UNION must follow a
pattern", and the query collapsed into a conjunction of two subqueries (a
correlated join on the shared projected var → one arm survives or 0 rows).
Fix (parser-only):
- Detect the `SubSelect` alternative at the top of `parse_group_graph_pattern`,
so every position that admits a group implements the grammar rule
`GroupGraphPattern ::= '{' ( SubSelect | GroupGraphPatternSub ) '}'`.
- Route the nested-`{` branch through `parse_group_graph_pattern` and run the
trailing-UNION check for a sub-SELECT left arm too.
Fixed queries compile to the already-optimised `UnionOperator` / sub-SELECT
`MINUS` plans, so there is no query-engine change and no hot-path cost.
Tests:
- AST parser tests (fluree-db-sparql): sub-SELECT in UNION (both arms + mixed),
MINUS right arm, OPTIONAL body.
- Integration tests (fluree-db-api): end-to-end UNION (both arms, mixed,
order-independent, == flat UNION), MINUS (== FILTER NOT EXISTS), OPTIONAL.
- W3C SPARQL suite: syntax-query 81→83 (syntax-SELECTscope1), subquery 11→12
(sq12); negation 12→12; zero regressions.
Fixes #1435
Follow-up to the review of #1436 (comment-only, no behavior change): - parse_group_graph_pattern: warn contributors not to re-add a per-caller KwSelect check that bypasses the centralized sub-SELECT detection — doing so would silently reintroduce the dropped-UNION / dropped-projection bug (#1435). - it_query_sparql_setop_subselect: note that the OPTIONAL sub-SELECT test relies on an intentional uncorrelated COUNT broadcast, so a future reader does not "correct" it into a per-row correlated count.
aaj3f
added a commit
that referenced
this pull request
Jul 7, 2026
aaj3f
added a commit
that referenced
this pull request
Jul 7, 2026
…1436) CI runs the PR merge commit, and #1436 landed on main after this branch's baseline: test_21/test_23/test_64 (sub-SELECT placement positives) and subquery12 now pass — removed per the both-way stale-entry policing that flagged them. test_65 (syntax-SELECTscope2) now parses instead of failing for the wrong reason, so it moves to the accepts-invalid class awaiting the SELECT-scope validation pass (burn-down PR-2).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1435. Repairs the root cause behind fluree/azure-chat#42 (sub-SELECT in
UNION) and fluree/azure-chat#43 (sub-SELECT inMINUS).The bug
A SPARQL sub-SELECT (
{ SELECT ... }) used as an operand of a set/graph operation was mis-parsed, silently returning wrong results:{ SELECT (…AS ?n) } UNION { SELECT (…AS ?n) }{ SELECT (…AS ?n) } MINUS { SELECT (…AS ?n) }{ plain } UNION { SELECT (…AS ?n) }OPTIONAL { SELECT (COUNT(…) AS ?c) … }?cbound?clost, rows duplicatedSilent (HTTP 200, no error), which is worse than a hard failure — NL→SPARQL / SQL→SPARQL generators emit
{ SELECT } UNION { SELECT }and{ SELECT } MINUS { SELECT }routinely for "either/or" and "without" questions.Root cause
Parser::parse_group_graph_pattern(fluree-db-sparql/src/parse/query/pattern.rs) only recognised a{ SELECT ... }sub-SELECT inside its own nested-{branch. Every other position that opens a group —OPTIONAL,GRAPH,SERVICE, the right arm ofMINUS, each arm ofUNION(parse_union_continuation),EXISTS/NOT EXISTS(the freeparse_group_graph_pattern), and a bareWHERE { SELECT … }— consumes the{itself and then callsparse_group_graph_patternpositioned at theSELECT, where the leadingSELECTwas treated as an unexpected token.UNIONcheck (the plain-group branch had one), soUNIONhit"UNION must follow a pattern"and was discarded — the query collapsed into two independent subqueries lowered as a conjunction. At execution that becomes a correlated join on the shared projected variable; the arms' values are disjoint, so one arm survives (the smaller-cardinality one; ties → left) or 0 rows. (So it is not "only the last arm," as azure-chat#42 guessed.)(expr AS ?v)projection dropped. MINUS then shares no bound variable with the left → subtracts nothing; OPTIONAL loses the projected/aggregate variable.Lowered IR confirms it — before:
{SELECT} UNION {SELECT}→[Subquery, Subquery](noUnion);{SELECT} MINUS {SELECT}→[Subquery, Minus(Triple×4)](projection gone).The fix (parser-only)
fluree-db-sparql/src/parse/query/pattern.rs:SubSelectalternative at the top ofparse_group_graph_pattern(the opening{is already consumed by the caller), so every caller implements the grammar ruleGroupGraphPattern ::= '{' ( SubSelect | GroupGraphPatternSub ) '}'. This fixes MINUS/OPTIONAL/GRAPH/SERVICE/EXISTS/UNION-right and bareWHERE { SELECT }in one place.{branch throughparse_group_graph_pattern(which now handles both a group and a sub-SELECT) and run the trailing-UNIONcheck for a sub-SELECT left arm too.Error recovery for a malformed sub-SELECT is preserved (skip to the matching
}).Performance
No query-engine change. Fixed queries now compile to the same
UnionOperatorthe equivalent flatUNIONalways used, and to a proper sub-SELECTMINUS— i.e. the already-optimised plans. The buggy path was actually a more expensive per-row correlatedSubqueryOperatorjoin chain. So this is neutral-to-positive on speed, with no memory impact.Testing
fluree-db-sparql): sub-SELECT in UNION (both arms + mixed group/sub-SELECT), MINUS right arm, OPTIONAL body.assert_parsesalso guards that the spurious"UNION must follow a pattern"diagnostic is gone.fluree-db-api/tests/it_query_sparql_setop_subselect.rs): the verbatim azure-chat 42 & 43 queries and near-neighbors, asserting end-to-end results (UNION == flat UNION; MINUS == FILTER NOT EXISTS; OPTIONAL binds the aggregate).syntax-SELECTscope1, a positive{SELECT} UNION {SELECT}syntax test), subquery 11 → 12 (sq12, bareWHERE { SELECT }in CONSTRUCT), negation 12 → 12 (no W3C test covers sub-SELECT-as-MINUS-operand — the new integration tests fill that gap). Zero regressions.grp_query_sparql(265) andgrp_queryJSON-LD (303) integration groups pass;cargo clippy -p fluree-db-sparql --all-features -- -D warningsclean.{query}-in-union already worked and still passes).Files
fluree-db-sparql/src/parse/query/pattern.rs— the fix.fluree-db-sparql/src/parse/query/tests.rs— parser AST regression tests.fluree-db-api/tests/it_query_sparql_setop_subselect.rs(+grp_query_sparql.rs) — integration regression tests.