Turtle in ten minutes
A Turtle file is a list of statements. Each statement is subject predicate object, ending with a full stop:
<http://example.org/hr#PersonShape> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/ns/shacl#NodeShape> .
Nobody writes it like that. Five pieces of shorthand make it readable.
1. Prefixes
Declare a short name for a namespace once, then write prefix:localName:
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <http://example.org/hr#> .
ex:PersonShape rdf:type sh:NodeShape .
The local name matters to shacl2cypher: ex:Person becomes the label Person, and ex:name becomes the property name.
2. a
a is short for rdf:type: ex:PersonShape a sh:NodeShape .
3. ; repeats the subject
ex:PersonShape a sh:NodeShape ;
sh:targetClass ex:Person .
4. , repeats subject and predicate
ex:PersonShape sh:property ex:NameShape , ex:EmailShape .
5. [ … ] is an unnamed node, ( … ) is a list
Brackets create an anonymous thing inline, which is how most property shapes are written. Parentheses create an ordered list:
ex:PersonShape sh:property [ sh:path ex:name ; sh:minCount 1 ] .
ex:StatusShape sh:in ( "active" "on-leave" "left" ) .
Literals
Strings are quoted. Numbers and booleans can be bare. Anything else carries a datatype with ^^:
"Ann" 42 3.5 true "2024-01-31"^^xsd:date
Comments start with #. That's the whole language, as far as this book needs it.
Your first shape
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix ex: <http://example.org/hr#> .
# Every node labelled :Person must have exactly one string name.
ex:PersonShape a sh:NodeShape ;
sh:targetClass ex:Person ;
sh:property [
sh:path ex:name ;
sh:datatype xsd:string ;
sh:minCount 1 ;
sh:maxCount 1 ;
] .
Read it top to bottom:
ex:PersonShape a sh:NodeShape: this is a shape about nodes.sh:targetClass ex:Person: it applies to every:Personnode. Each one becomes a focus node.sh:property [ … ]: a property shape. From each focus node, followsh:path ex:nameto get the values.- Three constraints on those values: they are strings, there is at least one, and there is at most one.
Compile it
shacl2cypher compile doc/examples/book/02-first-shape.ttl --dialect neo4j --node-key id -o out/
compiled 3 rules (3 with queries, 0 static diagnostics) for neo4j into out/
Three constraints, three rules: PersonShape.name.datatype, PersonShape.name.maxCount and PersonShape.name.minCount. Here is the detail query for minCount from out/queries.cypher, exactly as generated:
// name: PersonShape.name.minCount
// ruleId: ex:PersonShape/ex:name/sh:minCount
MATCH (v0:`Person`)
WITH v0
WHERE NOT (size(reduce(x2 = [], x1 IN CASE WHEN v0.`name` IS NULL THEN [] WHEN v0.`name` IS :: LIST<ANY> THEN v0.`name` ELSE [v0.`name`] END | CASE WHEN x1 IS NULL OR x1 IN x2 THEN x2 ELSE x2 + [x1] END)) >= 1)
RETURN 'PersonShape.name.minCount' AS ruleId, 'ex:PersonShape' AS shape, 'ex:name' AS path, 'sh:minCount' AS `constraint`, 'Violation' AS severity,
{label: head([l IN labels(v0) WHERE l IN ['Person']] + labels(v0)), key: 'id', keyValue: v0.`id`, elementId: elementId(v0)} AS focus, null AS value, 'ex:PersonShape violates sh:minCount on ex:name' AS message, [] AS details
LIMIT coalesce($limit, 9223372036854775807);
It is more than WHERE p.name IS NULL, and every part is there for a reason:
MATCH (v0:`Person`)is the target: every focus node.- The
CASEturnsnameinto a list of values:nullbecomes[], a list stays a list, and a single value becomes[value]. Neo4j lets any property hold a list, so SHACL's "number of values" has to handle both. reduce(…)removes nulls and duplicates. SHACL counts distinct values.WHERE NOT (… >= 1)keeps only focus nodes that fail.RETURNbuilds the report row: rule, shape, path, constraint, severity, the focus node identified by--node-keyand element id, and a message.
Next to it is a #summary query with the same WHERE. It returns one row with violationCount and a sample of focus nodes, even when the count is zero.
Run it
Against a database, validate compiles and runs in one step:
shacl2cypher validate doc/examples/book/02-first-shape.ttl --connect bolt://localhost:7687 --node-key id
Each failing node becomes a line under its rule, such as - Person id=p3: ex:PersonShape violates sh:minCount on ex:name. With --format json, the same violation is a structured row:
{
"ruleId": "PersonShape.name.minCount",
"shape": "ex:PersonShape",
"path": "ex:name",
"constraint": "sh:minCount",
"severity": "Violation",
"focus": { "label": "Person", "key": "id", "keyValue": "p3", "elementId": "4:…:12" },
"value": null,
"message": "ex:PersonShape violates sh:minCount on ex:name",
"details": []
}
Add sh:minLength 2 to the property shape and compile again. You get a fourth rule, PersonShape.name.minLength, and a fourth query.
Takeaways
- Turtle is statements, plus prefixes,
a,;,,,[ ]and( ). - A node shape has a target and property shapes. A property shape has a path and constraints.
- Each constraint compiles into one named rule with a detail query and a summary query.