Observed (v0.6.27, source + live)
FILTER EXISTS / FILTER NOT EXISTS are not translatable in any form — plain BGP or GRAPH-wrapped both refuse:
sparql: FILTER expression not translatable: Not(Exists(Bgp { … }))
sparql: FILTER expression not translatable: Not(Exists(Graph { … }))
Source-confirmed: the FILTER→SQL translator has no Expression::Exists arm (src/query/executor.rs — Exists appears only in the substitution pass, the walker no-op, and the contains-aggregate check). The refusal is honest and stable — never a wrong answer.
Why this matters
Downstream consumers are starting to route around SPARQL entirely for absence checks ("integrity as compiled SQL because SPARQL can't express it"). That belief is half wrong today — MINUS (graph-scoped) and OPTIONAL + FILTER(!BOUND(?x)) both translate and answer correctly, measured live:
# organs with no dependsOn — works on 0.6.27
SELECT ?o WHERE { GRAPH <urn:g> {
?o a ckp:Organ MINUS { ?o ckp:dependsOn ?x } } }
# same, negation-by-failure — also works
SELECT ?o WHERE { GRAPH <urn:g> {
?o a ckp:Organ . OPTIONAL { ?o ckp:dependsOn ?d } FILTER(!BOUND(?d)) } }
But NOT EXISTS is the form people write first, and the W3C-semantics differences (MINUS ignores disjoint-variable patterns; NOT EXISTS is a correlated anti-join) mean MINUS is not a universal substitute.
Proposal
Lower FILTER (NOT) EXISTS over a BGP onto the existing anti-join machinery: the SQL builder already implements graph-scoped anti-joins for MinusBlock; NOT EXISTS is the correlated variant (join on all shared in-scope variables), EXISTS the semi-join twin. GRAPH-wrapped inner patterns should reuse the same GraphScope plumbing MinusBlock.scope already has. Nested/complex inner patterns (OPTIONAL/UNION inside EXISTS) can stay a named refusal — scope this to the BGP-inner case that covers the integrity-check family.
v0.6.29+ candidate; not for the current release train.
Observed (v0.6.27, source + live)
FILTER EXISTS/FILTER NOT EXISTSare not translatable in any form — plain BGP or GRAPH-wrapped both refuse:Source-confirmed: the FILTER→SQL translator has no
Expression::Existsarm (src/query/executor.rs—Existsappears only in the substitution pass, the walker no-op, and the contains-aggregate check). The refusal is honest and stable — never a wrong answer.Why this matters
Downstream consumers are starting to route around SPARQL entirely for absence checks ("integrity as compiled SQL because SPARQL can't express it"). That belief is half wrong today —
MINUS(graph-scoped) andOPTIONAL + FILTER(!BOUND(?x))both translate and answer correctly, measured live:But
NOT EXISTSis the form people write first, and the W3C-semantics differences (MINUS ignores disjoint-variable patterns; NOT EXISTS is a correlated anti-join) mean MINUS is not a universal substitute.Proposal
Lower
FILTER (NOT) EXISTSover a BGP onto the existing anti-join machinery: the SQL builder already implements graph-scoped anti-joins forMinusBlock;NOT EXISTSis the correlated variant (join on all shared in-scope variables),EXISTSthe semi-join twin. GRAPH-wrapped inner patterns should reuse the sameGraphScopeplumbingMinusBlock.scopealready has. Nested/complex inner patterns (OPTIONAL/UNION inside EXISTS) can stay a named refusal — scope this to the BGP-inner case that covers the integrity-check family.v0.6.29+ candidate; not for the current release train.