Skip to content

feat: SPARQL parameters, fulltext() in SPARQL, structured SHACL rejections - #2024

Merged
bplatz merged 12 commits into
mainfrom
feat/engine-query-features
Oct 7, 2026
Merged

bplatz merged 12 commits into
mainfrom
feat/engine-query-features

Conversation

@bplatz

@bplatz bplatz commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Engine features the Python package uses that stand on their own for the server and CLI.

  • SPARQL parameters. fluree_db_sparql::substitute_params binds variables to values on the AST before lowering: ?x and $x are one variable; a projected parameter becomes (value AS ?x). Unknown names, a name given twice (x and $x), variables the query assigns (BIND, VALUES, AS), and parameters inside a remote SERVICE are refused, so a misspelt parameter cannot silently match everything. Values use JSON-LD term forms, with full IRIs; "@type": "@id" reads as an IRI, and an f:embeddingVector takes its numbers as a JSON array. A blank node must be a stored node's _:fdb-… id: any other label would lower to a variable and match every node, so it is refused. Wired through queries, explain, streams and updates (QueryExecutionOptions::with_params, builders' .params(), TxnOperation::SparqlUpdate(sparql, params)); a query without parameters takes no new work.
  • fulltext() in SPARQL, the twin of the JSON-LD function, with the docs updated to show full-text and vector scoring side by side and the stale "not supported in SPARQL" notes removed.
  • Structured SHACL rejections. TransactError::ShaclViolation carries the violations resolved to full IRIs (the ReportResult that validate() returns, now in fluree-db-transact) alongside the unchanged message, so callers can act on a rejection without parsing text. Its payload displays and derefs as the message, so existing matching on the text is unchanged. A unique-constraint violation now reports the clashing value as text rather than its debug form (a reference as its IRI).
  • Breaking for embedders: ShaclViolation's payload is now ShaclViolations, not String. Building it with .into() from a String or &str still compiles; code that binds the payload as a String needs .to_string() or .message().
  • Small reporting additions. A named-graph drop reports its commit id; a graph sync records a commit message (f:message) and reports the commit id it wrote.

Follow-up: #2033

@bplatz
bplatz added this pull request to stack #2026 October 6, 2026 03:23
@bplatz bplatz changed the title feat/engine query features feat: SPARQL parameters, fulltext() in SPARQL, structured SHACL rejections Oct 6, 2026
@bplatz bplatz added enhancement New feature or request area:sparql SPARQL/Turtle/TriG/JSON-LD parsing, lowering, UPDATE semantics, W3C conformance area:query Query execution, planning, fast paths, overlay, result formatting labels Oct 6, 2026
@bplatz
bplatz requested review from aaj3f and zonotope October 6, 2026 03:23
@bplatz
bplatz marked this pull request as ready for review October 6, 2026 03:24
@bplatz
bplatz force-pushed the feat/engine-query-features branch 2 times, most recently from 671cbbb to f7e5b8b Compare October 6, 2026 10:26

@aaj3f aaj3f 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.

@bplatz the parameter design is nice here and makes sense to me. I think really only two headline notes, one of which is covered in detail below: (1) the claim "a parameter fixes a variable to one RDF term for the whole request" (params.rs:3) and its guard against a parameter that "would otherwise leave its variable unbound and silently widen the match" doesn't seem to hold for blank-node values and the parameter becomes a wildcard, which would be particularly risky for DELETE WHERE, and (2) it seems in this PR Value::parse re-implements "JSON-LD term --> RDF term" with its own rules while we already have a parser for those same forms in fluree-db-query/src/parse/values.rs:142-262. Regardless of duplication redundancy, they disagree on input forms. I think in #2025 this gets a bit worse as the Python layer re-encodes vector arrays as JSON text because the params parser refuses them, which is just an example of a consumer adapting to and extending the divergence. I'll defer to you how much we address this in this PR rather than getting them to align. It makes me wonder (now or later) if we should have one shared term parser in fluree-graph-jsonl-d which then fluree-db-query and fluree-db-sparql can reach) and which take the IRI-expansion function as an argument (e.g. @context for JSON-LD, PREFIXes for SPARQL). Full review below:


✅ Approving to unblock you — with 1 thing that needs to be addressed before this merges.

The parameter design is really nice: substituting on the AST before lowering, with exhaustive matches so a new AST variant can't slip past it, mirrors what Cypher already does, and a_filter_inside_optional_sees_the_value pins exactly why a VALUES join would have been the wrong tool. fulltext() lowering to the same Function::Fulltext makes the JSON-LD parity structural rather than something the tests have to hold up, and the SHACL change shares one resolution with validate() instead of building a second.

Must address before merge:

  1. A blank-node parameter is a wildcard, not a value (fluree-db-sparql/src/params.rs:192). Any {"@id": "_:label"} whose label isn't a stored _:fdb-… id lowers to a non-distinguished variable, and since substitution runs after validate(), DELETE WHERE { $s ?p ?o } with s = {"@id": "_:x"} commits and deletes every triple in the ledger. I ran it on this branch. Fix: accept only fdb- labels in parse_object and refuse the rest. Details inline.

Should address (small, same file): {"@value": iri, "@type": "@id"} is read as a literal typed <@id> and silently matches nothing (params.rs:217), where the JSON-LD values clause reads it as an IRI. Nits inline: "name"/"$name" collapsing silently (params.rs:271), a ref value in the unique-constraint message (tx.rs:2049), the sync docs (admin.rs:159), and a release-notes line plus one assertion for the ShaclViolation change (error.rs:160).

What I checked and found clean, since these were the parts I was most worried about going in:

  • No parameter position widens what a query reads. FROM/FROM NAMED take IRIs only and substitution never touches the dataset clause; GRAPH $g resolves exactly as a bound GRAPH ?g does (system graphs stay private either way); SERVICE $svc only resolves ledgers already in the dataset, and pointing it at another ledger errors with "unknown ledger". The paths in view/dataset_query.rs that reuse a parsed AST only ever receive one that's already substituted.
  • The ShaclViolation change doesn't move the HTTP error body: the server never matches on the variant, and the body is its Display, which is the unchanged message. Solo's origin/main doesn't touch the payload, TxnOperation or SyncGraphOpts.
  • fulltext() gave identical scores and order in every SPARQL shape I tried on an indexed ledger (top-level BIND, a projection, inside a sub-SELECT, an ORDER BY expression, and with the query string as a parameter), and your tests already tie the BIND form to the JSON-LD query on indexed docs, novelty and a configured property.
  • JSON numbers come through as xsd:double (as in JSON-LD) and matched the same rows as an inline 2.5 against both decimal and double values.

One broader note, more context than ask: Value::parse is now the third place that turns a JSON-LD term object into an RDF term (fluree-db-query/src/parse/values.rs, and the where-pattern parser 40c14ba73 just brought into line with it, are the others), and the @type: "@id", vector and blank-node differences are that divergence; #2025 already re-encodes vectors as JSON text to get past this parser. A shared term parser that takes the IRI expander as an argument would close the class, but that's a cross-crate change and not for this PR.

Adherence to repo commitments:

  • Patterns/abstractions: ✔ substitution before lowering mirrors fluree_db_cypher::substitute_params; fulltext() reuses Function::Fulltext; report_results is shared with validate(). ⚠️ the term parser is a third copy (above).
  • Performance (speed first, memory second): ✔ no engine change. A query without parameters pays one Option check per entry point; the extra IRI resolution in the SHACL path runs only on rejection. No performance-degradation risk.
  • Deployment targets: ✔ params.rs is pure and wasm-safe; nothing in the diff adds a background task or a process-lifetime cache; solo is unaffected at its current pin and on a bump.
  • Testing: ⚠️ the new tests are wired (grp_query_sparql, grp_query, grp_ledger, grp_transact, the fluree-db-sparql and fluree-db-api unit suites), CI's --all-features nextest runs every one of them by name (13,849 passed), and they go red when I break the code under them. The blank-node and @type: "@id" cases aren't covered, and the "branch op keeps the results" claim isn't asserted.
  • Conventions: ✔ thorough multi-line commit bodies; fmt, clippy --all-features -D warnings, wasm32 and the W3C testsuite-sparql job are all green in CI.

Verified at f7e5b8beb: CI is green on this head; locally, the new tests pass by name (it_sparql_params 7, params::tests 13, the fulltext/SHACL/sync tests), breaking finish(), the fulltext keyword, the substitution seam and the SHACL results each turns its test red, and the findings above come from scratch probes run against this branch.

Cross-PR notes: Two cross-PR things. violation_carries_resolved_results upserts a shape onto a plain create_ledger and expects enforcement, which is the shapes-exist heuristic my #2009 removes (SHACL runs only where config sets f:shaclEnabled). Nothing to change here: I'll move the test to create_shacl_ledger when #2009 rebases, and #2009's branch-validation outcome will carry your structured ShaclViolations in its SHACL arm. The other is downstream: solo's fluree-lambda-transact tests build TransactError::ShaclViolation("…".into()) from a &str (handler.rs:2940, :2968), and ShaclViolations only has From<String>, so a solo pin past this stops compiling. An impl From<&str> for ShaclViolations would keep that a no-op; I recognize it's tiny, but I'd rather see it folded in here than discovered at the bump.

Approving now so you can merge without waiting on another pass from me — just be sure the blank-node fix is in before you do.

Comment thread fluree-db-sparql/src/params.rs Outdated
};
return match id.strip_prefix("_:") {
Some("") => Err(ParamError::new(name, "has an empty blank node label")),
Some(label) => Ok(Value::Blank(Arc::from(label))),

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.

🔴 Must address before merge — a blank-node parameter is a wildcard, not a value, and under DELETE WHERE it deletes every match.

Value::Blank puts BlankNode::labeled(label) into the AST, and lowering turns any label that doesn't start with fdb- into a non-distinguished variable (fluree-db-sparql/src/lower/term.rs:84-89 and :140-145). That's right for a blank node written inline, but it means this parameter fixes nothing, which is the same "unbound variable widens the match" hole finish() closes for misspelt names. Substitution also runs after validate() (fluree-db-api/src/query/helpers.rs:514-517, tx_builder.rs:132-138), and DELETE WHERE deliberately keeps Fluree's existential blank-node semantics (fluree-db-transact/src/lower_sparql_update.rs:589-591, with the API waiving the validator's §19.8 check for it at tx_builder.rs:123), so nothing that would refuse an inline _:x ever sees this one. DELETE templates are fine: the lowering backstop refuses them.

I ran these against this branch on a two-person ledger:

  • DELETE WHERE { $s ?p ?o } (the shape update_templates_and_where_are_substituted uses) with s = {"@id": "_:x"} commits, and the ledger is empty afterwards: COUNT(*) over ?s ?p ?o goes from 11 to 0.
  • DELETE WHERE { $s ex:name ?n } with the same value removes both people's names.
  • DELETE { ?s ex:age ?a } WHERE { ?s ex:age ?a ; ex:knows $o } with o = {"@id": "_:x"} deletes the age of everyone who knows anyone.
  • SELECT ?n WHERE { $s ex:name ?n } with s = {"@id": "_:x"} returns every name.

A stored id works as intended: {"@id": "_:fdb-…"} taken from a query result addresses exactly that node. But an app that wraps a caller-supplied id as {"@id": id}, which is exactly what parameters exist to make safe, hands that caller a wildcard. positions_constrain_the_term passes with {"@id": "_:b"} as a subject only because the AST no longer holds a Var; it is still a variable once lowered. (An INSERT template $b = _:t also mints a different node per solution, not "one RDF term for the whole request".)

Fix: accept a blank node only when its label is a stored id and refuse the rest. Illustrative (the fdb- prefix is already mirrored as a constant in validate/mod.rs:55, since fluree_db_core sits behind lowering):

return match id.strip_prefix("_:") {
    Some(label) if label.starts_with(STABLE_BLANK_NODE_LABEL_PREFIX) => {
        Ok(Value::Blank(Arc::from(label)))
    }
    Some(_) => Err(ParamError::new(
        name,
        "is a blank node label, which matches any node in a query; \
         only a stored node's `_:fdb-…` id can be a parameter",
    )),
    None => Ok(Value::Iri(Arc::from(id))),
};

…plus the "_:label" wording at params.rs:20 and docs/getting-started/rust-api.md:397, and a test that DELETE WHERE { $s ?p ?o } with _:x is refused.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 96cdb4b

"has a language tag, so `@value` and `@language` must be strings",
)),
},
(Some(datatype), None) => {

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.

🟠 Should address — {"@value": iri, "@type": "@id"} becomes a literal typed <@id> and silently matches nothing.

This arm takes any @type string as a datatype IRI, so the JSON-LD "IRI as a value object" form turns into a literal no stored value can equal. On this branch, SELECT ?s WHERE { ?s ex:knows $o } returns nothing with o = {"@value": "http://example.org/bob", "@type": "@id"} and returns Alice with o = {"@id": "http://example.org/bob"}. The JSON-LD values clause reads the first form as an IRI (fluree-db-query/src/parse/values.rs:216-227), so a value copied out of a JSON-LD query changes meaning here, and it's the one odd-looking input this parser accepts without a word when it refuses null, lists and unknown keys.

Minimally we could refuse "@id" and "@vocab" as a @type; reading "@id" as Value::Iri (through the same blank-node rule as above) would match values. Compact forms like {"@id": "ex:alice"} or "@type": "xsd:date" do fail loudly, which is good, though the error talks about angle brackets the caller never wrote; a sentence in the docs that parameter IRIs are always full IRIs would help. This is in the same file as the blocker, so if you agree it's right I'd rather see it folded in now than lost in the backlog.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 96cdb4b

Comment thread fluree-db-sparql/src/params.rs Outdated
let params = params
.iter()
.map(|(name, json)| {
let name = name.trim_start_matches(['?', '$']);

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.

🟡 Optional / nit — "name" and "$name" in one map silently become one parameter, and the last one wins.

trim_start_matches(['?', '$']) maps both keys to name, and collecting into the HashMap keeps whichever comes last; with {"name": "Alice", "$name": "Bob"} the query on this branch matched Bob. It's an unlikely input, but it's the kind of ambiguity the rest of this module refuses. Building the map in a loop and returning a ParamError ("is given twice") when insert returns Some would close it. Minor and non-blocking, but if you agree it's right, I'd rather see it in this PR than lost in the backlog.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in fafb66c

Comment thread fluree-db-api/src/tx.rs
.unwrap_or_else(|| format!("g_id={g_id}"))
};
let value_str = format!("{o:?}");
let value_str = match o {

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.

🟡 Optional / nit — a ref value still doesn't read as text here.

FlakeValue's Display renders a ref as ref:[<ns code>:<name>] (fluree-db-core/src/value.rs:1022, sid.rs:183), so a unique constraint on a ref-valued property reports something like ref:[101:alice] instead of the IRI. The subjects a few lines down already go through snapshot.decode_sid, so one more arm would finish what the commit sets out to do:

let value_str = match o {
    fluree_db_core::FlakeValue::String(s) => s.clone(),
    fluree_db_core::FlakeValue::Ref(sid) => {
        snapshot.decode_sid(sid).unwrap_or_else(|| sid.to_string())
    }
    other => other.to_string(),
};

Nothing pins the new message text either; asserting the reported value in one of the existing UniqueConstraintViolation tests (e.g. it_config_graph.rs:2488) would. Minor, and if you agree I'd rather see it folded in than lost in the backlog.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 3b1a4f6

/// cannot silently wipe the graph.
pub allow_empty: bool,
/// Recorded as the commit's `f:message`.
pub message: Option<String>,

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.

🟡 Optional / nit — the sync docs don't mention the new option or the commit ids.

docs/transactions/sync.md:126 still says "SyncGraphOpts { dry_run, allow_empty } mirror the query parameters", and nothing in the sync or drop docs mentions message or the new commit_id on SyncGraphReport/DropNamedGraphReport. A sentence there (that message is recorded as the commit's f:message, Rust API only for now) would keep the reference honest. Minor; fold-in close as above.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 976d58b

#[cfg(feature = "shacl")]
#[error("{0}")]
ShaclViolation(String),
ShaclViolation(ShaclViolations),

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.

🟡 Optional / nit — this is a breaking change to a public variant, and the body undersells it a little.

Matching or searching the payload as text still works through Display/Deref, as the body says, but anything that builds ShaclViolation(String) or binds the payload as a String no longer compiles; shacl_tests.rs:31 in this PR needed .to_string() for exactly that reason. Downstream is fine today: solo's origin/main never touches the payload (its only mention is a doc comment in fluree-lambda-router/src/routes/transactions.rs), and the HTTP error body is the unchanged Display. Since PR titles feed the release notes, a line in the body flagging it for embedders seems worth it.

Separately, the commit says a branch operation the shapes reject "keeps the results too", but it_branch_shacl.rs::assert_shacl_violation (:91-96) only matches the variant. Asserting !violations.results().is_empty() there would pin it across every branch-op test at once. Minor; fold-in close.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 1a7cf28; the PR body now flags the breaking change for embedders.

Ok(())
}

fn pattern(&mut self, pattern: &mut GraphPattern) -> Result<()> {

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.

👍 Substituting on the AST before lowering, with exhaustive matches (no _ arms) over QueryBody, UpdateOperation, GraphPattern and Expression, means a new AST variant is a compile error here rather than a silently unsubstituted position, and it lines up with how fluree_db_cypher::substitute_params already works. The remote-SERVICE refusal is right too: remote execution sends the captured body text (fluree-db-query/src/service.rs:354-357), so a substituted value would never reach the endpoint.

@bplatz
bplatz force-pushed the feat/engine-query-features branch from f7e5b8b to 639f395 Compare October 7, 2026 17:18
@bplatz
bplatz force-pushed the feat/engine-query-features branch 2 times, most recently from d48e861 to 7c97e5b Compare October 7, 2026 18:48
@bplatz
bplatz force-pushed the feat/engine-query-features branch 2 times, most recently from 8134eac to fbb9e29 Compare October 7, 2026 19:55
Base automatically changed from feat/engine-transactions to main October 7, 2026 22:39
bplatz added 6 commits October 7, 2026 18:39
`substitute_params(ast, params)` replaces every variable named in a
parameter map (`?name` or `$name`, the same variable) with its value,
throughout the request: subqueries, OPTIONAL, FILTER and EXISTS bodies,
CONSTRUCT and update templates, DESCRIBE targets and GRAPH names. The
lowering and planner only ever see constants, so a parameterized query
plans exactly as the inline one, and a request without parameters is
never touched.

- A projected parameter stays a column, as `(value AS ?name)`, or through
  its GROUP BY key when grouped on.
- A parameter the request never mentions is an error: its misspelt
  variable would otherwise be unbound and match everything. So is one the
  request assigns itself (BIND, VALUES, AS), a literal in an IRI-only
  position, and one inside a remote SERVICE, whose body is sent to the
  endpoint as written. Local `fluree:ledger:` services take parameters.
- Values use the JSON-LD forms: JSON scalars, `{"@id": ...}` (`_:label`
  for a blank node), `{"@value", "@type"}`, `{"@value", "@language"}`.
Parameters travel on `QueryExecutionOptions::with_params` (and a
`.params()` on every query builder) and are substituted into the parsed
query at each place a query is parsed for execution: single-graph,
within-ledger and cross-ledger datasets, tracked and R2RML variants, and
stream planning. Parses that only read the FROM clause take none, since
parameters never change it. A JSON-LD query with parameters is refused.

- `explain_sparql_with_params` explains the substituted query.
- SPARQL UPDATE takes them through `sparql_update_with_params` on both
  transact builders and `TxnOperation::SparqlUpdate(sparql, params)` in a
  `Transaction`, substituted after validation and before lowering.
- Without parameters no query is rewritten; the only cost is the check.

Tests pin that a parameterized query returns and plans the same as the
inline one, that a FILTER inside OPTIONAL sees the value (which a VALUES
join would not), projection and grouping, misspelt names, updates through
both write paths, and a connection-level FROM query.
`BIND(fulltext(?body, "graph databases") AS ?score)` scores a full-text
value against a query string in SPARQL, as `fulltext` already did in
JSON-LD: it lowers to the same function, so it reads the same BM25 arenas
over indexed and unindexed documents, for `@fulltext` values and
`f:fullTextDefaults` properties alike. Like the vector functions it is
case-insensitive with an underscore variant (`full_text`), and the
namespaced `f:fulltext(...)` names it too. It is a keyword only before a
call: `?fulltext` and a `fulltext:` prefix still parse.

Tests pair SPARQL with the JSON-LD query: equal scores and order on
indexed documents, after an unindexed commit, and on a configured
property, plus every spelling.

Docs: the full-text reference drops its "JSON-LD only, SPARQL planned"
section for the SPARQL form, the vector reference no longer labels its
functions JSON-LD-only, the SPARQL reference gains a search and
similarity functions section, and the search overview and cookbook show
SPARQL beside JSON-LD.
TransactError::ShaclViolation carried only the formatted report, so a
caller that wanted to act on a rejection (show which property failed)
had to parse text. It now carries ShaclViolations: the same message, plus
each violation resolved to full IRIs as validate() reports them. The
payload displays and derefs as the message, so matching and searching it
as text is unchanged. ReportResult moves to fluree-db-transact (re-exported
from fluree_db_api::validate), and the result conversion is shared with
validate(). A branch operation the shapes reject keeps the results too.

A unique-constraint violation now reports the clashing value as its text
rather than its debug form (String("x")).
DropNamedGraphReport carries the drop commit's id when it committed, so a
caller can name the commit without reading the log.
…it id

SyncGraphOpts gains an optional commit message, recorded as the commit's
f:message, and SyncGraphReport the new commit's id, so a caller can name
the commit a sync wrote.
@bplatz
bplatz force-pushed the feat/engine-query-features branch from fbb9e29 to ec0a234 Compare October 7, 2026 22:39
bplatz added 6 commits October 7, 2026 18:58
A parameter `{"@id": "_:x"}` became a labeled blank node, and lowering
reads any label other than a stored `_:fdb-…` id as a non-distinguished
variable. Substitution runs after validation, so nothing refused it:
`DELETE WHERE { $s ?p ?o }` with `s = {"@id": "_:x"}` deleted every
triple, and a SELECT matched every node. A blank-node parameter must now
be a stored node's `_:fdb-…` id; any other label is refused.

`{"@value": iri, "@type": "@id"}` was read as a literal typed `<@id>`,
which matches nothing. It is now an IRI (or a stored blank node, by the
same rule), as the JSON-LD `values` clause reads it; any other keyword
`@type` is refused. The docs say parameter IRIs are full IRIs.
`name`, `?name` and `$name` name one variable, so a map holding two of
them collapsed to whichever came last. It is now an error.
`{"@value": [0.1, 0.2], "@type": f:embeddingVector}` is the form the
JSON-LD `values` clause and writes take, but a parameter refused a list
`@value`, so a caller had to send the vector as its JSON text instead. A
list is now accepted for an f:embeddingVector, and refused for any other
type or when an item is not a number.
The clashing value was rendered with FlakeValue's Display, which writes a
reference as its encoded id (`ref:[13:badge-7]`). It is now decoded to its
IRI, as the subjects in the same message already are. Tests pin the
reported value for a string and for a reference.
…he results

TransactError::ShaclViolation now carries ShaclViolations, so code that
built the variant from a string literal (`ShaclViolation("…".into())`)
stopped compiling: only From<String> existed. From<&str> keeps that a
no-op for embedders.

The branch-operation SHACL tests matched only the variant; they now also
assert the rejection carries its resolved results, across every branch
operation they cover.
@bplatz

bplatz commented Oct 7, 2026

Copy link
Copy Markdown
Contributor Author

Addressed in 1a7cf28 (From<&str> for ShaclViolations).

Vector arrays: addressed in bac5419; #2025 no longer re-encodes vectors as JSON text.

Shared term parser: Follow-up: #2033

@bplatz
bplatz merged commit 1537cf3 into main Oct 7, 2026
16 checks passed
@bplatz
bplatz deleted the feat/engine-query-features branch October 7, 2026 23:29
bplatz added a commit that referenced this pull request Oct 11, 2026
…e-config-integrity

Conflicts:
- parse/jsonld.rs: kept this branch's graph scope over #2022's narrower
  nested-node fix (7d894b7), whose tests (it_materialize_named_graph,
  it_query_fulltext) pass on it. Ported #2008's ill-typed DELETE literal
  (ill_typed_as_stored). where_reads_ledger_default calls main's
  names_default_graph, so a where reading urn:default (#2005) counts as
  reading the ledger's default graph.
- branch_validation.rs, merge/revert previews: the rejection stays a
  TransactError; a SHACL rejection keeps #2024's resolved results through
  describe().
- tx.rs: inline_shapes_policy beside #2024's violation_namespaces; dropped
  the convert_named_graphs_to_templates tests that #2008 removed with the
  function.
- stage.rs: the per-graph policy stays required (explicit-only SHACL)
  alongside main's sh:sparql constraint-failure handling.
- it_config_graph.rs, override_control_identity.rs, update docs: both
  sides kept.

shacl_tests.rs: main's twelve new SHACL tests create their ledgers through
create_shacl_ledger, as the rest of the file does; with SHACL explicit-only
they otherwise enforced nothing and failed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:query Query execution, planning, fast paths, overlay, result formatting area:sparql SPARQL/Turtle/TriG/JSON-LD parsing, lowering, UPDATE semantics, W3C conformance enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants