Skip to content

feat(sparql): burn-down Wave 3 — within-ledger FROM (D-3), UPDATE graph-management (D-5/6), equality/EBV/promotion (D-12, #1447), USING+GRAPH over-delete (#1441), triple-term syntax (−145 tests)#1462

Open
aaj3f wants to merge 26 commits into
burndown/wave-2from
burndown/wave-3

Conversation

@aaj3f

@aaj3f aaj3f commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Wave-3 SPARQL W3C burn-down — within-ledger datasets, UPDATE graph-management, the equality/EBV/promotion lattice, and SPARQL 1.2 triple-term syntax

This is the capstone wave of the W3C SPARQL test-suite burn-down: seven thematically-coherent PRs integrated onto burndown/wave-2. It greens 145 W3C tests to a suite that is 0-failed / 0-stale in both directions (cd testsuite-sparql && cargo test36 passed), shrinking the skip register from 376 to 207 entry lines (343 → 198 unique tests). The wave is perf-neutral: the integrated A/B vs burndown/wave-2 is within the 5% budget on all ten measured scenarios (worst +2.34%). The seven PRs are PR-G2 (within-ledger FROM/FROM NAMED), PR-W1 (nested-group FILTER scope + sub-SELECT correlation), PR-W2BC (SPARQL 1.2 triple-term syntax), PR-W2 (CONSTRUCT blank-node instantiation), PR-U3 (UPDATE graph-management verbs), PR-U6 (USING + explicit-GRAPH delete scoping), and PR-X2 (the equality/EBV/numeric-promotion lattice).

Stacks on the Wave-2 PR (#1454) — this PR branches from burndown/wave-2; review and merge it after #1454.

Reviewer quick reference

End-user API / contract changes. SPARQL SELECT … FROM <g> FROM NAMED <n> against a single ledger's graphs now returns results instead of being rejected (PR-G2). Seven SPARQL UPDATE graph-management verbs — LOAD/CLEAR/DROP/CREATE/COPY/MOVE/ADD (+ SILENT/INTO) — now parse and execute, and the capability is exposed on the transact-builder surface (Txn::clear_graph/drop_graph/copy_graph/clear_default_graph, GraphTransactBuilder::clear_graph/drop_graph/copy_graph) (PR-U3). SPARQL/JSON-LD =/!=, effective-boolean-value, AND/OR, numeric promotion, CONCAT, and SUM/AVG are now spec-strict value semantics (PR-X2 — DELIBERATE SEMANTICS CHANGE (D-12), 2-reviewer sign-off requested) — this can return fewer rows for queries that relied on the old permissive behaviour; see the migration notes. DELETE … USING <g> WHERE { GRAPH <h> … } now scopes the explicit GRAPH block to the USING NAMED set (deletes fewer rows — a data-loss fix) (PR-U6). A CONSTRUCT whose template has a blank node now emits triples instead of an empty graph (PR-W2). SPARQL 1.2 triple-term syntax (TRIPLE/SUBJECT/PREDICATE/OBJECT/isTRIPLE and bare <<( s p o )>> values) now parses/validates and defers evaluation with a clean not_implemented (PR-W2BC). PR-W1 has no API surface change.

Performance verdict: WITHIN BUDGET. Integrated A/B vs burndown/wave-2, FLUREE_BENCH_PROFILE=full (n=100): all ten scenarios within the 5% budget, worst +2.34% (query q5). The whole wave — including the u3/u6 transact/staging path — is perf-neutral. Full table below.

Production bugs found & fixed (beyond W3C compliance). (1) USING-scoped delete with an explicit GRAPH block over-deleted from the default/other graph — a real data-loss bug, Closes #1441 (PR-U6). (2) Predicate-grouped COUNT mis-tagged xsd:long; SUM/AVG silently averaged over the numeric subset of a mixed group; AND/OR aborted the whole expression on the first operand error; CONCAT silently coerced non-strings — Closes #1447 (PR-X2). (3) A JSON-LD correlated sub-SELECT returned an extra non-conformant row when a correlation variable was bound only via an inner OPTIONAL (PR-W1). (4) PR-U2's cross-operation blank-node scope check over-rejected valid multi-op requests, and the W3C harness rejected a legitimate bare mf:result [] empty-store expectation (both fixed in PR-U3).

What this wave does (per PR)

PR-G2 — within-ledger FROM / FROM NAMED (D-3, Option A). Replaces the single-ledger dataset-clause rejection on the buffered query paths with within-ledger dataset construction: each clause IRI is resolved against the ledger's own graph registry (alias → default graph, registered named-graph IRI → that g_id), assembled into a single-snapshot DataSetDb, and executed through the existing DatasetOperator/dataset_query.rs path. A clause IRI that is not a graph in this ledger is still rejected (cross-ledger goes through the connection path). R2RML + within-ledger FROM is a documented unsupported combination (the R2RML dataset path is non-Send). Also fixes a mutual-async-recursion compile trap the delegation created — query_dataset_with_options gets a concrete boxed return (Pin<Box<dyn Future … + Send>>), dropping cargo check on the crate from 30+ min to ~7s.

PR-W1 — nested-group FILTER scope + sub-SELECT correlation. Two independent evaluation fixes. Family A: a single-child { } group whose child is scope-sensitive (FILTER/BIND/nested Group) stays wrapped as an uncorrelated subquery rather than being flattened into the enclosing scope (so a nested FILTER no longer sees an enclosing-scope variable). Family B: a sub-SELECT correlation variable bound only via OPTIONAL/UNION is reconciled at merge (excluded from the per-row seed so it binds independently, then dropped on a conflicting double-binding) — the compatible-mapping join SPARQL §18.4 requires. Common self-produced-correlation and BGP/OPTIONAL paths (incl. BSBM) stay byte-identical.

PR-W2BC — SPARQL 1.2 triple-term syntax (D-1, accept-then-defer). The lexer emits a single contextual TripleTermFn token for the five function names (not reserved keywords), the expression parser resolves + arity-validates them, and a new parse_triple_term_value accepts <<( s p o )>> in the subject/object/VALUES/BIND positions RDF 1.2 allows; lowering rejects all deferred forms with not_implemented. The §2 negative-suite guardrails (no path/collection/reified-triple inside a term; value-context vs pattern-context subject rules; bare <<( )>> . is not a statement) keep every negative test rejected. No new evaluable capability — this is syntax acceptance only.

PR-W2 — CONSTRUCT per-solution blank-node instantiation. A CONSTRUCT template blank node ([ … ], _:a, or a PR-1 collection cell) previously lowered to a never-bound variable and produced an empty graph. The fix records template blank-node variables at lowering (ConstructTemplate.bnode_vars) and mints a fresh blank node per solution row in the CONSTRUCT output path — shared within a row, distinct across rows — under a reserved cst{n} label prefix provably disjoint from stored (fdb-), BNODE() (b{hex}), and SERVICE blanks (a soundness hardening the isomorphism-based suite cannot catch).

PR-U3 — SPARQL UPDATE graph-management verbs (D-5, D-6). Grammar + AST + parse + lowering to a new GraphMgmtOp Transaction-IR directive + a whole-graph-scan staging primitive (stage_graph_mgmt). A named graph is a reserved GraphId; the execution primitive is a scan of a graph's flakes — clone-as-retraction for CLEAR/DROP, clone-with-rewritten-g for COPY/MOVE/ADD (datatype/lang/list-index ride along verbatim). CLEAR/DROP ALL|NAMED resolve their target set from the registry; COPY/MOVE compose over CLEAR with a set-difference so overlapping triples are never simultaneously asserted and retracted. The dispatch is a single if txn.graph_mgmt.is_some() check at the top of stage(), before any hot-path setup, so ordinary insert/upsert/update staging is byte-identical.

PR-U6 — USING + explicit-GRAPH dataset scoping (class F, #1441). SPARQL 1.1 §3.1.3: when USING/USING NAMED is present it defines the WHERE dataset exactly — the WHERE-visible named graphs are precisely the USING NAMED set (empty under a plain USING <g>). The bug treated an empty USING NAMED set as "no restriction" and registered every named graph, so DELETE { … } USING <g3> WHERE { GRAPH <g2> { … } } matched g2 and over-deleted. The fix restricts the runtime dataset's named graphs to exactly the USING NAMED set whenever any USING/USING NAMED clause is present; the no-USING path (plain DELETE WHERE { GRAPH <g> … }) stays byte-identical. Prepare-time only; SELECT queries never reach it.

PR-X2 — equality / EBV / numeric-promotion lattice (D-12). DELIBERATE SEMANTICS CHANGE — 2-reviewer sign-off requested. Five IR/engine value-semantics fixes on the shared fluree-db-query evaluator: D4 numeric promotion (ComparableValue gains first-class Float(f32) so integer < decimal < float < double holds and xsd:float no longer widens to xsd:double); D-EBV (a fallible, datatype-aware bare-variable effective-boolean-value — numeric-zero/empty-string falsy; lang-tagged/foreign-datatype/IRI/ill-typed/unbound → a type error excluding the row); D5/D7 (datatype-aware three-valued rdf_term_equal for =/!=, with §17.2 three-valued AND/OR); D11 (CONCAT type-errors on a non-string argument); and aggregates (predicate-grouped COUNTxsd:integer; SUM/AVG poison-to-unbound on a non-numeric member). The common int-int/iri-iri fast paths are kept first; the added per-row cost is one well-known-Sid tag-check on the xsd:double and xsd:string literal paths plus the EqOutcome wrapping on =/!=, measured neutral (query_hot_bsbm_bi/f2 −0.51% on X2's own standalone branch-vs-wave-2 n=100 A/B; the whole-wave integrated f2 is +1.80% in the perf table below — both well inside the 5% budget, and the difference between the two is measurement, not an X2 cost, since no other wave-3 PR touches the FILTER/eval hot path f2 exercises).

Decisions actioned in this wave

Per the standing transparency rule (ROADMAP §4), each decision point, the options weighed, and why the adopted option was chosen.

D-1 — SPARQL 1.2 triple-term syntax: accept-then-defer (PR-W2BC). Options: (4) accept + arity-validate the syntax, lower to not_implemented [adopted]; (3) reject-on-principle as a documented divergence; (2) desugar <<( )>> to the reifier model (dominated — a triple term is a value, a reifier is a resource, so isTRIPLE/SUBJECT/term-equality would silently diverge). Chosen Option 4: it makes Fluree parse standard SPARQL 1.2 today at zero engine/perf risk — exactly what a syntax-conformance suite rewards — while honestly deferring evaluation to the Option-1 first-class-value epic. The trade-off (a runtime not_implemented where there used to be a parse error) is named explicitly; failing fast at parse time is Option 3, a genuine either/or the team owns.

D-3 — within-ledger FROM/FROM NAMED datasets (PR-G2). Options: (A) engine within-ledger DataSet reusing the runtime dataset path [adopted]; (B) harness ledger-per-graph with query_connection_sparql (zero engine change). Both green the same 13 tests, so the tie-breakers are product value, reuse, and perf — all favour A. A fixes the actual user-facing gap (a real user can query one ledger's named graphs), extends #1279's within-ledger registry the natural way (a named graph is a g_id), and keeps a single snapshot (no cross-ledger provenance stamping, binary store stays enabled); B only satisfies the harness and spins up N ledgers per query.

D-5 — remote (non-SILENT) LOAD: documented divergence (PR-U3). Options: (a) parse LOAD, execute SILENT as a swallowed no-op, error on non-SILENT [adopted]; (b) add an opt-in HTTP fetch hook into the transact path. Chosen (a): it costs zero W3C coverage (the two eval tests LOAD a non-resolvable source under SILENT and expect the store unchanged; the syntax tests are parse-only), whereas (b) would drag an HTTP client and its failure/security surface into the embedded write path for no test benefit (ingesting a known document already has a first-class path — the insert API).

D-6 — DROP ≡ CLEAR + the empty-named-graph model (PR-U3). First half — DROP is implemented as CLEAR semantics now (the GraphRegistry is additive-only, and the two are indistinguishable to the harness, which compares/lists only non-empty graphs). Second half (a spotlighted product call) — whether a CLEAR'd/CREATE'd zero-triple named graph is enumerable by GRAPH ?g/FROM NAMED: Option A (persist a graph-existence record decoupled from flakes across storage/commit/query, greens bindings#graph + agg-empty-group-count-graph, −2) vs Option B (documented permanent divergence — a graph exists iff a flake carries its g_id) [adopted]. Chosen B: the capability is a query/dataset-model feature orthogonal to the UPDATE verbs, it is the same model fact that makes DROP≡CLEAR correct, and Option A's blast radius (three subsystems, a durable "empty graph" concept Fluree deliberately lacks) is disproportionate to −2 tests. Both tests stay registered with the full rationale.

D-12 — SPARQL strict-type-error mode vs unified strictness (PR-X2). Options: (a) a per-surface strictness flag threaded through evaluation; (b) one unified strict value-semantics path shared by all surfaces [adopted for equality/EBV/promotion]; (c) leave lenient. Chosen (b): equality/EBV/promotion are value semantics, correct on both the SPARQL and JSON-LD surfaces, and the parity tests assert the strict behaviour on both. Deliberate JSON-LD extensions are preserved by reading the binding directly rather than the surface (the lenient From<&Binding> EBVs stay for Cypher structural truthiness; PR-X1 already actioned the D2b DATATYPE/LANG @id carve-out).

D-11 (lexical-form preservation) is NOT actioned this wave — its tests (group01, the sameTerm/distinct/dawg-str lexical set) remain registered and are owned by the future PR-X3.

Register entries removed (145 unique; suite-driven, both directions)

  • PR-G2 (13): SPARQL10_QUERY_EVAL dawg-dataset-01..12b (12) + SPARQL11_CONSTRUCT constructwhere04.
  • PR-W1 (3): SPARQL10_QUERY_EVAL filter-nested-2, dawg-optional-filter-005-not-simplified, join-scope-1.
  • PR-W2BC (27): SPARQL12_SYNTAX_TRIPLE_TERMS_POSITIVE → empty (basic/bnode/compound/expr/inside/nested/subject/update-tripleterm + compound-all).
  • PR-W2 (3): SPARQL10_QUERY_EVAL construct-3, construct-4 + SPARQL11_CONSTRUCT constructlist.
  • PR-U3 (68 unique tests / 91 register lines): the remaining SPARQL11_UPDATE graph-management eval tests (add/clear/copy/create/drop/load/move + -silent variants, the 4 same-bnode joint tests) and the 23 double-registered SPARQL11_SYNTAX_UPDATE_1 test_* (both copies deleted per the standing rule, so 23 tests remove 46 lines; that register is now empty). Per-PR unique counts sum to the 145 headline (13 + 3 + 27 + 3 + 68 + 2 + 29); in raw register lines the wave removes 169.
  • PR-U6 (2): SPARQL11_UPDATE dawg-delete-using-02a, dawg-delete-using-06a (empties the last class-F cluster).
  • PR-X2 (29): agg02, agg-err-01, concat02, strlang03-rdf11, dawg-langMatches-1/2/3/basic, type-promotion-03/04/05/21/29/30, {add,subtract,multiply,divide}-numbers-cast, unplus-2, unminus-2, dawg-bev-1..6, eq-2-1, eq-2-2, open-eq-04.

Query-surface parity

Per docs/contributing/sparql-compliance.md §"Query Surface Parity" (JSON-LD only; Cypher excluded — we do not own the openCypher grammar): every IR/engine-level fix carries a JSON-LD regression test alongside the SPARQL one, since the W3C submodule guards only the SPARQL surface. PR-X2 (value semantics), PR-W1 (scope/correlation — incl. the JSON-LD-only correlated-subselect bug fix), and PR-G2 (within-ledger dataset, a single construction over both surfaces) are IR/engine (case a) with JSON-LD tests in it_query_expression_semantics.rs, it_query_filter_scope.rs, and it_query_dataset.rs. PR-U3 exposes the new graph-management capability on the transact-builder surface in the same PR (it_named_graphs.rs::test_graph_mgmt_transact_builder_parity); a JSON-LD document key syntax is a deliberately-deferred surface-design decision. PR-U6 is a SPARQL USING surface fix with a JSON-LD fromNamed guard test on the shared machinery (the symmetric JSON-LD from-without-fromNamed restriction is a scoped follow-up). PR-W2 (CONSTRUCT bnode) and PR-W2BC (parse-only) are SPARQL-only surface (case c) — the FQL construct form has no blank-node template syntax, and accept-then-defer introduces nothing newly executable.

Performance (integrated A/B vs burndown/wave-2 @ 3040d8e)

FLUREE_BENCH_PROFILE=full FLUREE_BENCH_SCALE=small (real n=100; query_hot_bsbm.rs:166 sets sample_size from the profile, so the env var is the real knob). Query benches whole-bench interleaved; insert/import benches per-scenario fine-interleaved (wave-2↔wave-3 back-to-back) to kill cross-run drift; self-gated on rustc=0; contended runs discarded. Δ% = (wave-3 − wave-2)/wave-2 (positive = wave-3 slower); budget 5% at small.

  • query_hot_bsbm: q3 −2.27%, q5 +2.34% (worst), q9 +0.80% — CIs 0.4–0.6%.
  • query_hot_bsbm_bi: f2 +1.80% — CI 0.4/0.6%.
  • insert_formats: jsonld/100nodes −1.28%, jsonld/10nodes +0.40%, turtle/100nodes +0.10%, turtle/10nodes +0.98%.
  • import_bulk: single_threaded −0.72%, default_threads +0.24%.

All within budget. An initial coarse-interleaved pass showed two apparent breaches (jsonld/100nodes +7.33%, import single_threaded +12.31%) that were foreign-build contention landing in the longer wave-3 windows, not a code regression: no wave-3 change touches the data-insert/bulk-import hot path (fluree-db-core + fluree-graph-turtle untouched; u3's shared-stage() change is a single per-transaction guard, byte-identical for inserts; u6's is behind sparql_where), and the clean fine-interleaved re-run collapsed both to −1.28% / −0.72% with tight CIs.

Deferrals (staying registered, with per-entry pointer comments)

The equality cluster's remaining tests are blocked outside the equality/EBV/promotion lattice and stay registered with X2-owned notes: the D5b scan-path EncodedLit datatype-carry family (eq-4, open-eq-02/05/06, dawg-lang-3, quotes-3/4, eq-graph-1/2/4 — bare-BGP object term-match, deliberately not risking the binary-index hot path), blank-node OUTPUT-identity isomorphism (open-eq-07/08/10/11/12), temporal timezone semantics (eq-dateTime, date-1), dawg-langMatches-4 (LANG-of-IRI error propagation), not-not (ill-typed literal preservation, → PR-X3), and agg-count-rows-distinct (COUNT(DISTINCT *) needs a new CountDistinctAll IR aggregate + whole-row group plumbing — a perf-neutral post-wave-3 follow-up). The D-6 empty-named-graph divergence keeps bindings#graph + agg-empty-group-count-graph registered (PR-U3). D-11 lexical-form tests (group01, sameTerm/distinct/dawg-str) are PR-X3's. nested-opt-1/2 (correlated-OPTIONAL independence) is PR-W1-OPT; join-combo-2 + optional-complex-2/3/4 are PR-G1 (GRAPH-variable) territory.

Verification & harness/CI notes

Verified: full W3C suite 36 passed; 0 failed; 0 ignored (0 stale, both directions); -p fluree-db-query -p fluree-db-api -p fluree-db-transact -p fluree-db-sparql 0-failed (32 bins; fluree-db-query 1178, grp_query 351 / grp_query_sparql 282 / grp_misc 243); cargo fmt --all --check clean; cargo clippy --all-targets --features native -- -D warnings 0 warnings.

Harness notes for reviewers: testsuite-sparql is an EXCLUDED separate cargo workspace — run the W3C suite with cd testsuite-sparql && cargo test, not top-level cargo test. A fresh git worktree add does NOT populate the testsuite-sparql/rdf-tests submodule — run git submodule update --init before the suite or every manifest spuriously fails. fluree-db-api uses grouped test bins (grp_query, grp_query_sparql, grp_misc, …), so scope with the full -p fluree-db-api; a per-file --test <name> matches nothing. The pre-existing resolve_shapes_source_g_ids dead-code warning in fluree-db-api/src/tx.rs:506 is base noise (untouched by this wave; does not fire under clippy --all-targets).

Migration / changelog

  • Equality/EBV are spec-strict: comparing to an unrecognized/ill-typed-datatype literal, and the truth value of a numeric-zero / empty-string / non-boolean-typed literal, follow SPARQL §17 (type errors exclude rows; numeric zero and empty strings are falsy); AND/OR apply three-valued dominance; CONCAT and SUM/AVG raise type errors rather than coercing; numeric promotion preserves xsd:float (so xsd:float(10)/4 serializes as a typed xsd:float literal, not a bare number). Queries relying on the old permissive behaviour may return fewer rows.
  • New SPARQL UPDATE verbs (LOAD/CLEAR/DROP/CREATE/COPY/MOVE/ADD + SILENT/INTO). DROP ≡ CLEAR (empties, does not unregister); CLEAR/DROP DEFAULT|ALL retract default-graph schema too (scope schema into a named graph if it must survive); CREATE is a no-op; non-SILENT remote LOAD errors (no HTTP client in the transact path — use LOAD SILENT or the insert API); empty named graphs are not enumerable. New builder API: GraphTransactBuilder/Txn clear_graph/drop_graph/copy_graph.
  • DELETE … USING <g> WHERE { GRAPH <h> … } now follows §3.1.3 — the GRAPH <h> block matches only when <h> is in USING NAMED; updates relying on the old over-reach should add USING NAMED <h>.
  • SPARQL CONSTRUCT templates with blank nodes now emit triples (a bug fix aligning with §16.2; no migration).
  • SPARQL 1.2 triple-term functions and <<( )>> values now parse/validate and produce a lower-time not_implemented where they previously produced a parse error.

Issues closed on merge

Closes #1441 (PR-U6 — the USING + explicit-GRAPH delete over-delete data-loss bug; PR-U6 greens dawg-delete-using-02a/06a, fully resolving it — the issue has no JSON-LD component). Closes #1447 (PR-X2 — aggregate / equality / EBV conformance). The symmetric JSON-LD from-without-fromNamed restriction PR-U6 identified is a separate scoped follow-up (file a new issue if it needs tracking), not part of #1441.

Mechanics: the Closes keywords fire only when this PR merges into the repository's DEFAULT branch. In the stacked burn-down flow (Wave 3 → burndown/wave-2burndown/wave-1main), merging this PR into burndown/wave-2 does NOT close #1441 or #1447 — GitHub auto-closes them only once the stack collapses to main and this PR is retargeted onto it. Until then the issue references stay linked but open.

aaj3f added 26 commits July 7, 2026 15:24
SPARQL CONSTRUCT templates containing blank nodes ([ ], _:a, or an RDF
collection ( )) evaluated to an empty @graph: template blank nodes lower
to variables the WHERE clause never binds, so instantiate_row dropped
every template triple at the (Some,Some,Some) guard.

Record the template blank-node variables (those whose registry name keeps
the `_:` prefix the term lowerer assigns) on ConstructTemplate.bnode_vars
at lowering, and mint a fresh blank node per solution row in the CONSTRUCT
formatter -- shared across a row's template triples, distinct across rows.
constructlist rides the same path via PR-1's collection desugaring.

Greens W3C construct-3, construct-4 (data-r2) and constructlist
(data-sparql11); their skip-register entries are removed suite-driven.
Add SPARQL CONSTRUCT regression tests proving a blank-node template mints
a fresh blank per solution -- one node per match, linking subject /
predicate / object within a row -- for both the anonymous [ ]
(construct-3 shape) and labeled _:a (construct-4 shape) forms.
…nting

Adversarial review found the per-solution CONSTRUCT blank labels were minted
as b{n}, which overlaps other blank-node producers' label spaces (BNODE() emits
_:b{hex}; SERVICE passes through _:b0). Safe today only because every real
Fluree producer uses the reserved fdb- prefix and external SERVICE is rejected,
but a mixed template could otherwise silently merge a minted blank with a data
blank — and the isomorphism-based W3C CONSTRUCT suite cannot catch that. Mint
into a reserved cst{n} prefix so the output is provably disjoint.
Recognize TRIPLE/SUBJECT/PREDICATE/OBJECT/isTRIPLE as arity-validated
function-call builtins that lower to not_implemented (burn-down D-1).

They are contextual function-call identifiers, not reserved keywords: the
lexer emits a single generic TripleTermFn token (carrying the canonical
name) and the expression parser resolves it via FunctionName::parse and
validates arity (TRIPLE/3, the rest/1). Greens the 3 bucket-B entries of
SPARQL12_SYNTAX_TRIPLE_TERMS_POSITIVE (expr-tripleterm-03/04/05).
Accept bare `<<( s p o )>>` triple-term values in the subject, object,
VALUES, and BIND positions the RDF 1.2 grammar allows, lowering them to
not_implemented (burn-down D-1). A shared parse_triple_term_value enforces
the grammar guardrails so the (empty) negative register stays rejected:
no path/collection/reified-triple inside a term; a value-context term
(VALUES/BIND) may not have a blank-node or nested-triple-term subject,
while a pattern-context term may; a bare `<<( )>> .` is not a statement.
BIND `<<( )>>` reuses the TRIPLE(...) function path. Greens the 24
bucket-C entries of SPARQL12_SYNTAX_TRIPLE_TERMS_POSITIVE (register now
empty). Two pr-w2a unit tests that asserted the pre-D-1 parse rejection
are updated to the accept-then-defer behavior; the rdf:reifies reifier
path keeps rejecting nested triple-term subjects.
A single-child group graph pattern was always unwrapped to its bare child
(parse_group_graph_pattern), erasing the `{ }` scope boundary. When the child is
a FILTER, BIND, or nested Group that boundary is load-bearing: the lowerer only
wraps *Group* children as uncorrelated subqueries, so an unwrapped
`{ FILTER(?v) }` or `OPTIONAL { { ... FILTER(?title) } }` lands in the enclosing
scope and the FILTER wrongly sees an enclosing-scope variable.

Keep a single-child group wrapped when the child is scope-sensitive
(GraphPattern::is_scope_sensitive: Filter/Bind/Group); the existing lowerer path
then routes it through an uncorrelated subquery evaluated in an independent
scope. Relax the lowerer's nested-Group invariant assertion accordingly.

Greens W3C filter-nested-2 and dawg-optional-filter-005-not-simplified. Adds
JSON-LD parity regression tests (it_query_filter_scope.rs) for the shared
filter-scope semantics guarded only on the SPARQL surface by the W3C submodule.
A correlation variable a sub-SELECT binds only via OPTIONAL/UNION is not
self-produced, so subquery.rs excluded it from the hash join key and the merge
kept the parent's value while silently dropping the subquery's conflicting
binding — W3C var-scope-join-1 returned rows where the natural join on that
variable must eliminate them.

Partition correlation_vars into join_keys (self-produced, unchanged hash path)
and reconcile_vars (the rest). Reconcile vars are (a) excluded from the per-row
seed so the subquery binds them independently instead of pinning them to the
parent value, and (b) checked at the merge: a row whose reconcile var is bound on
both sides to different terms is dropped (SPARQL 18.4 compatible-mapping join).
self_produced_vars and the join_key hash path are untouched, so BSBM-shape
correlated subqueries (self-produced correlation) stay byte-identical.

Greens W3C join-scope-1. The seed exclusion also brings the JSON-LD surface's
correlated sub-SELECT into parity: the SPARQL nested group lowers to an
uncorrelated subquery (join-mode), where the merge reconcile suffices; JSON-LD
`["query", ...]` is correlated (per-row seeded), where the merge check alone is a
no-op without the seed change. Adds the JSON-LD parity test for the join-scope
shape.
…he update harness

The UpdateEvaluationTest guard rejected any mf:result that parsed to no
ut:data/ut:graphData/ut:result, on the assumption that a deliberate
empty-store expectation always carries ut:result ut:success. That is false:
the W3C graph-management tests (DROP ALL, the update-silent/* cases) express
"expected: empty store" as a bare mf:result [] . Distinguish a present-but-
empty result node (legitimate empty expectation) from an entirely absent
mf:result (a real manifest mis-parse) via a new Test.result_present flag, and
fire the guard only on the latter.
…P/CREATE/COPY/MOVE/ADD)

Add grammar/AST/parse + execution for the SPARQL 1.1 Update graph-management
operations, plus SILENT/INTO. The verbs were already lexed; this is AST +
recursive-descent parse (parse/query/{mod,update}.rs, ast/update.rs) lowering
to a new Txn graph-management directive (transact/ir.rs) executed by a
whole-graph scan primitive at staging time (transact/stage.rs::stage_graph_mgmt):

- CLEAR/DROP GRAPH <g> | DEFAULT scan the g_id and emit retractions; CLEAR/DROP
  ALL|NAMED iterate GraphRegistry::iter_entries. DROP is semantically CLEAR
  (D-6): the registry is additive-only and an emptied graph is
  harness-indistinguishable from a dropped one.
- COPY/MOVE/ADD scan the source graph and re-home its flakes into the
  destination by rewriting only the flake's g (datatype/lang/list-index ride
  along), composed over the CLEAR primitive (COPY/MOVE clear the destination
  first, MOVE clears the source afterward). Set-difference against the
  destination avoids assert+retract of the same fact.
- CREATE and SILENT LOAD lower to an empty no-op transaction (Fluree cannot
  represent an empty named graph, D-6). Non-SILENT remote LOAD is a documented
  divergence (D-5): the embedded transact path has no HTTP client. No W3C eval
  test requires a real fetch.

The dispatch is a single Option check at the top of stage(); the ordinary
insert/upsert/update path and pr-u2's multi-op staging stay byte-identical.

Query-surface parity (compliance case 2): expose the new capability on the
non-SPARQL transact surface via Txn::clear_graph/drop_graph/copy_graph and
GraphTransactBuilder::clear_graph/drop_graph/copy_graph, with a parity test in
it_named_graphs.rs.

Also correct pr-u2's cross-operation blank-node scope check: it over-collected
from Modify (INSERT/DELETE ... WHERE) templates, wrongly rejecting the approved
positive tests insert-where-same-bnode/-2 (template bnodes are per-operation,
per-solution; reuse across operations yields distinct nodes). The scope rule
applies to ground DATA forms only (INSERT DATA / DELETE DATA), keeping the
syntax-update-54 negative test rejected.

Register: remove the 68 SPARQL11_UPDATE + 23 double-registered
SPARQL11_SYNTAX_UPDATE_1 graph-management entries (91 lines) that green.
Empty-named-graph model (D-6 second half) resolved as documented divergence:
bindings#graph and agg-empty-group-count-graph stay registered with a
divergence comment (Fluree models a graph as existing iff a flake carries it).
`v.as_array().map(|a| a.len())` trips clippy::redundant_closure_for_method_calls,
which is deny at the workspace level, so `cargo clippy --all-targets` failed to
compile the grp_graphsource test target. `as_array()` yields `Option<&Vec<Value>>`,
so neither `<[_]>::len` (expects &[_]; E0631) nor `Vec::len` (no inherent len)
works as a method reference — use an explicit match instead.
A SPARQL FROM / FROM NAMED clause naming graphs within the queried ledger (the
ledger alias -> the default graph; registered named-graph IRIs -> named graphs)
now builds a within-ledger DataSet over the one snapshot and runs through the
shared dataset execution path, reusing DatasetOperator, policy and reasoning.
Cross-ledger clause IRIs are rejected (use the connection path). Greens the 12
dawg-dataset tests + constructwhere04; the harness pre-loads clause-referenced
files as named graphs.

The delegation created a mutual async recursion between query_with_options and
query_dataset_with_options (whose single-ledger fast path delegates back), which
made the fluree-db-api type-check pathologically slow (30+ min) and defeated Send
inference. Broken by giving query_dataset_with_options a concrete boxed return
(-> Pin<Box<dyn Future + Send>>), which resolves the opaque-recursive auto-trait
cycle. R2RML + within-ledger FROM is a documented unsupported combination (the
R2RML dataset path is non-Send and runs from Send-required spawn contexts) and is
rejected rather than silently mis-executed.

Actions decision D-3 (within-ledger datasets, Option A engine construction).
`DELETE {..} USING <g> WHERE { GRAPH <h> {..} }` over-deleted: the explicit
`GRAPH <h>` block reached graph h even though `USING <g>` (no `USING NAMED <h>`)
scopes the WHERE dataset to default=g with an empty named-graph set. Per SPARQL
1.1 §3.1.3 the GRAPH block must then match nothing ("the GRAPH clause does not
override the USING clause").

`stream_where_into_accumulator` treated an empty `USING NAMED` set as "no
restriction" and registered every named graph in the runtime dataset, so the
`GRAPH <h>` probe found h and generated retractions against the default-graph
DELETE template. Restrict the WHERE-visible named graphs to exactly the
`USING NAMED` set whenever any `USING`/`USING NAMED` clause is present; keep the
ambient all-named-graphs dataset only when no USING clause is present at all, so
a plain `DELETE WHERE { GRAPH <g> {..} }` stays byte-identical.

Greens W3C dawg-delete-using-02a/06a and adds a JSON-LD `fromNamed` graph-scoped
delete-where parity test.
…egates

Predicate-grouped COUNT fast paths (StatsCountByPredicateOperator,
PredicateObjectCountFirsts) tagged the count xsd:long, but SPARQL COUNT is
xsd:integer (§18.5.1.6) — matching the materialized and streaming finalize
paths. Greens agg02.

SUM/AVG silently skipped bound non-numeric group members and averaged over
the numeric subset; per §18.5 a non-numeric member is a type error that
unbinds the whole aggregate. Poison both the materialized (aggregate.rs) and
streaming (group_aggregate.rs) paths; the streaming numeric hot path is kept
first so it stays byte-identical. Greens agg-err-01.
…lang match

CONCAT silently skipped a non-string (or unbound) argument, coercing e.g. an
xsd:integer away; per §17.4.3.5 a non-string argument is a type error, so the
whole result is unbound (concat02).

Language tags are case-insensitive (RDF 1.1 §3.3), and a language-tagged
literal's datatype is rdf:langString even though the SPARQL results format
writes only xml:lang. Normalize both in the harness result comparison so a
produced en-US / no-datatype literal matches an expected en-us / rdf:langString
fixture (strlang03-rdf11, dawg-langMatches-1/2/3/basic).
Numeric promotion collapsed xsd:float into xsd:double and produced xsd:decimal
for double+decimal. Add a first-class ComparableValue::Float(f32) so the XPath
lattice integer<decimal<float<double holds: float o {integer,decimal,float} =
float, float o double = double, double o decimal = double (op:numeric-*).

xsd:float is stored as an f64 tagged only by its datatype (coerce.rs), so
eval_to_comparable recovers the float type with a single datatype check on the
xsd:double literal path; the Long/String fast paths stay byte-identical.

Greens type-promotion-03/04/05/21/29/30 and expr-ops
{add,subtract,multiply,divide}-numbers-cast, unplus-2, unminus-2.
FILTER(?v) routed a bare variable through a bool-returning EBV where every
literal except xsd:boolean false was truthy and unbound was false. Per §17.2.2
the EBV of numeric-zero / empty-string is false, and a language-tagged or
foreign-datatype literal, an IRI, an ill-typed literal, or an unbound variable
is a type error (a demotable Comparison error, so a FILTER excludes the row and
a BIND leaves the variable unbound). Route the bare-variable path through a
fallible binding_effective_bool; the lenient From<&Binding>/From<ComparableValue>
EBVs stay for the Cypher structural surfaces (lists/maps/paths/rels).

Greens dawg-bev-1..6. not-not stays registered: its "z"^^xsd:boolean VALUES
row is stored coerced to Boolean(false), losing its ill-typedness before EBV —
blocked on ill-typed-literal preservation (D6 / PR-X3).
`=`/`!=` dropped datatypes: a foreign-datatype literal ("zzz"^^:t) compared
equal to a plain "zzz", and incomparable operands returned false/true rather
than a type error. Carry a foreign string datatype into the ComparableValue at
the eval boundary (the Long / xsd:double / xsd:string / lang fast paths stay
byte-identical; language tags are deliberately NOT carried, to keep the string
builtins working) and route `=`/`!=` through a three-valued RDFterm-equal
(§17.4.1.7): value equality with numeric promotion and plain = xsd:string; a
resource is never equal to a literal; two differently-typed recognized values
are known-unequal; an unrecognized / ill-typed datatype that might still denote
the same value is a type error excluding the row for both `=` and `!=`.

Also correct AND/OR to SPARQL three-valued logic (§17.2): false / true dominate
over an operand error instead of aborting on the first one (open-cmp-02).

Greens eq-2-1, eq-2-2, open-eq-04. The rest of the equality cluster stays
registered on separate blockers (typed-literal constant datatypes, blank-node
output identity, temporal timezone semantics, scan-path constraints).
… deferral notes

JSON-LD-surface regressions alongside the SPARQL ones (the W3C submodule guards
only the SPARQL surface): numeric-promotion result datatype (D4), datatype-aware
EBV (D-EBV), RDFterm-equal value equality vs known-unequal (D5), CONCAT
non-string type error (D11), AVG non-numeric poison (agg-err-01), plus
wave-2-failing discriminating regressions a revert would fail: foreign /
ill-typed datatype vs plain string is a type error under both = and != , and
three-valued OR (true dominates an operand error).

The predicate-grouped COUNT xsd:integer tests (agg02) are confirmatory-only, not
discriminating: a memory ledger routes through the general group_aggregate
finalize (already xsd:integer on wave-2) and bypasses the indexed
StatsCountByPredicateOperator fast path where the xsd:long bug lived; the fast
path is detected only for the bare `GROUP BY ?p COUNT(?o)` shape (no
post-aggregation bind), and xsd:long/xsd:integer both render as bare JSON
numbers, so the datatype can't be observed without dropping off the fast path.
The revert-catching coverage is the greened W3C agg02 test against indexed
storage (reviewer finding #4b).

Realigns two pre-existing float-division tests (grp_query / grp_query_sparql) to
the typed-literal rendering now that D4 keeps the result xsd:float instead of
widening it to xsd:double.

Also updates register comments to name PR-X2 as decision-owner for the deferred
cluster with the real root cause (scan-path BGP object-match for
eq-graph/open-eq-02/quotes; temporal for date-1/eq-dateTime; blank-node output
identity for open-eq-07..12; typed-constant lowering for open-eq-05/06). fmt
normalization of the eval files.
# Conflicts:
#	testsuite-sparql/tests/registers/mod.rs
# Conflicts:
#	fluree-db-transact/src/lower_sparql_update.rs
# Conflicts:
#	testsuite-sparql/tests/registers/mod.rs
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.

1 participant