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:
| Constraint | Holds when |
|---|---|
sh:lessThan ex:p | every value of the path is less than every value of p |
sh:lessThanOrEquals ex:p | every value is less than or equal to every value of p |
sh:equals ex:p | the two value sets are identical |
sh:disjoint ex:p | the 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:closedis 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.
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:equalsandsh:disjointrelate two properties on one focus node.sh:closed truerejects undeclared properties and outgoing relationships;sh:ignoredPropertiesmakes exceptions.- Incoming relationships and the node key are always allowed.