shacl2cypher
← Shapes for Property Graphs

7. Comparing properties and closing shapes

Some rules involve two properties at once: a start before an end, a login equal to an email. Others are about everything a shape didn't mention. SHACL has a constraint for each.

Examples: 07-compare-closed.ttl.

Property pairs

Four constraints compare the values of the property shape's path with the values of a second property on the same focus node:

ConstraintHolds when
sh:lessThan ex:pevery value of the path is less than every value of p
sh:lessThanOrEquals ex:pevery value is less than or equal to every value of p
sh:equals ex:pthe two value sets are identical
sh:disjoint ex:pthe two value sets share no value

Start before end

ex:ProjectShape a sh:NodeShape ;
    sh:targetClass ex:Project ;
    sh:property [ sh:path ex:startDate ; sh:lessThan ex:endDate ] .

If either date is missing there is nothing to compare, and the rule passes; add sh:minCount if both are required. Values that can't be compared, such as a date against a string, are violations.

Equal and disjoint

ex:PersonShape a sh:NodeShape ;
    sh:targetClass ex:Person ;
    sh:property [ sh:path ex:email ; sh:disjoint ex:backupEmail ] ;
    sh:property [ sh:path ex:login ; sh:equals ex:email ] .

Pair constraints report once per focus node. When the path is a relationship, they compare the nodes reached, and the rule gets the cost class quadratic, because every value is compared with every other.

Closed shapes

Everything so far says what must be there. A closed shape also says what must not: nothing beyond what the shape declares.

ex:AddressShape a sh:NodeShape ;
    sh:targetClass ex:Address ;
    sh:closed true ;
    sh:ignoredProperties ( ex:createdAt ) ;
    sh:property [ sh:path ex:street ; sh:datatype xsd:string ] ;
    sh:property [ sh:path ex:zip ; sh:datatype xsd:string ] ;
    sh:property [ sh:path ex:inCity ; sh:class ex:City ] .

An :Address may have the properties street, zip and createdAt, the outgoing relationship IN_CITY, and the node key. Anything else is a violation, for example a stray street_name left over from an old import.

What the query actually checks is easiest to see in a real one. LPG Modeler closes every node type by default, and for its Car shape shacl2cypher generates:

// name: CarShape.closed
MATCH (v0:`Car`)
WITH v0
WHERE NOT ((all(x1 IN keys(v0) WHERE x1 IN ['id', 'seats', 'type', 'vin'])
       AND all(x2 IN [(v0)-[x3]->() | type(x3)] WHERE x2 IN ['TYPE'])))

The allowed property keys are the shape's own (seats, vin), the node key (id), and whatever sh:ignoredProperties lists. That model ignores rdf:type, which maps to the property type and the relationship TYPE. The allowed outgoing relationship types come from the same lists.

How closure differs on a property graph

  • In RDF, sh:closed is about predicates. Here it covers both property keys and outgoing relationship types.
  • Incoming relationships are never constrained. Another node pointing at an address doesn't make the address invalid.
  • The node key is always allowed, even if no shape mentions it.
  • Only simple predicate paths count as declared. A property reached only through a sequence or an inverse path doesn't open the shape.
  • On LadybugDB, where every column exists on every row, closure means undeclared columns must be NULL.
Try it

Close PersonShape from chapter 4 and compile. How many properties does a real :Person in your graph have that the shape never mentioned? Closed shapes are an excellent way to find out.

Takeaways

  • sh:lessThan, sh:lessThanOrEquals, sh:equals and sh:disjoint relate two properties on one focus node.
  • sh:closed true rejects undeclared properties and outgoing relationships; sh:ignoredProperties makes exceptions.
  • Incoming relationships and the node key are always allowed.