Skip to content

test(sparql): burn-down Wave 0 — harness hygiene, parser accept-more, lexer/formatter, expression high-yield (−80 registers)#1452

Open
aaj3f wants to merge 25 commits into
mainfrom
burndown/wave-0
Open

test(sparql): burn-down Wave 0 — harness hygiene, parser accept-more, lexer/formatter, expression high-yield (−80 registers)#1452
aaj3f wants to merge 25 commits into
mainfrom
burndown/wave-0

Conversation

@aaj3f

@aaj3f aaj3f commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Reviewer quick reference

End-user API/contract changes: (a) xsd:double RDF-lexical output is now the XSD canonical form ("1000000"/"1E6""1.0E6") across SPARQL-JSON/XML, CSV/TSV, Turtle/N-Triples export, and LiteralValue::lexical() — downstream string-matchers must adjust (changelog note below); JSON-LD numeric output is deliberately unchanged. (b) SPARQL DATATYPE()/LANG() now accept expression arguments (additive), and DATATYPE on a non-literal is a type error on the SPARQL surface only (see Decisions). (c) Everything else in this wave is accept-more parsing or bug fixes — no previously-working query changes meaning.

Performance verdict: no hot-path regressions anywhere; all standard gates within regression-budget.json. One must-accept output cost: correct zero-length property-path answers are strictly larger result sets on shapes that previously returned wrong (incomplete) answers — details and numbers in Performance Notes below. This wave also adds a new pinned bench (query_hot_property_path).

Production bugs found & fixed along the way (not just W3C compliance): FILTER(true)/any variable-free filter silently returned zero rows (#1439); DATATYPE()/LANG() rejected composed expressions (#1440); Turtle import rejected valid _:label. (#1444); double serialization was non-canonical and internally inconsistent across formats (#1445); bare integers in VALUES minted xsd:long term identities (#1319 mechanism); and the W3C harness's own .rdf result parser dropped bound solutions on self-closing XML elements while keeping unbound ones.

Decisions actioned in this PR

D-12 (scoping of strict SPARQL type errors) — actioned by the expression component (pr-x1, D2b).

  • Decision point: when DATATYPE() is applied to a non-literal (IRI/bnode), SPARQL 1.1 requires a type error; Fluree's JSON-LD surface deliberately returns the @id extension value instead.
  • Options considered: (1) SPARQL-scoped strict errors, JSON-LD keeps its extension — chosen; (2) unified strictness across both surfaces — rejected: breaks a committed, test-pinned JSON-LD product behavior for zero correctness gain on that surface; (3) keep lenient behavior everywhere — rejected: fails W3C and leaves SPARQL spec-incorrect.
  • Why (1): the surfaces share the IR but not the contract; the parity guideline gets an explicit recorded carve-out, and both behaviors are pinned by tests so neither can drift silently. (Full table: docs/audit/burn-down/ROADMAP.md §4.)

D-13 (surfaced here, deliberately NOT actioned): the collections work revealed that Turtle ingest stores object-position RDF collections as native list_index items — rdf:first/rest triples never exist in storage, so basic#list-1..4 cannot green from parser work. Materialize-at-ingest vs translate-at-query vs documented divergence is an open team decision; the register entries say exactly this.


Wave 0 of the W3C SPARQL burn-down (6 components, ≈−80 register entries)

First implementation wave of the burn-down plan in docs/audit/burn-down/ROADMAP.md (see PR #1437 for the enforcement model). Every component was implemented against its adversarially-verified cluster audit, is individually suite-green, and was integrated here with register conflicts resolved to the mechanical fixpoint dictated by the both-way CI gate. Zero-to-low engine risk by design: this wave is harness fixes, parser accept-more, lexer/formatter corrections, and expression fixes on cold/erroring paths. All bench gates within regression-budget.json budgets.

Components

1. Harness + register hygiene (pr-h1, −3). Fixes the harness's .rdf/SRX result parsers: self-closing Event::Empty elements skewed the state stack (keeping unbound solutions and dropping bound ones — the mechanism was verified live against result-sort-3.rdf), plus missing rdf:nodeID blank-node value support. Applies the full register-comment correction batch so every entry names its root cause and owning future PR. Also commits the standalone-[workspace] marker for the harness (root path-exclusion doesn't reach git worktrees).

2. Parser accepts valid syntax (pr-1, −29). RDF collections ( … ) in triple patterns (desugared to rdf:first/rest/nil, guarded out of quoted-triple contexts per the RDF 1.2 grammar's negative tests), empty IRIREF <>, function-call NIL arglists, bare ORDER BY constraints, sub-SELECT placement, VALUES (), property paths inside blank-node property lists, and a SPARQL 1.2 codepoint-escape pre-pass (Cow fast path — zero alloc when no escapes). Notable finding: basic#list-1..4 do NOT green — Turtle ingest stores object-position collections as native list_index items, never rdf:first/rest triples; registered as decision D-13. Its P6 sub-SELECT work was later subsumed by main's #1436 and dropped cleanly. JSON-LD parity: @list ↔ first/rest equivalence test.

3. Turtle bnode-dot lexer (pr-l1, −5). _:o6. (blank-node label + statement dot) failed to lex (#1444). Fix is greedy-scan + trailing-dot rewind — the naive lookahead alternative was rejected because it broke spec-valid _:a..b (adversarial-verification catch, ROADMAP §1.1-9). Import-hot path: interleaved ABAB benches at n=100 (single runs measured ±11–15% noise on identical binaries); all within budget. Bonus green: entailment owlds02 (its data was _:x.-shaped).

4. Canonical xsd:double (pr-l2, −1). One shared zero-alloc canonical_xsd_double helper (fluree-graph-ir), routed through every RDF-lexical site: SPARQL-JSON/XML, CSV/TSV (drops the ryu dep), LiteralValue::lexical(), and the N-Triples/N-Quads export sites that emitted dotless 1E6 (#1445). Behavior change / changelog: RDF-lexical double output is now the XSD canonical form (1.0E6); JSON-LD numeric output deliberately untouched (native JSON numbers) with a guard test. R2RML inherits the canonical form (spec-correct; flagged for that workstream).

5. Expression high-yield fixes (pr-x1, −37). Six per-defect commits, each individually suite-green: variable-free FILTER dropped ALL rows (FILTER(true) → 0 results, #1439 — plan-time fix, var-full paths byte-identical); DATATYPE()/LANG() rejected expression arguments (#1440); xsd:dateTime/date/time casts; BNODE(label) per-solution identity; regex q flag; bare integers in VALUES lowered to xsd:integer (#1319 — at lowering, dt_compatible untouched per the verified mechanism). Decision transparency (D-12): the DATATYPE-on-non-literal type error is SPARQL-scoped only; the JSON-LD @id extension behavior is deliberate and pinned by parity tests (options and rationale in ROADMAP §4). Fixing D2 unmasked the deeper D4 promotion defect on tP-29/30 — honestly registered for PR-X2. BSBM benches at/inside the noise floor.

6. Property-path fixes (pr-pp, −2). pp16: the zero-length-path universe now spans ALL graph terms including literals (SPARQL §18.4), gated at plan level on the ZeroOrMore/ZeroOrOne modifiers — */+ traversal untouched; pp36: empty_schema_with_len guard for zero-column existence rows. Adds the query_hot_property_path bench + regression-budget.json entries (reconcile test passes). The slower closure numbers are spec-mandated larger output on previously-wrong shapes; seq_plus and BSBM unchanged.

Verification

  • Full W3C suite on the integrated branch: 36/36 suites, 0 failed, 0 stale.
  • fluree-db-sparql 601 unit tests; JSON-LD groups (grp_query, grp_query_sparql, grp_transact) green; fmt + clippy -D warnings clean.
  • Integration-fixpoint commits document every register resolution (union-then-prune driven by the both-way gate).

Issues closed by this wave when merged: #1439, #1440, #1444, #1445 (+ #1319 mechanism half).


Performance notes (per component)

Harness/registers (pr-h1): harness-only; no engine code.

Parser accept-more (pr-1): 100% parse-time; zero runtime code touched. The SPARQL 1.2 codepoint-escape pre-pass uses a Cow fast path — zero allocation and a single scan when a query contains no escapes (the overwhelmingly common case).

Turtle bnode-dot lexer (pr-l1) — import-hot path: the fix deliberately keeps the existing greedy per-character scan byte-identical (no added lookahead, no backtracking); trailing dots are handled by a post-scan rewind, one extra check per label, not per character. Measurement was rigorous because single runs proved noise-dominated (±11–15% measured on identical binaries): interleaved ABAB at n=100 gave import_bulk +1.0%/−9.2% (noise band), turtle 100-nodes −0.3%, turtle 10-nodes ~+2.1% attributable to code layout — the changed function is unreachable by the bench corpus, which contains no _: tokens (corpus gap flagged for the bench backlog). All within the 5% budgets.

Canonical double (pr-l2): formatter-layer only, no query-path involvement. The shared helper post-processes {:e} in a fixed stack buffer (zero heap allocation); the CSV/TSV writer gained a write-into-buffer variant and dropped the ryu dependency.

Expression high-yield (pr-x1): the variable-free FILTER fix (D1) is plan-time classification in the planner — variable-bearing filter paths are byte-identical; all other defects are on cold or previously-erroring evaluation paths. BSBM before/after (quick profile, declared): q3 +1.9–3.4%, q5 −1.6–−0.3%, q9 +2.0–3.9%, bi/f2 +1.1–2.2% — same-code reruns on this box swing ±1–4%, so deltas are at/inside the noise floor and within the small-budget; the nightly full-profile bench is the backstop. Each defect landed as its own suite-green commit for bisectability.

Property paths (pr-pp): the */+ traversal fast path is untouched; the zero-length term universe is selected at plan level, only for the SPARQL ZeroOrMore/ZeroOrOne shapes, and pair buffers carry full terms only on that path. Must-accept cost, stated plainly: star_closure 2.37→6.06 ms and zero_or_one 1.69→6.27 ms in the new bench — these are NOT like-for-like comparisons: the pre-fix numbers computed incomplete (wrong) results, and SPARQL §18.4 requires every graph term (including literals) as a zero-length self-pair, so the correct output is strictly larger on both-ends-unbounded queries. The hot signals hold: seq_plus 43.50→43.87 ms (noise), query_hot_bsbm unchanged (q3 0.92 / q5 1.42 / q9 2.06 ms). This PR adds query_hot_property_path + regression-budget.json entries, so path-traversal performance is pinned from now on (workspace_reconcile passes).

Issues closed on merge

Closes #1439
Closes #1440
Closes #1444
Closes #1445

Also fixes the reported mechanism of #1319 (bare VALUES/inline integers now lower to xsd:integer; the separate scan-path datatype-constraint question stays tracked there — left open deliberately rather than auto-closed).

Mechanics note: GitHub fires these keywords when the PR merges into the default branch. In the stacked flow (#1437 merges first, this PR is then auto-retargeted to main), that happens exactly when these fixes actually reach main.

aaj3f added 24 commits July 6, 2026 23:42
…nt terminator

A blank-node label must not end in a dot (BLANK_NODE_LABEL grammar), so
_:o6. is the label o6 followed by the statement terminator. The lexer
greedily consumed the dot into the label, then hard-errored (unexpected
character '_'), rejecting valid Turtle and blocking the four W3C
json-res tests' data load (#1444).

Keep the greedy per-char scan byte-identical (spec-valid interior
consecutive dots like _:a..b keep lexing exactly as before, per the
adversarial correction in ROADMAP 1.1-9) and rewind any trailing dots
after the scan so they lex as Dot tokens. The rewind branch only runs
on inputs that previously errored, so the change strictly widens what
lexes.

Adds lexer unit tests (trailing dot, space form, interior/consecutive
dots byte-identity, EOF, multiple trailing dots) and a fluree-db-api
integration test inserting _:x.-shaped Turtle and querying it back
through the JSON-LD surface (query-surface parity).
…e both-constant existence row (pp16, pp36)

Two contained defects in the property-path operator
(fluree-db-query/src/property_path.rs), per the burn-down roadmap PR-PP row
and residual-eval.md §3.2:

- pp16 — a zero-length path (`elt*` / `elt?`) with both endpoints unbound
  built its self-pair universe only from the path predicate's edges, and only
  from ref terms. Per SPARQL 1.1 §18.4 the universe is every term of the
  active graph — subjects and objects of any predicate, literals included.
  `compute_closure` (simple and composite branches) now emits self-pairs from
  a policy-filtered full-graph term scan, and the internal pair buffers carry
  full `Binding` terms instead of `(Sid, Sid)` so literal endpoints survive to
  output. Scoped to the SPARQL-lowered shape: typed, unbounded `*`/`?`.
  Cypher var-length shapes (untyped wildcard, hop-bounded) keep node-set
  semantics, and the `*`/`+` traversal itself (adjacency build, BFS, visited
  sets, bounded layering) is untouched — this changes what depth-0 emits, not
  how paths are walked.

- pp36 — a both-constant path (`:a0 (:p)* :a1`) computes its existence row,
  but the operator materialized it through `Batch::new` with an empty schema,
  which infers len from the missing first column and collapses to 0 rows
  (binding.rs `test_batch_new_loses_len_for_empty_schema`). Apply the sibling
  operators' guard (`GraphOperator`/`SubqueryOperator::drain_buffer`):
  `num_cols == 0` → `Batch::empty_schema_with_len(n)`, in both unseeded and
  correlated emission.

Register: SPARQL11_PROPERTY_PATH shrinks to pp34/pp35, which are graph-cluster
tests (constant-GRAPH-IRI resolution, ROADMAP §6.1 — owner PR-BASE/PR-G1);
comment corrected accordingly. Full W3C suite green (36 suites), 0 stale
entries; property-path suite 31 passed / 2 registered.
jsonres01-04 (SPARQL11_JSON_RES, now empty) and entailment owlds02 all
blocked on the _:label. lexer bug and now pass; the harness's
stale-entry gate forces their removal. sparqldl-03 shared the same
data-load failure mode: its data now loads but the test legitimately
fails as an entailment result mismatch (expected 1 solution, got 0),
so its entry moves to the result-mismatch group with the new rationale.

Full W3C suite: 36/36 suites green, 0 failed, 0 stale.
)

Every RDF-lexical serialization of a finite xsd:double used Rust's f64
Display (or {:E} in the N-Triples export), producing non-canonical forms
like "1000000" and "1E6". Add one shared canonical_xsd_double helper
(plus zero-alloc String/byte-buffer variants) in fluree-graph-ir and
route all RDF-lexical sites through it: SPARQL Results JSON, SPARQL
Results XML, CSV/TSV, LiteralValue::lexical() (Term Display, RDF/XML),
and the N-Triples/Turtle export. Doubles now serialize in the W3C
canonical form (1.0E6, 1.0E-3, 0.0E0); NaN/INF/-INF keep their XSD
spellings.

JSON-LD and the other JSON-native outputs (typed/cypher/agent JSON) are
deliberately untouched: a double there is a native JSON number, never a
lexical string, guarded by new parity tests.

Greens W3C csv03 (lexical CSV comparison); remove its register entry.

Decision transparency (ROADMAP PR-L2): scope extended to export.rs per
verification finding 1.1-8; the R2RML extractor's lexical() use is
flagged on the PR rather than changed independently.
The burn-down roadmap gates every property-path PR on a dedicated criterion
bench (ROADMAP §2 PR-PP, §7 DoD-3); none existed. Follow the query_hot_bsbm
pattern: file-backed ledger, full reindex, warm-cache GraphSnapshot reuse.

Synthetic graph of disjoint 8-node chain clusters (closure output stays
linear in scale) with one literal per node so the zero-length term universe
is exercised. Three scenarios pin the operator's distinct execution modes:

- star_closure: `?s ex:link* ?o` — both-variable closure + zero-length
  universe
- seq_plus: `ex:seqRoot ex:p1/ex:p2+ ?o` — BGP join into correlated `+`
  forward BFS (the distinct-node traversal fast path)
- zero_or_one: `?s ex:link? ?o` — ZeroOrOne closure branch

Registered in regression-budget.json (tiny/small/medium, standard budgets);
fluree-bench-support workspace_reconcile passes.
A FILTER referencing no variables (FILTER(true), FILTER(1 = 1),
FILTER(2 IN (1,2,3))) eliminated every solution. Two composing defects:

- reorder_patterns classified the filter as Deferred with an empty
  dependency set, so it drained to the very front of the pattern list —
  a FilterOperator over the synthetic unit seed, gating the whole group
  instead of its solutions. Var-free filters now anchor after the
  patterns that precede them (the MINUS/EXISTS order preservation);
  filters that reference variables keep dependency placement unchanged.
- filter_batch/filter_batch_with_exists rebuilt zero-column batches with
  Batch::new, which infers len = 0 from the absent first column and
  silently dropped the surviving unit row (ASK { FILTER(true) }).

Plan-time classification only; no per-row cost on existing paths.

Register shrink (5): dawg-boolean-literal, add-literals (also a
variable-free FILTER), in01, notin01, plain-string-same. JSON-LD parity
tests per compliance § Query Surface Parity in
it_query_expression_semantics.rs.

Fixes #1439.
DATATYPE() and LANG() bailed unless their argument was a bare variable,
so every FILTER(datatype(<arithmetic>) = xsd:T) / cast-result inspection
errored and demoted to false. Both builtins now evaluate the argument
expression and classify the resulting value; the bare-variable fast path
still reads the binding directly, without materializing.

Non-literal arguments split per surface (decision D-12, ROADMAP §4):
Function::{Datatype,Lang} carry a lowering-time `strict` mode. SPARQL
lowers strict — DATATYPE/LANG of an IRI/ref/bnode is a type error
evaluated as "no value", so a FILTER excludes the row (even under !=)
and a project expression/BIND leaves the variable unbound (§17.2/§18.5).
JSON-LD lowers lenient, preserving the deliberate `@id`-datatype
extension and the "" LANG fallback. Both behaviors are asserted in
it_query_expression_semantics.rs; the mode is fixed at lowering time, so
there is no per-row dispatch cost and the LANG(?o)="tag" scan fast paths
now match both modes structurally.

Register shrink (30): type-promotion-01/02/06..20/22, the six
cast-{str,flt,dbl,dec,int,bool}, dawg-datatype-2, dawg-lang-1/2,
projexp05 (SPARQL11_PROJECT_EXPRESSION now empty), and the two
SPARQL12_RDF11 constant-datatype tests. tP-29/30 are newly registered:
they expect ASK=false and previously passed only because DATATYPE(expr)
errored — evaluating the argument unmasks the D4 numeric-promotion
defect (PR-X2). tP-03/04/05/21 stay registered on the same defect.
dawg-lang-3 stays registered: it fails on scan-path lang-tag case
matching, not on LANG().

Fixes #1440.
The SPARQL cast lowering table stopped at the numeric/string/boolean
constructors, so xsd:dateTime(?v) fell through to Function::Custom and
an "Unknown function" error. Add Function::{XsdDateTime,XsdDate,XsdTime}
with cast arms that reuse the existing temporal parsers: identity casts
pass through, string lexical forms parse, and everything else produces
no value (W3C SPARQL 1.1 §17.5 — an invalid cast is a type error, not a
query error).

Parse-time lowering plus a cast that only runs when a query names the
constructor; no cost to queries that don't.

Register shrink (1): cast-dT. Surface note per compliance § Query
Surface Parity: XSD constructor casts are SPARQL-only syntax (the
JSON-LD query surface has no cast form) — recorded as a surface-only
decision in the PR description.
BNODE(str) hashed only its label, so the same argument produced the
same blank node across every solution; SPARQL 1.1 §17.4.2.9 requires
the same label to map to the same bnode only WITHIN one solution and to
fresh bnodes across solutions (bnode01).

The bnode key now folds in a solution-identity hash: a commutative
XOR-fold over the row's bound (var, value) pairs via a new
RowAccess::for_each_binding visitor. Blank-node-valued and
unbound/poisoned slots are excluded so a BNODE-produced column bound
between two BNODE(label) calls on the same solution does not perturb
the identity. The hash runs only inside BNODE(label) evaluation — no
cost to any other path.

Because BNODE is solution-scoped, a BIND containing it is now
order-sensitive: reorder_patterns and the block planner anchor it after
the patterns that precede it instead of floating it to the earliest
point its variables bind (evaluating against a partial join prefix
collapsed per-solution identity). Binds without BNODE keep dependency
placement unchanged.

Register shrink (1): bnode01.
build_regex_with_flags errored on the XPath fn:matches `q` flag, so
regex(?v, "a?+*.{}()[]c", "q") failed the whole FILTER. With `q` the
pattern is now escaped and matched literally; per XPath F&O §5.6.2 it
composes with `i` and neutralizes `m`/`s`/`x`. Unknown flags still
error. Compile-time only — the compiled regex stays behind the
thread-local LRU cache keyed by (pattern, flags).

Register shrink (2): regex-no-metacharacters,
regex-no-metacharacters-case-insensitive.
SPARQL VALUES rows and JSON-LD inline values tagged a bare integer
literal xsd:long, while storage, the Turtle parser, arithmetic results,
and triple-pattern lowering all tag xsd:integer. The scan post-filter's
integer-family compatibility masked it on the join path, but in
term-identity contexts (DISTINCT, GROUP BY, sameTerm) and in result
serialization the same number surfaced as two distinct terms.

Lower a bare integer to xsd:integer at both boundaries
(fluree-db-sparql lower/term.rs VALUES rows; fluree-db-query
parse/lower.rs JSON-LD inline values), matching RDF 1.1 and storage
tagging. Lowering-time only. Scan-path datatype constraints and
dt_compatible are deliberately untouched (that is D5b, PR-X2).

No W3C register entry fails solely on this; regression coverage lives
in it_query_expression_semantics.rs (SPARQL result datatype, sameTerm
and DISTINCT collapse, plus the JSON-LD values-integer parity tests).

Fixes #1319.
…pace

The root Cargo.toml excludes testsuite-sparql by relative path, which does
not apply when the repo is checked out as a nested git worktree — cargo then
walks up to the enclosing workspace root and refuses to build. An empty
[workspace] table makes the exclusion self-contained; behavior in a normal
checkout is unchanged.
parse_rdf_dawg_result_set treated Event::Start and Event::Empty identically,
pushing expectation state for self-closing elements that quick-xml never
emits an End event for. Every <rs:value rdf:resource=.../> therefore skewed
the state stack by one: the solution containing the unbound variable
survived while every bound solution after the first self-closing value was
dropped (the engine's sort output was correct all along). Complete
self-closing elements immediately instead of pushing them, add the
rdf:nodeID blank-node value form, and deregister dawg-sort-3/6/8, which now
pass.

The SRX parser had the same Start|Empty conflation: <literal/>, <uri/>,
<bnode/> and <result/> were never committed because their End handling never
ran, silently dropping empty-literal bindings and fully-unbound rows. Route
both parsers' completion logic through one shared helper per parser and
cover the self-closing and unbound-variable shapes with unit tests.
Comment-only edits from docs/audit/burn-down/ROADMAP.md \S6.1 — no entry is
added or removed. Corrects the misattributed root causes so later burn-down
PRs delete entries out of accurately-labeled groups: the insert-05a /
same-bnode / delete-insert-01c group is multi-op ';' truncation (#1438), not
named-graph INSERT loss; constructlist is a parse-time rejection, not a
query-execution error; pp34/pp35 are graph-resolution defects, not path
cardinality; the SPARQL12_VERSION failures are wave-2 reifier syntax, not
VERSION support; syntax-update test_36/38/39/40/54 are update classes D/C/B,
not graph-management grammar; plus cluster-ownership annotations (W-1/W-2,
PR-BASE, PR-G1, PR-X1/X2, PR-U1/U2/U3/U6, PR-PP, PR-L2, PR-W2A, D-6/D-8).
…audit

Per the burn-down verification (ROADMAP \S6.3): the \S8 'INSERT into a
not-yet-existing named graph silently loses the triples' and 'combined
DELETE/INSERT WHERE applies inserts without the deletes' findings are
refuted — both are multi-operation ';' truncation (#1438); \S4.2 item 7 no
longer lists pp06 (it passes and is deregistered) and notes the pp34/pp35
reattribution; the headline register split is 816 passing / 541 gaps / 63
not-applicable, not 815/538/67.
…own PR-1)

Accept-more, parse-time-only fixes for the W3C syntax positives the
parser still rejected (docs/audit/burn-down/parser-syntax-validation.md
Section 2, PR-1):

- P1: RDF collections '( ... )' in subject and object positions,
  desugared at parse time to rdf:first/rest/nil triples over fresh
  '#collN' blank-node cells riding the existing pending_bnpl_triples
  channel — no new AST, IR, or engine surface. '()' is the plain IRI
  rdf:nil. Collections stay rejected inside quoted triples and triple
  terms (rtSubject/rtObject exclude them; guards keep the four
  list-*/quoted-list-* negative tests red).
- P4: empty IRIREF '<>' — drop the lexer's empty-body rejection; the
  IRIREF production permits zero characters.
- P5a: extension-function calls with a NIL arg list 'f()' — the three
  iri-or-function guards in parse_primary_expr now accept the Nil token
  (parse_expression_list already handled it).
- P5b: bare Constraint as an ORDER BY condition ('ORDER BY str(?o)'),
  mirroring parse_group_condition's position/restore guard.
- P7: 'VALUES () { ... }' — NIL variable list (zero variables) with
  NIL rows (zero-binding rows).
- P8: property paths as the verb inside a blank-node property list
  ('[ :p|:q ?x ]') via a new pending_bnpl_patterns channel; template
  contexts (CONSTRUCT / INSERT / DELETE) reject it explicitly instead
  of silently dropping the pattern.
- SPARQL 1.2 codepoint-escape pre-pass: \uXXXX/\UXXXXXXXX are
  unescaped over the whole query string before tokenizing, with a Cow
  fast path (no allocation when no backslash / no codepoint escape is
  present). Escapes are processed exactly once, left to right, so
  "\\u0041" decodes to the invalid string escape \A and is rejected.

The roadmap's P6 (sub-select placement) is fully subsumed by main's
PR #1436 (sub-SELECT as a set-operation operand), which this branch is
rebased on — no P6 code lands here.

All fixes are parse-time; zero runtime/engine code is touched.
Removed (29 entries), verified by the both-way register policing:

- SPARQL10_SYNTAX: all 17 'parser rejects valid input' entries
  (syntax-lists-01..05 x2 suites, syntax-forms-01/02, syntax-order-07,
  syntax-qname-05, syntax-function-01..03).
- SPARQL11_SYNTAX_QUERY: test_35a, test_36a, test_63, test_pp_coll (4).
  (test_21/test_23/test_64 and subquery12 were already removed by the
  base branch's re-baseline for main's #1436 sub-SELECT fix, which
  subsumed this PR's P6 item; test_65 was likewise already registered
  under accepts-invalid awaiting PR-2's SELECT-scope validation.)
- SPARQL12_CODEPOINT_ESCAPES: all 6 (register now empty).
- SPARQL12_SYNTAX_TRIPLE_TERMS_POSITIVE: annotation-reifier-07 and
  annotation-anonreifier-07 — their only blocker was the path-in-'[]'
  form inside the annotation block (P8). Cross-register removal
  coordinated with PR-W2A per ROADMAP Section 1.1-12.

NOT removed, with corrected register comment:

- SPARQL10_QUERY_EVAL basic#list-1..4: the cluster doc's claim that P1
  greens them is refuted — Fluree's Turtle ingest emits OBJECT-position
  collections as Fluree list_index items (parse_collection_as_list) and
  drops '()' objects entirely, so the rdf:first/rest triples the
  desugared query pattern needs are never stored. Eval-side ingest/model
  gap, not parser territory.
Per docs/contributing/sparql-compliance.md 'Query Surface Parity': P1
(collection patterns) is surface sugar over rdf:first/rest/nil triples
the shared engine already executes, so the JSON-LD surface must be able
to match exactly the data a SPARQL '( ... )' pattern matches.

Two tests over an explicitly-seeded first/rest chain:
- a JSON-LD where clause spelling out rdf:first/rdf:rest/rdf:nil
  returns identical rows to 'ex:x ex:letters (?v ?w)';
- a JSON-LD rdf:nil object pattern returns identical rows to the empty
  collection 'ex:s ex:letters ()'.

The seed uses an explicit first/rest chain because Fluree's JSON-LD
@list ingest stores ordered list_index values, not first/rest triples —
recorded in the test header and the PR description.
# Conflicts:
#	testsuite-sparql/tests/registers/mod.rs
# Conflicts:
#	testsuite-sparql/tests/registers/mod.rs
# Conflicts:
#	testsuite-sparql/tests/registers/mod.rs
# Conflicts:
#	testsuite-sparql/tests/registers/mod.rs
@aaj3f aaj3f requested review from bplatz and zonotope July 7, 2026 17:43
Comment thread fluree-db-query/src/property_path.rs
Comment thread fluree-db-api/benches/query_hot_property_path.rs
Comment thread fluree-db-sparql/src/parse/query/modifier.rs
Comment thread fluree-db-sparql/src/lex/lexer.rs

@bplatz bplatz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few comments, approved pending your review.

Base automatically changed from test/sparql-testsuite-full-coverage to main July 8, 2026 04:10
Four items from bplatz's review of the Wave-0 property-path and parser
work.

1. Property-path zero-length universe (fluree-db-query property_path.rs).
   The full-graph term scan behind a both-unbound `?s :p* ?o` had no
   cancellation poll -- unlike every other traverse/closure loop in the
   operator -- and ran before the guarded BFS, so it was uninterruptible.
   Poll `bail_if_cancelled` in the scan loop so a runaway enumeration is
   bounded by the query timeout. No hard term cap is added: the universe
   is inherently O(graph) per SPARQL 1.1 §18.4 (the pp16 fix requires
   every graph term as a self-pair, literals and non-path terms
   included), and `max_visited` is a traversal-depth bound (always 10k)
   that would reject spec-valid queries on modestly large graphs. The
   up-front `range_with_overlay` materialization is unchanged (shared
   with the wildcard closure) and documented as a separate concern.

2. Property-path bench coverage (query_hot_property_path.rs). The
   link/p2-everywhere graph made the zero-length universe scale with the
   path domain, so no scenario exercised a selective predicate over a
   large graph -- the shape where the universe scan dominates. Add a
   `rare_star` scenario (one fixed `ex:rare` edge) that isolates the
   full-graph universe-scan cost, and tighten the medium budget from 5.0
   to 3.0 to match the bsbm siblings (still a placeholder pending the
   first nightly baseline).

3. Bare ORDER BY / GROUP BY (fluree-db-sparql parse/query/modifier.rs).
   The bare condition branch used full `parse_expression` where the
   grammar allows only `Constraint | Var` (Constraint =
   BrackettedExpression | BuiltInCall | FunctionCall), so `ORDER BY
   ?x - 1` silently parsed as two keys [?x, -1], corrupting the sort.
   Restrict the bare branch (ORDER BY and GROUP BY) to a call-shaped
   Constraint via `is_bare_constraint`: `str(?o)` / `ex:fn(?o)` stay
   valid, a trailing `- 1` is no longer absorbed as a key, and a leading
   bare arithmetic hard-errors. Adds parser regression tests.

4. Codepoint-escape corners (fluree-db-sparql lex/lexer.rs). The SPARQL
   1.2 §19.2 pre-pass is correctly global-before-tokenizing
   (esc-01/07/08 require it), so it is comment-agnostic, and an escape
   that decodes to a delimiter acts as that delimiter (a decoded `>`
   closes an IRI, the same rule as esc-08's decoded `"` opening a
   string). Neither corner is covered by the W3C suite; pin both with
   unit tests and document them on the pre-pass. No behavior change.

Validation: W3C SPARQL suite 36/36 green, 0 stale; fluree-db-sparql lib
521 tests green; property-path bench compiles and runs all four
scenarios; fmt and clippy clean.
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