Examples: 08-compose.ttl.
Reusable shapes with sh:node
# A reusable shape with no target: it only matters where another shape uses it.
ex:AddressShape a sh:NodeShape ;
sh:property [ sh:path ex:street ; sh:minCount 1 ] ;
sh:property [ sh:path ex:zip ; sh:minCount 1 ; sh:pattern "^[0-9]{5}$" ] .
ex:PersonShape a sh:NodeShape ;
sh:targetClass ex:Person ;
sh:property [ sh:path ex:livesAt ; sh:node ex:AddressShape ] .
AddressShape has no target, so on its own it checks nothing. sh:node ex:AddressShape says: every node a person LIVES_AT must conform to it. Define the rules for an address once and use them wherever an address appears.
Which part failed?
A standard SHACL report would only say "this address violates sh:node". shacl2cypher inlines the inner shape's constraints into the query and also computes which ones failed. The row's details column lists them:
// name: PersonShape.livesAt.node
MATCH (v0:`Person`)
MATCH (v0)-[:`LIVES_AT`]->(v1)
WITH DISTINCT v0, v1
WHERE NOT ( … street, zip and pattern all hold for v1 … )
RETURN …, [x11 IN [
CASE WHEN … street missing … THEN 'ex:AddressShape/ex:street/sh:minCount' END,
CASE WHEN … zip missing … THEN 'ex:AddressShape/ex:zip/sh:minCount' END,
CASE WHEN … zip pattern … THEN 'ex:AddressShape/ex:zip/sh:pattern' END
] WHERE x11 IS NOT NULL] AS details
So a row reads: person p7, value address a3, details ["ex:AddressShape/ex:zip/sh:pattern"]. You know it was the zip code without rerunning anything.
Logical constraints
ex:PersonShape
# At least one way to reach them.
sh:or (
[ sh:path ex:email ; sh:minCount 1 ]
[ sh:path ex:phone ; sh:minCount 1 ]
) ;
# A person is never also flagged as a bot.
sh:not [ sh:path ex:isBot ; sh:hasValue true ] .
| Constraint | The focus node conforms when |
|---|---|
sh:and ( A B … ) | it conforms to every listed shape |
sh:or ( A B … ) | it conforms to at least one |
sh:xone ( A B … ) | it conforms to exactly one |
sh:not A | it does not conform to A |
The listed shapes are usually anonymous [ … ] shapes, as above. For a blank branch, details extends the outer rule id with its position, e.g. ex:PersonShape/sh:or[0]/ex:email/sh:minCount.
Exactly one
ex:TeamShape a sh:NodeShape ;
sh:targetClass ex:Team ;
sh:xone (
[ sh:path ex:lead ; sh:nodeKind sh:IRI ; sh:minCount 1 ]
[ sh:path ex:sponsor ; sh:nodeKind sh:IRI ; sh:minCount 1 ]
) .
A team has an internal lead or an external sponsor, never both and never neither. sh:nodeKind sh:IRI says the values are nodes, so lead and sponsor are relationships (chapter 6).
Qualified counts
sh:minCount counts all values. Sometimes you want to count only the values that match a shape:
sh:property [
sh:path ex:member ;
sh:nodeKind sh:IRI ;
sh:qualifiedValueShape [ sh:class ex:Manager ] ;
sh:qualifiedMinCount 1 ;
sh:qualifiedMaxCount 2 ;
] .
"Among a team's members, between one and two are managers." For Neo4j this becomes a counting subquery over distinct neighbours:
// name: TeamShape.member.qualifiedMinCount
MATCH (v0:`Team`)
WITH v0
WHERE NOT (COUNT { MATCH (v0)-[:`MEMBER`]->(x1) WHERE (x1:`Manager`) RETURN DISTINCT x1 } >= 1)
Add sh:qualifiedValueShapesDisjoint true when sibling property shapes each count a different kind of neighbour, and one node shouldn't count for both.
What you can't compose
Because inner shapes are inlined into the query, a shape can't reach itself, for example ex:PersonShape using sh:node ex:PersonShape for ex:manager. Recursive shapes are a compile error that shows the shortest cycle with a location for each step. Property shapes nested directly inside other property shapes are not supported yet.
Takeaways
sh:nodereuses a shape; shapes without targets exist to be reused.sh:and,sh:or,sh:xoneandsh:notcombine shapes logically.sh:qualifiedValueShapecounts only the neighbours that match.- Rows for nested shapes carry
details: the inner rules that actually failed.