shacl2cypher
← Shapes for Property Graphs

8. Composing shapes

Real rules come in combinations: "this or that", "never this", "the address must itself be valid", "at least one member is a manager". SHACL composes shapes out of other shapes, and shacl2cypher tells you which part of the combination failed.

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 ] .
ConstraintThe 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 Ait 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:node reuses a shape; shapes without targets exist to be reused.
  • sh:and, sh:or, sh:xone and sh:not combine shapes logically.
  • sh:qualifiedValueShape counts only the neighbours that match.
  • Rows for nested shapes carry details: the inner rules that actually failed.