SHACL-SPARQL
SHACL-SPARQL lets a shape carry a constraint written as a SPARQL SELECT query (W3C SHACL, SPARQL-based constraints). Every solution the query returns for a focus node is a violation.
pgRDF evaluates these in mode => 'pgrdf', which also evaluates every SHACL Core constraint in the same shapes graph. You can mix both kinds in one shapes graph.
Example: no two people share an email address
SELECT pgrdf.add_graph('http://example.org/staff');
SELECT pgrdf.add_graph('http://example.org/staff-shapes');
SELECT pgrdf.parse_turtle('
@prefix ex: <http://example.org/> .
ex:ann ex:email "ann@example.org" .
ex:ben ex:email "shared@example.org" .
ex:cat ex:email "shared@example.org" .
', pgrdf.graph_id('http://example.org/staff'));
SELECT pgrdf.parse_turtle('
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <http://example.org/> .
ex:UniqueEmailShape a sh:NodeShape ;
sh:targetSubjectsOf ex:email ;
sh:sparql [
a sh:SPARQLConstraint ;
sh:message "Email address is shared with another person" ;
sh:select """
PREFIX ex: <http://example.org/>
SELECT $this ?value WHERE {
GRAPH <http://example.org/staff> {
$this ex:email ?value .
?other ex:email ?value .
FILTER (?other != $this)
}
}
""" ;
] .
', pgrdf.graph_id('http://example.org/staff-shapes'));
SELECT jsonb_pretty(pgrdf.validate(pgrdf.graph_id('http://example.org/staff'),
pgrdf.graph_id('http://example.org/staff-shapes'),
mode => 'pgrdf'));{
"mode": "pgrdf",
"results": [
{
"value": "shared@example.org",
"focusNode": "http://example.org/ben",
"resultPath": null,
"sourceShape": "http://example.org/UniqueEmailShape",
"resultMessage": "Email address is shared with another person",
"resultSeverity": "sh:Violation",
"sourceConstraintComponent": "http://www.w3.org/ns/shacl#SPARQLConstraintComponent"
},
{
"value": "shared@example.org",
"focusNode": "http://example.org/cat",
"resultPath": null,
"sourceShape": "http://example.org/UniqueEmailShape",
"resultMessage": "Email address is shared with another person",
"resultSeverity": "sh:Violation",
"sourceConstraintComponent": "http://www.w3.org/ns/shacl#SPARQLConstraintComponent"
}
],
"conforms": false,
"elapsed_ms": 19.588,
"data_triples": 3,
"data_graph_id": 8,
"shapes_triples": 6,
"shapes_graph_id": 9
}The GRAPH <http://example.org/staff> { … } wrapper matters. The known limitation below explains why.
How the query is read
$thisis bound, in turn, to each focus node selected by the shape's targets.- Each solution is one result. If the query projects
?value, it appears asvaluein the report. sh:messagebecomesresultMessage. Without one,resultMessageisnull.sourceShapeis the node shape's IRI, andsourceConstraintComponentissh:SPARQLConstraintComponent.- The query runs on pgRDF's own SPARQL engine, so the features listed under SPARQL queries are what you can use inside
sh:select.
What 'pgrdf' mode evaluates
| Form | Evaluated |
|---|---|
sh:sparql with sh:select, on a node shape | Yes |
| SHACL Core constraints in the same shapes graph | Yes |
sh:sparql nested inside a property shape (sh:property [ … sh:sparql … ]) | No |
Custom constraint components (sh:ConstraintComponent with a SPARQL validator such as sh:ask) | No |
The two unsupported forms are not refused by strict mode. A shape that relies on them passes without being checked. Write those checks as a node-shape sh:select instead. For example, "every email address must be at example.org" as a node-shape constraint:
ex:EmailDomainShape a sh:NodeShape ;
sh:targetSubjectsOf ex:email ;
sh:sparql [
a sh:SPARQLConstraint ;
sh:message "Email must be at example.org" ;
sh:select """
PREFIX ex: <http://example.org/>
SELECT $this ?value WHERE {
GRAPH <http://example.org/staff> {
$this ex:email ?value .
FILTER (!STRENDS(?value, "@example.org"))
}
}
""" ;
] .Known limitation: the query sees every graph
In 'pgrdf' mode, the query inside sh:select runs over all graphs in the database, not only the data graph passed to validate. A match in another graph can produce a false violation or duplicate results.
Take the shapes above without the GRAPH wrapper, and add a triple to a different graph that happens to reuse Ann's address:
SELECT pgrdf.add_graph('http://example.org/other');
SELECT pgrdf.parse_turtle('
@prefix ex: <http://example.org/> .
ex:zed ex:email "ann@example.org" .
', pgrdf.graph_id('http://example.org/other'));Validating http://example.org/staff now also reports http://example.org/ann, because the query found ex:zed in the other graph.
Workaround: wrap the query's pattern in GRAPH <data-graph-iri> { … }, as in the examples on this page. With the wrapper, the same run reports only ben and cat.
Choosing a mode
mode | Evaluates | Use it for |
|---|---|---|
'native' (default) | SHACL Core | Shapes graphs without SPARQL constraints. W3C SHACL Core suite 25/25. |
'pgrdf' | SHACL Core and node-shape sh:sparql / sh:select | Shapes graphs with SPARQL-based constraints. |
'sparql' | An alternative engine; does not evaluate sh:sparql | Not recommended. |
Under the default strict => true, a shapes graph containing sh:sparql refuses to run in 'native' mode rather than pass unchecked:
SELECT pgrdf.validate(pgrdf.graph_id('http://example.org/staff'),
pgrdf.graph_id('http://example.org/staff-shapes'));
-- ERROR: validate: unenforced constraint component in shapes graph under mode "native":
-- sh:sparql (SHACL-SPARQL constraint component — use mode 'pgrdf', which evaluates
-- it). This engine does not evaluate it, so a verdict would be meaningless. Re-run
-- with strict => false to validate the remaining constraints anyway (the named
-- component stays unevaluated).With strict => false the same call returns conforms: true, with the sh:sparql constraint left unevaluated. An unknown mode refuses with SQLSTATE 22023: validate: unknown mode "bogus" (supported: 'native', 'sparql', 'pgrdf'). See Errors and diagnostics.