Validation
pgrdf.validate checks a data graph against a SHACL shapes graph and returns a W3C-shaped validation report as JSONB. It never writes. The code is in src/validation/shacl.rs (the Core pipeline and the dispatcher) and src/validation/pgrdf_sparql.rs (the SHACL-SPARQL evaluator). The user guide is Validation.
pgrdf.validate(data_graph_id BIGINT,
shapes_graph_id BIGINT,
mode TEXT DEFAULT 'native',
strict BOOLEAN DEFAULT true) → JSONBModes
| Mode | Engine | Evaluates |
|---|---|---|
'native' (default) | rudof's shacl crate, native engine | SHACL Core |
'pgrdf' | the native Core engine plus pgRDF's SHACL-SPARQL evaluator; the two reports are merged | SHACL Core and sh:sparql / sh:select constraints |
'sparql' | rudof's SPARQL engine | SHACL Core only, and not sh:minCount / sh:maxCount; does not evaluate sh:sparql. Not recommended. |
An unknown mode is refused before any work, with 22023:
ERROR: validate: unknown mode "endpoint" (supported: 'native', 'sparql', 'pgrdf')Use 'native' for Core-only shapes graphs and 'pgrdf' whenever a shapes graph contains SHACL-SPARQL constraints.
Strict mode
strict => true (the default) refuses a validation whose verdict would be meaningless.
- A shapes graph that targets nothing. If none of
sh:targetClass,sh:targetNode,sh:targetSubjectsOfandsh:targetObjectsOfoccurs, and no node is bothsh:NodeShapeandrdfs:Class(implicit class targeting), no focus node can be selected, and every data graph would "conform". A wrong or empty graph id produces exactly this case. - A constraint component the mode does not evaluate.
sh:sparqlunder'native'or'sparql', andsh:minCount/sh:maxCountunder'sparql'. Otherwise the component would contribute neither a violation nor an error, andconforms: truewould hide it.
ERROR: validate: unenforced constraint component in shapes graph under mode "native":
sh:sparql (SHACL-SPARQL constraint component — use mode 'pgrdf', which evaluates it). …The predicates used for these checks are read from _pgrdf_quads, not scanned out of serialised text. strict => false skips both checks and validates whatever it can; the unevaluated components stay unevaluated. Passing the same graph as data and shapes raises a WARNING, because common targets then select the shape declarations themselves as focus nodes.
Report
{
"conforms": false,
"results": [
{
"focusNode": "http://example.org/alice",
"resultPath": "http://example.org/age",
"value": null,
"sourceShape": "_:c1eff7dd8f08c03dae4be24cc25c7798",
"sourceConstraintComponent": "http://www.w3.org/ns/shacl#MinCountConstraintComponent",
"resultSeverity": "sh:Violation",
"resultMessage": "MinCount(1) not satisfied"
}
],
"mode": "native",
"data_graph_id": 46,
"shapes_graph_id": 47,
"data_triples": 5,
"shapes_triples": 10,
"elapsed_ms": 1.41
}conforms is true exactly when results is empty. Severities are normalised to sh:Violation, sh:Warning, sh:Info (and so on), and terms are rendered as IRIs, blank-node labels or literals. The report shape is the same for every mode.
The Core pipeline
For 'native', and the Core half of 'pgrdf':
- Read back. Both graphs are read from
_pgrdf_quadsjoined to the dictionary. Asserted and inferred rows are included, somaterializefollowed byvalidatechecks the entailed closure. - Serialise. Each graph is written to N-Triples in memory with oxttl's serialiser.
- Parse. rudof's
InMemoryGraphparses each N-Triples text. - Compile. The shapes graph is compiled into rudof's shape IR.
- Validate. rudof's
GraphValidationruns the selected engine. - Map. Each result becomes a JSON object in the shape above.
Everything runs inside the calling backend and transaction: there is no external endpoint and no file I/O. Blank nodes in the report carry the labels minted during read-back, so they are stable within one call but not across calls.
The SHACL-SPARQL evaluator
Under 'pgrdf', after the Core pass, run_pgrdf_sparql:
- walks the compiled shapes for
sh:sparqlconstraints and theirsh:selectqueries; - resolves each shape's targets against the data graph (node, class, implicit class, subjects-of, objects-of) into a set of focus nodes;
- for each focus node, substitutes
$thisinto thesh:selecttext and runs the query throughpgrdf.sparql, so it uses the hexastore indexes and the plan cache rather than an in-memory copy; - turns each returned row into a result with
sourceConstraintComponent = sh:SPARQLConstraintComponent, takingsh:messageas the message.
The Core and SPARQL results are merged into one report.
Known limitation. The query inside sh:select runs through pgrdf.sparql, which sees all graphs, not only the data graph. A pattern that also matches triples in other graphs produces false positives or duplicate results. Until this is scoped automatically, wrap the pattern in GRAPH <data-graph-iri> { … }:
SELECT pgrdf.add_graph('http://example.org/data');
SELECT pgrdf.parse_turtle('
@prefix ex: <http://example.org/> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
ex:alice a foaf:Person ; foaf:name "Alice" .
ex:bob a foaf:Person ; foaf:name "Bob" ; ex:age "30"^^xsd:integer .
', pgrdf.graph_id('http://example.org/data'));
SELECT pgrdf.add_graph('http://example.org/name-shapes');
SELECT pgrdf.parse_turtle('
@prefix ex: <http://example.org/> .
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
ex:NameShape a sh:NodeShape ;
sh:targetClass foaf:Person ;
sh:sparql [
sh:message "Name is shorter than four characters" ;
sh:select """SELECT $this ?n WHERE {
GRAPH <http://example.org/data> { $this <http://xmlns.com/foaf/0.1/name> ?n }
FILTER(STRLEN(?n) < 4) }""" ] .
', pgrdf.graph_id('http://example.org/name-shapes'));
SELECT pgrdf.validate(pgrdf.graph_id('http://example.org/data'),
pgrdf.graph_id('http://example.org/name-shapes'),
'pgrdf');
-- conforms: false; one result, focusNode http://example.org/bob,
-- sourceConstraintComponent sh:SPARQLConstraintComponentThe same call with the default 'native' mode is refused by strict mode, because 'native' does not evaluate sh:sparql.
Scope
- SHACL Core node and property shapes with the standard Core constraint components (cardinality, value type, value range, string, property pair, logical and shape-based).
- SHACL-SPARQL
sh:sparql/sh:selectconstraints, under'pgrdf'. - Not supported: JavaScript constraints (
sh:js), SHACL Advanced Features such assh:rule, and RDF-star quoted triples as focus nodes.
Which components are actually enforced on a given build is measured, not assumed, by the capability harness below.
Dependencies
shacl0.3.2 andrudof_rdf0.3 from the rudof project provide the Core engine, the shape compiler and the in-memory graph.shaclenables RDF 1.2 support inoxrdfunconditionally. Thereasonablefork patched inCargo.tomlexists so both crates can share oneoxrdfbuild.
Conformance gates and tests
- W3C SHACL Core suite (
tests/w3c-shacl/). A vendored, hermetic subset of the W3Cdata-shapesCore tests, run byjust test-shacl-manifest: 25 of 25 pass. The gate comparesconforms, not the number of results, because the read-back relabels blank nodes and can shift a violation count by one without changing the verdict. Each fixture keeps the unmodified W3C source (<name>.w3c.ttl) beside the file the harness loads. - SHACL-SPARQL fixtures.
just test-shacl-manifest --pgrdfruns them through'pgrdf'against the W3C expected verdicts;--sparqlchecks that the'sparql'dispatch keeps its documented contract. Both run in CI. - Capability document (
tests/shacl-capability/). For each constraint component and target type, a shapes file, a violating data file and a control data file. A component counts as enforced only when the violating case reportsconforms: falseand the control reportsconforms: true. The result is written toCAPABILITY.json. - Regression files.
70-validate-stub.sql(report shape and degenerate cases),71-shacl-real.sql(a failing focus node), and122-shacl-modes.sql(modes, unknown mode, validation of a materialised graph). #[pg_test]s inshacl.rscover conforming and failing graphs, unknown graphs, the mode default and refusals, strict-mode refusals and validation after materialisation.
See Testing for how these fit into CI.