A Semantechs teaching resource

The Bookshop Trail a SHACL course

Seventy shapes graphs that take a beginner from sh:minCount 1 to SPARQL constraints, custom constraint components, SHACL-AF rules and the parts of SHACL 1.2 that run today, against the SPARQL course's dataset and in the same browser editor. Every lesson has been run through the engine, and the report it produced is printed beside it.

70lessons
12modules
36numbered faults
113SHACL terms shown
1engine, in the browser

Start here

Open the editor at semantechs.co.uk/turtle-editor-viewer, or press Open in the editor on lesson s01 below: the data arrives in one tab and the shapes in another, already selected. Press Validate. Then read the lab for what the panel shows, and start on module 01.

Each lesson is one file: what it checks, how it works, a diagram, what to take away, and the shapes graph itself, with a link that loads it. Read the header before you press Validate; the report will make more sense.

This course and the SPARQL course are two halves of one package. They share the Bookshop Trail data and the editor, and lessons here point at the queries there when a query asks the same question a shape answers. A shape says what well-formed data looks like and reports where the data falls short; a query asks a question and returns a table. Both are worth knowing, and knowing one makes the other quicker.

A suggested order

00 → 01 → 02 (s10, s12, s14) → 03 (s20, s22) → 04 (s24, s27, s28) → 05 in full → 06 (s39) → 07 in full → 10 → 11. Module 08 needs the RDF 1.2 edition of the data and reads best after the SPARQL course's module 11; module 09 after its module 18; module 12 is reference.

Module 00

The lab

Twenty minutes on the tool before the first shape. Everything runs in the Turtle Editor Viewer at semantechs.co.uk/turtle-editor-viewer; the parser, the SPARQL engine and the SHACL engine run inside the page, and nothing you load leaves your browser. The full text is shapes/00-the-lab/README.md.

Two tabs and a button

The data goes in the first tab. Press + for a second tab and load the shapes into it. Switch back to the data tab, choose the shapes tab in the Shapes dropdown, leave Inference at none, press Validate.

Every lesson's Open in the editor button does all of that: ?dot= carries the data, &shapes= the shapes, &inference= the mode.

Reading the report

The headline: Conforms or not, the counts by severity, and how many shapes compiled. Then one row per result: severity, focus node, path, value, message, and which shape said so.

Report as tab opens the report as RDF, so the SPARQL panel can query it. Export report saves it.

The Inference dropdown

none is SHACL as specified. RDFS validates the closure of the data (module 09). rules runs the shapes graph's sh:rules once first (module 07); rules, iterated repeats them to a fixpoint.

The tab's text is never changed; the expanded graph lives for the run only.

Two things about the headline

The shapes count is how many shapes compiled; compare it with what you wrote. And Conforms means no results were found, which is also what it says when nothing was checked. Lesson s09 is about that.

The same engine, from the command line

npm install --prefix scripts
node scripts/validate.mjs --data data/bookshop-trail-faulty.ttl --shapes shapes/01-first-shapes/s02-reading-a-violation.ttl
node scripts/validate.mjs --data data/bookshop-trail-1.1.ttl --shapes shapes/07-rules/s51-to-a-fixpoint.ttl --inference rules-iterated
node scripts/validate.mjs ... --format turtle > report.ttl

scripts/validate.mjs names the nested property shapes and renders the compound paths as the editor does, so its rows match the browser's. python scripts/check_shapes.py runs every lesson through it and compares the report with what the lesson claims; python scripts/check_faults.py confirms every numbered fault is caught by the lesson that names it.

Module 01

First shapes

Everything here runs in the browser: the data in one tab, the shapes in another, then Validate. A shape has two halves. The target says which nodes to look at; the constraints say what must be true of them. The report lists each place the data falls short. This module makes that structure familiar, and teaches you to read one result in full before you write anything complicated.

In the standards
S01

Every bookshop has a name

Every bs:Bookshop has at least one rdfs:label.

Data: 04-bookshops.ttl

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix sh:   <http://www.w3.org/ns/shacl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .

bt:BookshopShape
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:property [
        sh:path      rdfs:label ;
        sh:minCount  1 ;
    ] .

How it works

A shape has two parts. sh:targetClass bs:Bookshop is the target: it selects every node in the data with rdf:type bs:Bookshop, and each of those becomes a focus node in turn. sh:property points at a property shape, which is the constraint: sh:path names the property to look at and sh:minCount 1 says there must be at least one value. For each of the 33 shops the engine collects the values of rdfs:label, counts them, and finds one. Nothing is reported, so the report says conforms.

Diagram

   shapes graph                          data graph

   bt:BookshopShape                      bt:shop-inkwell
     sh:targetClass bs:Bookshop  ---->     a bs:Bookshop            <- focus node
     sh:property [                         rdfs:label "The Inkwell"@en
        sh:path     rdfs:label  ------->   ^ value nodes of the path: 1
        sh:minCount 1                      1 >= 1, nothing to report
     ]

   33 focus nodes, 33 counts, 0 results   =>   sh:conforms true

What to take away

  • A shape is a target plus constraints. The target picks the focus nodes; the constraints are checked at each one.
  • A property shape constrains the values reached from the focus node along sh:path.
  • No results means conforms. The headline also counts the shapes it compiled -- two here, the node shape and the property shape inside it.

Try it

In the data tab, delete the rdfs:label line of one shop and press Validate again. One violation appears, naming the shop and the path. Put the line back and it goes.

The report

Conforms · 0 violations, 0 warnings, 0 info · 2 shapes

Nothing to report.

Defined in

S02

Reading a violation

The same shape, on the faulty edition of the data: one shop has no name.

Data: bookshop-trail-faulty.ttl

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix sh:   <http://www.w3.org/ns/shacl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .

bt:BookshopShape
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:property [
        sh:path      rdfs:label ;
        sh:minCount  1 ;
        sh:severity  sh:Violation ;
        sh:message   "Every bookshop needs a name: {$this} has none." ;
    ] .

How it works

bookshop-trail-faulty.ttl is the clean dataset with data/faults.ttl appended; fault F01 is a shop with no rdfs:label. The report has one row. Read it left to right: the severity (Violation, the default), the focus node (the shop), the path (rdfs:label), the value (none, because the complaint is about absence), the message, and the source shape -- shown as 'bt:BookshopShape > property 1', because the property shape is a blank node and has no name of its own. Report as tab shows the same row as RDF: a sh:ValidationResult with sh:focusNode, sh:resultPath, sh:sourceConstraintComponent sh:MinCountConstraintComponent, sh:resultSeverity and sh:resultMessage. The severity here is written out; sh:Violation is what a shape gets when it says nothing.

Diagram

   one result, seven things to read

   sh:resultSeverity            sh:Violation
   sh:focusNode                 bt:shop-halfmoon          <- which node
   sh:resultPath                rdfs:label                <- which property
   sh:value                     (none: nothing to point at)
   sh:sourceConstraintComponent sh:MinCountConstraintComponent   <- which rule
   sh:sourceShape               the property shape        <- which shape said so
   sh:resultMessage             "Every bookshop needs a name ..."

   {$this} in sh:message is replaced by the focus node.

What to take away

  • A validation result is a node in an RDF graph, and the table in the editor is one rendering of it.
  • sh:value is present only when there is a value to point at. Cardinality results have none.
  • sh:message is a template. {$this}, {$path} and {$value} are filled in from the result.
  • The source shape of a property constraint is the property shape, not the node shape around it. That matters for severities later.

The report

Does not conform · 1 violation, 0 warnings, 0 info · 2 shapes

SeverityFocus nodePathValueMessageShape
Violationbt:shop-halfmoonrdfs:labelEvery bookshop needs a name: bt:shop-halfmoon has none.bt:BookshopShape › property 1

Afterwards, in the SPARQL panel

The result as RDF (on the report)
PREFIX sh: <http://www.w3.org/ns/shacl#>
SELECT ?focus ?path ?component ?severity ?message
WHERE {
  ?r a sh:ValidationResult ;
     sh:focusNode ?focus ;
     sh:sourceConstraintComponent ?component ;
     sh:resultSeverity ?severity .
  OPTIONAL { ?r sh:resultPath ?path }
  OPTIONAL { ?r sh:resultMessage ?message }
}

Defined in

S03

A constraint the data disagrees with

Every bookshop has a website -- which six of them do not.

Data: bookshop-trail-1.1.ttl

@prefix bt: <https://example.org/bookshop-trail/> .
@prefix bs: <https://example.org/bookshop-trail/schema#> .
@prefix sh: <http://www.w3.org/ns/shacl#> .

bt:WebsiteExpected
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:property [
        sh:path      bs:website ;
        sh:minCount  1 ;
        sh:severity  sh:Warning ;
        sh:message   "{$this} has no website on record. Six shops do not; check with the shop before treating it as an error." ;
    ] .

How it works

The shape is the same pattern as s01 with bs:website in place of rdfs:label, and on the clean data it reports six shops. Nothing is wrong with the data: the vocabulary declares bs:website functional, which means at most one, and says nothing about needing one. The shape states a policy the data was never built to. Deciding who is right is not the validator's job; recording the decision is, and sh:severity is where it goes. sh:Violation means the data is wrong. sh:Warning means someone should look. sh:Info means this is a fact you asked to be told. Here it is a warning, and the report says the data does not conform, because by default every one of those three severities counts. A report can say otherwise: sh:conformanceDisallows lists the severities being judged by, and the verdict then travels with the rule that produced it.

Diagram

   the shape says        every shop  --website-->  something
   the data says         27 shops do, 6 do not

   who is right?  that is a decision about the policy, not about the data

   sh:severity records the decision:
     sh:Violation   the data is wrong           counts against conforms
     sh:Warning     someone should look         counts
     sh:Info        a fact you asked for        counts
     sh:Debug       for the shape author        does not   (1.2)
     sh:Trace       for the shape author        does not   (1.2)

   a report may narrow that:  sh:conformanceDisallows sh:Violation
                              -> only violations count, and the report says so

What to take away

  • A shape is a statement of policy. When the data disagrees, check the policy before you fix the data.
  • Severity is a property of the shape, and it is how you say what kind of finding this is.
  • Warning and Info stop a report from conforming, as the specification says and as pySHACL has always done. This engine counted only violations until 0.3.0; module 12 records the difference and when it went.

Try it

Change sh:Warning to sh:Info and validate again. Same six rows, same verdict: all three severities count. Then add sh:conformanceDisallows sh:Violation to ask for a narrower judgement, and the headline turns to Conforms while the six rows stay.

The report

Does not conform · 0 violations, 6 warnings, 0 info · 2 shapes

SeverityFocus nodePathValueMessageShape
Warningbt:shop-castle-stepsbs:websitebt:shop-castle-steps has no website on record. Six shops do not; check with the shop before treating it as an error.bt:WebsiteExpected › property 1
Warningbt:shop-dales-foliobs:websitebt:shop-dales-folio has no website on record. Six shops do not; check with the shop before treating it as an error.bt:WebsiteExpected › property 1
Warningbt:shop-ex-librisbs:websitebt:shop-ex-libris has no website on record. Six shops do not; check with the shop before treating it as an error.bt:WebsiteExpected › property 1
Warningbt:shop-marginaliabs:websitebt:shop-marginalia has no website on record. Six shops do not; check with the shop before treating it as an error.bt:WebsiteExpected › property 1
Warningbt:shop-signaturebs:websitebt:shop-signature has no website on record. Six shops do not; check with the shop before treating it as an error.bt:WebsiteExpected › property 1
Warningbt:shop-taff-marginbs:websitebt:shop-taff-margin has no website on record. Six shops do not; check with the shop before treating it as an error.bt:WebsiteExpected › property 1

Defined in

S04

Exactly one town

Each shop is in exactly one settlement, has exactly one founding year, and at most one website.

Data: bookshop-trail-faulty.ttl

@prefix bt: <https://example.org/bookshop-trail/> .
@prefix bs: <https://example.org/bookshop-trail/schema#> .
@prefix sh: <http://www.w3.org/ns/shacl#> .

bt:BookshopShape
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:property [
        sh:path      bs:locatedIn ;
        sh:minCount  1 ;
        sh:maxCount  1 ;
        sh:message   "A shop is in exactly one settlement." ;
    ] ;
    sh:property [
        sh:path      bs:founded ;
        sh:minCount  1 ;
        sh:maxCount  1 ;
        sh:message   "A shop records the one year it opened." ;
    ] ;
    sh:property [
        sh:path      bs:website ;
        sh:maxCount  1 ;
        sh:message   "At most one website." ;
    ] .

How it works

sh:minCount and sh:maxCount together give a cardinality range. [1..1] is 'exactly one'; [0..1] is 'optional, but not more than one'; a bare sh:minCount 1 is 'at least one'. On the faulty data three shops fail, one per property: The Inkwell has been placed in a second town (F04), the shop with no name also has no founding year (F01), and The Foxed Page lists two websites (F06). The website maximum needs no minimum: six real shops have none, and s03 decided that was allowed.

Diagram

                      minCount  maxCount     reads as
   bs:locatedIn          1         1         exactly one
   bs:founded            1         1         exactly one
   bs:website            -         1         at most one

   bt:shop-inkwell   bs:locatedIn  bt:place-wigtown, bt:place-hay-on-wye    2 > 1
   bt:shop-halfmoon  bs:founded    (nothing)                                0 < 1
   bt:shop-foxed-page bs:website   "www...", "https://..."                  2 > 1

What to take away

  • minCount and maxCount are independent. Use both for 'exactly one', one of them for 'at least' or 'at most'.
  • A maxCount result has no sh:value either: it is about the count, not any one value.
  • Property shapes are cheap. One node shape can carry as many as the class has properties, and a report row names the one that fired.

The report

Does not conform · 3 violations, 0 warnings, 0 info · 4 shapes

SeverityFocus nodePathValueMessageShape
Violationbt:shop-foxed-pagebs:websiteAt most one website.bt:BookshopShape › property 3
Violationbt:shop-halfmoonbs:foundedA shop records the one year it opened.bt:BookshopShape › property 2
Violationbt:shop-inkwellbs:locatedInA shop is in exactly one settlement.bt:BookshopShape › property 1

Defined in

S05

Messages and severities

Where a severity has to be written for it to take effect, and what a message template can say.

Data: bookshop-trail-faulty.ttl

@prefix bt: <https://example.org/bookshop-trail/> .
@prefix bs: <https://example.org/bookshop-trail/schema#> .
@prefix sh: <http://www.w3.org/ns/shacl#> .

# The severity is on the node shape. The constraint is on the property shape.
# The results come from the property shape, so they are Violations.
bt:WebsiteAdvice
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:severity     sh:Info ;
    sh:property [
        sh:path      bs:website ;
        sh:minCount  1 ;
        sh:message   "No website on record for {$this}. (severity written on the node shape)" ;
    ] .

# The same constraint with the severity where it takes effect.
bt:WebsiteAdviceFixed
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:property [
        sh:path      bs:website ;
        sh:minCount  1 ;
        sh:severity  sh:Info ;
        sh:message   "No website on record for {$this}. (severity written on the property shape)" ;
    ] .

# A constraint with a value to report, so {$value} has something to say.
bt:StaffedShape
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:property [
        sh:path          bs:staffCount ;
        sh:minInclusive  1 ;
        sh:severity      sh:Warning ;
        sh:message       "{$this} reports {$value} staff. A shop with nobody in it is not trading."@en ;
    ] .

How it works

Two shapes report the shops without a website. bt:WebsiteAdvice puts sh:severity sh:Info on the node shape; its results come back as Violations. bt:WebsiteAdviceFixed puts the same severity on the property shape, and its results are Infos. The rule is that a result takes its severity from its source shape, and the source shape of a property constraint is the property shape. A severity on the node shape applies only to constraints written on the node shape itself. The third shape shows a message using {$value}: sh:minInclusive (module 02) reports the offending value, so the template has something to fill in. Messages can carry a language tag, and a shape can have several.

Diagram

   bt:WebsiteAdvice                        bt:WebsiteAdviceFixed
     sh:severity sh:Info   <- ignored        sh:property [
     sh:property [            by this          sh:path bs:website ;
        sh:path bs:website ;  property         sh:minCount 1 ;
        sh:minCount 1 ]       shape            sh:severity sh:Info ]   <- used
          |                                        |
          v                                        v
     7 x Violation                            7 x Info

   the source shape of a property constraint is the property shape

What to take away

  • Write sh:severity on the shape that owns the constraint. For a property constraint that is the property shape.
  • {$value} is filled only when the result has a value. A message that mentions it on a minCount constraint keeps the braces.
  • Several sh:message values are allowed, usually one per language. This engine joins them into one string; keep to one per shape if that matters.

Try it

Move sh:severity sh:Info from bt:WebsiteAdvice onto its property shape. The seven violations become infos and the headline changes to Conforms.

The report

Does not conform · 7 violations, 1 warning, 7 info · 6 shapes

SeverityFocus nodePathValueMessageShape
Violationbt:shop-castle-stepsbs:websiteNo website on record for bt:shop-castle-steps. (severity written on the node shape)bt:WebsiteAdvice › property 1
Violationbt:shop-dales-foliobs:websiteNo website on record for bt:shop-dales-folio. (severity written on the node shape)bt:WebsiteAdvice › property 1
Violationbt:shop-ex-librisbs:websiteNo website on record for bt:shop-ex-libris. (severity written on the node shape)bt:WebsiteAdvice › property 1
Violationbt:shop-halfmoonbs:websiteNo website on record for bt:shop-halfmoon. (severity written on the node shape)bt:WebsiteAdvice › property 1
Violationbt:shop-marginaliabs:websiteNo website on record for bt:shop-marginalia. (severity written on the node shape)bt:WebsiteAdvice › property 1
Violationbt:shop-signaturebs:websiteNo website on record for bt:shop-signature. (severity written on the node shape)bt:WebsiteAdvice › property 1
Violationbt:shop-taff-marginbs:websiteNo website on record for bt:shop-taff-margin. (severity written on the node shape)bt:WebsiteAdvice › property 1
Warningbt:shop-halfmoonbs:staffCount0bt:shop-halfmoon reports 0 staff. A shop with nobody in it is not trading.bt:StaffedShape › property 1
Infobt:shop-castle-stepsbs:websiteNo website on record for bt:shop-castle-steps. (severity written on the property shape)bt:WebsiteAdviceFixed › property 1
Infobt:shop-dales-foliobs:websiteNo website on record for bt:shop-dales-folio. (severity written on the property shape)bt:WebsiteAdviceFixed › property 1
Infobt:shop-ex-librisbs:websiteNo website on record for bt:shop-ex-libris. (severity written on the property shape)bt:WebsiteAdviceFixed › property 1
Infobt:shop-halfmoonbs:websiteNo website on record for bt:shop-halfmoon. (severity written on the property shape)bt:WebsiteAdviceFixed › property 1

and 3 more

Defined in

S06

Pinning one node, and switching a shape off

Check one named shop against several constraints, and keep a shape in the file without running it.

Data: bookshop-trail-faulty.ttl

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix sh:   <http://www.w3.org/ns/shacl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .

bt:OneShopShape
    a              sh:NodeShape ;
    sh:targetNode  bt:shop-foxed-page ;
    sh:property [ sh:path rdfs:label ;   sh:minCount 1 ] ;
    sh:property [ sh:path bs:locatedIn ; sh:minCount 1 ] ;
    sh:property [ sh:path bs:website ;   sh:maxCount 1 ;
                  sh:message "{$this} lists more than one website." ] .

bt:EveryShopShape
    a               sh:NodeShape ;
    sh:deactivated  true ;
    sh:targetClass  bs:Bookshop ;
    sh:property [ sh:path bs:website ; sh:minCount 1 ] .

How it works

sh:targetNode names the focus nodes one by one. While a shape is being written, pointing it at a single node you know keeps the report short and the cause of each row obvious; the SPARQL course does the same thing with a VALUES clause in q94. The Foxed Page has two websites, so the one violation is that. sh:deactivated true takes a shape out of validation without deleting it: bt:EveryShopShape would report every shop without a website, and reports nothing. It still counts in the shapes total.

Diagram

   sh:targetNode bt:shop-foxed-page       one focus node, whatever else is in the data
        |
        +-- rdfs:label   minCount 1     "The Foxed Page"        ok
        +-- bs:locatedIn minCount 1     "Kendal"                ok  (a string, but present)
        +-- bs:website   maxCount 1     two values              1 violation

   sh:deactivated true                    compiled, counted, never run

What to take away

  • sh:targetNode is the fastest way to develop a shape: one node, one report row per problem.
  • Several targets on one shape are unioned. targetNode and targetClass can be used together.
  • sh:deactivated keeps a shape in the file and out of the report. Remember to take it off.

The report

Does not conform · 1 violation, 0 warnings, 0 info · 6 shapes

SeverityFocus nodePathValueMessageShape
Violationbt:shop-foxed-pagebs:websitebt:shop-foxed-page lists more than one website.bt:OneShopShape › property 3

Defined in

S07

The four kinds of target

One shape for each way of choosing focus nodes: by class, by name, as the subject of a property, as its object.

Data: bookshop-trail-faulty.ttl

@prefix bt: <https://example.org/bookshop-trail/> .
@prefix bs: <https://example.org/bookshop-trail/schema#> .
@prefix sh: <http://www.w3.org/ns/shacl#> .

bt:TypedEventShape
    a               sh:NodeShape ;
    sh:targetClass  bs:Event ;
    sh:property [ sh:path bs:featuring ; sh:minCount 1 ;
                  sh:message "An event features at least one author." ] .

bt:HeldAtSubjectShape
    a                    sh:NodeShape ;
    sh:targetSubjectsOf  bs:heldAt ;
    sh:class             bs:Event ;
    sh:message           "{$this} has a bs:heldAt but is not typed as a bs:Event." .

bt:HeldAtObjectShape
    a                   sh:NodeShape ;
    sh:targetObjectsOf  bs:heldAt ;
    sh:class            bs:Bookshop ;
    sh:message          "Events are held at bookshops; {$this} is not one." .

bt:InkwellShape
    a              sh:NodeShape ;
    sh:targetNode  bt:shop-inkwell ;
    sh:property [ sh:path bs:locatedIn ; sh:maxCount 1 ] .

How it works

sh:targetClass bs:Event picks the typed events; the faulty event with nobody featuring (F20) fails sh:minCount on bs:featuring. sh:targetSubjectsOf bs:heldAt picks everything that has a bs:heldAt, typed or not, and sh:class bs:Event at node level then catches the event nobody typed (F21) -- a node sh:targetClass could never have seen. sh:targetObjectsOf bs:heldAt picks the things events are held at, and sh:class bs:Bookshop catches the event held at a publisher. sh:targetNode bt:shop-inkwell is one node by name. Note that sh:class on a node shape constrains the focus node itself, so the value in the report is the focus node.

Diagram

   sh:targetClass      bs:Event      ->  every ?x with  ?x rdf:type bs:Event (or a subclass)
   sh:targetSubjectsOf bs:heldAt     ->  every ?x with  ?x bs:heldAt ?o
   sh:targetObjectsOf  bs:heldAt     ->  every ?o with  ?s bs:heldAt ?o
   sh:targetNode       bt:shop-inkwell -> that node

   the untyped event         bs:heldAt bt:shop-inkwell     seen by SubjectsOf, invisible to targetClass
   the event at a publisher  bs:heldAt bt:pub-pica         its object fails sh:class bs:Bookshop

What to take away

  • targetClass depends on rdf:type. Data that is described but never typed is invisible to it.
  • targetSubjectsOf and targetObjectsOf select by use of a property. They are how you check the untyped, and how you check what a property points at.
  • sh:class on a node shape tests the focus node; on a property shape it tests each value.

The report

Does not conform · 4 violations, 0 warnings, 0 info · 6 shapes

SeverityFocus nodePathValueMessageShape
Violationbt:event-foxed-page-2025-05-01bs:featuringAn event features at least one author.bt:TypedEventShape › property 1
Violationbt:event-inkwell-2025-06-01bt:event-inkwell-2025-06-01bt:event-inkwell-2025-06-01 has a bs:heldAt but is not typed as a bs:Event.bt:HeldAtSubjectShape
Violationbt:pub-picabt:pub-picaEvents are held at bookshops; bt:pub-pica is not one.bt:HeldAtObjectShape
Violationbt:shop-inkwellbs:locatedInDoes not satisfy sh:maxCountbt:InkwellShape › property 1

Defined in

S08

A class that is its own shape

bs:Translation declared as a class and a shape at once: every translation names what it translates, and who translated it.

Data: bookshop-trail-1.1.ttl

@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix sh:   <http://www.w3.org/ns/shacl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .

bs:Translation
    a  rdfs:Class, sh:NodeShape ;
    sh:property [
        sh:path      bs:translationOf ;
        sh:minCount  1 ;
        sh:maxCount  1 ;
        sh:message   "A translation is of exactly one work." ;
    ] ;
    sh:property [
        sh:path      bs:translatedBy ;
        sh:minCount  1 ;
        sh:message   "{$this} does not say who translated it." ;
    ] .

# The SHACL 1.2 spelling of the same idea: one type instead of two.
bs:Series
    a  sh:ShapeClass ;
    sh:property [ sh:path rdfs:label ; sh:minCount 1 ] .

How it works

A node that is both an rdfs:Class and a sh:NodeShape targets its own instances; no sh:targetClass is needed. SHACL 1.2 spells the same thing sh:ShapeClass, and this engine accepts both. The rdf:type triples for the class live in the shapes graph; the rdfs:subClassOf triples SHACL follows when it decides what an instance is are read from the data graph, which here declares bs:Translation a subclass of bs:Work. On the clean data one of the six translations, the Hebrew edition of Cold Harbour, has no bs:translatedBy, and the shape reports it. Whether that is a gap in the data or a translator nobody recorded is a question for the data's maintainer; the shape has said what it was asked to.

Diagram

   bs:Translation  a  rdfs:Class, sh:NodeShape        <- implicit target: its instances
        sh:property [ sh:path bs:translationOf ; minCount 1 ; maxCount 1 ]
        sh:property [ sh:path bs:translatedBy  ; minCount 1 ]

   data graph:  bt:book-the-dark-sea-ar  a  bs:Translation, bs:Work   -> a focus node
                (rdfs:subClassOf triples are read from here too)

   SHACL 1.2:   bs:Translation  a  sh:ShapeClass     same meaning, one type

What to take away

  • An implicit class target is a shape typed rdfs:Class. It is convenient when the schema and the shapes are maintained together.
  • Class membership follows rdfs:subClassOf in the data graph, not in the shapes graph. Keep the hierarchy with the data if targets are to follow it.
  • A finding on clean data is still a finding. Read it before deciding whether the data or the shape is at fault.

The report

Does not conform · 1 violation, 0 warnings, 0 info · 5 shapes

SeverityFocus nodePathValueMessageShape
Violationbt:book-cold-harbour-hebs:translatedBybt:book-cold-harbour-he does not say who translated it.bs:Translation › property 2

Defined in

S09

Validating against nothing

A shape whose target matches no node reports nothing, and 'conforms' is the result. How to notice.

Data: 04-bookshops.ttl

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix sh:   <http://www.w3.org/ns/shacl#> .
@prefix rdf:  <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .

bt:MistypedShape
    a               sh:NodeShape ;
    sh:targetClass  bs:BookShop ;
    sh:property [ sh:path rdfs:label ; sh:minCount 1 ] .

# A constraint that cannot pass, on a node that certainly exists.
bt:Canary
    a              sh:NodeShape ;
    sh:targetNode  bt:shop-inkwell ;
    sh:property [
        sh:path      rdf:type ;
        sh:maxCount  0 ;
        sh:message   "The canary: validation ran and the data is loaded. Remove this shape when the real ones report." ;
    ] .

How it works

bt:MistypedShape targets bs:BookShop, with a capital S. No node has that type, so the shape has no focus nodes, checks nothing, and contributes nothing to the report. On its own the report would say Conforms, which is true and useless. The second shape is a canary: it targets one node that certainly exists and asks for something certainly false, sh:maxCount 0 on rdf:type. Its one violation proves the data was loaded and validation ran. Take it out once the real shapes are reporting. The headline's shape count is the other check: if it is lower than you expect, something did not compile as a shape.

Diagram

   sh:targetClass bs:BookShop      no such class in the data
        |
        no focus nodes  ->  no checks  ->  no results  ->  "Conforms"

   the canary
   sh:targetNode bt:shop-inkwell ; sh:property [ sh:path rdf:type ; sh:maxCount 0 ]
        |
        1 violation, always  ->  proof that validation happened

What to take away

  • Conforms means nothing that counts was found. It does not mean anything was checked.
  • While writing shapes, keep one constraint that must fail. When it stops failing, the data is not what you think.
  • The shapes count in the headline tells you how many shapes compiled. Compare it with how many you wrote.

Try it

Fix the capital S and validate again: the shape now checks 33 shops and finds nothing to report. Then delete the canary.

The report

Does not conform · 1 violation, 0 warnings, 0 info · 4 shapes

SeverityFocus nodePathValueMessageShape
Violationbt:shop-inkwellrdf:typeThe canary: validation ran and the data is loaded. Remove this shape when the real ones report.bt:Canary › property 1

Defined in

Module 02

What a value may be

The constraint components that look at one value at a time: its datatype and node kind, its class, its numeric range, its length and pattern, its language tag, and how it compares with another property of the same node. The difference between a value and its lexical form matters here, and an xsd:gYear causes the same trouble it causes in SPARQL.

In the standards
S10

The right kind of value

Years are xsd:gYear, counts are integers, prices are decimals, flags are booleans, and a date is a date.

Data: bookshop-trail-faulty.ttl

@prefix bt:  <https://example.org/bookshop-trail/> .
@prefix bs:  <https://example.org/bookshop-trail/schema#> .
@prefix sh:  <http://www.w3.org/ns/shacl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

bt:BookshopValues
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:property [ sh:path bs:founded ;    sh:datatype xsd:gYear ;
                  sh:message "A founding year is an xsd:gYear, not {$value}." ] ;
    sh:property [ sh:path bs:staffCount ; sh:datatype xsd:integer ] ;
    sh:property [ sh:path bs:floorArea ;  sh:datatype xsd:decimal ] ;
    sh:property [ sh:path bs:hasCafe ;    sh:datatype xsd:boolean ] ;
    sh:property [ sh:path bs:website ;    sh:datatype xsd:anyURI ;
                  sh:message "A website is an xsd:anyURI. {$value} is a plain string." ] .

bt:EventValues
    a               sh:NodeShape ;
    sh:targetClass  bs:Event ;
    sh:property [ sh:path bs:eventDate ;   sh:datatype xsd:date ;
                  sh:message "{$value} is not a well-formed xsd:date." ] ;
    sh:property [ sh:path bs:ticketPrice ; sh:datatype xsd:decimal ] ;
    sh:property [ sh:path bs:attendance ;  sh:datatype xsd:integer ] .

bt:SettlementValues
    a               sh:NodeShape ;
    sh:targetClass  bs:Settlement ;
    sh:property [ sh:path bs:population ; sh:datatype xsd:integer ] ;
    sh:property [ sh:path bs:isBookTown ; sh:datatype xsd:boolean ] .

How it works

sh:datatype compares the datatype IRI of each value with the one named, and also checks that the lexical form is valid for that datatype. So "1985"^^xsd:integer fails a gYear constraint although the digits are the right ones; "3.5"^^xsd:decimal fails an integer constraint; the plain string "yes" fails a boolean constraint; a website written without ^^xsd:anyURI is an xsd:string and fails; and "2025-13-01"^^xsd:date carries the right datatype but is ill-formed, which the specification says is a violation too. Seven values on the faulty data fail, all of them from the three resources that fault file F06, F20 and F25 describe.

Diagram

   value                              sh:datatype        result
   "1979"^^xsd:gYear                  xsd:gYear          ok
   "1985"^^xsd:integer                xsd:gYear          violation: wrong datatype, same digits
   "3.5"^^xsd:decimal                 xsd:integer        violation
   "yes"                              xsd:boolean        violation: a plain string is xsd:string
   "www.foxed-page.example"           xsd:anyURI         violation: xsd:string
   "2025-13-01"^^xsd:date             xsd:date           violation: right datatype, ill-formed
   "11000.0"^^xsd:decimal             xsd:integer        violation

What to take away

  • sh:datatype checks the datatype IRI and the well-formedness of the lexical form, nothing more.
  • A number written without a datatype is what Turtle makes of it: 12 is an integer, 12.5 a decimal, "12" a string.
  • An ill-formed literal is a violation even when its datatype matches. Validators are allowed to catch what parsers let through.

The report

Does not conform · 7 violations, 0 warnings, 0 info · 13 shapes

SeverityFocus nodePathValueMessageShape
Violationbt:event-foxed-page-2025-05-01bs:eventDate2025-13-012025-13-01 is not a well-formed xsd:date.bt:EventValues › property 1
Violationbt:event-foxed-page-2025-05-01bs:ticketPricefreeDoes not satisfy sh:datatypebt:EventValues › property 2
Violationbt:place-newtownbs:population11000.0Does not satisfy sh:datatypebt:SettlementValues › property 1
Violationbt:shop-foxed-pagebs:founded1985A founding year is an xsd:gYear, not 1985.bt:BookshopValues › property 1
Violationbt:shop-foxed-pagebs:hasCafeyesDoes not satisfy sh:datatypebt:BookshopValues › property 4
Violationbt:shop-foxed-pagebs:staffCount3.5Does not satisfy sh:datatypebt:BookshopValues › property 2
Violationbt:shop-foxed-pagebs:websitewww.foxed-page.exampleA website is an xsd:anyURI. www.foxed-page.example is a plain string.bt:BookshopValues › property 5

Defined in

S11

IRI, literal or blank node

A shop's town is a node, not a string; a shop's name is a literal with a language tag.

Data: bookshop-trail-faulty.ttl

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix sh:   <http://www.w3.org/ns/shacl#> .
@prefix rdf:  <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix geo:  <http://www.opengis.net/ont/geosparql#> .

bt:BookshopTerms
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:property [
        sh:path      bs:locatedIn ;
        sh:nodeKind  sh:IRI ;
        sh:message   "A shop's town is a place, not the string {$value}." ;
    ] ;
    sh:property [
        sh:path      rdfs:label ;
        sh:nodeKind  sh:Literal ;
        sh:datatype  rdf:langString ;
        sh:message   "Names carry a language tag: \"{$value}\" has none." ;
    ] ;
    sh:property [
        sh:path      bs:specialises ;
        sh:nodeKind  sh:IRI ;
    ] ;
    sh:property [
        sh:path      geo:hasGeometry ;
        sh:nodeKind  sh:BlankNodeOrIRI ;
    ] ;
    sh:property [
        sh:path      bs:website ;
        sh:nodeKind  sh:Literal ;
    ] .

How it works

sh:nodeKind sorts values into the three kinds of RDF term and the three pairs: sh:IRI, sh:Literal, sh:BlankNode, sh:BlankNodeOrIRI, sh:BlankNodeOrLiteral, sh:IRIOrLiteral. bs:locatedIn "Kendal" fails sh:nodeKind sh:IRI. The labels are literals, so sh:Literal passes for all of them, including "The Foxed Page" with no language tag; what catches that one is sh:datatype rdf:langString, because a literal with a tag has that datatype and one without is an xsd:string. The SPARQL course's shapes.ttl makes the same point in its first property shape.

Diagram

                                sh:IRI   sh:Literal   sh:BlankNode
   bt:place-kendal                ok        -             -
   "Kendal"                       -         ok            -
   [ a bs:Place ]                 -         -             ok

   "The Inkwell"@en    datatype rdf:langString
   "The Foxed Page"    datatype xsd:string      <- no tag, so not a langString

What to take away

  • sh:nodeKind is about the kind of term. It cannot tell a well-formed IRI from a nonsense one; sh:pattern or sh:class can.
  • A language-tagged string has datatype rdf:langString. Asking for xsd:string on labels rejects every tagged one.
  • Use sh:nodeKind sh:IRI on object properties as a first line of defence: it catches the string where a node should be before sh:class has to.

The report

Does not conform · 2 violations, 0 warnings, 0 info · 6 shapes

SeverityFocus nodePathValueMessageShape
Violationbt:shop-foxed-pagebs:locatedInKendalA shop's town is a place, not the string Kendal.bt:BookshopTerms › property 1
Violationbt:shop-foxed-pagerdfs:labelThe Foxed PageNames carry a language tag: "The Foxed Page" has none.bt:BookshopTerms › property 2

Defined in

S12

sh:class, and why it needs rdf:type

What a shop, a person, an event, a record and a settlement may point at.

Data: bookshop-trail-faulty.ttl

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix sh:   <http://www.w3.org/ns/shacl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

bt:BookshopLinks
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:property [ sh:path bs:locatedIn ;   sh:class bs:Settlement ;
                  sh:message "{$this} is located in {$value}, which is not a settlement." ] ;
    sh:property [ sh:path bs:specialises ; sh:class skos:Concept ;
                  sh:message "A specialism is a concept from the genre scheme, not {$value}." ] .

bt:PersonLinks
    a               sh:NodeShape ;
    sh:targetClass  bs:Person ;
    sh:property [ sh:path bs:basedIn ; sh:class bs:Settlement ] .

bt:EventLinks
    a               sh:NodeShape ;
    sh:targetClass  bs:Event ;
    sh:property [ sh:path bs:heldAt ;    sh:class bs:Bookshop ] ;
    sh:property [ sh:path bs:featuring ; sh:class bs:Author ] .

bt:StockLinks
    a               sh:NodeShape ;
    sh:targetClass  bs:StockRecord ;
    sh:property [ sh:path bs:atShop ; sh:class bs:Bookshop ;
                  sh:message "{$value} is not typed as a bookshop." ] ;
    sh:property [ sh:path bs:ofWork ; sh:class bs:Work ] .

bt:SettlementLinks
    a               sh:NodeShape ;
    sh:targetClass  bs:Settlement ;
    sh:property [ sh:path bs:within ; sh:class bs:CouncilArea ;
                  sh:message "A settlement sits directly inside a council area; {$value} is not one." ] .

bt:WorkLinks
    a               sh:NodeShape ;
    sh:targetClass  bs:Work ;
    sh:property [ sh:path bs:publishedBy ; sh:class bs:Publisher ] ;
    sh:property [ sh:path bs:author ;      sh:class bs:Author ] ;
    sh:property [ sh:path bs:translationOf ; sh:class bs:Work ] .

How it works

sh:class C passes a value when the value has rdf:type C, or a type that is an rdfs:subClassOf C in the data graph. It fails everything else: a literal, a node of the wrong class, and a node with no rdf:type at all. That last case is the one to remember. The Ghost Shop (F07) has a label, a town and a founding year, and fails sh:class bs:Bookshop because nobody typed it. Six values fail on the faulty data: the string town and the place used as a genre (F06), an author based in a country (F16), the record at the untyped shop (F24), Brecon placed inside Wales with no council area between (F26), and the event held at a publisher (F20). bt:pub-orbit is typed bs:Publisher and passes sh:class bs:Publisher, whatever else is wrong with it; module 04 comes back to that with sh:node.

Diagram

   sh:class bs:Bookshop  passes a value v  when the data has

        v  rdf:type  bs:Bookshop
   or   v  rdf:type  C .   C  rdfs:subClassOf+  bs:Bookshop        (in the data graph)

   bt:shop-ghost  rdfs:label "The Ghost Shop"@en ; bs:locatedIn ... ; bs:founded ...
                  (no rdf:type)                                     -> fails

   bt:book-cold-harbour-he  a bs:Translation .   bs:Translation rdfs:subClassOf bs:Work
                                                                    -> passes sh:class bs:Work

What to take away

  • sh:class is a test of rdf:type, with rdfs:subClassOf followed in the data graph. It says nothing about the node's other properties.
  • An untyped node fails sh:class however well described it is. If your data does not type everything, say so with sh:node instead (module 04).
  • sh:class on a literal fails. That is the intended answer, not an error.

The report

Does not conform · 6 violations, 0 warnings, 0 info · 17 shapes

SeverityFocus nodePathValueMessageShape
Violationbt:author-owen-harkerbs:basedInbt:place-walesDoes not satisfy sh:classbt:PersonLinks › property 1
Violationbt:event-foxed-page-2025-05-01bs:heldAtbt:pub-picaDoes not satisfy sh:classbt:EventLinks › property 1
Violationbt:place-breconbs:withinbt:place-walesA settlement sits directly inside a council area; bt:place-wales is not one.bt:SettlementLinks › property 1
Violationbt:shop-foxed-pagebs:locatedInKendalbt:shop-foxed-page is located in Kendal, which is not a settlement.bt:BookshopLinks › property 1
Violationbt:shop-foxed-pagebs:specialisesbt:place-kendalA specialism is a concept from the genre scheme, not bt:place-kendal.bt:BookshopLinks › property 2
Violationbt:stock-ghost--the-book-townbs:atShopbt:shop-ghostbt:shop-ghost is not typed as a bookshop.bt:StockLinks › property 1

Defined in

S13

Ranges

Coordinates on the globe, counts that are not negative, prices that are not negative, a confidence between 0 and 1.

Data: bookshop-trail-faulty.ttl

@prefix bt:    <https://example.org/bookshop-trail/> .
@prefix bs:    <https://example.org/bookshop-trail/schema#> .
@prefix sh:    <http://www.w3.org/ns/shacl#> .
@prefix wgs84: <http://www.w3.org/2003/01/geo/wgs84_pos#> .

bt:PlaceRanges
    a               sh:NodeShape ;
    sh:targetClass  bs:Place ;
    sh:property [ sh:path wgs84:lat ;  sh:minInclusive -90 ;  sh:maxInclusive 90 ;
                  sh:message "Latitude {$value} is off the globe." ] ;
    sh:property [ sh:path wgs84:long ; sh:minInclusive -180 ; sh:maxInclusive 180 ] ;
    sh:property [ sh:path bs:population ; sh:minInclusive 0 ] .

bt:BookshopRanges
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:property [ sh:path bs:staffCount ; sh:minInclusive 1 ;
                  sh:message "{$this} reports {$value} staff." ] ;
    sh:property [ sh:path bs:floorArea ;  sh:minExclusive 0 ;
                  sh:message "Floor area must be positive, not {$value}." ] .

bt:WorkRanges
    a               sh:NodeShape ;
    sh:targetClass  bs:Work ;
    sh:property [ sh:path bs:pages ; sh:minInclusive 1 ; sh:message "{$value} pages is not a book." ] ;
    sh:property [ sh:path bs:rrp ;   sh:minInclusive 0 ; sh:message "A negative price: {$value}." ] .

bt:StockRanges
    a               sh:NodeShape ;
    sh:targetClass  bs:StockRecord ;
    sh:property [ sh:path bs:copies ;     sh:minInclusive 0 ] ;
    sh:property [ sh:path bs:shelfPrice ; sh:minInclusive 0 ] .

bt:SegmentRanges
    a               sh:NodeShape ;
    sh:targetClass  bs:TrailSegment ;
    sh:property [ sh:path bs:distanceKm ; sh:minExclusive 0 ; sh:maxExclusive 1000 ;
                  sh:message "A segment of {$value} km goes nowhere, or too far." ] .

bt:EventRanges
    a               sh:NodeShape ;
    sh:targetClass  bs:Event ;
    sh:property [ sh:path bs:attendance ;  sh:minInclusive 0 ] ;
    sh:property [ sh:path bs:ticketPrice ; sh:minInclusive 0 ;
                  sh:message "Ticket price {$value} cannot be compared with 0." ] .

bt:SourceRanges
    a               sh:NodeShape ;
    sh:targetClass  bs:Source ;
    sh:property [ sh:path bs:confidence ; sh:minInclusive 0 ; sh:maxInclusive 1 ;
                  sh:message "Confidence is a proportion; {$value} is more than certain." ] .

How it works

The four range components compare each value with a constant: sh:minInclusive and sh:maxInclusive allow equality, sh:minExclusive and sh:maxExclusive do not. They use SPARQL's comparison, so numbers of different numeric types compare by value. A value that cannot be compared at all -- the ticket price "free" against 0 -- is a violation, which is what the specification says should happen. Ten values fail on the faulty data, each named in the message with {$value}.

Diagram

   sh:minInclusive 1      v >= 1        0 fails,  1 passes
   sh:minExclusive 0      v >  0        0 fails,  0.1 passes
   sh:maxInclusive 90     v <= 90       152.5 fails
   sh:maxExclusive ...    v <  ...

   "free"  compared with 0    cannot be compared  ->  violation

What to take away

  • Inclusive or exclusive is the whole difference between the pairs. 'Positive' is minExclusive 0; 'not negative' is minInclusive 0.
  • Comparison is SPARQL's: an integer and a decimal compare by value, a string and a number do not compare and the value fails.
  • Ranges are where {$value} in a message earns its place. Say the number.

The report

Does not conform · 10 violations, 0 warnings, 0 info · 20 shapes

SeverityFocus nodePathValueMessageShape
Violationbt:book-the-margin-notesbs:pages00 pages is not a book.bt:WorkRanges › property 1
Violationbt:book-the-margin-notesbs:rrp-4.99A negative price: -4.99.bt:WorkRanges › property 2
Violationbt:event-foxed-page-2025-05-01bs:attendance-5Does not satisfy sh:minInclusivebt:EventRanges › property 1
Violationbt:event-foxed-page-2025-05-01bs:ticketPricefreeTicket price free cannot be compared with 0.bt:EventRanges › property 2
Violationbt:place-newtownwgs84:lat152.5132Latitude 152.5132 is off the globe.bt:PlaceRanges › property 1
Violationbt:seg-inkwell-inkwellbs:distanceKm0.0A segment of 0.0 km goes nowhere, or too far.bt:SegmentRanges › property 1
Violationbt:shop-foxed-pagebs:floorArea-20.0Floor area must be positive, not -20.0.bt:BookshopRanges › property 2
Violationbt:shop-halfmoonbs:staffCount0bt:shop-halfmoon reports 0 staff.bt:BookshopRanges › property 1
Violationbt:source-rumourbs:confidence1.4Confidence is a proportion; 1.4 is more than certain.bt:SourceRanges › property 1
Violationbt:stock-foxed-page--the-book-townbs:copies-2Does not satisfy sh:minInclusivebt:StockRanges › property 1

Defined in

S14

Comparing years

No shop opened before 1800, and nobody died before they were born -- said in a way this engine can evaluate.

Data: bookshop-trail-faulty.ttl

@prefix bt:  <https://example.org/bookshop-trail/> .
@prefix bs:  <https://example.org/bookshop-trail/schema#> .
@prefix sh:  <http://www.w3.org/ns/shacl#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

# Prefixes for the SPARQL inside this file. @prefix lines are Turtle syntax
# and do not reach the query engine; sh:declare does.
bt:prefixes
    a  owl:Ontology ;
    sh:declare [ sh:prefix "bt" ;   sh:namespace "https://example.org/bookshop-trail/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "bs" ;   sh:namespace "https://example.org/bookshop-trail/schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdf" ;  sh:namespace "http://www.w3.org/1999/02/22-rdf-syntax-ns#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdfs" ; sh:namespace "http://www.w3.org/2000/01/rdf-schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "xsd" ;  sh:namespace "http://www.w3.org/2001/XMLSchema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "skos" ; sh:namespace "http://www.w3.org/2004/02/skos/core#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "dct" ;  sh:namespace "http://purl.org/dc/terms/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "geo" ;  sh:namespace "http://www.opengis.net/ont/geosparql#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "schema" ; sh:namespace "https://schema.org/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "sh" ;   sh:namespace "http://www.w3.org/ns/shacl#"^^xsd:anyURI ] .

# The shape you would write first. Wrong on this engine: gYear does not compare.
bt:YearRangeNaive
    a              sh:NodeShape ;
    sh:targetNode  bt:shop-inkwell ;
    sh:property [
        sh:path          bs:founded ;
        sh:minInclusive  "1800"^^xsd:gYear ;
        sh:message       "sh:minInclusive on a gYear: reported although 1979 is after 1800." ;
    ] .

# The same rule, evaluated on integers.
bt:YearRangeShape
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:message   "{$this} opened in {$value}, before 1800." ;
        sh:select    """
            SELECT $this ?value WHERE {
              $this bs:founded ?value .
              FILTER ( xsd:integer(STR(?value)) < 1800 )
            }
        """ ;
    ] .

# Nobody dies before they are born.
bt:LifespanShape
    a               sh:NodeShape ;
    sh:targetClass  bs:Person ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:message   "{$this} died in {$value}, before being born." ;
        sh:select    """
            SELECT $this ?value WHERE {
              $this bs:born ?born ; bs:died ?value .
              FILTER ( xsd:integer(STR(?value)) < xsd:integer(STR(?born)) )
            }
        """ ;
    ] .

How it works

The years in this dataset are xsd:gYear, and SPARQL's < is not defined for that datatype. bt:YearRangeNaive puts sh:minInclusive "1800"^^xsd:gYear on The Inkwell's founding year of 1979, and the engine reports a violation: it cannot make the comparison, and a comparison it cannot make is a failure. The same shape would flag all 33 shops. The SPARQL course meets this in q07 and q13, and the fix is the same: go through the string. bt:YearRangeShape is a SPARQL constraint (module 05 covers the syntax) that casts xsd:integer(STR(?y)) and compares the integers; it passes The Inkwell and everyone else. bt:LifespanShape does the same for born and died, and finds the one author in the faulty data who died before he was born.

Diagram

   "1979"^^xsd:gYear  >=  "1800"^^xsd:gYear ?

     sh:minInclusive          SPARQL has no < for gYear   ->  violation, on every shop
     xsd:integer(STR(?y))     1979 >= 1800                ->  true

   The SPARQL course's shapes.ttl has sh:lessThan between bs:born and
   bs:died. On this engine that is eight findings, all of them on authors
   whose dates are in the right order. Same cause.

What to take away

  • Range comparisons on xsd:gYear do not work on this engine, and fail closed: every value is reported. xsd:date compares as expected.
  • Cast through STR() and compare integers, in a SPARQL constraint. It is longer, and it is right.
  • When a range shape reports every node, suspect the datatype before the data.

The report

Does not conform · 2 violations, 0 warnings, 0 info · 4 shapes

SeverityFocus nodePathValueMessageShape
Violationbt:author-owen-harker1965https://example.org/bookshop-trail/author-owen-harker died in 1965, before being born.bt:LifespanShape
Violationbt:shop-inkwellbs:founded1979sh:minInclusive on a gYear: reported although 1979 is after 1800.bt:YearRangeNaive › property 1

Defined in

S15

Strings: length and pattern

An ISBN-13 is thirteen digits starting 978 or 979; a website starts with a scheme; an event kind is one of seven words.

Data: bookshop-trail-faulty.ttl

@prefix bt: <https://example.org/bookshop-trail/> .
@prefix bs: <https://example.org/bookshop-trail/schema#> .
@prefix sh: <http://www.w3.org/ns/shacl#> .

bt:WorkStrings
    a               sh:NodeShape ;
    sh:targetClass  bs:Work ;
    sh:property [
        sh:path       bs:isbn ;
        sh:pattern    "^97[89][0-9]{10}$" ;
        sh:minLength  13 ;
        sh:maxLength  13 ;
        sh:message    "An ISBN-13 is 978 or 979 and ten more digits, no hyphens: {$value}." ;
    ] .

bt:BookshopStrings
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:property [
        sh:path     bs:website ;
        sh:pattern  "^https?://" ;
        sh:message  "A website starts with http:// or https://: {$value}." ;
    ] .

bt:EventStrings
    a               sh:NodeShape ;
    sh:targetClass  bs:Event ;
    sh:property [
        sh:path     bs:eventKind ;
        sh:pattern  "^(reading|signing|panel|workshop|launch|lecture|book club)$" ;
        sh:flags    "i" ;
        sh:message  "{$value} is not one of the seven kinds of event." ;
    ] .

How it works

sh:pattern is a SPARQL REGEX over the string form of each value, with sh:flags passed through, so "i" makes it case-insensitive. sh:minLength and sh:maxLength count characters of the string form. The hyphenated ISBN (F08) fails the pattern and the maximum length, one row each. The ISBN with a wrong check digit (F10) passes: it is thirteen digits starting 978, and no pattern can do arithmetic. Module 06 writes the constraint component that can. The website without a scheme (F06) fails ^https?://, and the eventKind "Recital" fails the list of seven, spelled as an alternation.

Diagram

   sh:pattern "^97[89][0-9]{10}$"
   "9787063088596"        matches                 ok
   "978-0-14-118776-1"    hyphens                 violation  (and 17 > 13 for maxLength)
   "9780141187762"        wrong check digit       passes -- a pattern cannot add up

   sh:pattern "^(reading|signing|panel|workshop|launch|lecture|book club)$" ; sh:flags "i"
   "Launch"               matches case-insensitively
   "Recital"              violation

What to take away

  • sh:pattern works on the lexical form of any literal, gYears included. It is a regular expression, so anchor it.
  • Length is measured in characters of the string form. "12" and 12 both have length 2.
  • A pattern checks shape, not truth. Anything that needs arithmetic or a lookup needs SPARQL.

The report

Does not conform · 4 violations, 0 warnings, 0 info · 6 shapes

SeverityFocus nodePathValueMessageShape
Violationbt:book-the-margin-notesbs:isbn978-0-14-118776-1An ISBN-13 is 978 or 979 and ten more digits, no hyphens: 978-0-14-118776-1.bt:WorkStrings › property 1
Violationbt:book-the-margin-notesbs:isbn978-0-14-118776-1An ISBN-13 is 978 or 979 and ten more digits, no hyphens: 978-0-14-118776-1.bt:WorkStrings › property 1
Violationbt:event-foxed-page-2025-05-01bs:eventKindRecitalRecital is not one of the seven kinds of event.bt:EventStrings › property 1
Violationbt:shop-foxed-pagebs:websitewww.foxed-page.exampleA website starts with http:// or https://: www.foxed-page.example.bt:BookshopStrings › property 1

Defined in

S16

Languages

Place names are in English, Welsh or Gaelic, and one name per language; shop names carry a tag.

Data: bookshop-trail-faulty.ttl

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix sh:   <http://www.w3.org/ns/shacl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .

bt:PlaceNames
    a               sh:NodeShape ;
    sh:targetClass  bs:Place ;
    sh:property [
        sh:path        rdfs:label ;
        sh:languageIn  ( "en" "cy" "gd" ) ;
        sh:message     "Place names are in English, Welsh or Gaelic: {$value}." ;
    ] .

bt:SettlementNames
    a               sh:NodeShape ;
    sh:targetClass  bs:Settlement ;
    sh:property [
        sh:path        rdfs:label ;
        sh:uniqueLang  true ;
        sh:message     "{$this} has more than one name in the same language." ;
    ] .

bt:ShopNames
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:property [
        sh:path        rdfs:label ;
        sh:languageIn  ( "en" "cy" "gd" ) ;
        sh:message     "\"{$value}\" has no language tag, or one not in the list." ;
    ] .

How it works

sh:languageIn takes a list of language tags and passes a value whose tag matches one of them, with the usual prefix rule, so cy-GB would match cy. A literal with no tag matches nothing, which is how the shop named without a tag is caught a second way. sh:uniqueLang true says no two values share a language tag; Newtown (F25) has two English labels and fails. Kendal's French label (F28) fails sh:languageIn. The SPARQL course's q11 lists the Welsh and Gaelic names these shapes are about.

Diagram

   sh:languageIn ( "en" "cy" "gd" )
   "Wigtown"@en             ok
   "Baile na h-Uige"@gd     ok
   "Kendal"@fr              violation
   "The Foxed Page"         violation: no tag, so no match

   sh:uniqueLang true
   "Newtown"@en, "New Town"@en, "Y Drenewydd"@cy     two @en  ->  violation

What to take away

  • sh:languageIn is an allow-list of tags. It rejects untagged literals, which is often what you want on a label.
  • sh:uniqueLang is one per language, not one in total. It is the constraint for 'one preferred label per language'.
  • A uniqueLang result has no sh:value: the problem is the pair, not either member.

The report

Does not conform · 3 violations, 0 warnings, 0 info · 6 shapes

SeverityFocus nodePathValueMessageShape
Violationbt:place-kendalrdfs:labelKendalPlace names are in English, Welsh or Gaelic: Kendal.bt:PlaceNames › property 1
Violationbt:place-newtownrdfs:labelbt:place-newtown has more than one name in the same language.bt:SettlementNames › property 1
Violationbt:shop-foxed-pagerdfs:labelThe Foxed Page"The Foxed Page" has no language tag, or one not in the list.bt:ShopNames › property 1

Defined in

S17

A fixed list of values

Kinds of event, kinds of source and writing languages come from short lists, and every country is inside Great Britain.

Data: bookshop-trail-faulty.ttl

@prefix bt: <https://example.org/bookshop-trail/> .
@prefix bs: <https://example.org/bookshop-trail/schema#> .
@prefix sh: <http://www.w3.org/ns/shacl#> .

bt:EventKinds
    a               sh:NodeShape ;
    sh:targetClass  bs:Event ;
    sh:property [
        sh:path     bs:eventKind ;
        sh:in       ( "Reading" "Signing" "Panel" "Workshop" "Launch" "Lecture" "Book Club" ) ;
        sh:message  "{$value} is not a kind of event this trail runs." ;
    ] .

bt:SourceKinds
    a               sh:NodeShape ;
    sh:targetClass  bs:Source ;
    sh:property [
        sh:path     bs:sourceKind ;
        sh:in       ( "guidebook" "survey" "self-reported" "register" "newspaper" ) ;
        sh:message  "{$value} is not a recognised kind of source." ;
    ] .

bt:AuthorLanguages
    a               sh:NodeShape ;
    sh:targetClass  bs:Author ;
    sh:property [
        sh:path     bs:writesIn ;
        sh:in       ( "en" "cy" "gd" "ar" ) ;
        sh:message  "The dataset has four writing languages; {$value} is not one." ;
    ] .

bt:CountryShape
    a               sh:NodeShape ;
    sh:targetClass  bs:Country ;
    sh:property [
        sh:path      bs:within ;
        sh:hasValue  bt:place-gb ;
        sh:message   "Every country in this dataset is part of Great Britain." ;
    ] .

How it works

sh:in lists the allowed values, and every value of the path must be one of them. "Recital" (F20), "hearsay" (F31) and "fr" (F17) are not. sh:hasValue is the other direction: at least one value of the path must be the one named, and the other values do not matter. bt:CountryShape uses it to say that each country's bs:within includes bt:place-gb, which is true of all three. Both compare terms exactly: "Reading" and "reading" are different, and so are 1 and "1".

Diagram

   sh:in ( "Reading" "Signing" "Panel" "Workshop" "Launch" "Lecture" "Book Club" )
        every value of the path must be in the list       "Recital"  ->  violation

   sh:hasValue bt:place-gb
        some value of the path must be this term          bt:place-scotland bs:within bt:place-gb   ok

   comparison is by term:  "Reading" != "reading",  1 != "1",  "1979"^^xsd:gYear != 1979

What to take away

  • sh:in is 'all values from this list'; sh:hasValue is 'this value is among them'.
  • Both are exact term comparisons. Datatype and language tag are part of the term.
  • A short closed list belongs in sh:in. A long or changing one belongs in the data, where a sh:class or a SPARQL constraint can look it up (module 05).

The report

Does not conform · 3 violations, 0 warnings, 0 info · 8 shapes

SeverityFocus nodePathValueMessageShape
Violationbt:author-owen-harkerbs:writesInfrThe dataset has four writing languages; fr is not one.bt:AuthorLanguages › property 1
Violationbt:event-foxed-page-2025-05-01bs:eventKindRecitalRecital is not a kind of event this trail runs.bt:EventKinds › property 1
Violationbt:source-rumourbs:sourceKindhearsayhearsay is not a recognised kind of source.bt:SourceKinds › property 1

Defined in

S18

Comparing two properties

A segment's two ends differ; a work's label and title agree; a translation is not older than its original; and the gYear problem again.

Data: bookshop-trail-faulty.ttl

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix sh:   <http://www.w3.org/ns/shacl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix dct:  <http://purl.org/dc/terms/> .

bt:SegmentEnds
    a               sh:NodeShape ;
    sh:targetClass  bs:TrailSegment ;
    sh:property [
        sh:path      bs:segmentFrom ;
        sh:disjoint  bs:segmentTo ;
        sh:message   "{$this} starts and ends at {$value}." ;
    ] .

bt:WorkTitles
    a               sh:NodeShape ;
    sh:targetClass  bs:Work ;
    sh:property [
        sh:path     rdfs:label ;
        sh:equals   dct:title ;
        sh:message  "rdfs:label and dct:title disagree on {$this}: {$value}." ;
    ] .

# gYear: reported for every translation, in order or not. See s14.
bt:TranslationDates
    a               sh:NodeShape ;
    sh:targetClass  bs:Translation ;
    sh:property [
        sh:path              ( bs:translationOf bs:publicationYear ) ;
        sh:lessThanOrEquals  bs:publicationYear ;
        sh:message           "Original {$value}: cannot be compared with the translation's gYear on this engine." ;
    ] .

# Decimals compare. Which stock is sold below the recommended price?
bt:DiscountShape
    a               sh:NodeShape ;
    sh:targetClass  bs:StockRecord ;
    sh:property [
        sh:path              ( bs:ofWork bs:rrp ) ;
        sh:lessThanOrEquals  bs:shelfPrice ;
        sh:severity          sh:Info ;
        sh:message           "{$this}: RRP {$value} is above the shelf price. A discount." ;
    ] .

How it works

The property pair components compare the values of sh:path with the values of another property of the same focus node. sh:disjoint bs:segmentTo on bs:segmentFrom says no value is shared; the segment from The Inkwell to The Inkwell (F22) fails. sh:equals dct:title on rdfs:label says the two value sets are identical; the work whose title is hyphenated and whose label is not (F13) fails, and the result names the value on each side. sh:lessThanOrEquals compares by value, and the second property must be a plain predicate, but sh:path can be a sequence: (bs:translationOf bs:publicationYear) reaches the original's year, to be compared with the translation's own. Those are gYears, so on this engine every translation is reported, the one published before its original (F14) among six that are in order. bt:DiscountShape does the same comparison on decimals and behaves: it reports, as information, every stock record whose shelf price is below the recommended price.

Diagram

   focus node          sh:path values        compared with      component
   bt:seg-...          bs:segmentFrom         bs:segmentTo       sh:disjoint      shared value -> violation
   bt:book-...         rdfs:label             dct:title          sh:equals        sets differ  -> violation
   bt:book-...-fr      translationOf/pubYear  bs:publicationYear sh:lessThanOrEquals   gYear: cannot compare
   bt:stock-...        ofWork/rrp             bs:shelfPrice      sh:lessThanOrEquals   rrp <= shelf?  no -> info

   the second property is always a single predicate; the first may be a path

What to take away

  • sh:equals and sh:disjoint compare value sets; sh:lessThan and sh:lessThanOrEquals compare every pair of values.
  • The compared property is a predicate on the focus node. To compare with something further away, put the path on sh:path and the predicate on the other side.
  • sh:lessThan on xsd:gYear reports every pair on this engine. Use the STR cast from s14 when years are involved.

The report

Does not conform · 10 violations, 0 warnings, 58 info · 8 shapes

SeverityFocus nodePathValueMessageShape
Violationbt:book-cold-harbour-hebs:translationOf / bs:publicationYear1948Original 1948: cannot be compared with the translation's gYear on this engine.bt:TranslationDates › property 1
Violationbt:book-the-book-town-cybs:translationOf / bs:publicationYear2004Original 2004: cannot be compared with the translation's gYear on this engine.bt:TranslationDates › property 1
Violationbt:book-the-dark-sea-arbs:translationOf / bs:publicationYear2016Original 2016: cannot be compared with the translation's gYear on this engine.bt:TranslationDates › property 1
Violationbt:book-the-dark-sea-frbs:translationOf / bs:publicationYear2016Original 2016: cannot be compared with the translation's gYear on this engine.bt:TranslationDates › property 1
Violationbt:book-the-long-station-cybs:translationOf / bs:publicationYear2013Original 2013: cannot be compared with the translation's gYear on this engine.bt:TranslationDates › property 1
Violationbt:book-the-selkie-ledger-arbs:translationOf / bs:publicationYear1986Original 1986: cannot be compared with the translation's gYear on this engine.bt:TranslationDates › property 1
Violationbt:book-the-tarn-gdbs:translationOf / bs:publicationYear2017Original 2017: cannot be compared with the translation's gYear on this engine.bt:TranslationDates › property 1
Violationbt:book-unnumberedrdfs:labelUn-numberedrdfs:label and dct:title disagree on bt:book-unnumbered: Un-numbered.bt:WorkTitles › property 1
Violationbt:book-unnumberedrdfs:labelUnnumberedrdfs:label and dct:title disagree on bt:book-unnumbered: Unnumbered.bt:WorkTitles › property 1
Violationbt:seg-inkwell-inkwellbs:segmentFrombt:shop-inkwellbt:seg-inkwell-inkwell starts and ends at bt:shop-inkwell.bt:SegmentEnds › property 1
Infobt:stock-bookbarrow--screebs:ofWork / bs:rrp20.41bt:stock-bookbarrow--scree: RRP 20.41 is above the shelf price. A discount.bt:DiscountShape › property 1
Infobt:stock-bookbarrow--the-tarnbs:ofWork / bs:rrp17.43bt:stock-bookbarrow--the-tarn: RRP 17.43 is above the shelf price. A discount.bt:DiscountShape › property 1

and 56 more

Defined in

Module 03

Property paths in shapes

sh:path is not always a single predicate. A property shape can follow a sequence of steps, walk a link backwards, take either of two routes, or continue for any number of hops. These are the paths SPARQL has, written as RDF. One lesson repeats the SPARQL course's point about fixed-length chains, because the same mistake is just as easy to make in a shape.

In the standards
S19

A path through two hops

Every shop's town is inside a council area; every stock record's shop is in a settlement.

Data: bookshop-trail-faulty.ttl

@prefix bt: <https://example.org/bookshop-trail/> .
@prefix bs: <https://example.org/bookshop-trail/schema#> .
@prefix sh: <http://www.w3.org/ns/shacl#> .

bt:ShopInCouncilArea
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:property [
        sh:path      ( bs:locatedIn bs:within ) ;
        sh:minCount  1 ;
        sh:class     bs:CouncilArea ;
        sh:message   "Two hops from {$this} should reach a council area." ;
    ] .

bt:RecordInSettlement
    a               sh:NodeShape ;
    sh:targetClass  bs:StockRecord ;
    sh:property [
        sh:path      ( bs:atShop bs:locatedIn ) ;
        sh:minCount  1 ;
        sh:class     bs:Settlement ;
        sh:message   "The shop holding {$this} is in {$value}, which is not a settlement." ;
    ] .

How it works

A sequence path is an RDF list. sh:path ( bs:locatedIn bs:within ) starts at the focus node, follows bs:locatedIn, then follows bs:within from wherever that arrived, and the value nodes are the ends of the walk. The constraints then apply to those ends as they would to any values: sh:class bs:CouncilArea and sh:minCount 1. The Foxed Page (F06) has "Kendal" for a town, a string with no bs:within, so the walk ends nowhere and sh:minCount reports it. Its stock record walks bs:atShop then bs:locatedIn and arrives at the same string, which fails sh:class bs:Settlement. The record at the untyped Ghost Shop passes: the walk goes through the shop whatever its type, and Durham is a settlement.

Diagram

   sh:path ( bs:locatedIn bs:within )

   bt:shop-inkwell --locatedIn--> bt:place-wigtown --within--> bt:place-dumfries-galloway
                                                                 ^ value node: a CouncilArea, ok

   bt:shop-foxed-page --locatedIn--> "Kendal" --within--> (nothing)
                                                             no value nodes: minCount 1 fails

   sh:path ( bs:atShop bs:locatedIn )
   bt:stock-ghost --atShop--> bt:shop-ghost --locatedIn--> bt:place-durham     a Settlement, ok

What to take away

  • A sequence path is a list: ( p1 p2 ). The value nodes are where the whole walk ends.
  • Constraints see only the ends. Anything wrong in the middle shows up as a missing or wrong end, not as its own result.
  • sh:minCount 1 on a path is 'the walk arrives somewhere'. It is the check for a broken chain.

The report

Does not conform · 2 violations, 0 warnings, 0 info · 4 shapes

SeverityFocus nodePathValueMessageShape
Violationbt:shop-foxed-pagebs:locatedIn / bs:withinTwo hops from bt:shop-foxed-page should reach a council area.bt:ShopInCouncilArea › property 1
Violationbt:stock-foxed-page--the-book-townbs:atShop / bs:locatedInKendalThe shop holding bt:stock-foxed-page--the-book-town is in Kendal, which is not a settlement.bt:RecordInSettlement › property 1

Defined in

S20

The fixed chain that misses twenty shops

Every shop is inside Great Britain -- first with a fixed number of hops, then with a path of any length.

Data: bookshop-trail-1.1.ttl

@prefix bt: <https://example.org/bookshop-trail/> .
@prefix bs: <https://example.org/bookshop-trail/schema#> .
@prefix sh: <http://www.w3.org/ns/shacl#> .

# Wrong: assumes every shop is exactly four hops from Great Britain.
bt:FixedChain
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:property [
        sh:path      ( bs:locatedIn bs:within bs:within bs:within ) ;
        sh:hasValue  bt:place-gb ;
        sh:message   "Fixed chain: {$this} 'is not in Great Britain'. It is; the chain is the wrong length." ;
    ] .

# Right: any number of bs:within hops.
bt:AnyDepth
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:property [
        sh:path      ( bs:locatedIn [ sh:oneOrMorePath bs:within ] ) ;
        sh:hasValue  bt:place-gb ;
        sh:message   "{$this} is not inside Great Britain at any depth." ;
    ] .

How it works

The place hierarchy is uneven, and the SPARQL course's q28 is built on that. An English town sits in a council area in a region in a country in Great Britain; a Scottish or Welsh town has no region. bt:FixedChain walks bs:locatedIn and then exactly three bs:within hops and asks for bt:place-gb among the ends. From a Scottish shop three hops reach Great Britain; from an English shop they reach England, and the shape reports twenty English shops as outside the country. bt:AnyDepth replaces the three hops with [ sh:oneOrMorePath bs:within ], whose value nodes are every place reached by one or more hops. Great Britain is among them for all 33 shops, and it reports nothing. The wrong shape produces no error, and twenty rows that are all wrong.

Diagram

   England (4 levels)                    Scotland / Wales (3 levels)

   shop --locatedIn--> york              shop --locatedIn--> edinburgh
          --within--> north-yorkshire           --within--> edinburgh-city
          --within--> yorkshire                 --within--> scotland
          --within--> england                   --within--> gb           <- 3 hops arrive
          --within--> gb                <- needs 4

   ( bs:locatedIn bs:within bs:within bs:within )   sh:hasValue bt:place-gb
        Scottish and Welsh shops pass, 20 English shops fail

   ( bs:locatedIn [ sh:oneOrMorePath bs:within ] )  sh:hasValue bt:place-gb
        every hop's destination is a value node; gb is among them for all 33

What to take away

  • A fixed number of hops encodes an assumption about the data's depth. When the depth varies, the shape is wrong for part of the data and silent about it.
  • [ sh:oneOrMorePath p ] and [ sh:zeroOrMorePath p ] make every intermediate node a value node. sh:hasValue then asks whether the one you want is among them.
  • Twenty violations on data you believe to be right is a reason to reread the shape first.

The report

Does not conform · 20 violations, 0 warnings, 0 info · 4 shapes

SeverityFocus nodePathValueMessageShape
Violationbt:shop-bookbarrowbs:locatedIn / bs:within / bs:within / bs:withinFixed chain: bt:shop-bookbarrow 'is not in Great Britain'. It is; the chain is the wrong length.bt:FixedChain › property 1
Violationbt:shop-bookwyrmbs:locatedIn / bs:within / bs:within / bs:withinFixed chain: bt:shop-bookwyrm 'is not in Great Britain'. It is; the chain is the wrong length.bt:FixedChain › property 1
Violationbt:shop-borderprintbs:locatedIn / bs:within / bs:within / bs:withinFixed chain: bt:shop-borderprint 'is not in Great Britain'. It is; the chain is the wrong length.bt:FixedChain › property 1
Violationbt:shop-broads-binderybs:locatedIn / bs:within / bs:within / bs:withinFixed chain: bt:shop-broads-bindery 'is not in Great Britain'. It is; the chain is the wrong length.bt:FixedChain › property 1
Violationbt:shop-candlemasbs:locatedIn / bs:within / bs:within / bs:withinFixed chain: bt:shop-candlemas 'is not in Great Britain'. It is; the chain is the wrong length.bt:FixedChain › property 1
Violationbt:shop-chapter-versebs:locatedIn / bs:within / bs:within / bs:withinFixed chain: bt:shop-chapter-verse 'is not in Great Britain'. It is; the chain is the wrong length.bt:FixedChain › property 1
Violationbt:shop-cotton-quartobs:locatedIn / bs:within / bs:within / bs:withinFixed chain: bt:shop-cotton-quarto 'is not in Great Britain'. It is; the chain is the wrong length.bt:FixedChain › property 1
Violationbt:shop-crescentbs:locatedIn / bs:within / bs:within / bs:withinFixed chain: bt:shop-crescent 'is not in Great Britain'. It is; the chain is the wrong length.bt:FixedChain › property 1
Violationbt:shop-dales-foliobs:locatedIn / bs:within / bs:within / bs:withinFixed chain: bt:shop-dales-folio 'is not in Great Britain'. It is; the chain is the wrong length.bt:FixedChain › property 1
Violationbt:shop-dog-earedbs:locatedIn / bs:within / bs:within / bs:withinFixed chain: bt:shop-dog-eared 'is not in Great Britain'. It is; the chain is the wrong length.bt:FixedChain › property 1
Violationbt:shop-endpapersbs:locatedIn / bs:within / bs:within / bs:withinFixed chain: bt:shop-endpapers 'is not in Great Britain'. It is; the chain is the wrong length.bt:FixedChain › property 1
Violationbt:shop-erratabs:locatedIn / bs:within / bs:within / bs:withinFixed chain: bt:shop-errata 'is not in Great Britain'. It is; the chain is the wrong length.bt:FixedChain › property 1

and 8 more

Defined in

S21

Walking a link backwards

Towns with no bookshop, and genres nobody stocks or specialises in -- found by following properties against their direction.

Data: bookshop-trail-1.1.ttl

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix sh:   <http://www.w3.org/ns/shacl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

bt:TownWithShop
    a               sh:NodeShape ;
    sh:targetClass  bs:Settlement ;
    sh:property [
        sh:path      [ sh:inversePath bs:locatedIn ] ;
        sh:minCount  1 ;
        sh:severity  sh:Warning ;
        sh:message   "{$this} has no bookshop. Four towns do not; see q16 in the SPARQL course." ;
    ] .

bt:GenreInUse
    a               sh:NodeShape ;
    sh:targetClass  skos:Concept ;
    sh:property [
        sh:path      [ sh:alternativePath ( [ sh:inversePath bs:genre ] [ sh:inversePath bs:specialises ] ) ] ;
        sh:minCount  1 ;
        sh:severity  sh:Info ;
        sh:message   "No work is filed under {$this} and no shop specialises in it." ;
    ] .

bt:ShopWithEvents
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:property [ sh:path [ sh:inversePath bs:heldAt ] ; sh:minCount 1 ;
                  sh:message "{$this} has never held an event." ] .

bt:AuthorWithWork
    a               sh:NodeShape ;
    sh:targetClass  bs:Author ;
    sh:property [ sh:path [ sh:inversePath bs:author ] ; sh:minCount 1 ] .

bt:PublisherWithWork
    a               sh:NodeShape ;
    sh:targetClass  bs:Publisher ;
    sh:property [ sh:path [ sh:inversePath bs:publishedBy ] ; sh:minCount 1 ] .

How it works

[ sh:inversePath bs:locatedIn ] at a settlement yields the things located in it. sh:minCount 1 then says every settlement has at least one, and four do not: Durham, Perth, Fort William and Truro, the same four the SPARQL course's q16 finds with FILTER NOT EXISTS. That is a fact about the data rather than a fault, so the shape reports it as a warning. bt:GenreInUse combines an inverse with an alternative: a concept is reached backwards along bs:genre from a work or backwards along bs:specialises from a shop, and a concept reached by neither is reported as information: five of them, the scheme's top concept and its broad divisions among them, which works are filed beneath rather than under. The three other shapes check that every shop has held an event, every author has a work and every publisher has published, and report nothing on the clean data.

Diagram

   forward      bt:shop-inkwell --bs:locatedIn--> bt:place-wigtown
   inverse      bt:place-wigtown --[ sh:inversePath bs:locatedIn ]--> bt:shop-inkwell, bt:shop-marginalia

   sh:targetClass bs:Settlement ; sh:path [ sh:inversePath bs:locatedIn ] ; sh:minCount 1
        bt:place-durham   ->  no shops  ->  warning   (q16 in the SPARQL course)

   [ sh:alternativePath ( [ sh:inversePath bs:genre ] [ sh:inversePath bs:specialises ] ) ]
        a concept nobody files a book under and no shop specialises in

What to take away

  • An inverse path turns 'what does this point at' into 'what points at this'. Constraints on the count then read as 'is this used'.
  • A shape can be a question. Warning and Info severities keep the answers out of the conformance verdict.
  • Inverse, sequence and alternative paths nest freely. Read a compound path from the outside in.

The report

Does not conform · 0 violations, 4 warnings, 5 info · 10 shapes

SeverityFocus nodePathValueMessageShape
Warningbt:place-durham^bs:locatedInbt:place-durham has no bookshop. Four towns do not; see q16 in the SPARQL course.bt:TownWithShop › property 1
Warningbt:place-fort-william^bs:locatedInbt:place-fort-william has no bookshop. Four towns do not; see q16 in the SPARQL course.bt:TownWithShop › property 1
Warningbt:place-perth^bs:locatedInbt:place-perth has no bookshop. Four towns do not; see q16 in the SPARQL course.bt:TownWithShop › property 1
Warningbt:place-truro^bs:locatedInbt:place-truro has no bookshop. Four towns do not; see q16 in the SPARQL course.bt:TownWithShop › property 1
Infobt:genre-biography(^bs:genre | ^bs:specialises)No work is filed under bt:genre-biography and no shop specialises in it.bt:GenreInUse › property 1
Infobt:genre-fiction(^bs:genre | ^bs:specialises)No work is filed under bt:genre-fiction and no shop specialises in it.bt:GenreInUse › property 1
Infobt:genre-literature(^bs:genre | ^bs:specialises)No work is filed under bt:genre-literature and no shop specialises in it.bt:GenreInUse › property 1
Infobt:genre-non-fiction(^bs:genre | ^bs:specialises)No work is filed under bt:genre-non-fiction and no shop specialises in it.bt:GenreInUse › property 1
Infobt:genre-portal-fantasy(^bs:genre | ^bs:specialises)No work is filed under bt:genre-portal-fantasy and no shop specialises in it.bt:GenreInUse › property 1

Defined in

S22

Any number of hops

Every genre leads up to the top concept, and every place leads up to Great Britain.

Data: bookshop-trail-faulty.ttl

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix sh:   <http://www.w3.org/ns/shacl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

bt:GenreReachesTop
    a               sh:NodeShape ;
    sh:targetClass  skos:Concept ;
    sh:property [
        sh:path      [ sh:zeroOrMorePath skos:broader ] ;
        sh:hasValue  bt:genre-literature ;
        sh:message   "{$this} does not lead up to the top of the genre scheme." ;
    ] .

bt:PlaceReachesGB
    a               sh:NodeShape ;
    sh:targetClass  bs:Place ;
    sh:property [
        sh:path      [ sh:zeroOrMorePath bs:within ] ;
        sh:hasValue  bt:place-gb ;
        sh:message   "{$this} is not inside Great Britain at any depth." ;
    ] .

bt:WorkHasAuthor
    a               sh:NodeShape ;
    sh:targetClass  bs:Work ;
    sh:property [
        sh:path      ( [ sh:zeroOrOnePath bs:translationOf ] bs:author ) ;
        sh:minCount  1 ;
        sh:message   "Neither {$this} nor the work it translates names an author." ;
    ] .

How it works

[ sh:zeroOrMorePath skos:broader ] at a concept yields the concept itself and everything above it, however far up. sh:hasValue bt:genre-literature asks for the top of the scheme among them. Three concepts in the faulty data never get there: bt:genre-stray, which is under nothing (F29), and the two that are each broader than the other (F30) -- the walk goes round for ever in the data, and the path evaluation still terminates, because a path visits each node once. The same shape on bs:within finds the two council areas inside each other (F27). [ sh:zeroOrOnePath bs:translationOf ] is the third form: the focus node, or the one step from it; bt:WorkHasAuthor uses it to say a work or the work it translates has an author.

Diagram

   [ sh:zeroOrMorePath skos:broader ]   from bt:genre-cosy-crime
        { cosy-crime, crime-fiction, fiction, literature }        literature present: ok

   from bt:genre-loop-a    { loop-a, loop-b }    round and round, visited once each
                                                  literature absent: violation

   [ sh:zeroOrOnePath bs:translationOf ]   { the work, the work it translates if any }

What to take away

  • zeroOrMore includes the start; oneOrMore does not. When the start could itself satisfy the constraint, that matters.
  • A cycle in the data does not stop a path. It stops a constraint from ever being satisfied, which is how you find the cycle.
  • hasValue over a * or + path is the SHACL spelling of 'reachable from'.

The report

Does not conform · 5 violations, 0 warnings, 0 info · 6 shapes

SeverityFocus nodePathValueMessageShape
Violationbt:genre-loop-askos:broader*bt:genre-loop-a does not lead up to the top of the genre scheme.bt:GenreReachesTop › property 1
Violationbt:genre-loop-bskos:broader*bt:genre-loop-b does not lead up to the top of the genre scheme.bt:GenreReachesTop › property 1
Violationbt:genre-strayskos:broader*bt:genre-stray does not lead up to the top of the genre scheme.bt:GenreReachesTop › property 1
Violationbt:place-loop-abs:within*bt:place-loop-a is not inside Great Britain at any depth.bt:PlaceReachesGB › property 1
Violationbt:place-loop-bbs:within*bt:place-loop-b is not inside Great Britain at any depth.bt:PlaceReachesGB › property 1

Defined in

S23

Either route, and the path in the report

Every shop has a trail neighbour in one direction or the other; which shops are junctions; and what a compound path looks like in the report graph.

Data: bookshop-trail-1.1.ttl

@prefix bt: <https://example.org/bookshop-trail/> .
@prefix bs: <https://example.org/bookshop-trail/schema#> .
@prefix sh: <http://www.w3.org/ns/shacl#> .

bt:Connected
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:property [
        sh:path      [ sh:alternativePath ( bs:connectsTo [ sh:inversePath bs:connectsTo ] ) ] ;
        sh:minCount  1 ;
        sh:message   "{$this} is on no trail segment." ;
    ] .

bt:Junction
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:property [
        sh:path      [ sh:alternativePath ( bs:connectsTo [ sh:inversePath bs:connectsTo ] ) ] ;
        sh:maxCount  2 ;
        sh:severity  sh:Info ;
        sh:message   "{$this} is a junction: three or more neighbours on the trail." ;
    ] .

How it works

bs:connectsTo is asserted in one direction per segment, so a shop's neighbours are the union of what it connects to and what connects to it: [ sh:alternativePath ( bs:connectsTo [ sh:inversePath bs:connectsTo ] ) ]. Every shop has at least one, so bt:Connected reports nothing. bt:Junction uses the same path with sh:maxCount 2 and severity Info, which lists the shops with three or more neighbours: the points where the SPARQL course's two branches leave the main line. Open the report as a tab and look at sh:resultPath: it is not an IRI but a blank node carrying sh:alternativePath, an rdf:List and an sh:inversePath, a copy of the path from the shapes graph. The editor renders it as an expression; the query below reads it as RDF.

Diagram

   [ sh:alternativePath ( bs:connectsTo [ sh:inversePath bs:connectsTo ] ) ]

   bt:shop-inkwell --connectsTo--> bt:shop-marginalia          forward
   bt:shop-quire   --connectsTo--> bt:shop-inkwell             backward from the Inkwell's side
        neighbours of the Inkwell: { marginalia, quire, ... }

   in the report:
   _:r  sh:resultPath  [ sh:alternativePath ( bs:connectsTo [ sh:inversePath bs:connectsTo ] ) ]
        a structure, not a name; the editor shows it as (bs:connectsTo | ^bs:connectsTo)

What to take away

  • An alternative path is a union of routes. It is how you treat an asserted-one-way property as symmetric.
  • sh:resultPath carries the path as it was written, structure and all. Code that reads reports must expect a blank node there.
  • sh:maxCount with an Info severity lists the nodes above a threshold without failing anything.

The report

Does not conform · 0 violations, 0 warnings, 3 info · 4 shapes

SeverityFocus nodePathValueMessageShape
Infobt:shop-colophon(bs:connectsTo | ^bs:connectsTo)bt:shop-colophon is a junction: three or more neighbours on the trail.bt:Junction › property 1
Infobt:shop-crescent(bs:connectsTo | ^bs:connectsTo)bt:shop-crescent is a junction: three or more neighbours on the trail.bt:Junction › property 1
Infobt:shop-endpapers(bs:connectsTo | ^bs:connectsTo)bt:shop-endpapers is a junction: three or more neighbours on the trail.bt:Junction › property 1

Afterwards, in the SPARQL panel

The path structure inside a result (on the report)
PREFIX sh:  <http://www.w3.org/ns/shacl#>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
SELECT ?focus ?first ?inverseOf
WHERE {
  ?r sh:focusNode ?focus ;
     sh:resultPath ?path .
  ?path sh:alternativePath ?list .
  ?list rdf:first ?first ;
        rdf:rest/rdf:first ?second .
  ?second sh:inversePath ?inverseOf .
}
LIMIT 5

Defined in

Module 04

Shapes inside shapes, and logic

A constraint can hand a value to another shape. sh:node, sh:not, sh:and, sh:or and sh:xone are all built on that idea, as are qualified value shapes ('at least one of the values is a ...'), closed shapes that reject any property not listed, and shapes that refer to themselves.

In the standards
S24

A shape for the value

A work's publisher is a well-formed publisher; a stock record's shop is a well-formed shop -- typed or not.

Data: bookshop-trail-faulty.ttl

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix sh:   <http://www.w3.org/ns/shacl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd:  <http://www.w3.org/2001/XMLSchema#> .

bt:PublisherShape
    a  sh:NodeShape ;
    sh:property [ sh:path rdfs:label ;   sh:minCount 1 ] ;
    sh:property [ sh:path bs:locatedIn ; sh:maxCount 1 ; sh:class bs:Settlement ] .

bt:ShopShape
    a  sh:NodeShape ;
    sh:property [ sh:path rdfs:label ;   sh:minCount 1 ] ;
    sh:property [ sh:path bs:locatedIn ; sh:minCount 1 ; sh:nodeKind sh:IRI ] ;
    sh:property [ sh:path bs:founded ;   sh:minCount 1 ; sh:datatype xsd:gYear ] .

bt:WorkShape
    a               sh:NodeShape ;
    sh:targetClass  bs:Work ;
    sh:property [
        sh:path     bs:publishedBy ;
        sh:node     bt:PublisherShape ;
        sh:message  "{$value} is not a well-formed publisher." ;
    ] .

bt:StockShopShape
    a               sh:NodeShape ;
    sh:targetClass  bs:StockRecord ;
    sh:property [
        sh:path     bs:atShop ;
        sh:node     bt:ShopShape ;
        sh:message  "{$value} does not look like a shop." ;
    ] .

How it works

sh:node hands each value to another node shape and asks whether it conforms. bt:PublisherShape has no target of its own: it is only ever reached through sh:node, and it says a publisher has a name and is located in at most one settlement. bt:pub-orbit (F19) is located in a country, so the work it published fails. The result is on the work, names the publisher as the value, and says no more: what went wrong inside the nested check is not reported. Compare sh:class in s12. The record at the untyped Ghost Shop fails sh:class bs:Bookshop because the shop has no type, and passes sh:node bt:ShopShape because it has everything the shape asks for. The record at The Foxed Page is the other way round: typed, so it passes sh:class, and not well formed, so it fails sh:node.

Diagram

   bt:WorkShape                            bt:PublisherShape   (no target)
     sh:property [                           sh:property [ rdfs:label   minCount 1 ]
        sh:path bs:publishedBy ;             sh:property [ bs:locatedIn maxCount 1 ; class bs:Settlement ]
        sh:node bt:PublisherShape ]  ---->   conformance check of the value, yes or no
                                             |
   bt:book-unnumbered bs:publishedBy bt:pub-orbit     pub-orbit locatedIn bt:place-wales: no
        result on bt:book-unnumbered, value bt:pub-orbit, NodeConstraintComponent

   sh:class  asks  what is it typed as?            ghost shop: fails    foxed page: passes
   sh:node   asks  does it look right?             ghost shop: passes   foxed page: fails

What to take away

  • sh:node is a conformance check of the value against a node shape: a boolean. The inner results are not in the report.
  • A shape with no target is a definition. Give it a name and reuse it from as many sh:node constraints as you like.
  • sh:class tests rdf:type; sh:node tests structure. Data that is described but not typed needs the second.

The report

Does not conform · 2 violations, 0 warnings, 0 info · 11 shapes

SeverityFocus nodePathValueMessageShape
Violationbt:book-unnumberedbs:publishedBybt:pub-orbitbt:pub-orbit is not a well-formed publisher.bt:WorkShape › property 1
Violationbt:stock-foxed-page--the-book-townbs:atShopbt:shop-foxed-pagebt:shop-foxed-page does not look like a shop.bt:StockShopShape › property 1

Defined in

S25

Not

A bookshop is not a publisher, a settlement is not a shop, and nobody specialises in 'Literature'.

Data: bookshop-trail-faulty.ttl

@prefix bt: <https://example.org/bookshop-trail/> .
@prefix bs: <https://example.org/bookshop-trail/schema#> .
@prefix sh: <http://www.w3.org/ns/shacl#> .

bt:BookshopShape
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:not          [ sh:class bs:Publisher ] ;
    sh:message      "{$this} is typed as both a bookshop and a publisher; the vocabulary says those are disjoint." ;
    sh:property [
        sh:path     bs:specialises ;
        sh:not      [ sh:hasValue bt:genre-literature ] ;
        sh:message  "A specialism narrower than the whole of literature, please." ;
    ] .

bt:SettlementShape
    a               sh:NodeShape ;
    sh:targetClass  bs:Settlement ;
    sh:not          [ sh:class bs:Bookshop ] .

How it works

sh:not takes a shape and passes a node that does not conform to it. On a node shape it tests the focus node: bt:BookshopShape says a shop does not conform to [ sh:class bs:Publisher ], and the shop that is typed as both (F03) fails. The vocabulary declares the two classes disjoint with owl:AllDisjointClasses; that is a statement for a reasoner, and this is the same statement for a validator. On a property shape sh:not tests each value: [ sh:hasValue bt:genre-literature ] as a node shape means 'is that node', so sh:not of it means 'is not that node', and no shop specialises in the top of the scheme.

Diagram

   sh:not S      passes n   when   n does not conform to S

   bt:shop-halfmoon  a bs:Bookshop, bs:Publisher
        conforms to [ sh:class bs:Publisher ]  ->  sh:not fails  ->  violation

   sh:property [ sh:path bs:specialises ; sh:not [ sh:hasValue bt:genre-literature ] ]
        each value v:  v conforms to [ sh:hasValue X ]  iff  v = X

What to take away

  • sh:not is a conformance check turned round. Anything that can be a shape can be negated.
  • owl:disjointWith is advice to a reasoner; sh:not [ sh:class ... ] is the check.
  • A node shape with sh:hasValue and no path tests whether the node is that value. Inside sh:not it is 'anything but'.

The report

Does not conform · 1 violation, 0 warnings, 0 info · 6 shapes

SeverityFocus nodePathValueMessageShape
Violationbt:shop-halfmoonbt:shop-halfmoonbt:shop-halfmoon is typed as both a bookshop and a publisher; the vocabulary says those are disjoint.bt:BookshopShape

Defined in

S26

Or, and, exactly one

A work has an ISBN or predates them; a place is exactly one kind of place; a shop is named and located.

Data: bookshop-trail-faulty.ttl

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix sh:   <http://www.w3.org/ns/shacl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

bt:WorkShape
    a               sh:NodeShape ;
    sh:targetClass  bs:Work ;
    sh:or (
        [ sh:property [ sh:path bs:isbn ; sh:minCount 1 ] ]
        [ sh:property [ sh:path bs:publicationYear ; sh:pattern "^(1[0-8][0-9][0-9]|19[0-6][0-9])$" ] ]
    ) ;
    sh:message  "{$this} has no ISBN and was published after 1969." .

# A place is exactly one kind of place. Great Britain is none of them.
bt:PlaceKind
    a               sh:NodeShape ;
    sh:targetClass  bs:Place ;
    sh:xone (
        [ sh:class bs:Country ]
        [ sh:class bs:Region ]
        [ sh:class bs:CouncilArea ]
        [ sh:class bs:Settlement ]
    ) ;
    sh:message  "{$this} is not exactly one kind of place." .

# The same rule, with the root of the hierarchy allowed for.
bt:PlaceKindFixed
    a               sh:NodeShape ;
    sh:targetClass  bs:Place ;
    sh:xone (
        [ sh:class bs:Country ]
        [ sh:class bs:Region ]
        [ sh:class bs:CouncilArea ]
        [ sh:class bs:Settlement ]
        [ sh:property [ sh:path skos:notation ; sh:hasValue "GB" ] ]
    ) .

bt:Named    a sh:NodeShape ; sh:property [ sh:path rdfs:label ;   sh:minCount 1 ] .
bt:Located  a sh:NodeShape ; sh:property [ sh:path bs:locatedIn ; sh:minCount 1 ] .

bt:ShopShape
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:and          ( bt:Named bt:Located ) ;
    sh:message      "{$this} is not both named and located." .

How it works

sh:or, sh:and and sh:xone each take a list of shapes and combine the conformance checks. bt:WorkShape says a work conforms to one of two shapes: it has an ISBN, or its year matches a pattern for 1000 to 1969 -- sh:pattern works on the lexical form, so it can do to a gYear what sh:maxInclusive cannot. Eleven clean works from before 1970 pass by the second branch; the 1998 work without an ISBN (F12) fails both. bt:PlaceKind uses sh:xone: a place is exactly one of country, region, council area, settlement. Great Britain is none of them, and it is reported -- on the clean data as well. That is a true finding about how the data was modelled, and bt:PlaceKindFixed records the decision by adding a fifth branch for the one node with skos:notation "GB". bt:ShopShape uses sh:and to compose two named shapes; the shop with no name (F01) fails the first of them.

Diagram

   sh:or   ( S1 S2 )     at least one conforms
   sh:and  ( S1 S2 )     all conform
   sh:xone ( S1 S2 )     exactly one conforms

   bt:book-unnumbered   isbn? no    year 1998 matches ^1[0-8]..|^19[0-6]. ? no    -> or fails
   bt:book-hedgerow-alphabet   isbn? no    year 1966 matches                       -> or passes

   bt:place-gb   Country? Region? CouncilArea? Settlement?   none  -> xone fails
                 ... or skos:notation "GB"?                  yes   -> fixed xone passes

What to take away

  • The list operators combine conformance checks of the focus node. Each branch is a full shape and can be as complex as you like.
  • sh:pattern on a year is a way round the gYear comparison problem when the range has a lexical description.
  • A finding on clean data is a modelling decision surfacing. Change the shape to record the decision, and say why in a comment.

The report

Does not conform · 3 violations, 0 warnings, 0 info · 22 shapes

SeverityFocus nodePathValueMessageShape
Violationbt:book-unnumberedbt:book-unnumberedbt:book-unnumbered has no ISBN and was published after 1969.bt:WorkShape
Violationbt:place-gbbt:place-gbbt:place-gb is not exactly one kind of place.bt:PlaceKind
Violationbt:shop-halfmoonbt:shop-halfmoonbt:shop-halfmoon is not both named and located.bt:ShopShape

Defined in

S27

Closed shapes

A bookshop has no properties other than the ones listed, and the typo bs:foundedIn is caught.

Data: bookshop-trail-faulty.ttl

@prefix bt:    <https://example.org/bookshop-trail/> .
@prefix bs:    <https://example.org/bookshop-trail/schema#> .
@prefix sh:    <http://www.w3.org/ns/shacl#> .
@prefix rdf:   <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs:  <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:   <http://www.w3.org/2002/07/owl#> .
@prefix geo:   <http://www.opengis.net/ont/geosparql#> .
@prefix wgs84: <http://www.w3.org/2003/01/geo/wgs84_pos#> .

bt:BookshopClosed
    a                     sh:NodeShape ;
    sh:targetClass        bs:Bookshop ;
    sh:closed             true ;
    sh:ignoredProperties  ( rdf:type ) ;
    sh:message            "{$this} has a property this shape does not know: {$path}." ;
    sh:property [ sh:path rdfs:label ] ;
    sh:property [ sh:path bs:locatedIn ] ;
    sh:property [ sh:path bs:founded ] ;
    sh:property [ sh:path bs:floorArea ] ;
    sh:property [ sh:path bs:staffCount ] ;
    sh:property [ sh:path bs:specialises ] ;
    sh:property [ sh:path bs:sellsSecondHand ] ;
    sh:property [ sh:path bs:hasCafe ] ;
    sh:property [ sh:path bs:website ] ;
    sh:property [ sh:path bs:connectsTo ] ;
    sh:property [ sh:path bs:stocks ] ;
    sh:property [ sh:path wgs84:lat ] ;
    sh:property [ sh:path wgs84:long ] ;
    sh:property [ sh:path geo:hasGeometry ] ;
    sh:property [ sh:path geo:hasDefaultGeometry ] ;
    sh:property [ sh:path sh:shape ] .

bt:SettlementClosed
    a                     sh:NodeShape ;
    sh:targetClass        bs:Settlement ;
    sh:closed             true ;
    sh:ignoredProperties  ( rdf:type owl:sameAs ) ;
    sh:property [ sh:path rdfs:label ] ;
    sh:property [ sh:path bs:within ] ;
    sh:property [ sh:path bs:population ] ;
    sh:property [ sh:path bs:isBookTown ] ;
    sh:property [ sh:path wgs84:lat ] ;
    sh:property [ sh:path wgs84:long ] ;
    sh:property [ sh:path geo:hasGeometry ] ;
    sh:property [ sh:path geo:hasDefaultGeometry ] .

How it works

sh:closed true turns a node shape into an allow-list: any predicate on the focus node that is not the sh:path of one of its property shapes, and not in sh:ignoredProperties, is a violation. The Inkwell carries bs:foundedIn (F05), a misspelling of bs:founded that nothing else in this course notices, because every other shape looks for the properties it knows about and ignores the rest. The cost is that the shape must list every legitimate property, including the geometry and coordinate ones, and rdf:type has to be ignored explicitly. bt:SettlementClosed shows the other common addition: some places carry owl:sameAs links to DBpedia, which the shape ignores rather than lists. Remove owl:sameAs from its sh:ignoredProperties and validate again to see them.

Diagram

   sh:closed true ; sh:ignoredProperties ( rdf:type )
   allowed:  the sh:path of every sh:property on the shape, plus the ignored list

   bt:shop-inkwell
     rdf:type          ignored
     rdfs:label        listed
     bs:locatedIn      listed
     bs:founded        listed
     bs:foundedIn      not listed   ->  violation, value "1979"^^xsd:gYear
     ...

   the SPARQL course's q96 finds the same kind of mistake with a query

What to take away

  • A closed shape is the only Core check that notices a property you did not expect. It is how a typo in a predicate gets found.
  • Every legitimate property must be listed, with or without constraints. An empty property shape [ sh:path p ] is enough to allow p.
  • sh:ignoredProperties is for the properties you allow but do not want to enumerate: rdf:type, and links added by other tools.

The report

Does not conform · 1 violation, 0 warnings, 0 info · 26 shapes

SeverityFocus nodePathValueMessageShape
Violationbt:shop-inkwellbs:foundedIn1979bt:shop-inkwell has a property this shape does not know: bs:foundedIn.bt:BookshopClosed

Defined in

S28

Some of the values

A shop is in exactly one country; every country contains a book town; no council area contains two.

Data: bookshop-trail-faulty.ttl

@prefix bt: <https://example.org/bookshop-trail/> .
@prefix bs: <https://example.org/bookshop-trail/schema#> .
@prefix sh: <http://www.w3.org/ns/shacl#> .

bt:ShopInOneCountry
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:property [
        sh:path                 ( bs:locatedIn [ sh:oneOrMorePath bs:within ] ) ;
        sh:qualifiedValueShape  [ sh:class bs:Country ] ;
        sh:qualifiedMinCount    1 ;
        sh:qualifiedMaxCount    1 ;
        sh:message              "{$this} is in some number of countries other than one." ;
    ] .

bt:CountryHasBookTown
    a               sh:NodeShape ;
    sh:targetClass  bs:Country ;
    sh:property [
        sh:path                 [ sh:inversePath [ sh:oneOrMorePath bs:within ] ] ;
        sh:qualifiedValueShape  [ sh:property [ sh:path bs:isBookTown ; sh:hasValue true ] ] ;
        sh:qualifiedMinCount    1 ;
        sh:message              "{$this} has no book town." ;
    ] .

bt:CouncilAreaBookTowns
    a               sh:NodeShape ;
    sh:targetClass  bs:CouncilArea ;
    sh:property [
        sh:path                 [ sh:inversePath bs:within ] ;
        sh:qualifiedValueShape  [ sh:property [ sh:path bs:isBookTown ; sh:hasValue true ] ] ;
        sh:qualifiedMaxCount    1 ;
        sh:message              "{$this} contains more than one book town." ;
    ] .

How it works

sh:class on a path with many values requires all of them to be of the class. To say 'at least one of them is', use sh:qualifiedValueShape with sh:qualifiedMinCount: the values that conform to the qualified shape are counted, and the count is compared. bt:ShopInOneCountry walks up from the shop's town through every bs:within and requires exactly one of the places reached to be a bs:Country. On the faulty data The Inkwell, placed in Wigtown and Hay-on-Wye (F04), reaches Scotland and Wales, and The Foxed Page, placed in a string, reaches nothing. bt:CouncilAreaBookTowns looks down instead, with an inverse path, and sh:qualifiedMaxCount 1 catches Powys once Newtown (F25) joins Hay-on-Wye as a book town.

Diagram

   sh:path ( bs:locatedIn [ sh:oneOrMorePath bs:within ] )
   bt:shop-inkwell  ->  { wigtown's areas ..., scotland, gb,  hay's areas ..., wales }
                        qualified shape [ sh:class bs:Country ]:  scotland, wales  ->  2
                        sh:qualifiedMinCount 1  ok      sh:qualifiedMaxCount 1  violation

   sh:path [ sh:inversePath bs:within ]   at bt:place-powys
                        { hay-on-wye, newtown, ... }
                        qualified shape [ bs:isBookTown true ]:  2   ->  qualifiedMaxCount 1 fails

What to take away

  • A plain constraint on a property shape applies to every value. A qualified value shape counts the values that conform to it.
  • qualifiedMinCount 1 is 'some value is a ...'; qualifiedMinCount 1 with qualifiedMaxCount 1 is 'exactly one value is a ...'.
  • sh:qualifiedValueShapesDisjoint true stops one value from being counted by two sibling qualified shapes on the same property shape.

The report

Does not conform · 3 violations, 0 warnings, 0 info · 11 shapes

SeverityFocus nodePathValueMessageShape
Violationbt:place-powys^bs:withinbt:place-powys contains more than one book town.bt:CouncilAreaBookTowns › property 1
Violationbt:shop-foxed-pagebs:locatedIn / bs:within+bt:shop-foxed-page is in some number of countries other than one.bt:ShopInOneCountry › property 1
Violationbt:shop-inkwellbs:locatedIn / bs:within+bt:shop-inkwell is in some number of countries other than one.bt:ShopInOneCountry › property 1

Defined in

S29

A shape that refers to itself

A place conforms if every place above it conforms -- and what that finds, and what it cannot.

Data: bookshop-trail-faulty.ttl

@prefix bt:  <https://example.org/bookshop-trail/> .
@prefix bs:  <https://example.org/bookshop-trail/schema#> .
@prefix sh:  <http://www.w3.org/ns/shacl#> .
@prefix geo: <http://www.opengis.net/ont/geosparql#> .

bt:PlaceShape
    a              sh:NodeShape ;
    sh:targetNode  bt:place-wigtown, bt:place-loop-a ;
    sh:property [
        sh:path      geo:hasGeometry ;
        sh:minCount  1 ;
        sh:message   "{$this} has no geometry." ;
    ] ;
    sh:property [
        sh:path      bs:within ;
        sh:maxCount  1 ;
        sh:node      bt:PlaceShape ;
        sh:message   "Something above {$value} does not conform to bt:PlaceShape." ;
    ] .

How it works

bt:PlaceShape requires a geometry, and requires the place's bs:within to conform to bt:PlaceShape. The specification says the result of validating a recursive shape is undefined; this engine follows the reference and treats a check that is already in progress for the same node and shape as passing. Two things follow. Wigtown has a geometry and so does everything above it except Great Britain, three hops up, which has none; the failure surfaces at Wigtown as a sh:node result naming Dumfries and Galloway, with no trace of where in the chain it came from. And the two council areas inside each other (F27) do not trip the recursion at all: Loop A fails only because it has no geometry itself. The path shapes of s22 answer both questions better -- a cycle is a place that never reaches Great Britain, and a missing geometry is a result on the place that lacks it.

Diagram

   bt:PlaceShape
     geo:hasGeometry minCount 1
     bs:within  sh:node bt:PlaceShape      <- refers to itself

   wigtown -> dumfries-galloway -> scotland -> gb (no geometry)
      ^ one result here: "something above dumfries-galloway does not conform"

   loop-a -> loop-b -> loop-a (in progress: assumed to conform) ...
      results: loop-a has no geometry; loop-a's bs:within fails sh:node because loop-b has none
      the cycle itself is never reported

What to take away

  • Recursive shapes are allowed by the syntax and undefined by the specification. Engines differ; this one terminates and assumes an in-progress check passes.
  • A failure deep inside a sh:node chain is reported at the top with no detail. For a long chain, prefer a path and constrain the nodes it reaches.
  • A cycle in the data is found by a reachability constraint, not by recursion.

The report

Does not conform · 3 violations, 0 warnings, 0 info · 3 shapes

SeverityFocus nodePathValueMessageShape
Violationbt:place-loop-abs:withinbt:place-loop-bSomething above bt:place-loop-b does not conform to bt:PlaceShape.bt:PlaceShape › property 2
Violationbt:place-loop-ageo:hasGeometrybt:place-loop-a has no geometry.bt:PlaceShape › property 1
Violationbt:place-wigtownbs:withinbt:place-dumfries-gallowaySomething above bt:place-dumfries-galloway does not conform to bt:PlaceShape.bt:PlaceShape › property 2

Defined in

Module 05

Targets and constraints in SPARQL

Some conditions cannot be written with the Core components: a join between two nodes, an arithmetic comparison, a path that must not lead back to its start. SHACL-SPARQL lets a SELECT query decide, with $this bound to the node under test. The module also writes targets in SPARQL, sets out what pre-binding forbids, and queries the report as the RDF graph it is.

In the standards
S30

The constraint you cannot write in Core

Nothing contains itself: no place, genre, publisher or author is above itself in its own hierarchy.

Data: bookshop-trail-faulty.ttl

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix sh:   <http://www.w3.org/ns/shacl#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix xsd:  <http://www.w3.org/2001/XMLSchema#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

# Prefixes for the SPARQL inside this file. @prefix lines are Turtle syntax
# and do not reach the query engine; sh:declare does.
bt:prefixes
    a  owl:Ontology ;
    sh:declare [ sh:prefix "bt" ;   sh:namespace "https://example.org/bookshop-trail/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "bs" ;   sh:namespace "https://example.org/bookshop-trail/schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdf" ;  sh:namespace "http://www.w3.org/1999/02/22-rdf-syntax-ns#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdfs" ; sh:namespace "http://www.w3.org/2000/01/rdf-schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "xsd" ;  sh:namespace "http://www.w3.org/2001/XMLSchema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "skos" ; sh:namespace "http://www.w3.org/2004/02/skos/core#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "dct" ;  sh:namespace "http://purl.org/dc/terms/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "geo" ;  sh:namespace "http://www.opengis.net/ont/geosparql#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "schema" ; sh:namespace "https://schema.org/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "sh" ;   sh:namespace "http://www.w3.org/ns/shacl#"^^xsd:anyURI ] .

bt:PlaceHierarchyShape
    a               sh:NodeShape ;
    sh:targetClass  bs:Place ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:message   "{$this} contains itself, directly or through a chain of bs:within." ;
        sh:select    """
            SELECT $this WHERE { $this bs:within+ $this }
        """ ;
    ] .

bt:GenreHierarchyShape
    a               sh:NodeShape ;
    sh:targetClass  skos:Concept ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:message   "{$this} is broader than itself." ;
        sh:select    """
            SELECT $this WHERE { $this skos:broader+ $this }
        """ ;
    ] .

bt:ImprintShape
    a               sh:NodeShape ;
    sh:targetClass  bs:Publisher ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:message   "{$this} is an imprint of itself." ;
        sh:select    """
            SELECT $this WHERE { $this bs:imprintOf+ $this }
        """ ;
    ] .

# The clean data has one cycle of influence on purpose (q34 in the SPARQL
# course), so this is a warning rather than a violation.
bt:InfluenceShape
    a               sh:NodeShape ;
    sh:targetClass  bs:Author ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:severity  sh:Warning ;
        sh:message   "{$this} is in a cycle of influence." ;
        sh:select    """
            SELECT $this WHERE { $this bs:influencedBy+ $this }
        """ ;
    ] .

How it works

A sh:sparql constraint holds a SELECT query. The engine runs it once per focus node with $this bound to that node, and every row that comes back is a validation result. 'This place is inside itself' is $this bs:within+ $this -- a property path back to the start, which no Core component can express. The prefixes the query uses are declared with sh:declare on bt:prefixes and named by sh:prefixes; the @prefix lines at the top of the file are Turtle syntax and never reach the query engine. Four shapes, one pattern. The two council areas inside each other (F27), the two genres broader than each other (F30) and the publisher that is an imprint of itself (F19) are violations. bt:InfluenceShape is a warning, because the clean data has a cycle of three authors on purpose -- the SPARQL course's q34 -- and the author influenced by himself (F18) joins them.

Diagram

   sh:sparql [
       sh:prefixes  bt:prefixes ;                <- names the sh:declare block
       sh:select    "SELECT $this WHERE { $this bs:within+ $this }" ;
   ]

   for each focus node n:   bind $this = n, run the query
        no rows    ->  nothing
        a row      ->  one validation result, focus node n

   bt:place-loop-a --within--> bt:place-loop-b --within--> bt:place-loop-a     a row

What to take away

  • A SPARQL constraint returns rows; each row is a result. An empty result set is conformance.
  • $this is the focus node, pre-bound. Every row must have it; project ?value and ?path too if you want them in the result (s32).
  • Declare prefixes for the query with sh:declare and sh:prefixes. @prefix does not reach it.

The report

Does not conform · 5 violations, 4 warnings, 0 info · 5 shapes

SeverityFocus nodePathValueMessageShape
Violationbt:genre-loop-abt:genre-loop-ahttps://example.org/bookshop-trail/genre-loop-a is broader than itself.bt:GenreHierarchyShape
Violationbt:genre-loop-bbt:genre-loop-bhttps://example.org/bookshop-trail/genre-loop-b is broader than itself.bt:GenreHierarchyShape
Violationbt:place-loop-abt:place-loop-ahttps://example.org/bookshop-trail/place-loop-a contains itself, directly or through a chain of bs:within.bt:PlaceHierarchyShape
Violationbt:place-loop-bbt:place-loop-bhttps://example.org/bookshop-trail/place-loop-b contains itself, directly or through a chain of bs:within.bt:PlaceHierarchyShape
Violationbt:pub-orbitbt:pub-orbithttps://example.org/bookshop-trail/pub-orbit is an imprint of itself.bt:ImprintShape
Warningbt:author-kirsty-lammondbt:author-kirsty-lammondhttps://example.org/bookshop-trail/author-kirsty-lammond is in a cycle of influence.bt:InfluenceShape
Warningbt:author-owen-harkerbt:author-owen-harkerhttps://example.org/bookshop-trail/author-owen-harker is in a cycle of influence.bt:InfluenceShape
Warningbt:author-rab-fingalbt:author-rab-fingalhttps://example.org/bookshop-trail/author-rab-fingal is in a cycle of influence.bt:InfluenceShape
Warningbt:author-tam-brodiebt:author-tam-brodiehttps://example.org/bookshop-trail/author-tam-brodie is in a cycle of influence.bt:InfluenceShape

Defined in

S31

A join, an arithmetic comparison, and $PATH

A shelf price is at most twice the recommended price; no work predates its author; an event does not feature the dead.

Data: bookshop-trail-faulty.ttl

@prefix bt:  <https://example.org/bookshop-trail/> .
@prefix bs:  <https://example.org/bookshop-trail/schema#> .
@prefix sh:  <http://www.w3.org/ns/shacl#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

# Prefixes for the SPARQL inside this file. @prefix lines are Turtle syntax
# and do not reach the query engine; sh:declare does.
bt:prefixes
    a  owl:Ontology ;
    sh:declare [ sh:prefix "bt" ;   sh:namespace "https://example.org/bookshop-trail/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "bs" ;   sh:namespace "https://example.org/bookshop-trail/schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdf" ;  sh:namespace "http://www.w3.org/1999/02/22-rdf-syntax-ns#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdfs" ; sh:namespace "http://www.w3.org/2000/01/rdf-schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "xsd" ;  sh:namespace "http://www.w3.org/2001/XMLSchema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "skos" ; sh:namespace "http://www.w3.org/2004/02/skos/core#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "dct" ;  sh:namespace "http://purl.org/dc/terms/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "geo" ;  sh:namespace "http://www.opengis.net/ont/geosparql#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "schema" ; sh:namespace "https://schema.org/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "sh" ;   sh:namespace "http://www.w3.org/ns/shacl#"^^xsd:anyURI ] .

bt:StockPriceShape
    a               sh:NodeShape ;
    sh:targetClass  bs:StockRecord ;
    sh:property [
        sh:path  bs:shelfPrice ;
        sh:sparql [
            sh:prefixes  bt:prefixes ;
            sh:message   "{$value} is more than twice the recommended price." ;
            sh:select    """
                SELECT $this ?value WHERE {
                  $this $PATH ?value ;
                        bs:ofWork/bs:rrp ?rrp .
                  FILTER ( ?value > 2 * ?rrp )
                }
            """ ;
        ] ;
    ] .

bt:WorkAfterBirth
    a               sh:NodeShape ;
    sh:targetClass  bs:Work ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:message   "{$this} was published in {$value}, before its author was born." ;
        sh:select    """
            SELECT $this ?value WHERE {
              $this bs:publicationYear ?value ;
                    bs:author/bs:born ?born .
              FILTER ( xsd:integer(STR(?value)) < xsd:integer(STR(?born)) )
            }
        """ ;
    ] .

bt:LivingFeature
    a               sh:NodeShape ;
    sh:targetClass  bs:Event ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:severity  sh:Warning ;
        sh:message   "{$this} features {$value}, who had died by then. A memorial, or a mistake?" ;
        sh:select    """
            SELECT $this ?value WHERE {
              $this bs:featuring ?value ; bs:eventDate ?date .
              ?value bs:died ?died .
              FILTER ( xsd:integer(SUBSTR(STR(?date), 1, 4)) > xsd:integer(STR(?died)) )
            }
        """ ;
    ] .

How it works

Each of these needs a value from a second node, which is a join, and Core has none. bt:StockPriceShape puts the sh:sparql on a property shape, where $PATH stands for the shape's sh:path and the results carry the path automatically. It joins the record to its work's bs:rrp and compares. The one record at three times the recommended price (F23) is reported, with the offending price as ?value. bt:WorkAfterBirth joins a work to its author's birth year, with the STR cast from s14, and finds the book published five years before its author was born (F11). bt:LivingFeature is on the clean data too: a lecture in 2025 by an author who died in 2004. That may be a memorial event or a mistake, so it is a warning. Its year comes from SUBSTR(STR(?date), 1, 4): YEAR() is defined on xsd:dateTime, and on an xsd:date this engine leaves it unbound, so the filter would never be true.

Diagram

   property shape:  sh:path bs:shelfPrice ;  sh:sparql [ ... $this $PATH ?value ... ]
        $PATH is replaced by the shape's path before the query runs
        the result gets sh:resultPath bs:shelfPrice without being told

   $this --shelfPrice--> 68.07
   $this --ofWork--> work --rrp--> 22.69        68.07 > 2 * 22.69  ->  a row

   xsd:integer(SUBSTR(STR("2025-04-05"^^xsd:date), 1, 4)) = 2025
        >  xsd:integer(STR("2004"^^xsd:gYear)) = 2004
   (YEAR() of an xsd:date is unbound on this engine; see q130 in the SPARQL course)

What to take away

  • A join is a SPARQL constraint. If the condition mentions two nodes, it is not a Core constraint.
  • On a property shape, $PATH is the shape's path, and the result's path is filled in for you.
  • Project ?value to say which value the row is about. It becomes sh:value and fills {$value}.

The report

Does not conform · 2 violations, 1 warning, 0 info · 5 shapes

SeverityFocus nodePathValueMessageShape
Violationbt:book-precocious1990https://example.org/bookshop-trail/book-precocious was published in 1990, before its author was born.bt:WorkAfterBirth
Violationbt:stock-foxed-page--the-book-townbs:shelfPrice68.0768.07 is more than twice the recommended price.bt:StockPriceShape › property 1
Warningbt:event-severn-leaf-2025-04-05bt:author-perrin-oakeshttps://example.org/bookshop-trail/event-severn-leaf-2025-04-05 features https://example.org/bookshop-trail/author-perrin-oakes, who had died by then. A memo…bt:LivingFeature

Defined in

S32

What a query can put in the result

The six shops without a website again, with the path and the value chosen by the query, and what this engine does with ?message.

Data: bookshop-trail-1.1.ttl

@prefix bt:  <https://example.org/bookshop-trail/> .
@prefix bs:  <https://example.org/bookshop-trail/schema#> .
@prefix sh:  <http://www.w3.org/ns/shacl#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

# Prefixes for the SPARQL inside this file. @prefix lines are Turtle syntax
# and do not reach the query engine; sh:declare does.
bt:prefixes
    a  owl:Ontology ;
    sh:declare [ sh:prefix "bt" ;   sh:namespace "https://example.org/bookshop-trail/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "bs" ;   sh:namespace "https://example.org/bookshop-trail/schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdf" ;  sh:namespace "http://www.w3.org/1999/02/22-rdf-syntax-ns#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdfs" ; sh:namespace "http://www.w3.org/2000/01/rdf-schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "xsd" ;  sh:namespace "http://www.w3.org/2001/XMLSchema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "skos" ; sh:namespace "http://www.w3.org/2004/02/skos/core#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "dct" ;  sh:namespace "http://purl.org/dc/terms/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "geo" ;  sh:namespace "http://www.opengis.net/ont/geosparql#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "schema" ; sh:namespace "https://schema.org/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "sh" ;   sh:namespace "http://www.w3.org/ns/shacl#"^^xsd:anyURI ] .

bt:NoWebsite
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:severity  sh:Info ;
        sh:message   "{$value} has no {$path}." ;
        sh:select    """
            SELECT $this ?value ?path WHERE {
              $this rdfs:label ?value .
              FILTER NOT EXISTS { $this bs:website ?w }
              BIND ( bs:website AS ?path )
            }
        """ ;
    ] .

# Binds ?message. On this engine the rows show sh:message instead.
bt:MessageProbe
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:severity  sh:Info ;
        sh:message   "Text from sh:message. A bound ?message would replace this on an engine that honours it." ;
        sh:select    """
            SELECT $this ?message WHERE {
              FILTER NOT EXISTS { $this bs:website ?w }
              BIND ( "Text from the ?message variable." AS ?message )
            }
        """ ;
    ] .

How it works

Beyond $this, three projected variables have meaning. ?value becomes sh:value; ?path becomes sh:resultPath, which is how a node-shape constraint gets a path into its results; ?message is meant to override sh:message row by row. bt:NoWebsite binds ?path to bs:website and ?value to the shop's label, so the rows read as 'this shop, this property, this name'. bt:MessageProbe binds ?message, and its rows still show the sh:message text: this build ignores a bound ?message, and it also leaves {?var} templates unfilled. Keep row-specific detail in ?value and the words in sh:message. sh:severity inside the sh:sparql block works, and is used here to make both shapes report information rather than violations. That placement is SHACL 1.2: the 1.0 specification allows sh:severity on shapes only, and pySHACL, which follows 1.0, reports these twelve rows as violations. Where shapes have to run on a 1.0 validator, give the constraint a shape of its own and put the severity there.

Diagram

   SELECT $this ?value ?path WHERE { ... BIND(bs:website AS ?path) ... }

   projected     becomes              in the message template
   $this         sh:focusNode         {$this}
   ?value        sh:value             {$value}
   ?path         sh:resultPath        {$path}
   ?message      sh:resultMessage     -- not on this engine: sh:message is used

   {?anyOtherVariable}   left as written

What to take away

  • ?value and ?path are the two projections worth using. They shape the row the reader sees.
  • A node-shape SPARQL constraint has no path unless the query binds ?path.
  • Portability: ?message and {?var} are in the specification and not in this engine; sh:severity on a sh:sparql block is in SHACL 1.2 and not in 1.0 validators. sh:message with {$value}, and severity on the shape, work everywhere.

The report

Does not conform · 0 violations, 0 warnings, 12 info · 4 shapes

SeverityFocus nodePathValueMessageShape
Infobt:shop-castle-stepsbt:shop-castle-stepsText from sh:message. A bound ?message would replace this on an engine that honours it.bt:MessageProbe
Infobt:shop-castle-stepsbs:websiteCastle Steps BooksCastle Steps Books has no https://example.org/bookshop-trail/schema#website.bt:NoWebsite
Infobt:shop-dales-foliobt:shop-dales-folioText from sh:message. A bound ?message would replace this on an engine that honours it.bt:MessageProbe
Infobt:shop-dales-foliobs:websiteThe Dales FolioThe Dales Folio has no https://example.org/bookshop-trail/schema#website.bt:NoWebsite
Infobt:shop-ex-librisbt:shop-ex-librisText from sh:message. A bound ?message would replace this on an engine that honours it.bt:MessageProbe
Infobt:shop-ex-librisbs:websiteEx LibrisEx Libris has no https://example.org/bookshop-trail/schema#website.bt:NoWebsite
Infobt:shop-marginaliabt:shop-marginaliaText from sh:message. A bound ?message would replace this on an engine that honours it.bt:MessageProbe
Infobt:shop-marginaliabs:websiteMarginaliaMarginalia has no https://example.org/bookshop-trail/schema#website.bt:NoWebsite
Infobt:shop-signaturebt:shop-signatureText from sh:message. A bound ?message would replace this on an engine that honours it.bt:MessageProbe
Infobt:shop-signaturebs:websiteSignature BooksSignature Books has no https://example.org/bookshop-trail/schema#website.bt:NoWebsite
Infobt:shop-taff-marginbt:shop-taff-marginText from sh:message. A bound ?message would replace this on an engine that honours it.bt:MessageProbe
Infobt:shop-taff-marginbs:websiteTaff MarginTaff Margin has no https://example.org/bookshop-trail/schema#website.bt:NoWebsite

Defined in

S33

A target written in SPARQL

Every book town has a bookshop; a shop with a cafe has room for one; a city has more than one shop.

Data: bookshop-trail-faulty.ttl

@prefix bt:  <https://example.org/bookshop-trail/> .
@prefix bs:  <https://example.org/bookshop-trail/schema#> .
@prefix sh:  <http://www.w3.org/ns/shacl#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

# Prefixes for the SPARQL inside this file. @prefix lines are Turtle syntax
# and do not reach the query engine; sh:declare does.
bt:prefixes
    a  owl:Ontology ;
    sh:declare [ sh:prefix "bt" ;   sh:namespace "https://example.org/bookshop-trail/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "bs" ;   sh:namespace "https://example.org/bookshop-trail/schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdf" ;  sh:namespace "http://www.w3.org/1999/02/22-rdf-syntax-ns#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdfs" ; sh:namespace "http://www.w3.org/2000/01/rdf-schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "xsd" ;  sh:namespace "http://www.w3.org/2001/XMLSchema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "skos" ; sh:namespace "http://www.w3.org/2004/02/skos/core#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "dct" ;  sh:namespace "http://purl.org/dc/terms/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "geo" ;  sh:namespace "http://www.opengis.net/ont/geosparql#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "schema" ; sh:namespace "https://schema.org/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "sh" ;   sh:namespace "http://www.w3.org/ns/shacl#"^^xsd:anyURI ] .

bt:BookTownShape
    a  sh:NodeShape ;
    sh:target [
        a            sh:SPARQLTarget ;
        sh:prefixes  bt:prefixes ;
        sh:select    "SELECT ?this WHERE { ?this bs:isBookTown true }" ;
    ] ;
    sh:property [
        sh:path      [ sh:inversePath bs:locatedIn ] ;
        sh:minCount  1 ;
        sh:message   "{$this} is a book town with no bookshop." ;
    ] .

# SHACL 1.2: a select expression as the target.
bt:CafeShape
    a  sh:NodeShape ;
    sh:target [
        sh:prefixes  bt:prefixes ;
        sh:select    "SELECT ?this WHERE { ?this bs:hasCafe true }" ;
    ] ;
    sh:property [
        sh:path          bs:floorArea ;
        sh:minInclusive  100 ;
        sh:severity      sh:Info ;
        sh:message       "{$this} has a cafe in {$value} square metres." ;
    ] .

# SHACL 1.2: a shape as the target.
bt:CityShape
    a  sh:NodeShape ;
    sh:targetWhere [
        sh:class     bs:Settlement ;
        sh:property  [ sh:path bs:population ; sh:minInclusive 100000 ] ;
    ] ;
    sh:property [
        sh:path      [ sh:inversePath bs:locatedIn ] ;
        sh:minCount  2 ;
        sh:severity  sh:Info ;
        sh:message   "{$this} is a city with fewer than two shops on the trail." ;
    ] .

How it works

A target need not be a class. sh:target [ a sh:SPARQLTarget ; sh:select ... ] makes the focus nodes whatever the query binds to ?this, and bt:BookTownShape uses it to pick the settlements with bs:isBookTown true and require at least one shop in each. On the faulty data Newtown (F25) is a book town with none. SHACL 1.2 shortens the same idea to sh:target [ sh:select ... ], a select expression, which bt:CafeShape uses for the shops with a cafe; four of them have under a hundred square metres, reported as information. sh:targetWhere, also 1.2, takes a shape instead of a query: the settlements that conform to 'population at least 100,000' are the cities, and three of them have fewer than two shops. All three forms run in this engine.

Diagram

   sh:target [ a sh:SPARQLTarget ; sh:select "SELECT ?this WHERE { ?this bs:isBookTown true }" ]
        focus nodes = the bindings of ?this          (SHACL-AF)

   sh:target [ sh:select "..." ]                     (SHACL 1.2: a select expression)

   sh:targetWhere [ sh:class bs:Settlement ; sh:property [ sh:path bs:population ; sh:minInclusive 100000 ] ]
        focus nodes = the nodes that conform to this shape     (SHACL 1.2)

What to take away

  • A SPARQL target is any SELECT that binds ?this. Use it when the population is defined by a condition rather than a class.
  • sh:targetWhere is the same idea without SPARQL: the target is 'whatever conforms to this shape'.
  • A target query that fails to parse selects nothing and reports nothing on this engine. Check the shapes count and keep a canary (s09).

The report

Does not conform · 1 violation, 0 warnings, 7 info · 8 shapes

SeverityFocus nodePathValueMessageShape
Violationbt:place-newtown^bs:locatedInbt:place-newtown is a book town with no bookshop.bt:BookTownShape › property 1
Infobt:place-exeter^bs:locatedInbt:place-exeter is a city with fewer than two shops on the trail.bt:CityShape › property 1
Infobt:place-liverpool^bs:locatedInbt:place-liverpool is a city with fewer than two shops on the trail.bt:CityShape › property 1
Infobt:place-manchester^bs:locatedInbt:place-manchester is a city with fewer than two shops on the trail.bt:CityShape › property 1
Infobt:shop-cliff-roadbs:floorArea95.0bt:shop-cliff-road has a cafe in 95.0 square metres.bt:CafeShape › property 1
Infobt:shop-dog-earedbs:floorArea75.0bt:shop-dog-eared has a cafe in 75.0 square metres.bt:CafeShape › property 1
Infobt:shop-penwithbs:floorArea65.0bt:shop-penwith has a cafe in 65.0 square metres.bt:CafeShape › property 1
Infobt:shop-sea-marginbs:floorArea60.0bt:shop-sea-margin has a cafe in 60.0 square metres.bt:CafeShape › property 1

Defined in

S34

What pre-binding forbids

Works nobody stocks -- written the way pre-binding allows, with the three ways it does not.

Data: bookshop-trail-1.1.ttl

@prefix bt:  <https://example.org/bookshop-trail/> .
@prefix bs:  <https://example.org/bookshop-trail/schema#> .
@prefix sh:  <http://www.w3.org/ns/shacl#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

# Prefixes for the SPARQL inside this file. @prefix lines are Turtle syntax
# and do not reach the query engine; sh:declare does.
bt:prefixes
    a  owl:Ontology ;
    sh:declare [ sh:prefix "bt" ;   sh:namespace "https://example.org/bookshop-trail/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "bs" ;   sh:namespace "https://example.org/bookshop-trail/schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdf" ;  sh:namespace "http://www.w3.org/1999/02/22-rdf-syntax-ns#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdfs" ; sh:namespace "http://www.w3.org/2000/01/rdf-schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "xsd" ;  sh:namespace "http://www.w3.org/2001/XMLSchema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "skos" ; sh:namespace "http://www.w3.org/2004/02/skos/core#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "dct" ;  sh:namespace "http://purl.org/dc/terms/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "geo" ;  sh:namespace "http://www.opengis.net/ont/geosparql#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "schema" ; sh:namespace "https://schema.org/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "sh" ;   sh:namespace "http://www.w3.org/ns/shacl#"^^xsd:anyURI ] .

bt:UnstockedWork
    a               sh:NodeShape ;
    sh:targetClass  bs:Work ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:severity  sh:Info ;
        sh:message   "No shop on the trail stocks {$this}." ;
        sh:select    """
            SELECT $this WHERE {
              FILTER NOT EXISTS { ?record bs:ofWork $this }
            }
        """ ;
    ] .

How it works

$this is pre-bound: the engine substitutes the focus node before the query runs, and the specification lists what a query may not then contain. MINUS, VALUES and SERVICE are forbidden, a sub-select must project $this, and $this may not be assigned with AS. This engine refuses the whole shapes graph with a message naming the construct, so the file cannot demonstrate them and stay runnable; the diagram shows the three forbidden spellings and the text the engine returns for each. The shape uses FILTER NOT EXISTS, which is allowed and is what MINUS would have meant, and reports the eight works no shop stocks as information. The SPARQL course's q17 is about why MINUS and NOT EXISTS differ; here only one of them is available.

Diagram

   forbidden                                          the engine says
   SELECT $this WHERE { $this a bs:Work
       MINUS { ?r bs:ofWork $this } }                 MINUS cannot be combined with SHACL pre-binding
   SELECT $this WHERE { VALUES $this { ... } }        VALUES cannot be combined with SHACL pre-binding
   SELECT $this WHERE { SERVICE <...> { ... } }       SERVICE cannot be combined with SHACL pre-binding

   allowed
   SELECT $this WHERE { FILTER NOT EXISTS { ?r bs:ofWork $this } }
   SELECT $this WHERE { { SELECT $this (COUNT(?x) AS ?n) WHERE { ... } GROUP BY $this } ... }

What to take away

  • Pre-binding is substitution, and MINUS, VALUES and SERVICE do not survive it. Write NOT EXISTS, put the list in the shapes graph (s37), and keep remote data out of shapes.
  • A sub-select inside a constraint must project $this, or the outer pattern has nothing to join on.
  • This engine fails loudly on all three. Some validators do not; a shape that passes everywhere but one engine is worth a second look at its query.

Try it

Replace FILTER NOT EXISTS { ?r bs:ofWork $this } with MINUS { ?r bs:ofWork $this } and validate. The engine refuses the shapes graph and names MINUS.

The report

Does not conform · 0 violations, 0 warnings, 8 info · 2 shapes

SeverityFocus nodePathValueMessageShape
Infobt:book-high-waterbt:book-high-waterNo shop on the trail stocks https://example.org/bookshop-trail/book-high-water.bt:UnstockedWork
Infobt:book-nine-wintersbt:book-nine-wintersNo shop on the trail stocks https://example.org/bookshop-trail/book-nine-winters.bt:UnstockedWork
Infobt:book-the-govan-inheritancebt:book-the-govan-inheritanceNo shop on the trail stocks https://example.org/bookshop-trail/book-the-govan-inheritance.bt:UnstockedWork
Infobt:book-the-lamp-roombt:book-the-lamp-roomNo shop on the trail stocks https://example.org/bookshop-trail/book-the-lamp-room.bt:UnstockedWork
Infobt:book-the-long-riverbt:book-the-long-riverNo shop on the trail stocks https://example.org/bookshop-trail/book-the-long-river.bt:UnstockedWork
Infobt:book-the-pilot-boatbt:book-the-pilot-boatNo shop on the trail stocks https://example.org/bookshop-trail/book-the-pilot-boat.bt:UnstockedWork
Infobt:book-the-selkie-ledger-arbt:book-the-selkie-ledger-arNo shop on the trail stocks https://example.org/bookshop-trail/book-the-selkie-ledger-ar.bt:UnstockedWork
Infobt:book-the-shielingbt:book-the-shielingNo shop on the trail stocks https://example.org/bookshop-trail/book-the-shieling.bt:UnstockedWork

Defined in

S35

Aggregation in a constraint

No shop holds more than a hundred copies in total, and no shop has held more than three events.

Data: bookshop-trail-1.1.ttl

@prefix bt:  <https://example.org/bookshop-trail/> .
@prefix bs:  <https://example.org/bookshop-trail/schema#> .
@prefix sh:  <http://www.w3.org/ns/shacl#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

# Prefixes for the SPARQL inside this file. @prefix lines are Turtle syntax
# and do not reach the query engine; sh:declare does.
bt:prefixes
    a  owl:Ontology ;
    sh:declare [ sh:prefix "bt" ;   sh:namespace "https://example.org/bookshop-trail/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "bs" ;   sh:namespace "https://example.org/bookshop-trail/schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdf" ;  sh:namespace "http://www.w3.org/1999/02/22-rdf-syntax-ns#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdfs" ; sh:namespace "http://www.w3.org/2000/01/rdf-schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "xsd" ;  sh:namespace "http://www.w3.org/2001/XMLSchema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "skos" ; sh:namespace "http://www.w3.org/2004/02/skos/core#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "dct" ;  sh:namespace "http://purl.org/dc/terms/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "geo" ;  sh:namespace "http://www.opengis.net/ont/geosparql#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "schema" ; sh:namespace "https://schema.org/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "sh" ;   sh:namespace "http://www.w3.org/ns/shacl#"^^xsd:anyURI ] .

bt:StockTotal
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:severity  sh:Warning ;
        sh:message   "{$this} holds more than a hundred copies in total." ;
        sh:select    """
            SELECT $this WHERE {
              { SELECT $this (SUM(?c) AS ?total)
                WHERE { ?record bs:atShop $this ; bs:copies ?c }
                GROUP BY $this }
              FILTER ( ?total > 100 )
            }
        """ ;
    ] .

bt:BusyShop
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:severity  sh:Info ;
        sh:message   "{$this} has held more than three events." ;
        sh:select    """
            SELECT $this WHERE {
              { SELECT $this (COUNT(?event) AS ?n)
                WHERE { ?event bs:heldAt $this }
                GROUP BY $this }
              FILTER ( ?n > 3 )
            }
        """ ;
    ] .

How it works

A SPARQL constraint can aggregate, as long as the grouping keeps $this. bt:StockTotal sums bs:copies over every record at the shop, GROUP BY $this, and keeps the groups over a hundred with HAVING. One shop on the clean data, Ex Libris, holds 101. bt:BusyShop counts events the same way. Both are facts rather than faults, so both are warnings or information. One limitation of this build: an aggregate projected as ?value does not reach the result, so the rows name the shop and not the total. The SPARQL course's module 04 is the reference for the aggregation itself.

Diagram

   SELECT $this WHERE {
     { SELECT $this (SUM(?c) AS ?total)
       WHERE { ?r bs:atShop $this ; bs:copies ?c }
       GROUP BY $this }
     FILTER ( ?total > 100 )
   }

   $this is pre-bound in the inner query too: one group, one row, one test
   bt:shop-ex-libris   101   ->  a row

What to take away

  • Aggregate inside a sub-select that groups by $this and projects it; test the total outside.
  • HAVING and a FILTER on the projected total are equivalent here. The sub-select form is the one that keeps $this in scope for both.
  • On this engine an aggregate does not become sh:value. Say the threshold in sh:message; the focus node says which shop.

The report

Does not conform · 0 violations, 1 warning, 1 info · 4 shapes

SeverityFocus nodePathValueMessageShape
Warningbt:shop-ex-librisbt:shop-ex-librishttps://example.org/bookshop-trail/shop-ex-libris holds more than a hundred copies in total.bt:StockTotal
Infobt:shop-ex-librisbt:shop-ex-librishttps://example.org/bookshop-trail/shop-ex-libris has held more than three events.bt:BusyShop

Defined in

S36

Reachability as a constraint

Every shop can be walked to from The Inkwell -- and two cannot, by design.

Data: bookshop-trail-1.1.ttl

@prefix bt:  <https://example.org/bookshop-trail/> .
@prefix bs:  <https://example.org/bookshop-trail/schema#> .
@prefix sh:  <http://www.w3.org/ns/shacl#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

# Prefixes for the SPARQL inside this file. @prefix lines are Turtle syntax
# and do not reach the query engine; sh:declare does.
bt:prefixes
    a  owl:Ontology ;
    sh:declare [ sh:prefix "bt" ;   sh:namespace "https://example.org/bookshop-trail/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "bs" ;   sh:namespace "https://example.org/bookshop-trail/schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdf" ;  sh:namespace "http://www.w3.org/1999/02/22-rdf-syntax-ns#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdfs" ; sh:namespace "http://www.w3.org/2000/01/rdf-schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "xsd" ;  sh:namespace "http://www.w3.org/2001/XMLSchema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "skos" ; sh:namespace "http://www.w3.org/2004/02/skos/core#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "dct" ;  sh:namespace "http://purl.org/dc/terms/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "geo" ;  sh:namespace "http://www.opengis.net/ont/geosparql#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "schema" ; sh:namespace "https://schema.org/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "sh" ;   sh:namespace "http://www.w3.org/ns/shacl#"^^xsd:anyURI ] .

bt:UnreachableShopShape
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:severity  sh:Warning ;
        sh:message   "{$this} cannot be reached on foot from The Inkwell. Two shops cannot, by design; a third would be a mistake." ;
        sh:select    """
            SELECT $this WHERE {
              FILTER NOT EXISTS {
                bt:shop-inkwell (bs:connectsTo|^bs:connectsTo)+ $this .
              }
            }
        """ ;
    ] .

How it works

The SPARQL course's q31 and q32 walk the trail with the path (bs:connectsTo|^bs:connectsTo)+ and find that two shops in the south west connect only to each other. The same path inside FILTER NOT EXISTS is a constraint: a shop that the path from The Inkwell never reaches is reported. The shape comes from the SPARQL course's shapes-advanced.ttl, and it is a warning there for the same reason it is here: two unreachable shops are the intended shape of the trail, and a third would be a mistake.

Diagram

   bt:shop-inkwell --(connectsTo | ^connectsTo)+--> every shop on the main line and its branches

   FILTER NOT EXISTS { bt:shop-inkwell (bs:connectsTo|^bs:connectsTo)+ $this }

   bt:shop-penwith  <--> bt:shop-west-quay        a pair joined to nothing else
        two rows, both warnings

What to take away

  • A reachability check is a property path from a fixed node to $this, negated.
  • Constraints written for the SPARQL course's queries port with almost no change: the query becomes the sh:select, the interesting variable becomes $this.
  • Known exceptions get a warning, not a lower threshold. The report should still say they are there.

The report

Does not conform · 0 violations, 2 warnings, 0 info · 2 shapes

SeverityFocus nodePathValueMessageShape
Warningbt:shop-penwithbt:shop-penwithhttps://example.org/bookshop-trail/shop-penwith cannot be reached on foot from The Inkwell. Two shops cannot, by design; a third would be a mistake.bt:UnreachableShopShape
Warningbt:shop-west-quaybt:shop-west-quayhttps://example.org/bookshop-trail/shop-west-quay cannot be reached on foot from The Inkwell. Two shops cannot, by design; a third would be a mistake.bt:UnreachableShopShape

Defined in

S37

Two graphs, one constraint

An event's kind is one the shapes graph lists -- with the list kept as data in the shapes graph, not in a sh:in.

Data: bookshop-trail-faulty.ttl

@prefix bt:  <https://example.org/bookshop-trail/> .
@prefix bs:  <https://example.org/bookshop-trail/schema#> .
@prefix sh:  <http://www.w3.org/ns/shacl#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

# Prefixes for the SPARQL inside this file. @prefix lines are Turtle syntax
# and do not reach the query engine; sh:declare does.
bt:prefixes
    a  owl:Ontology ;
    sh:declare [ sh:prefix "bt" ;   sh:namespace "https://example.org/bookshop-trail/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "bs" ;   sh:namespace "https://example.org/bookshop-trail/schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdf" ;  sh:namespace "http://www.w3.org/1999/02/22-rdf-syntax-ns#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdfs" ; sh:namespace "http://www.w3.org/2000/01/rdf-schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "xsd" ;  sh:namespace "http://www.w3.org/2001/XMLSchema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "skos" ; sh:namespace "http://www.w3.org/2004/02/skos/core#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "dct" ;  sh:namespace "http://purl.org/dc/terms/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "geo" ;  sh:namespace "http://www.opengis.net/ont/geosparql#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "schema" ; sh:namespace "https://schema.org/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "sh" ;   sh:namespace "http://www.w3.org/ns/shacl#"^^xsd:anyURI ] .

bt:eventKinds
    bs:allows  "Reading", "Signing", "Panel", "Workshop", "Launch", "Lecture", "Book Club" .

bt:EventKindShape
    a               sh:NodeShape ;
    sh:targetClass  bs:Event ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:message   "{$value} is not a kind of event listed in the shapes graph." ;
        sh:select    """
            SELECT $this ?value WHERE {
              $this bs:eventKind ?value .
              FILTER NOT EXISTS { GRAPH $shapesGraph { bt:eventKinds bs:allows ?value } }
            }
        """ ;
    ] .

How it works

A SPARQL constraint can see the shapes graph as well as the data, through the pre-bound $shapesGraph. bt:eventKinds is an ordinary node in the shapes file with one bs:allows per kind, and the query asks, for each value of bs:eventKind, whether GRAPH $shapesGraph holds it. The list can now be edited, queried and reused without touching the constraint, which sh:in does not allow. The event whose kind is "Recital" (F20) is the one result. s34 said VALUES is forbidden under pre-binding; this is where a list goes instead.

Diagram

   shapes graph                                  data graph
   bt:eventKinds bs:allows "Reading", "Signing", ...    bt:event-... bs:eventKind "Recital"

   SELECT $this ?value WHERE {
     $this bs:eventKind ?value .
     FILTER NOT EXISTS { GRAPH $shapesGraph { bt:eventKinds bs:allows ?value } }
   }

What to take away

  • $shapesGraph gives a constraint read access to the shapes graph. Reference data that belongs with the shapes can live there.
  • The specification makes $shapesGraph optional; this engine supports it. Say so in a comment if the shapes may move.
  • sh:in for a short fixed list; a lookup in $shapesGraph or in the data graph for a longer or shared one.

The report

Does not conform · 1 violation, 0 warnings, 0 info · 1 shapes

SeverityFocus nodePathValueMessageShape
Violationbt:event-foxed-page-2025-05-01RecitalRecital is not a kind of event listed in the shapes graph.bt:EventKindShape

Defined in

S38

The report is a graph

A short shapes graph that finds a dozen faults, and three queries over its report.

Data: bookshop-trail-faulty.ttl

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix sh:   <http://www.w3.org/ns/shacl#> .
@prefix rdf:  <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd:  <http://www.w3.org/2001/XMLSchema#> .

bt:BookshopShape
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:property [ sh:path rdfs:label ;    sh:minCount 1 ; sh:datatype rdf:langString ] ;
    sh:property [ sh:path bs:locatedIn ;  sh:minCount 1 ; sh:maxCount 1 ; sh:class bs:Settlement ] ;
    sh:property [ sh:path bs:founded ;    sh:minCount 1 ; sh:datatype xsd:gYear ] ;
    sh:property [ sh:path bs:staffCount ; sh:datatype xsd:integer ; sh:minInclusive 1 ] ;
    sh:property [ sh:path bs:floorArea ;  sh:minExclusive 0 ] ;
    sh:property [ sh:path bs:hasCafe ;    sh:datatype xsd:boolean ] ;
    sh:property [ sh:path bs:website ;    sh:maxCount 1 ; sh:datatype xsd:anyURI ; sh:severity sh:Warning ] .

bt:WorkShape
    a               sh:NodeShape ;
    sh:targetClass  bs:Work ;
    sh:property [ sh:path bs:isbn ;  sh:pattern "^97[89][0-9]{10}$" ] ;
    sh:property [ sh:path bs:pages ; sh:minInclusive 1 ] ;
    sh:property [ sh:path bs:rrp ;   sh:minInclusive 0 ] .

How it works

Press Validate, then Report as tab. What opens is an RDF graph: one sh:ValidationReport with sh:conforms and one sh:result per row, each a sh:ValidationResult. The SPARQL panel now queries that tab, so the report can be summarised the way any graph can. The first query counts results by severity; the second by the constraint component that produced them; the third lists the focus nodes with the most results, which is where to start fixing. The SPARQL course's q88 produces a report of this shape from a query; here the validator produces it and the query reads it. Note that the report tab does not contain the data: to join a result to its focus node's label, paste the report into the data tab first, or use the CONSTRUCT in module 11.

Diagram

   _:report  a sh:ValidationReport ;
             sh:conforms false ;
             sh:result _:r1, _:r2, ... .

   _:r1  a sh:ValidationResult ;
         sh:focusNode bt:shop-foxed-page ;
         sh:resultPath bs:founded ;
         sh:value "1985"^^xsd:integer ;
         sh:resultSeverity sh:Violation ;
         sh:sourceConstraintComponent sh:DatatypeConstraintComponent ;
         sh:sourceShape <...> ;
         sh:resultMessage "..." .

   Validate  ->  Report as tab  ->  the SPARQL panel now sees this graph

What to take away

  • The report is RDF. Everything the SPARQL course teaches about querying applies to it.
  • Group by severity to see how bad; by component to see what kind of wrong; by focus node to see where to start.
  • The report and the data are separate graphs in the editor. Joining them needs both in one tab.

The report

Does not conform · 13 violations, 2 warnings, 0 info · 12 shapes

SeverityFocus nodePathValueMessageShape
Violationbt:book-the-margin-notesbs:isbn978-0-14-118776-1Does not satisfy sh:patternbt:WorkShape › property 1
Violationbt:book-the-margin-notesbs:pages0Does not satisfy sh:minInclusivebt:WorkShape › property 2
Violationbt:book-the-margin-notesbs:rrp-4.99Does not satisfy sh:minInclusivebt:WorkShape › property 3
Violationbt:shop-foxed-pagebs:floorArea-20.0Does not satisfy sh:minExclusivebt:BookshopShape › property 5
Violationbt:shop-foxed-pagebs:founded1985Does not satisfy sh:datatypebt:BookshopShape › property 3
Violationbt:shop-foxed-pagebs:hasCafeyesDoes not satisfy sh:datatypebt:BookshopShape › property 6
Violationbt:shop-foxed-pagebs:locatedInKendalDoes not satisfy sh:classbt:BookshopShape › property 2
Violationbt:shop-foxed-pagebs:staffCount3.5Does not satisfy sh:datatypebt:BookshopShape › property 4
Violationbt:shop-foxed-pagerdfs:labelThe Foxed PageDoes not satisfy sh:datatypebt:BookshopShape › property 1
Violationbt:shop-halfmoonbs:foundedDoes not satisfy sh:minCountbt:BookshopShape › property 3
Violationbt:shop-halfmoonbs:staffCount0Does not satisfy sh:minInclusivebt:BookshopShape › property 4
Violationbt:shop-halfmoonrdfs:labelDoes not satisfy sh:minCountbt:BookshopShape › property 1

and 3 more

Afterwards, in the SPARQL panel

The headline, as a query (on the report)
PREFIX sh: <http://www.w3.org/ns/shacl#>
SELECT ?conforms (COUNT(?r) AS ?results)
WHERE {
  ?report a sh:ValidationReport ; sh:conforms ?conforms .
  OPTIONAL { ?report sh:result ?r }
}
GROUP BY ?conforms
How bad: results by severity (on the report)
PREFIX sh: <http://www.w3.org/ns/shacl#>
SELECT ?severity (COUNT(?r) AS ?results)
WHERE { ?r a sh:ValidationResult ; sh:resultSeverity ?severity }
GROUP BY ?severity
ORDER BY ?severity
What kind of wrong: results by constraint component (on the report)
PREFIX sh: <http://www.w3.org/ns/shacl#>
SELECT ?component (COUNT(?r) AS ?results)
WHERE { ?r a sh:ValidationResult ; sh:sourceConstraintComponent ?component }
GROUP BY ?component
ORDER BY DESC(?results)
Where to start: the focus nodes with the most results (on the report)
PREFIX sh: <http://www.w3.org/ns/shacl#>
SELECT ?focus (COUNT(?r) AS ?results) (GROUP_CONCAT(DISTINCT ?path; separator=", ") AS ?paths)
       (GROUP_CONCAT(DISTINCT STR(?value); separator=", ") AS ?values)
WHERE {
  ?r a sh:ValidationResult ; sh:focusNode ?focus .
  OPTIONAL { ?r sh:resultPath ?path }
  OPTIONAL { ?r sh:value ?value }
}
GROUP BY ?focus
ORDER BY DESC(?results)

Defined in

Module 06

Your own constraint components

sh:minCount is a parameter and a validator, and a shapes graph can declare new components on the same terms. A constraint component gives a SPARQL check a name and parameters, so the shapes that use it read like Core and the query is written once. Appendix D of the specification defines every Core component this way.

In the standards
S39

An ISBN check digit

Every ISBN-13's last digit is the check digit its first twelve imply.

Data: bookshop-trail-faulty.ttl

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix sh:   <http://www.w3.org/ns/shacl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix xsd:  <http://www.w3.org/2001/XMLSchema#> .

# Prefixes for the SPARQL inside this file. @prefix lines are Turtle syntax
# and do not reach the query engine; sh:declare does.
bt:prefixes
    a  owl:Ontology ;
    sh:declare [ sh:prefix "bt" ;   sh:namespace "https://example.org/bookshop-trail/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "bs" ;   sh:namespace "https://example.org/bookshop-trail/schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdf" ;  sh:namespace "http://www.w3.org/1999/02/22-rdf-syntax-ns#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdfs" ; sh:namespace "http://www.w3.org/2000/01/rdf-schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "xsd" ;  sh:namespace "http://www.w3.org/2001/XMLSchema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "skos" ; sh:namespace "http://www.w3.org/2004/02/skos/core#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "dct" ;  sh:namespace "http://purl.org/dc/terms/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "geo" ;  sh:namespace "http://www.opengis.net/ont/geosparql#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "schema" ; sh:namespace "https://schema.org/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "sh" ;   sh:namespace "http://www.w3.org/ns/shacl#"^^xsd:anyURI ] .

bt:ISBN13ConstraintComponent
    a  sh:ConstraintComponent ;
    rdfs:label  "ISBN-13 check digit" ;
    sh:parameter [ sh:path bt:isbn13 ; sh:datatype xsd:boolean ] ;
    sh:validator [
        a  sh:SPARQLAskValidator ;
        sh:prefixes  bt:prefixes ;
        sh:message   "The check digit of {$value} is wrong." ;
        sh:ask  """
            ASK {
              BIND ( STR($value) AS ?s )
              BIND (   xsd:integer(SUBSTR(?s, 1, 1))  + 3 * xsd:integer(SUBSTR(?s, 2, 1))
                     + xsd:integer(SUBSTR(?s, 3, 1))  + 3 * xsd:integer(SUBSTR(?s, 4, 1))
                     + xsd:integer(SUBSTR(?s, 5, 1))  + 3 * xsd:integer(SUBSTR(?s, 6, 1))
                     + xsd:integer(SUBSTR(?s, 7, 1))  + 3 * xsd:integer(SUBSTR(?s, 8, 1))
                     + xsd:integer(SUBSTR(?s, 9, 1))  + 3 * xsd:integer(SUBSTR(?s, 10, 1))
                     + xsd:integer(SUBSTR(?s, 11, 1)) + 3 * xsd:integer(SUBSTR(?s, 12, 1))
                     AS ?sum )
              # SPARQL has no modulus operator: x mod 10 is x - 10 * FLOOR(x / 10)
              BIND ( 10 - (?sum - 10 * FLOOR(?sum / 10)) AS ?c )
              BIND ( ?c - 10 * FLOOR(?c / 10) AS ?check )
              FILTER ( !REGEX(?s, "^97[89][0-9]{10}$") || ?check = xsd:integer(SUBSTR(?s, 13, 1)) )
            }
        """ ;
    ] .

bt:WorkShape
    a               sh:NodeShape ;
    sh:targetClass  bs:Work ;
    sh:property [
        sh:path    bs:isbn ;
        bt:isbn13  true ;
    ] .

How it works

s15 showed that a pattern cannot add up. A constraint component can: it is a sh:ConstraintComponent with one or more sh:parameter declarations and a validator, and once declared it is used like a Core component -- here as bt:isbn13 true on a property shape. The validator is a sh:SPARQLAskValidator: an ASK that must come back true for a value to pass, with $value bound to the value and each parameter bound to its own variable. The query weights the twelve digits 1, 3, 1, 3 and so on, sums them, and compares the check digit. All 63 ISBNs in the clean data pass; the one with the wrong last digit (F10) fails. A value that does not match the thirteen-digit pattern is left to s15's sh:pattern and passes here, so the hyphenated ISBN is not reported twice. SPARQL has no modulus operator, so the arithmetic is written with FLOOR, in BINDs that keep the ASK readable.

Diagram

   bt:ISBN13ConstraintComponent
     sh:parameter [ sh:path bt:isbn13 ]           <- the property that switches it on
     sh:validator [ a sh:SPARQLAskValidator ;
        sh:ask "ASK { FILTER ( <check digit arithmetic on $value> ) }" ]

   a property shape uses it:   sh:property [ sh:path bs:isbn ; bt:isbn13 true ]

   9 7 8 0 1 4 1 1 8 7 7 6 | 2
   x1x3x1x3x1x3x1x3x1x3x1x3          sum 109  ->  (10 - 9) mod 10 = 1  !=  2   -> fails

   SPARQL has no modulus operator; x mod 10 is written x - 10 * FLOOR(x / 10)

What to take away

  • A constraint component is a parameter plus a validator. Declaring one turns a query into vocabulary.
  • An ASK validator answers per value: true passes. $value is the value, $this the focus node, $PATH the path, and every parameter is a variable of the same name.
  • Leave what a Core component already checks to that component. A custom validator should test one thing.

The report

Does not conform · 1 violation, 0 warnings, 0 info · 3 shapes

SeverityFocus nodePathValueMessageShape
Violationbt:book-precociousbs:isbn9780141187762The check digit of 9780141187762 is wrong.bt:WorkShape › property 1

Defined in

S40

A SELECT validator with parameters

A value is at most some multiple of a value reached through two properties -- a shelf price against a recommended price, said generally.

Data: bookshop-trail-faulty.ttl

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix sh:   <http://www.w3.org/ns/shacl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix xsd:  <http://www.w3.org/2001/XMLSchema#> .

# Prefixes for the SPARQL inside this file. @prefix lines are Turtle syntax
# and do not reach the query engine; sh:declare does.
bt:prefixes
    a  owl:Ontology ;
    sh:declare [ sh:prefix "bt" ;   sh:namespace "https://example.org/bookshop-trail/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "bs" ;   sh:namespace "https://example.org/bookshop-trail/schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdf" ;  sh:namespace "http://www.w3.org/1999/02/22-rdf-syntax-ns#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdfs" ; sh:namespace "http://www.w3.org/2000/01/rdf-schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "xsd" ;  sh:namespace "http://www.w3.org/2001/XMLSchema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "skos" ; sh:namespace "http://www.w3.org/2004/02/skos/core#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "dct" ;  sh:namespace "http://purl.org/dc/terms/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "geo" ;  sh:namespace "http://www.opengis.net/ont/geosparql#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "schema" ; sh:namespace "https://schema.org/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "sh" ;   sh:namespace "http://www.w3.org/ns/shacl#"^^xsd:anyURI ] .

bt:MaxMultipleOfConstraintComponent
    a  sh:ConstraintComponent ;
    rdfs:label        "at most a multiple of a value elsewhere" ;
    sh:labelTemplate  "at most {$maxMultipleOf} times {$viaProperty}/{$comparedWith}" ;
    sh:parameter [ sh:path bt:maxMultipleOf ; sh:datatype xsd:decimal ] ;
    sh:parameter [ sh:path bt:viaProperty ;   sh:nodeKind sh:IRI ] ;
    sh:parameter [ sh:path bt:comparedWith ;  sh:nodeKind sh:IRI ] ;
    sh:propertyValidator [
        a  sh:SPARQLSelectValidator ;
        sh:prefixes  bt:prefixes ;
        sh:message   "{$value} is more than the allowed multiple of the reference price." ;
        sh:select  """
            SELECT $this ?value WHERE {
              $this $PATH ?value ;
                    $viaProperty ?other .
              ?other $comparedWith ?reference .
              FILTER ( ?value > $maxMultipleOf * ?reference )
            }
        """ ;
    ] .

bt:StockRecordShape
    a               sh:NodeShape ;
    sh:targetClass  bs:StockRecord ;
    sh:property [
        sh:path           bs:shelfPrice ;
        bt:maxMultipleOf  2.0 ;
        bt:viaProperty    bs:ofWork ;
        bt:comparedWith   bs:rrp ;
    ] .

How it works

s31 wrote 'shelf price at most twice the RRP' as a SPARQL constraint with the properties spelled out. bt:MaxMultipleOfConstraintComponent makes the same check reusable: three parameters, bt:maxMultipleOf for the factor, bt:viaProperty for the hop to the other node and bt:comparedWith for the property there. A sh:propertyValidator with a SELECT returns one row per failing value, and $PATH inside it is the using shape's path. sh:labelTemplate gives the constraint a readable name that tools can show. The one record at three times the recommended price (F23) is reported, exactly as in s31, and the shape that uses the component is three lines.

Diagram

   component            parameters                  validator
   bt:MaxMultipleOf...  bt:maxMultipleOf (decimal)   SELECT $this ?value WHERE {
                        bt:viaProperty   (IRI)         $this $PATH ?value ; $viaProperty ?other .
                        bt:comparedWith  (IRI)         ?other $comparedWith ?reference .
                                                       FILTER ( ?value > $maxMultipleOf * ?reference ) }

   use    sh:property [ sh:path bs:shelfPrice ;
                        bt:maxMultipleOf 2.0 ; bt:viaProperty bs:ofWork ; bt:comparedWith bs:rrp ]

What to take away

  • A SELECT validator returns the failing rows, like sh:sparql; an ASK validator answers per value. Use SELECT when the query is a join.
  • Parameters that are IRIs can stand for predicates in the pattern, which is how a component stays general.
  • sh:propertyValidator is for property shapes and may use $PATH; sh:nodeValidator is for node shapes; sh:validator serves both.

The report

Does not conform · 1 violation, 0 warnings, 0 info · 5 shapes

SeverityFocus nodePathValueMessageShape
Violationbt:stock-foxed-page--the-book-townbs:shelfPrice68.0768.07 is more than the allowed multiple of the reference price.bt:StockRecordShape › property 1

Defined in

S41

A required language, and an optional parameter

Every Welsh place has a Welsh name -- with an option to accept one from skos:altLabel.

Data: bookshop-trail-1.1.ttl

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix sh:   <http://www.w3.org/ns/shacl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix xsd:  <http://www.w3.org/2001/XMLSchema#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

# Prefixes for the SPARQL inside this file. @prefix lines are Turtle syntax
# and do not reach the query engine; sh:declare does.
bt:prefixes
    a  owl:Ontology ;
    sh:declare [ sh:prefix "bt" ;   sh:namespace "https://example.org/bookshop-trail/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "bs" ;   sh:namespace "https://example.org/bookshop-trail/schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdf" ;  sh:namespace "http://www.w3.org/1999/02/22-rdf-syntax-ns#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdfs" ; sh:namespace "http://www.w3.org/2000/01/rdf-schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "xsd" ;  sh:namespace "http://www.w3.org/2001/XMLSchema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "skos" ; sh:namespace "http://www.w3.org/2004/02/skos/core#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "dct" ;  sh:namespace "http://purl.org/dc/terms/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "geo" ;  sh:namespace "http://www.opengis.net/ont/geosparql#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "schema" ; sh:namespace "https://schema.org/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "sh" ;   sh:namespace "http://www.w3.org/ns/shacl#"^^xsd:anyURI ] .

bt:RequiredLanguageConstraintComponent
    a  sh:ConstraintComponent ;
    rdfs:label  "a value in a required language" ;
    sh:parameter [ sh:path bt:requiredLanguage ; sh:datatype xsd:string ] ;
    sh:parameter [ sh:path bt:alsoAccept ; sh:nodeKind sh:IRI ; sh:optional true ] ;
    sh:propertyValidator [
        a  sh:SPARQLSelectValidator ;
        sh:prefixes  bt:prefixes ;
        sh:message   "{$this} has no name in the required language." ;
        sh:select  """
            SELECT $this WHERE {
              FILTER NOT EXISTS {
                { $this $PATH ?l }
                UNION
                { $this ?p ?l . FILTER ( BOUND($alsoAccept) && ?p = $alsoAccept ) }
                FILTER ( LANG(?l) = $requiredLanguage )
              }
            }
        """ ;
    ] .

bt:WelshPlaceShape
    a  sh:NodeShape ;
    sh:target [
        a            sh:SPARQLTarget ;
        sh:prefixes  bt:prefixes ;
        sh:select    "SELECT ?this WHERE { ?this bs:within+ bt:place-wales }" ;
    ] ;
    sh:property [
        sh:path              rdfs:label ;
        bt:requiredLanguage  "cy" ;
        bt:alsoAccept        skos:altLabel ;
        sh:severity          sh:Warning ;
    ] .

How it works

sh:languageIn says which languages are allowed; nothing in Core says one is required. bt:RequiredLanguageConstraintComponent does: bt:requiredLanguage names a tag, and the property validator reports a focus node with no value of $PATH in that language. A second parameter, bt:alsoAccept, is marked sh:optional true: when the using shape leaves it out the variable is unbound, and the query copes with COALESCE. The shape targets the places inside Wales with a SPARQL target from s33. Three council areas -- Powys, Ceredigion and the City of Cardiff -- have English names only, and are reported as warnings; the towns all have Welsh ones.

Diagram

   sh:parameter [ sh:path bt:requiredLanguage ]                      required
   sh:parameter [ sh:path bt:alsoAccept ; sh:optional true ]         may be absent

   SELECT $this WHERE {
     FILTER NOT EXISTS {
       { $this $PATH ?l }
       UNION
       { $this ?p ?l . FILTER ( BOUND($alsoAccept) && ?p = $alsoAccept ) }
       FILTER ( LANG(?l) = $requiredLanguage )
     }
   }

   bt:place-powys   rdfs:label "Powys"@en        no @cy  ->  warning

What to take away

  • A component can require what Core can only permit. 'At least one value in language X' is one line once the component exists.
  • sh:optional true on a parameter means the variable may be unbound. Test with BOUND or COALESCE rather than assuming.
  • Targets and components compose: the component says what to check, the target says of whom.

The report

Does not conform · 0 violations, 3 warnings, 0 info · 4 shapes

SeverityFocus nodePathValueMessageShape
Warningbt:place-ceredigionrdfs:labelCeredigionhttps://example.org/bookshop-trail/place-ceredigion has no name in the required language.bt:WelshPlaceShape › property 1
Warningbt:place-city-of-cardiffrdfs:labelCity of Cardiffhttps://example.org/bookshop-trail/place-city-of-cardiff has no name in the required language.bt:WelshPlaceShape › property 1
Warningbt:place-powysrdfs:labelPowyshttps://example.org/bookshop-trail/place-powys has no name in the required language.bt:WelshPlaceShape › property 1

Defined in

S42

Core components are SPARQL too

sh:minCount and sh:minInclusive written as constraint components -- the second in a version that understands years.

Data: bookshop-trail-1.1.ttl

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix sh:   <http://www.w3.org/ns/shacl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix xsd:  <http://www.w3.org/2001/XMLSchema#> .

# Prefixes for the SPARQL inside this file. @prefix lines are Turtle syntax
# and do not reach the query engine; sh:declare does.
bt:prefixes
    a  owl:Ontology ;
    sh:declare [ sh:prefix "bt" ;   sh:namespace "https://example.org/bookshop-trail/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "bs" ;   sh:namespace "https://example.org/bookshop-trail/schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdf" ;  sh:namespace "http://www.w3.org/1999/02/22-rdf-syntax-ns#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdfs" ; sh:namespace "http://www.w3.org/2000/01/rdf-schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "xsd" ;  sh:namespace "http://www.w3.org/2001/XMLSchema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "skos" ; sh:namespace "http://www.w3.org/2004/02/skos/core#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "dct" ;  sh:namespace "http://purl.org/dc/terms/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "geo" ;  sh:namespace "http://www.opengis.net/ont/geosparql#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "schema" ; sh:namespace "https://schema.org/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "sh" ;   sh:namespace "http://www.w3.org/ns/shacl#"^^xsd:anyURI ] .

bt:AtLeastConstraintComponent
    a  sh:ConstraintComponent ;
    rdfs:label  "at least N values of a property (sh:minCount, written out)" ;
    sh:parameter [ sh:path bt:countProperty ; sh:nodeKind sh:IRI ] ;
    sh:parameter [ sh:path bt:atLeast ;       sh:datatype xsd:integer ] ;
    sh:nodeValidator [
        a  sh:SPARQLSelectValidator ;
        sh:prefixes  bt:prefixes ;
        sh:message   "{$this} has fewer values of the counted property than required." ;
        sh:select  """
            SELECT $this WHERE {
              { SELECT $this (COUNT(?v) AS ?n)
                WHERE { OPTIONAL { $this $countProperty ?v } }
                GROUP BY $this }
              FILTER ( ?n < $atLeast )
            }
        """ ;
    ] .

bt:MinYearConstraintComponent
    a  sh:ConstraintComponent ;
    rdfs:label  "not before a year (sh:minInclusive for xsd:gYear)" ;
    sh:parameter [ sh:path bt:minYear ; sh:datatype xsd:integer ] ;
    sh:validator [
        a  sh:SPARQLAskValidator ;
        sh:prefixes  bt:prefixes ;
        sh:message   "{$value} is earlier than the year allowed." ;
        sh:ask  "ASK { FILTER ( xsd:integer(STR($value)) >= $minYear ) }" ;
    ] .

bt:TranslationShape
    a                 sh:NodeShape ;
    sh:targetClass    bs:Translation ;
    bt:countProperty  bs:translatedBy ;
    bt:atLeast        1 .

bt:YearShape
    a               sh:NodeShape ;
    sh:targetClass  bs:Work ;
    sh:property [ sh:path bs:publicationYear ; bt:minYear 1950 ; sh:severity sh:Info ] .

bt:FoundedShape
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:property [ sh:path bs:founded ; bt:minYear 1900 ] .

How it works

Appendix D of the specification defines every Core component as a SPARQL validator. bt:AtLeastConstraintComponent is sh:minCount in that form, with one difference forced by this engine: a SELECT property validator here runs once per value node, so a validator that counts values never runs when there are none. The component is therefore declared for node shapes, with the property to count as a second parameter, and a sh:nodeValidator counts the values of $countProperty per focus node. It finds the Hebrew translation without a translator that s08 found. bt:MinYearConstraintComponent is what sh:minInclusive would be if it cast a gYear to an integer first, which is the fix s14 wrote by hand: an ASK validator comparing xsd:integer(STR($value)) with the parameter. Two works in the clean data were published before 1950, and are reported as information. Writing a Core component out this way is the quickest way to see what it does, and the quickest way to make a version that does something slightly different.

Diagram

   Appendix D, sh:minCount, as a query:
     SELECT $this WHERE {
       { SELECT $this (COUNT(?v) AS ?n) WHERE { OPTIONAL { $this $PATH ?v } } GROUP BY $this }
       FILTER ( ?n < $minCount ) }

   here, as a node-shape component:   bt:TranslationShape  bt:countProperty bs:translatedBy ; bt:atLeast 1
     (a property validator would run per value on this engine, and a node with no values has none)

   bt:minYear 1950 on bs:publicationYear:
     ASK { FILTER ( xsd:integer(STR($value)) >= $minYear ) }
     "1948"^^xsd:gYear -> 1948 >= 1950 is false -> reported, where sh:minInclusive could not compare at all

What to take away

  • Every Core component has a SPARQL definition in Appendix D. When one behaves unexpectedly, read that first.
  • A custom component can be a Core component with one change. That is often the right fix for a datatype the engine will not compare.
  • On this engine, SELECT property validators run per value, ASK validators run per value, and node validators run per focus node. Count with a node validator.

The report

Does not conform · 1 violation, 0 warnings, 2 info · 8 shapes

SeverityFocus nodePathValueMessageShape
Violationbt:book-cold-harbour-hebt:book-cold-harbour-hehttps://example.org/bookshop-trail/book-cold-harbour-he has fewer values of the counted property than required.bt:TranslationShape
Infobt:book-cold-harbourbs:publicationYear19481948 is earlier than the year allowed.bt:YearShape › property 1
Infobt:book-north-of-the-tweedbs:publicationYear19471947 is earlier than the year allowed.bt:YearShape › property 1

Defined in

Module 07

SHACL rules

The Advanced Features note lets a shape infer triples as well as check them. A triple rule builds one triple from node expressions; a SPARQL rule runs a CONSTRUCT. Rules run before validation, so a result can depend on an inferred triple. The editor's Inference dropdown switches them on. A validator's only output is its report, so each lesson here uses a shape to make its inferences visible.

In the standards
S43

Your first rule

Every bookshop is also a schema:BookStore -- inferred, then made visible in the report.

Data: 04-bookshops.ttl · Inference rules

@prefix bt:     <https://example.org/bookshop-trail/> .
@prefix bs:     <https://example.org/bookshop-trail/schema#> .
@prefix sh:     <http://www.w3.org/ns/shacl#> .
@prefix rdf:    <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs:   <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:    <http://www.w3.org/2002/07/owl#> .
@prefix xsd:    <http://www.w3.org/2001/XMLSchema#> .
@prefix schema: <https://schema.org/> .

# Prefixes for the SPARQL inside this file. @prefix lines are Turtle syntax
# and do not reach the query engine; sh:declare does.
bt:prefixes
    a  owl:Ontology ;
    sh:declare [ sh:prefix "bt" ;   sh:namespace "https://example.org/bookshop-trail/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "bs" ;   sh:namespace "https://example.org/bookshop-trail/schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdf" ;  sh:namespace "http://www.w3.org/1999/02/22-rdf-syntax-ns#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdfs" ; sh:namespace "http://www.w3.org/2000/01/rdf-schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "xsd" ;  sh:namespace "http://www.w3.org/2001/XMLSchema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "skos" ; sh:namespace "http://www.w3.org/2004/02/skos/core#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "dct" ;  sh:namespace "http://purl.org/dc/terms/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "geo" ;  sh:namespace "http://www.opengis.net/ont/geosparql#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "schema" ; sh:namespace "https://schema.org/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "sh" ;   sh:namespace "http://www.w3.org/ns/shacl#"^^xsd:anyURI ] .

bt:BookshopShape
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:rule [
        a             sh:TripleRule ;
        sh:subject    sh:this ;
        sh:predicate  rdf:type ;
        sh:object     schema:BookStore ;
    ] .

# Reports each inferred type as information.
bt:TypeProbe
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:severity  sh:Info ;
        sh:message   "inferred: {$this} rdf:type {$value}" ;
        sh:select    """
            SELECT $this ?value WHERE {
              $this rdf:type ?value .
              FILTER ( ?value = schema:BookStore )
            }
        """ ;
    ] .

# A shape whose target only exists once the rule has run.
bt:BookStoreShape
    a               sh:NodeShape ;
    sh:targetClass  schema:BookStore ;
    sh:property [ sh:path rdfs:label ; sh:minCount 1 ] .

How it works

sh:rule attaches a rule to a shape, and the rule fires once for each of the shape's focus nodes. A sh:TripleRule builds one triple from three node expressions: sh:subject sh:this is the focus node, sh:predicate rdf:type is a constant, sh:object schema:BookStore is a constant. With the Inference dropdown at none the rule is ignored; at rules it runs before validation, and the data the shapes see now has 33 triples nobody wrote. A validator's only output is its report, so bt:TypeProbe makes the inferences visible: a SPARQL constraint at Info severity that selects the new type, one row per shop. bt:BookStoreShape shows the other consequence: its target is the inferred class, and under rules it has 33 focus nodes. The data in the tab is not changed; the inferred graph lives only for the run.

Diagram

   bt:BookshopShape
     sh:targetClass bs:Bookshop              33 focus nodes
     sh:rule [ a sh:TripleRule ;
        sh:subject   sh:this ;               <- the focus node
        sh:predicate rdf:type ;              <- a constant
        sh:object    schema:BookStore ]      <- a constant

   Inference: none     data as written             probe: 0 rows    BookStoreShape: 0 targets
   Inference: rules    data + 33 inferred triples  probe: 33 rows   BookStoreShape: 33 targets

   the report is the only window onto the inferences

What to take away

  • A rule fires on its shape's focus nodes. A shape with no target has no focus nodes and its rules never run.
  • Rules run before validation. Everything downstream -- targets, constraints, counts -- sees the inferred triples.
  • To see what a rule produced, write a shape that reports it. An Info-severity SPARQL constraint is the usual form.

Try it

Set Inference back to none and validate again: no rows, and the headline still says Conforms. Then set it to rules.

The report

Does not conform · 0 violations, 0 warnings, 33 info · 5 shapes · inference rules

SeverityFocus nodePathValueMessageShape
Infobt:shop-bookbarrowschema:BookStoreinferred: https://example.org/bookshop-trail/shop-bookbarrow rdf:type https://schema.org/BookStorebt:TypeProbe
Infobt:shop-bookwyrmschema:BookStoreinferred: https://example.org/bookshop-trail/shop-bookwyrm rdf:type https://schema.org/BookStorebt:TypeProbe
Infobt:shop-borderprintschema:BookStoreinferred: https://example.org/bookshop-trail/shop-borderprint rdf:type https://schema.org/BookStorebt:TypeProbe
Infobt:shop-broads-binderyschema:BookStoreinferred: https://example.org/bookshop-trail/shop-broads-bindery rdf:type https://schema.org/BookStorebt:TypeProbe
Infobt:shop-broken-spineschema:BookStoreinferred: https://example.org/bookshop-trail/shop-broken-spine rdf:type https://schema.org/BookStorebt:TypeProbe
Infobt:shop-candlemasschema:BookStoreinferred: https://example.org/bookshop-trail/shop-candlemas rdf:type https://schema.org/BookStorebt:TypeProbe
Infobt:shop-castle-stepsschema:BookStoreinferred: https://example.org/bookshop-trail/shop-castle-steps rdf:type https://schema.org/BookStorebt:TypeProbe
Infobt:shop-chapter-verseschema:BookStoreinferred: https://example.org/bookshop-trail/shop-chapter-verse rdf:type https://schema.org/BookStorebt:TypeProbe
Infobt:shop-cliff-roadschema:BookStoreinferred: https://example.org/bookshop-trail/shop-cliff-road rdf:type https://schema.org/BookStorebt:TypeProbe
Infobt:shop-clock-towerschema:BookStoreinferred: https://example.org/bookshop-trail/shop-clock-tower rdf:type https://schema.org/BookStorebt:TypeProbe
Infobt:shop-colophonschema:BookStoreinferred: https://example.org/bookshop-trail/shop-colophon rdf:type https://schema.org/BookStorebt:TypeProbe
Infobt:shop-cotton-quartoschema:BookStoreinferred: https://example.org/bookshop-trail/shop-cotton-quarto rdf:type https://schema.org/BookStorebt:TypeProbe

and 21 more

Defined in

S44

A value copied along a path

Each shop gets a bs:townName: the label of the town it is in -- every label, in every language.

Data: bookshop-trail-1.1.ttl · Inference rules

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix sh:   <http://www.w3.org/ns/shacl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix xsd:  <http://www.w3.org/2001/XMLSchema#> .

# Prefixes for the SPARQL inside this file. @prefix lines are Turtle syntax
# and do not reach the query engine; sh:declare does.
bt:prefixes
    a  owl:Ontology ;
    sh:declare [ sh:prefix "bt" ;   sh:namespace "https://example.org/bookshop-trail/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "bs" ;   sh:namespace "https://example.org/bookshop-trail/schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdf" ;  sh:namespace "http://www.w3.org/1999/02/22-rdf-syntax-ns#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdfs" ; sh:namespace "http://www.w3.org/2000/01/rdf-schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "xsd" ;  sh:namespace "http://www.w3.org/2001/XMLSchema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "skos" ; sh:namespace "http://www.w3.org/2004/02/skos/core#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "dct" ;  sh:namespace "http://purl.org/dc/terms/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "geo" ;  sh:namespace "http://www.opengis.net/ont/geosparql#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "schema" ; sh:namespace "https://schema.org/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "sh" ;   sh:namespace "http://www.w3.org/ns/shacl#"^^xsd:anyURI ] .

bt:BookshopShape
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:rule [
        a             sh:TripleRule ;
        sh:subject    sh:this ;
        sh:predicate  bs:townName ;
        sh:object     [ sh:path ( bs:locatedIn rdfs:label ) ] ;
    ] .

bt:TownNameProbe
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:severity  sh:Info ;
        sh:message   "inferred: {$this} bs:townName {$value}" ;
        sh:select    "SELECT $this ?value WHERE { $this bs:townName ?value }" ;
    ] .

bt:OneTownName
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:property [
        sh:path      bs:townName ;
        sh:maxCount  1 ;
        sh:message   "{$this} has more than one town name: the rule copied every label." ;
    ] .

How it works

sh:object can be a path expression, [ sh:path P ], whose values are the values of P at the focus node. ( bs:locatedIn rdfs:label ) is a sequence path, so the rule copies the town's labels onto the shop. A triple rule produces the cross product of its three expressions, and here that matters: Wigtown has an English and a Gaelic label, so both shops there get two town names, and so does every shop in a town with a Welsh or Gaelic name. The probe lists 45 inferred triples for 33 shops. bt:OneTownName is a Core constraint on the derived property with sh:maxCount 1, and under rules it reports the twelve shops with two: a constraint on an inferred property behaves like any other, and here it is telling you the rule copied more than you meant. Filtering the expression is s45's subject.

Diagram

   sh:rule [ a sh:TripleRule ;
       sh:subject   sh:this ;
       sh:predicate bs:townName ;
       sh:object    [ sh:path ( bs:locatedIn rdfs:label ) ] ]

   bt:shop-inkwell --locatedIn--> bt:place-wigtown --label--> "Wigtown"@en, "Baile na h-Uige"@gd
        inferred:  bt:shop-inkwell bs:townName "Wigtown"@en
                   bt:shop-inkwell bs:townName "Baile na h-Uige"@gd      cross product: two triples

   bt:OneTownName   sh:path bs:townName ; sh:maxCount 1   ->  reports the bilingual towns' shops

What to take away

  • [ sh:path P ] as a node expression is 'the values of P at the focus node', and P can be any SHACL path.
  • A triple rule multiplies out its expressions. Several values on one side mean several triples.
  • Constraints apply to inferred triples exactly as to asserted ones. Use them to check what your rules did.

The report

Does not conform · 12 violations, 0 warnings, 45 info · 6 shapes · inference rules

SeverityFocus nodePathValueMessageShape
Violationbt:shop-broken-spinebs:townNamebt:shop-broken-spine has more than one town name: the rule copied every label.bt:OneTownName › property 1
Violationbt:shop-castle-stepsbs:townNamebt:shop-castle-steps has more than one town name: the rule copied every label.bt:OneTownName › property 1
Violationbt:shop-cliff-roadbs:townNamebt:shop-cliff-road has more than one town name: the rule copied every label.bt:OneTownName › property 1
Violationbt:shop-clock-towerbs:townNamebt:shop-clock-tower has more than one town name: the rule copied every label.bt:OneTownName › property 1
Violationbt:shop-colophonbs:townNamebt:shop-colophon has more than one town name: the rule copied every label.bt:OneTownName › property 1
Violationbt:shop-inkwellbs:townNamebt:shop-inkwell has more than one town name: the rule copied every label.bt:OneTownName › property 1
Violationbt:shop-marginaliabs:townNamebt:shop-marginalia has more than one town name: the rule copied every label.bt:OneTownName › property 1
Violationbt:shop-northern-lightbs:townNamebt:shop-northern-light has more than one town name: the rule copied every label.bt:OneTownName › property 1
Violationbt:shop-quirebs:townNamebt:shop-quire has more than one town name: the rule copied every label.bt:OneTownName › property 1
Violationbt:shop-sea-marginbs:townNamebt:shop-sea-margin has more than one town name: the rule copied every label.bt:OneTownName › property 1
Violationbt:shop-taff-marginbs:townNamebt:shop-taff-margin has more than one town name: the rule copied every label.bt:OneTownName › property 1
Violationbt:shop-versobs:townNamebt:shop-verso has more than one town name: the rule copied every label.bt:OneTownName › property 1

and 45 more

Defined in

S45

Filtering the values

Each shop gets a bs:inCountry: the one place above it that is a country.

Data: bookshop-trail-1.1.ttl · Inference rules

@prefix bt:  <https://example.org/bookshop-trail/> .
@prefix bs:  <https://example.org/bookshop-trail/schema#> .
@prefix sh:  <http://www.w3.org/ns/shacl#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

# Prefixes for the SPARQL inside this file. @prefix lines are Turtle syntax
# and do not reach the query engine; sh:declare does.
bt:prefixes
    a  owl:Ontology ;
    sh:declare [ sh:prefix "bt" ;   sh:namespace "https://example.org/bookshop-trail/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "bs" ;   sh:namespace "https://example.org/bookshop-trail/schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdf" ;  sh:namespace "http://www.w3.org/1999/02/22-rdf-syntax-ns#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdfs" ; sh:namespace "http://www.w3.org/2000/01/rdf-schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "xsd" ;  sh:namespace "http://www.w3.org/2001/XMLSchema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "skos" ; sh:namespace "http://www.w3.org/2004/02/skos/core#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "dct" ;  sh:namespace "http://purl.org/dc/terms/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "geo" ;  sh:namespace "http://www.opengis.net/ont/geosparql#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "schema" ; sh:namespace "https://schema.org/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "sh" ;   sh:namespace "http://www.w3.org/ns/shacl#"^^xsd:anyURI ] .

bt:BookshopShape
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:rule [
        a             sh:TripleRule ;
        sh:subject    sh:this ;
        sh:predicate  bs:inCountry ;
        sh:object     [
            sh:filterShape  [ sh:class bs:Country ] ;
            sh:nodes        [ sh:path ( bs:locatedIn [ sh:oneOrMorePath bs:within ] ) ] ;
        ] ;
    ] .

bt:CountryProbe
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:severity  sh:Info ;
        sh:message   "inferred: {$this} bs:inCountry {$value}" ;
        sh:select    "SELECT $this ?value WHERE { $this bs:inCountry ?value }" ;
    ] .

bt:CountryRequired
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:property [
        sh:path      bs:inCountry ;
        sh:minCount  1 ;
        sh:maxCount  1 ;
        sh:class     bs:Country ;
        sh:message   "{$this} has no single country. Is the Inference dropdown set to rules?" ;
    ] .

How it works

A filter shape expression takes the values of one expression and keeps those that conform to a shape. sh:nodes is the input, here the path ( bs:locatedIn [ sh:oneOrMorePath bs:within ] ) that s20 used: every place above the shop's town. sh:filterShape is [ sh:class bs:Country ], and one place survives it. The rule asserts bs:inCountry, the probe shows 33 inferences, and bt:CountryRequired is a Core shape that only conforms once the rule has run: exactly one bs:inCountry, of class bs:Country. Set the dropdown to none and it reports 33 shops with none. The SPARQL course's q36 gets the same answer from a query.

Diagram

   sh:object [
       sh:filterShape  [ sh:class bs:Country ] ;
       sh:nodes        [ sh:path ( bs:locatedIn [ sh:oneOrMorePath bs:within ] ) ]
   ]

   nodes:        { dumfries-galloway, scotland, gb }        from bt:shop-inkwell
   filterShape:  keep those that conform to [ sh:class bs:Country ]
   result:       { scotland }

   inferred:  bt:shop-inkwell  bs:inCountry  bt:place-scotland

What to take away

  • A filter shape expression is a WHERE clause for node expressions: input values in, conforming values out.
  • Any shape can be the filter, so a filter can be as selective as a constraint.
  • A Core shape that requires the inferred property is the cleanest test that a rule set did its job.

The report

Does not conform · 0 violations, 0 warnings, 33 info · 7 shapes · inference rules

SeverityFocus nodePathValueMessageShape
Infobt:shop-bookbarrowbt:place-englandinferred: https://example.org/bookshop-trail/shop-bookbarrow bs:inCountry https://example.org/bookshop-trail/place-englandbt:CountryProbe
Infobt:shop-bookwyrmbt:place-englandinferred: https://example.org/bookshop-trail/shop-bookwyrm bs:inCountry https://example.org/bookshop-trail/place-englandbt:CountryProbe
Infobt:shop-borderprintbt:place-englandinferred: https://example.org/bookshop-trail/shop-borderprint bs:inCountry https://example.org/bookshop-trail/place-englandbt:CountryProbe
Infobt:shop-broads-binderybt:place-englandinferred: https://example.org/bookshop-trail/shop-broads-bindery bs:inCountry https://example.org/bookshop-trail/place-englandbt:CountryProbe
Infobt:shop-broken-spinebt:place-scotlandinferred: https://example.org/bookshop-trail/shop-broken-spine bs:inCountry https://example.org/bookshop-trail/place-scotlandbt:CountryProbe
Infobt:shop-candlemasbt:place-englandinferred: https://example.org/bookshop-trail/shop-candlemas bs:inCountry https://example.org/bookshop-trail/place-englandbt:CountryProbe
Infobt:shop-castle-stepsbt:place-walesinferred: https://example.org/bookshop-trail/shop-castle-steps bs:inCountry https://example.org/bookshop-trail/place-walesbt:CountryProbe
Infobt:shop-chapter-versebt:place-englandinferred: https://example.org/bookshop-trail/shop-chapter-verse bs:inCountry https://example.org/bookshop-trail/place-englandbt:CountryProbe
Infobt:shop-cliff-roadbt:place-walesinferred: https://example.org/bookshop-trail/shop-cliff-road bs:inCountry https://example.org/bookshop-trail/place-walesbt:CountryProbe
Infobt:shop-clock-towerbt:place-walesinferred: https://example.org/bookshop-trail/shop-clock-tower bs:inCountry https://example.org/bookshop-trail/place-walesbt:CountryProbe
Infobt:shop-colophonbt:place-scotlandinferred: https://example.org/bookshop-trail/shop-colophon bs:inCountry https://example.org/bookshop-trail/place-scotlandbt:CountryProbe
Infobt:shop-cotton-quartobt:place-englandinferred: https://example.org/bookshop-trail/shop-cotton-quarto bs:inCountry https://example.org/bookshop-trail/place-englandbt:CountryProbe

and 21 more

Defined in

S46

Union and intersection

Everyone who worked on a work, and the stocked works that match a shop's specialism.

Data: bookshop-trail-1.1.ttl · Inference rules

@prefix bt:  <https://example.org/bookshop-trail/> .
@prefix bs:  <https://example.org/bookshop-trail/schema#> .
@prefix sh:  <http://www.w3.org/ns/shacl#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

# Prefixes for the SPARQL inside this file. @prefix lines are Turtle syntax
# and do not reach the query engine; sh:declare does.
bt:prefixes
    a  owl:Ontology ;
    sh:declare [ sh:prefix "bt" ;   sh:namespace "https://example.org/bookshop-trail/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "bs" ;   sh:namespace "https://example.org/bookshop-trail/schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdf" ;  sh:namespace "http://www.w3.org/1999/02/22-rdf-syntax-ns#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdfs" ; sh:namespace "http://www.w3.org/2000/01/rdf-schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "xsd" ;  sh:namespace "http://www.w3.org/2001/XMLSchema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "skos" ; sh:namespace "http://www.w3.org/2004/02/skos/core#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "dct" ;  sh:namespace "http://purl.org/dc/terms/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "geo" ;  sh:namespace "http://www.opengis.net/ont/geosparql#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "schema" ; sh:namespace "https://schema.org/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "sh" ;   sh:namespace "http://www.w3.org/ns/shacl#"^^xsd:anyURI ] .

bt:WorkShape
    a               sh:NodeShape ;
    sh:targetClass  bs:Work ;
    sh:rule [
        a             sh:TripleRule ;
        sh:subject    sh:this ;
        sh:predicate  bs:contributor ;
        sh:object     [ sh:union ( [ sh:path bs:author ] [ sh:path bs:translatedBy ] ) ] ;
    ] .

bt:BookshopShape
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:rule [
        a             sh:TripleRule ;
        sh:subject    sh:this ;
        sh:predicate  bs:stocksInSpecialism ;
        sh:object     [ sh:intersection (
                            [ sh:path bs:stocks ]
                            [ sh:path ( bs:specialises [ sh:inversePath bs:genre ] ) ]
                        ) ] ;
    ] .

bt:ContributorProbe
    a               sh:NodeShape ;
    sh:targetClass  bs:Translation ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:severity  sh:Info ;
        sh:message   "inferred: {$this} bs:contributor {$value}" ;
        sh:select    "SELECT $this ?value WHERE { $this bs:contributor ?value }" ;
    ] .

bt:SpecialismProbe
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:severity  sh:Info ;
        sh:message   "inferred: {$this} bs:stocksInSpecialism {$value}" ;
        sh:select    "SELECT $this ?value WHERE { $this bs:stocksInSpecialism ?value }" ;
    ] .

How it works

sh:union and sh:intersection combine node expressions. bt:ContributorRule unions the author and the translator of a work into bs:contributor -- the SPARQL course's q18, as a rule -- which gives 74 works their authors and 5 translations their translators as well. bt:SpecialismRule intersects two sets at a shop: the works it stocks, and the works filed directly under the genre it specialises in, reached by ( bs:specialises [ sh:inversePath bs:genre ] ). What survives is bs:stocksInSpecialism, the stock that matches the sign over the door. The two probes count them.

Diagram

   sh:union ( [ sh:path bs:author ] [ sh:path bs:translatedBy ] )
        { elin-morgan } U { noor-haddad }  =  { elin-morgan, noor-haddad }     two bs:contributor triples

   sh:intersection ( [ sh:path bs:stocks ]
                     [ sh:path ( bs:specialises [ sh:inversePath bs:genre ] ) ] )
        works stocked  n  works in the shop's own genre

What to take away

  • sh:union takes a list of expressions and yields their values together; sh:intersection yields the values in every one.
  • Both take lists, so three or more expressions combine at once.
  • An inverse path inside an expression is how a rule looks backwards -- here from a genre to the works filed under it.

The report

Does not conform · 0 violations, 0 warnings, 54 info · 10 shapes · inference rules

SeverityFocus nodePathValueMessageShape
Infobt:book-cold-harbour-hebt:author-rhona-blackwoodinferred: https://example.org/bookshop-trail/book-cold-harbour-he bs:contributor https://example.org/bookshop-trail/author-rhona-blackwoodbt:ContributorProbe
Infobt:book-the-book-town-cybt:author-cerys-lloydinferred: https://example.org/bookshop-trail/book-the-book-town-cy bs:contributor https://example.org/bookshop-trail/author-cerys-lloydbt:ContributorProbe
Infobt:book-the-book-town-cybt:author-rab-fingalinferred: https://example.org/bookshop-trail/book-the-book-town-cy bs:contributor https://example.org/bookshop-trail/author-rab-fingalbt:ContributorProbe
Infobt:book-the-dark-sea-arbt:author-elin-morganinferred: https://example.org/bookshop-trail/book-the-dark-sea-ar bs:contributor https://example.org/bookshop-trail/author-elin-morganbt:ContributorProbe
Infobt:book-the-dark-sea-arbt:author-noor-haddadinferred: https://example.org/bookshop-trail/book-the-dark-sea-ar bs:contributor https://example.org/bookshop-trail/author-noor-haddadbt:ContributorProbe
Infobt:book-the-long-station-cybt:author-elin-morganinferred: https://example.org/bookshop-trail/book-the-long-station-cy bs:contributor https://example.org/bookshop-trail/author-elin-morganbt:ContributorProbe
Infobt:book-the-long-station-cybt:author-marek-oyelaraninferred: https://example.org/bookshop-trail/book-the-long-station-cy bs:contributor https://example.org/bookshop-trail/author-marek-oyelaranbt:ContributorProbe
Infobt:book-the-selkie-ledger-arbt:author-noor-haddadinferred: https://example.org/bookshop-trail/book-the-selkie-ledger-ar bs:contributor https://example.org/bookshop-trail/author-noor-haddadbt:ContributorProbe
Infobt:book-the-selkie-ledger-arbt:author-torin-mackayinferred: https://example.org/bookshop-trail/book-the-selkie-ledger-ar bs:contributor https://example.org/bookshop-trail/author-torin-mackaybt:ContributorProbe
Infobt:book-the-tarn-gdbt:author-bram-tillotsoninferred: https://example.org/bookshop-trail/book-the-tarn-gd bs:contributor https://example.org/bookshop-trail/author-bram-tillotsonbt:ContributorProbe
Infobt:book-the-tarn-gdbt:author-torin-mackayinferred: https://example.org/bookshop-trail/book-the-tarn-gd bs:contributor https://example.org/bookshop-trail/author-torin-mackaybt:ContributorProbe
Infobt:shop-bookbarrowbt:book-screeinferred: https://example.org/bookshop-trail/shop-bookbarrow bs:stocksInSpecialism https://example.org/bookshop-trail/book-screebt:SpecialismProbe

and 42 more

Defined in

S47

A rule written in SPARQL

The value of each stock line, and a flag on every work without an ISBN.

Data: bookshop-trail-1.1.ttl · Inference rules

@prefix bt:  <https://example.org/bookshop-trail/> .
@prefix bs:  <https://example.org/bookshop-trail/schema#> .
@prefix sh:  <http://www.w3.org/ns/shacl#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

# Prefixes for the SPARQL inside this file. @prefix lines are Turtle syntax
# and do not reach the query engine; sh:declare does.
bt:prefixes
    a  owl:Ontology ;
    sh:declare [ sh:prefix "bt" ;   sh:namespace "https://example.org/bookshop-trail/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "bs" ;   sh:namespace "https://example.org/bookshop-trail/schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdf" ;  sh:namespace "http://www.w3.org/1999/02/22-rdf-syntax-ns#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdfs" ; sh:namespace "http://www.w3.org/2000/01/rdf-schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "xsd" ;  sh:namespace "http://www.w3.org/2001/XMLSchema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "skos" ; sh:namespace "http://www.w3.org/2004/02/skos/core#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "dct" ;  sh:namespace "http://purl.org/dc/terms/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "geo" ;  sh:namespace "http://www.opengis.net/ont/geosparql#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "schema" ; sh:namespace "https://schema.org/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "sh" ;   sh:namespace "http://www.w3.org/ns/shacl#"^^xsd:anyURI ] .

bt:StockRecordShape
    a               sh:NodeShape ;
    sh:targetClass  bs:StockRecord ;
    sh:rule [
        a             sh:SPARQLRule ;
        sh:prefixes   bt:prefixes ;
        sh:construct  """
            CONSTRUCT { $this bs:stockValue ?v }
            WHERE {
              $this bs:copies ?c ; bs:shelfPrice ?p .
              BIND ( ?c * ?p AS ?v )
            }
        """ ;
    ] .

bt:WorkShape
    a               sh:NodeShape ;
    sh:targetClass  bs:Work ;
    sh:rule [
        a             sh:SPARQLRule ;
        sh:prefixes   bt:prefixes ;
        sh:construct  """
            CONSTRUCT { $this bs:isbnMissing true }
            WHERE { FILTER NOT EXISTS { $this bs:isbn ?i } }
        """ ;
    ] .

bt:ValueProbe
    a               sh:NodeShape ;
    sh:targetClass  bs:StockRecord ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:severity  sh:Info ;
        sh:message   "inferred: {$this} bs:stockValue {$value} (over 200)" ;
        sh:select    "SELECT $this ?value WHERE { $this bs:stockValue ?value FILTER ( ?value > 200 ) }" ;
    ] .

bt:UnnumberedProbe
    a               sh:NodeShape ;
    sh:targetClass  bs:Work ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:severity  sh:Info ;
        sh:message   "inferred: {$this} bs:isbnMissing true" ;
        sh:select    "SELECT $this WHERE { $this bs:isbnMissing true }" ;
    ] .

How it works

A sh:SPARQLRule runs a CONSTRUCT with $this pre-bound to the focus node, and every triple the template produces is inferred. Anything a triple rule cannot say -- arithmetic, a BIND, a FILTER NOT EXISTS -- goes here. bt:StockValueRule multiplies copies by shelf price, which is the SPARQL course's q21 done once per record and kept. bt:UnnumberedRule flags the works with no bs:isbn. The probes list the stock lines worth more than two hundred pounds and the eleven flagged works. The same pre-binding rules as sh:sparql apply: no MINUS, VALUES or SERVICE, and the prefixes come from sh:prefixes.

Diagram

   sh:rule [ a sh:SPARQLRule ;
       sh:construct """
           CONSTRUCT { $this bs:stockValue ?v }
           WHERE     { $this bs:copies ?c ; bs:shelfPrice ?p . BIND ( ?c * ?p AS ?v ) }
       """ ]

   bt:stock-inkwell--the-book-town   12 x 9.99   ->   bs:stockValue 119.88

   CONSTRUCT { $this bs:isbnMissing true } WHERE { FILTER NOT EXISTS { $this bs:isbn ?i } }

What to take away

  • A SPARQL rule is a CONSTRUCT per focus node. It is the escape hatch when a triple rule's three expressions are not enough.
  • $this in the template is the focus node. Triples about other nodes can be built too; the template is free.
  • The pre-binding restrictions of s34 apply to sh:construct as they do to sh:select.

The report

Does not conform · 0 violations, 0 warnings, 27 info · 6 shapes · inference rules

SeverityFocus nodePathValueMessageShape
Infobt:book-afon-hirbt:book-afon-hirinferred: https://example.org/bookshop-trail/book-afon-hir bs:isbnMissing truebt:UnnumberedProbe
Infobt:book-cold-harbourbt:book-cold-harbourinferred: https://example.org/bookshop-trail/book-cold-harbour bs:isbnMissing truebt:UnnumberedProbe
Infobt:book-evensong-for-a-thiefbt:book-evensong-for-a-thiefinferred: https://example.org/bookshop-trail/book-evensong-for-a-thief bs:isbnMissing truebt:UnnumberedProbe
Infobt:book-hedgerow-alphabetbt:book-hedgerow-alphabetinferred: https://example.org/bookshop-trail/book-hedgerow-alphabet bs:isbnMissing truebt:UnnumberedProbe
Infobt:book-herring-and-hempbt:book-herring-and-hempinferred: https://example.org/bookshop-trail/book-herring-and-hemp bs:isbnMissing truebt:UnnumberedProbe
Infobt:book-llyfr-y-mynyddbt:book-llyfr-y-mynyddinferred: https://example.org/bookshop-trail/book-llyfr-y-mynydd bs:isbnMissing truebt:UnnumberedProbe
Infobt:book-north-of-the-tweedbt:book-north-of-the-tweedinferred: https://example.org/bookshop-trail/book-north-of-the-tweed bs:isbnMissing truebt:UnnumberedProbe
Infobt:book-the-lamp-roombt:book-the-lamp-roominferred: https://example.org/bookshop-trail/book-the-lamp-room bs:isbnMissing truebt:UnnumberedProbe
Infobt:book-the-long-riverbt:book-the-long-riverinferred: https://example.org/bookshop-trail/book-the-long-river bs:isbnMissing truebt:UnnumberedProbe
Infobt:book-the-quarry-glassbt:book-the-quarry-glassinferred: https://example.org/bookshop-trail/book-the-quarry-glass bs:isbnMissing truebt:UnnumberedProbe
Infobt:book-the-shielingbt:book-the-shielinginferred: https://example.org/bookshop-trail/book-the-shieling bs:isbnMissing truebt:UnnumberedProbe
Infobt:stock-bookbarrow--the-tarn203inferred: https://example.org/bookshop-trail/stock-bookbarrow--the-tarn bs:stockValue 203 (over 200)bt:ValueProbe

and 15 more

Defined in

S48

Conditions, and a rule switched off

Shops with a cafe get an amenity; shops without a website get a status; a third rule is present and inactive.

Data: bookshop-trail-1.1.ttl · Inference rules

@prefix bt:  <https://example.org/bookshop-trail/> .
@prefix bs:  <https://example.org/bookshop-trail/schema#> .
@prefix sh:  <http://www.w3.org/ns/shacl#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

# Prefixes for the SPARQL inside this file. @prefix lines are Turtle syntax
# and do not reach the query engine; sh:declare does.
bt:prefixes
    a  owl:Ontology ;
    sh:declare [ sh:prefix "bt" ;   sh:namespace "https://example.org/bookshop-trail/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "bs" ;   sh:namespace "https://example.org/bookshop-trail/schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdf" ;  sh:namespace "http://www.w3.org/1999/02/22-rdf-syntax-ns#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdfs" ; sh:namespace "http://www.w3.org/2000/01/rdf-schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "xsd" ;  sh:namespace "http://www.w3.org/2001/XMLSchema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "skos" ; sh:namespace "http://www.w3.org/2004/02/skos/core#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "dct" ;  sh:namespace "http://purl.org/dc/terms/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "geo" ;  sh:namespace "http://www.opengis.net/ont/geosparql#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "schema" ; sh:namespace "https://schema.org/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "sh" ;   sh:namespace "http://www.w3.org/ns/shacl#"^^xsd:anyURI ] .

bt:HasCafe     a sh:NodeShape ; sh:property [ sh:path bs:hasCafe ; sh:hasValue true ] .
bt:HasWebsite  a sh:NodeShape ; sh:property [ sh:path bs:website ; sh:minCount 1 ] .

bt:BookshopShape
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:rule [
        a             sh:TripleRule ;
        sh:condition  bt:HasCafe ;
        sh:subject    sh:this ;
        sh:predicate  bs:amenity ;
        sh:object     "cafe" ;
    ] ;
    sh:rule [
        a             sh:TripleRule ;
        sh:condition  [ sh:not bt:HasWebsite ] ;
        sh:subject    sh:this ;
        sh:predicate  bs:status ;
        sh:object     "offline" ;
    ] ;
    sh:rule [
        a               sh:TripleRule ;
        sh:deactivated  true ;
        sh:subject      sh:this ;
        sh:predicate    bs:amenity ;
        sh:object       "second-hand" ;
    ] .

bt:AmenityProbe
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:severity  sh:Info ;
        sh:message   "inferred: {$this} has {$value}" ;
        sh:select    """
            SELECT $this ?value WHERE {
              { $this bs:amenity ?value } UNION { $this bs:status ?value }
            }
        """ ;
    ] .

How it works

sh:condition names one or more shapes the focus node must conform to before the rule fires. bt:CafeRule fires only for shops that conform to bt:HasCafe, and 22 do. bt:OfflineRule uses a negated condition, [ sh:not bt:HasWebsite ], and fires for the six without one: negation as failure, which s52 comes back to. bt:SecondHandRule carries sh:deactivated true and produces nothing; a rule can be kept in the file and out of the run exactly as a shape can. The probe shows the 28 inferred triples.

Diagram

   sh:rule [ a sh:TripleRule ;
       sh:condition bt:HasCafe ;                  <- fires only if $this conforms to this shape
       sh:subject sh:this ; sh:predicate bs:amenity ; sh:object "cafe" ]

   bt:HasCafe  a sh:NodeShape ; sh:property [ sh:path bs:hasCafe ; sh:hasValue true ]

   sh:condition [ sh:not bt:HasWebsite ]         <- fires if $this does NOT conform

   sh:deactivated true                           <- compiled, never fired

What to take away

  • sh:condition is a guard: a conformance check of the focus node before the rule runs. Several conditions must all hold.
  • [ sh:not S ] as a condition is negation as failure: the rule fires because something is absent.
  • sh:deactivated works on rules as on shapes.

The report

Does not conform · 0 violations, 0 warnings, 28 info · 8 shapes · inference rules

SeverityFocus nodePathValueMessageShape
Infobt:shop-bookwyrmcafeinferred: https://example.org/bookshop-trail/shop-bookwyrm has cafebt:AmenityProbe
Infobt:shop-candlemascafeinferred: https://example.org/bookshop-trail/shop-candlemas has cafebt:AmenityProbe
Infobt:shop-castle-stepscafeinferred: https://example.org/bookshop-trail/shop-castle-steps has cafebt:AmenityProbe
Infobt:shop-castle-stepsofflineinferred: https://example.org/bookshop-trail/shop-castle-steps has offlinebt:AmenityProbe
Infobt:shop-chapter-versecafeinferred: https://example.org/bookshop-trail/shop-chapter-verse has cafebt:AmenityProbe
Infobt:shop-cliff-roadcafeinferred: https://example.org/bookshop-trail/shop-cliff-road has cafebt:AmenityProbe
Infobt:shop-colophoncafeinferred: https://example.org/bookshop-trail/shop-colophon has cafebt:AmenityProbe
Infobt:shop-cotton-quartocafeinferred: https://example.org/bookshop-trail/shop-cotton-quarto has cafebt:AmenityProbe
Infobt:shop-crescentcafeinferred: https://example.org/bookshop-trail/shop-crescent has cafebt:AmenityProbe
Infobt:shop-dales-foliocafeinferred: https://example.org/bookshop-trail/shop-dales-folio has cafebt:AmenityProbe
Infobt:shop-dales-folioofflineinferred: https://example.org/bookshop-trail/shop-dales-folio has offlinebt:AmenityProbe
Infobt:shop-dog-earedcafeinferred: https://example.org/bookshop-trail/shop-dog-eared has cafebt:AmenityProbe

and 16 more

Defined in

S49

Order, and rules that feed rules

A shop's country, then the country's name from it -- and the same second rule placed where it sees nothing.

Data: bookshop-trail-1.1.ttl · Inference rules

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix sh:   <http://www.w3.org/ns/shacl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix xsd:  <http://www.w3.org/2001/XMLSchema#> .

# Prefixes for the SPARQL inside this file. @prefix lines are Turtle syntax
# and do not reach the query engine; sh:declare does.
bt:prefixes
    a  owl:Ontology ;
    sh:declare [ sh:prefix "bt" ;   sh:namespace "https://example.org/bookshop-trail/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "bs" ;   sh:namespace "https://example.org/bookshop-trail/schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdf" ;  sh:namespace "http://www.w3.org/1999/02/22-rdf-syntax-ns#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdfs" ; sh:namespace "http://www.w3.org/2000/01/rdf-schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "xsd" ;  sh:namespace "http://www.w3.org/2001/XMLSchema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "skos" ; sh:namespace "http://www.w3.org/2004/02/skos/core#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "dct" ;  sh:namespace "http://purl.org/dc/terms/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "geo" ;  sh:namespace "http://www.opengis.net/ont/geosparql#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "schema" ; sh:namespace "https://schema.org/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "sh" ;   sh:namespace "http://www.w3.org/ns/shacl#"^^xsd:anyURI ] .

bt:CountryShape
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:order        0 ;
    sh:rule [
        a             sh:TripleRule ;
        sh:subject    sh:this ;
        sh:predicate  bs:inCountry ;
        sh:object     [ sh:filterShape [ sh:class bs:Country ] ;
                        sh:nodes [ sh:path ( bs:locatedIn [ sh:oneOrMorePath bs:within ] ) ] ] ;
    ] ;
    # Same order as the rule it depends on: sees no bs:inCountry, infers nothing.
    sh:rule [
        a             sh:TripleRule ;
        sh:subject    sh:this ;
        sh:predicate  bs:countryNameTooEarly ;
        sh:object     [ sh:path ( bs:inCountry rdfs:label ) ] ;
    ] .

bt:NameShape
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:order        1 ;
    sh:rule [
        a             sh:TripleRule ;
        sh:subject    sh:this ;
        sh:predicate  bs:countryName ;
        sh:object     [ sh:path ( bs:inCountry rdfs:label ) ] ;
    ] .

bt:NameProbe
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:severity  sh:Info ;
        sh:message   "inferred at order 1: {$this} bs:countryName {$value}" ;
        sh:select    "SELECT $this ?value WHERE { $this bs:countryName ?value }" ;
    ] .

bt:TooEarlyProbe
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:severity  sh:Warning ;
        sh:message   "inferred at order 0: {$this} bs:countryNameTooEarly {$value} -- should never appear" ;
        sh:select    "SELECT $this ?value WHERE { $this bs:countryNameTooEarly ?value }" ;
    ] .

How it works

Rules run in ascending sh:order, and every rule at one order sees the graph as it stood before that order began. bt:CountryRule at order 0 infers bs:inCountry as in s45. bt:NameRule at order 1 reads bs:inCountry and copies the country's labels to bs:countryName; the probe shows it fired for every shop. bt:NameRuleTooEarly is the same rule at order 0, in the same shape as the rule it depends on, and infers nothing: bs:inCountry did not exist when order 0 began. The second probe reports zero rows for it. sh:order is set on the shape, and it is the only sequencing SHACL-AF offers.

Diagram

   order 0    bt:CountryRule        shop bs:inCountry country
              bt:NameRuleTooEarly   reads bs:inCountry ... which is not there yet   -> nothing
   order 1    bt:NameRule           reads bs:inCountry                             -> countryName

   a rule sees the graph as it was when its order began

What to take away

  • A rule that consumes what another produces must be at a higher sh:order, on a different shape.
  • Same order means same view of the data. Two rules at order 0 cannot see each other's results.
  • When a rule infers nothing, check the order before the expressions.

The report

Does not conform · 0 violations, 0 warnings, 46 info · 10 shapes · inference rules

SeverityFocus nodePathValueMessageShape
Infobt:shop-bookbarrowEnglandinferred at order 1: https://example.org/bookshop-trail/shop-bookbarrow bs:countryName Englandbt:NameProbe
Infobt:shop-bookwyrmEnglandinferred at order 1: https://example.org/bookshop-trail/shop-bookwyrm bs:countryName Englandbt:NameProbe
Infobt:shop-borderprintEnglandinferred at order 1: https://example.org/bookshop-trail/shop-borderprint bs:countryName Englandbt:NameProbe
Infobt:shop-broads-binderyEnglandinferred at order 1: https://example.org/bookshop-trail/shop-broads-bindery bs:countryName Englandbt:NameProbe
Infobt:shop-broken-spineAlbainferred at order 1: https://example.org/bookshop-trail/shop-broken-spine bs:countryName Albabt:NameProbe
Infobt:shop-broken-spineScotlandinferred at order 1: https://example.org/bookshop-trail/shop-broken-spine bs:countryName Scotlandbt:NameProbe
Infobt:shop-candlemasEnglandinferred at order 1: https://example.org/bookshop-trail/shop-candlemas bs:countryName Englandbt:NameProbe
Infobt:shop-castle-stepsCymruinferred at order 1: https://example.org/bookshop-trail/shop-castle-steps bs:countryName Cymrubt:NameProbe
Infobt:shop-castle-stepsWalesinferred at order 1: https://example.org/bookshop-trail/shop-castle-steps bs:countryName Walesbt:NameProbe
Infobt:shop-chapter-verseEnglandinferred at order 1: https://example.org/bookshop-trail/shop-chapter-verse bs:countryName Englandbt:NameProbe
Infobt:shop-cliff-roadCymruinferred at order 1: https://example.org/bookshop-trail/shop-cliff-road bs:countryName Cymrubt:NameProbe
Infobt:shop-cliff-roadWalesinferred at order 1: https://example.org/bookshop-trail/shop-cliff-road bs:countryName Walesbt:NameProbe

and 34 more

Defined in

S50

One pass

bs:within made transitive by a rule, run once: how far a single pass reaches.

Data: bookshop-trail-1.1.ttl · Inference rules

@prefix bt:  <https://example.org/bookshop-trail/> .
@prefix bs:  <https://example.org/bookshop-trail/schema#> .
@prefix sh:  <http://www.w3.org/ns/shacl#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

# Prefixes for the SPARQL inside this file. @prefix lines are Turtle syntax
# and do not reach the query engine; sh:declare does.
bt:prefixes
    a  owl:Ontology ;
    sh:declare [ sh:prefix "bt" ;   sh:namespace "https://example.org/bookshop-trail/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "bs" ;   sh:namespace "https://example.org/bookshop-trail/schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdf" ;  sh:namespace "http://www.w3.org/1999/02/22-rdf-syntax-ns#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdfs" ; sh:namespace "http://www.w3.org/2000/01/rdf-schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "xsd" ;  sh:namespace "http://www.w3.org/2001/XMLSchema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "skos" ; sh:namespace "http://www.w3.org/2004/02/skos/core#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "dct" ;  sh:namespace "http://purl.org/dc/terms/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "geo" ;  sh:namespace "http://www.opengis.net/ont/geosparql#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "schema" ; sh:namespace "https://schema.org/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "sh" ;   sh:namespace "http://www.w3.org/ns/shacl#"^^xsd:anyURI ] .

bt:PlaceShape
    a               sh:NodeShape ;
    sh:targetClass  bs:Place ;
    sh:rule [
        a             sh:SPARQLRule ;
        sh:prefixes   bt:prefixes ;
        sh:construct  """
            CONSTRUCT { $this bs:within ?c }
            WHERE     { $this bs:within ?b . ?b bs:within ?c . }
        """ ;
    ] .

# Counts every bs:within link a place has, asserted or inferred.
bt:WithinProbe
    a               sh:NodeShape ;
    sh:targetClass  bs:Place ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:severity  sh:Info ;
        sh:message   "{$this} bs:within {$value}" ;
        sh:select    "SELECT $this ?value WHERE { $this bs:within ?value }" ;
    ] .

# True only once the closure is complete.
bt:ReachesGB
    a               sh:NodeShape ;
    sh:targetClass  bs:Settlement ;
    sh:property [
        sh:path      bs:within ;
        sh:hasValue  bt:place-gb ;
        sh:severity  sh:Warning ;
        sh:message   "{$this} has no direct bs:within link to Great Britain yet." ;
    ] .

How it works

bs:within is declared transitive in the vocabulary, and the clean data asserts 63 direct links. bt:TransitiveRule is the transitivity axiom as a SPARQL rule: where $this is within B and B is within C, $this is within C. SHACL-AF defines one pass, so each place gains the links two hops long and no more: the probe counts 63 asserted plus the two-hop pairs, and a settlement four levels below Great Britain still has no direct link to it. The SPARQL course's module 18 measures the same closure with a reasoner and with a property path: 186 pairs. s51 is this file again, iterated.

Diagram

   asserted      york -> north-yorkshire -> yorkshire -> england -> gb          (4 links)
   one pass adds york -> yorkshire, north-yorkshire -> england, yorkshire -> gb  (2 hops)
   still missing york -> england, york -> gb, north-yorkshire -> gb             (3 and 4 hops)

   Inference: rules            one pass, as SHACL-AF defines
   Inference: rules, iterated  repeated to a fixpoint (s51)

What to take away

  • A SHACL-AF rule set runs once. A rule whose output is its own input reaches one more step, and stops.
  • The specification declines to define repetition. What an engine does about that is the engine's, not SHACL's.
  • Before writing a transitive rule, ask whether a property path at query time would do (the SPARQL course, q27 and q140).

The report

Does not conform · 0 violations, 30 warnings, 123 info · 5 shapes · inference rules

SeverityFocus nodePathValueMessageShape
Warningbt:place-aberystwythbs:withinbt:place-aberystwyth has no direct bs:within link to Great Britain yet.bt:ReachesGB › property 1
Warningbt:place-bathbs:withinbt:place-bath has no direct bs:within link to Great Britain yet.bt:ReachesGB › property 1
Warningbt:place-berwickbs:withinbt:place-berwick has no direct bs:within link to Great Britain yet.bt:ReachesGB › property 1
Warningbt:place-cambridgebs:withinbt:place-cambridge has no direct bs:within link to Great Britain yet.bt:ReachesGB › property 1
Warningbt:place-cardiffbs:withinbt:place-cardiff has no direct bs:within link to Great Britain yet.bt:ReachesGB › property 1
Warningbt:place-durhambs:withinbt:place-durham has no direct bs:within link to Great Britain yet.bt:ReachesGB › property 1
Warningbt:place-edinburghbs:withinbt:place-edinburgh has no direct bs:within link to Great Britain yet.bt:ReachesGB › property 1
Warningbt:place-exeterbs:withinbt:place-exeter has no direct bs:within link to Great Britain yet.bt:ReachesGB › property 1
Warningbt:place-fort-williambs:withinbt:place-fort-william has no direct bs:within link to Great Britain yet.bt:ReachesGB › property 1
Warningbt:place-glasgowbs:withinbt:place-glasgow has no direct bs:within link to Great Britain yet.bt:ReachesGB › property 1
Warningbt:place-hay-on-wyebs:withinbt:place-hay-on-wye has no direct bs:within link to Great Britain yet.bt:ReachesGB › property 1
Warningbt:place-invernessbs:withinbt:place-inverness has no direct bs:within link to Great Britain yet.bt:ReachesGB › property 1

and 141 more

Defined in

S51

To a fixpoint

The same rule, repeated until nothing new appears: the full closure, and the count the SPARQL course measured.

Data: bookshop-trail-1.1.ttl · Inference rules-iterated

@prefix bt:  <https://example.org/bookshop-trail/> .
@prefix bs:  <https://example.org/bookshop-trail/schema#> .
@prefix sh:  <http://www.w3.org/ns/shacl#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

# Prefixes for the SPARQL inside this file. @prefix lines are Turtle syntax
# and do not reach the query engine; sh:declare does.
bt:prefixes
    a  owl:Ontology ;
    sh:declare [ sh:prefix "bt" ;   sh:namespace "https://example.org/bookshop-trail/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "bs" ;   sh:namespace "https://example.org/bookshop-trail/schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdf" ;  sh:namespace "http://www.w3.org/1999/02/22-rdf-syntax-ns#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdfs" ; sh:namespace "http://www.w3.org/2000/01/rdf-schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "xsd" ;  sh:namespace "http://www.w3.org/2001/XMLSchema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "skos" ; sh:namespace "http://www.w3.org/2004/02/skos/core#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "dct" ;  sh:namespace "http://purl.org/dc/terms/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "geo" ;  sh:namespace "http://www.opengis.net/ont/geosparql#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "schema" ; sh:namespace "https://schema.org/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "sh" ;   sh:namespace "http://www.w3.org/ns/shacl#"^^xsd:anyURI ] .

bt:PlaceShape
    a               sh:NodeShape ;
    sh:targetClass  bs:Place ;
    sh:rule [
        a             sh:SPARQLRule ;
        sh:prefixes   bt:prefixes ;
        sh:construct  """
            CONSTRUCT { $this bs:within ?c }
            WHERE     { $this bs:within ?b . ?b bs:within ?c . }
        """ ;
    ] .

bt:WithinProbe
    a               sh:NodeShape ;
    sh:targetClass  bs:Place ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:severity  sh:Info ;
        sh:message   "{$this} bs:within {$value}" ;
        sh:select    "SELECT $this ?value WHERE { $this bs:within ?value }" ;
    ] .

bt:ReachesGB
    a               sh:NodeShape ;
    sh:targetClass  bs:Settlement ;
    sh:property [
        sh:path      bs:within ;
        sh:hasValue  bt:place-gb ;
        sh:severity  sh:Warning ;
        sh:message   "{$this} has no direct bs:within link to Great Britain yet." ;
    ] .

How it works

This is s50's file with the Inference dropdown at rules, iterated. The engine runs the rule set, adds what it produced, and runs it again until a round adds nothing, up to ten rounds. The probe now counts 186 bs:within pairs -- the number the SPARQL course's reasoning table gives for the OWL reasoners and for the path bs:within+ -- and bt:ReachesGB reports nothing, because every settlement now has its direct link to Great Britain. Iteration is outside SHACL-AF. It is useful for a rule like this one and unsafe for a rule that tests for absence, which is s52.

Diagram

   round 1   two-hop links appear
   round 2   three- and four-hop links appear
   round 3   nothing new  ->  stop

   63 asserted  ->  186 pairs, the same 186 as bs:within+ in the SPARQL course

What to take away

  • Iterating a rule set gives the least fixpoint for rules that only add. The result is what a reasoner would materialise.
  • The ten-round cap is a safety net. A rule that mints a new term every round never settles, and the engine stops with an error rather than running out of memory.
  • A validator does not persist inferences. To keep the closure, run the same CONSTRUCT in the SPARQL course's module 17 and store it.

The report

Does not conform · 0 violations, 0 warnings, 186 info · 5 shapes · inference rules-iterated

SeverityFocus nodePathValueMessageShape
Infobt:place-aberystwythbt:place-ceredigionhttps://example.org/bookshop-trail/place-aberystwyth bs:within https://example.org/bookshop-trail/place-ceredigionbt:WithinProbe
Infobt:place-aberystwythbt:place-gbhttps://example.org/bookshop-trail/place-aberystwyth bs:within https://example.org/bookshop-trail/place-gbbt:WithinProbe
Infobt:place-aberystwythbt:place-waleshttps://example.org/bookshop-trail/place-aberystwyth bs:within https://example.org/bookshop-trail/place-walesbt:WithinProbe
Infobt:place-bathbt:place-englandhttps://example.org/bookshop-trail/place-bath bs:within https://example.org/bookshop-trail/place-englandbt:WithinProbe
Infobt:place-bathbt:place-gbhttps://example.org/bookshop-trail/place-bath bs:within https://example.org/bookshop-trail/place-gbbt:WithinProbe
Infobt:place-bathbt:place-somersethttps://example.org/bookshop-trail/place-bath bs:within https://example.org/bookshop-trail/place-somersetbt:WithinProbe
Infobt:place-bathbt:place-south-west-englandhttps://example.org/bookshop-trail/place-bath bs:within https://example.org/bookshop-trail/place-south-west-englandbt:WithinProbe
Infobt:place-berwickbt:place-englandhttps://example.org/bookshop-trail/place-berwick bs:within https://example.org/bookshop-trail/place-englandbt:WithinProbe
Infobt:place-berwickbt:place-gbhttps://example.org/bookshop-trail/place-berwick bs:within https://example.org/bookshop-trail/place-gbbt:WithinProbe
Infobt:place-berwickbt:place-north-east-englandhttps://example.org/bookshop-trail/place-berwick bs:within https://example.org/bookshop-trail/place-north-east-englandbt:WithinProbe
Infobt:place-berwickbt:place-northumberlandhttps://example.org/bookshop-trail/place-berwick bs:within https://example.org/bookshop-trail/place-northumberlandbt:WithinProbe
Infobt:place-cambridgebt:place-cambridgeshirehttps://example.org/bookshop-trail/place-cambridge bs:within https://example.org/bookshop-trail/place-cambridgeshirebt:WithinProbe

and 174 more

Defined in

S52

Negation that goes stale

Mark the shops without a website, then give every such shop a placeholder website -- and see the mark outlive its reason.

Data: bookshop-trail-1.1.ttl · Inference rules

@prefix bt:  <https://example.org/bookshop-trail/> .
@prefix bs:  <https://example.org/bookshop-trail/schema#> .
@prefix sh:  <http://www.w3.org/ns/shacl#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

# Prefixes for the SPARQL inside this file. @prefix lines are Turtle syntax
# and do not reach the query engine; sh:declare does.
bt:prefixes
    a  owl:Ontology ;
    sh:declare [ sh:prefix "bt" ;   sh:namespace "https://example.org/bookshop-trail/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "bs" ;   sh:namespace "https://example.org/bookshop-trail/schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdf" ;  sh:namespace "http://www.w3.org/1999/02/22-rdf-syntax-ns#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdfs" ; sh:namespace "http://www.w3.org/2000/01/rdf-schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "xsd" ;  sh:namespace "http://www.w3.org/2001/XMLSchema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "skos" ; sh:namespace "http://www.w3.org/2004/02/skos/core#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "dct" ;  sh:namespace "http://purl.org/dc/terms/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "geo" ;  sh:namespace "http://www.opengis.net/ont/geosparql#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "schema" ; sh:namespace "https://schema.org/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "sh" ;   sh:namespace "http://www.w3.org/ns/shacl#"^^xsd:anyURI ] .

bt:HasWebsite  a sh:NodeShape ; sh:property [ sh:path bs:website ; sh:minCount 1 ] .

bt:MarkShape
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:order        1 ;
    sh:rule [
        a             sh:TripleRule ;
        sh:condition  [ sh:not bt:HasWebsite ] ;
        sh:subject    sh:this ;
        sh:predicate  bs:status ;
        sh:object     "offline" ;
    ] .

bt:FillShape
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:order        2 ;
    sh:rule [
        a             sh:TripleRule ;
        sh:condition  [ sh:not bt:HasWebsite ] ;
        sh:subject    sh:this ;
        sh:predicate  bs:website ;
        sh:object     <https://example.org/bookshop-trail/website-pending> ;
    ] .

bt:StaleProbe
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:severity  sh:Warning ;
        sh:message   "{$this} is marked offline and has the website {$value}: the mark outlived its reason." ;
        sh:select    """
            SELECT $this ?value WHERE {
              $this bs:status "offline" ; bs:website ?value .
            }
        """ ;
    ] .

How it works

bt:MarkRule at order 1 gives a shop with no website a bs:status "offline", a conclusion drawn from absence. bt:FillRule at order 2 gives the same shops a placeholder bs:website. Rules only add, so after the run six shops have both a website and a status that says they have none. The probe finds them. Swapping the orders would fix this case, because the mark would then be tested after the fill, and the true fix in general is to make sure nothing later supplies what a rule tested the absence of. SHACL 1.2 Rules (SPARQL-RL) requires a rule set to be stratified so that negation only looks at strata that are already complete; SHACL-AF has no such requirement, and the responsibility is the author's.

Diagram

   order 1   MarkRule    condition [ sh:not HasWebsite ]   ->  shop bs:status "offline"     (6 shops)
   order 2   FillRule    condition [ sh:not HasWebsite ]   ->  shop bs:website <placeholder> (6 shops)

   after the run:   bt:shop-marginalia  bs:website <...pending> ;  bs:status "offline" .
                    both true in the graph; the second no longer true of the data

   SPARQL-RL: stratify, so that NOT EXISTS is only ever evaluated over a finished stratum

What to take away

  • A rule that tests for absence draws a conclusion that a later rule can invalidate. Rules never retract.
  • Order the rule set so that nothing after a negation supplies what it tested for, or keep to a single pass and check by hand.
  • Stratified negation is what SHACL 1.2 Rules adds; until then it is a discipline rather than a guarantee.

The report

Does not conform · 0 violations, 6 warnings, 0 info · 8 shapes · inference rules

SeverityFocus nodePathValueMessageShape
Warningbt:shop-castle-stepsbt:website-pendinghttps://example.org/bookshop-trail/shop-castle-steps is marked offline and has the website https://example.org/bookshop-trail/website-pending: the mark outli…bt:StaleProbe
Warningbt:shop-dales-foliobt:website-pendinghttps://example.org/bookshop-trail/shop-dales-folio is marked offline and has the website https://example.org/bookshop-trail/website-pending: the mark outliv…bt:StaleProbe
Warningbt:shop-ex-librisbt:website-pendinghttps://example.org/bookshop-trail/shop-ex-libris is marked offline and has the website https://example.org/bookshop-trail/website-pending: the mark outlived…bt:StaleProbe
Warningbt:shop-marginaliabt:website-pendinghttps://example.org/bookshop-trail/shop-marginalia is marked offline and has the website https://example.org/bookshop-trail/website-pending: the mark outlive…bt:StaleProbe
Warningbt:shop-signaturebt:website-pendinghttps://example.org/bookshop-trail/shop-signature is marked offline and has the website https://example.org/bookshop-trail/website-pending: the mark outlived…bt:StaleProbe
Warningbt:shop-taff-marginbt:website-pendinghttps://example.org/bookshop-trail/shop-taff-margin is marked offline and has the website https://example.org/bookshop-trail/website-pending: the mark outliv…bt:StaleProbe

Defined in

Module 08

SHACL 1.2 and RDF 1.2

The RDF 1.2 edition of the data has statements about statements: a shop with two founding dates, each claimed by a source. SHACL 1.2 can validate the claims themselves, target nodes by a shape rather than by a class, and declare a class that is its own shape. The module uses what the engine in the editor runs today and lists the parts of the 1.2 drafts it does not.

In the standards
S53

Two founding dates

A shop has one founding year -- unless each year is a claim with a source, in which case it may have several.

Data: bookshop-trail-faulty-1.2.ttl

@prefix bt: <https://example.org/bookshop-trail/> .
@prefix bs: <https://example.org/bookshop-trail/schema#> .
@prefix sh: <http://www.w3.org/ns/shacl#> .

bt:OneFoundingYear
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:property [
        sh:path      bs:founded ;
        sh:maxCount  1 ;
        sh:severity  sh:Warning ;
        sh:message   "{$this} has more than one founding year on record." ;
    ] .

bt:SourcedClaims
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:property [
        sh:path                  bs:founded ;
        sh:reifierShape [
            sh:property [ sh:path bs:claimedBy ;  sh:minCount 1 ; sh:class bs:Source ] ;
            sh:property [ sh:path bs:confidence ; sh:maxInclusive 1 ; sh:minInclusive 0 ] ;
        ] ;
        sh:reificationRequired   true ;
        sh:message               "A founding year of {$value} is claimed without a proper source." ;
    ] .

How it works

bookshop-trail-1.2.ttl is the RDF 1.2 edition: the same data plus annotations. Four shops have two founding years, each written as bs:founded 1919 {| bs:claimedBy ...; bs:confidence ... |}, and the SPARQL course's q61 to q63 are about choosing between them. bt:OneFoundingYear is s04's sh:maxCount 1, and on this data it reports the four, plus the faulty claim on Marginalia (F33), as warnings. bt:SourcedClaims is the SHACL 1.2 answer: allow several values, and require every reifier of a bs:founded triple to conform to a shape -- a source of class bs:Source, a confidence no higher than 1. sh:reifierShape checks the annotation rather than the value. The claim on Marginalia has no source and a confidence of 1.4, and is the one violation. sh:reificationRequired true would also demand that every founding year be a claim; this build of the engine reads it and does not enforce it, so the 28 unannotated years pass.

Diagram

   bt:shop-ex-libris  bs:founded "1919"^^xsd:gYear {| bs:claimedBy bt:source-national-register ; bs:confidence 0.99 |} .
   bt:shop-ex-libris  bs:founded "1921"^^xsd:gYear {| bs:claimedBy bt:source-local-paper ;        bs:confidence 0.40 |} .

   sh:maxCount 1 on bs:founded         two values  ->  reported

   sh:reifierShape [ ... ]  on bs:founded
        for each reifier of << shop bs:founded year >>:  does it conform?
        Marginalia's claim: no bs:claimedBy, confidence 1.4   ->  violation, value 1960

What to take away

  • In RDF 1.2 a triple can carry annotations through a reifier. SHACL 1.2's sh:reifierShape validates the reifier, not the value.
  • A cardinality that was right for plain data may be wrong for annotated data. Decide whether several claims are allowed before you constrain the count.
  • sh:reificationRequired is in the draft and parsed by this engine, and not enforced by this build. Check what your validator does with it.

The report

Does not conform · 30 violations, 5 warnings, 0 info · 7 shapes

SeverityFocus nodePathValueMessageShape
Violationbt:shop-bookbarrowbs:founded1990A founding year of 1990 is claimed without a proper source.bt:SourcedClaims › property 1
Violationbt:shop-bookwyrmbs:founded2013A founding year of 2013 is claimed without a proper source.bt:SourcedClaims › property 1
Violationbt:shop-borderprintbs:founded1972A founding year of 1972 is claimed without a proper source.bt:SourcedClaims › property 1
Violationbt:shop-broads-binderybs:founded1992A founding year of 1992 is claimed without a proper source.bt:SourcedClaims › property 1
Violationbt:shop-broken-spinebs:founded2008A founding year of 2008 is claimed without a proper source.bt:SourcedClaims › property 1
Violationbt:shop-chapter-versebs:founded1958A founding year of 1958 is claimed without a proper source.bt:SourcedClaims › property 1
Violationbt:shop-cliff-roadbs:founded2000A founding year of 2000 is claimed without a proper source.bt:SourcedClaims › property 1
Violationbt:shop-clock-towerbs:founded1977A founding year of 1977 is claimed without a proper source.bt:SourcedClaims › property 1
Violationbt:shop-colophonbs:founded1963A founding year of 1963 is claimed without a proper source.bt:SourcedClaims › property 1
Violationbt:shop-cotton-quartobs:founded1974A founding year of 1974 is claimed without a proper source.bt:SourcedClaims › property 1
Violationbt:shop-crescentbs:founded1961A founding year of 1961 is claimed without a proper source.bt:SourcedClaims › property 1
Violationbt:shop-dales-foliobs:founded1968A founding year of 1968 is claimed without a proper source.bt:SourcedClaims › property 1

and 23 more

Defined in

S54

Constraining the claims themselves

Every claim names one statement and a real source, and a claim about a founding year is about a bookshop.

Data: bookshop-trail-faulty-1.2.ttl

@prefix bt:  <https://example.org/bookshop-trail/> .
@prefix bs:  <https://example.org/bookshop-trail/schema#> .
@prefix sh:  <http://www.w3.org/ns/shacl#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

# Prefixes for the SPARQL inside this file. @prefix lines are Turtle syntax
# and do not reach the query engine; sh:declare does.
bt:prefixes
    a  owl:Ontology ;
    sh:declare [ sh:prefix "bt" ;   sh:namespace "https://example.org/bookshop-trail/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "bs" ;   sh:namespace "https://example.org/bookshop-trail/schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdf" ;  sh:namespace "http://www.w3.org/1999/02/22-rdf-syntax-ns#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdfs" ; sh:namespace "http://www.w3.org/2000/01/rdf-schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "xsd" ;  sh:namespace "http://www.w3.org/2001/XMLSchema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "skos" ; sh:namespace "http://www.w3.org/2004/02/skos/core#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "dct" ;  sh:namespace "http://purl.org/dc/terms/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "geo" ;  sh:namespace "http://www.opengis.net/ont/geosparql#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "schema" ; sh:namespace "https://schema.org/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "sh" ;   sh:namespace "http://www.w3.org/ns/shacl#"^^xsd:anyURI ] .

bt:ClaimShape
    a                    sh:NodeShape ;
    sh:targetSubjectsOf  bs:claimedBy ;
    sh:nodeKind          sh:BlankNode ;
    sh:property [
        sh:path      rdf:reifies ;
        sh:minCount  1 ;
        sh:maxCount  1 ;
        sh:message   "A claim reifies exactly one statement." ;
    ] ;
    sh:property [
        sh:path      bs:claimedBy ;
        sh:class     bs:Source ;
        sh:message   "{$value} is not a bs:Source." ;
    ] ;
    sh:property [
        sh:path          bs:confidence ;
        sh:datatype      xsd:decimal ;
        sh:minInclusive  0 ;
        sh:maxInclusive  1 ;
    ] ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:message   "A founding-year claim about {$value}, which is not a bookshop." ;
        sh:select    """
            SELECT $this ?value WHERE {
              $this rdf:reifies <<( ?value bs:founded ?year )>> .
              FILTER NOT EXISTS { ?value a bs:Bookshop }
            }
        """ ;
    ] ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:severity  sh:Info ;
        sh:message   "{$this} is a claim about {$value}." ;
        sh:select    """
            SELECT $this ?value WHERE {
              $this rdf:reifies <<( ?value ?p ?o )>> .
            }
        """ ;
    ] .

How it works

A reifier is a node like any other, so it can be a focus node. sh:targetSubjectsOf bs:claimedBy picks every claim in the data, and the first two property shapes say it reifies exactly one statement and names a bs:Source. The attendance claimed by "the manager" (F34) fails the second. The third constraint looks inside the statement: rdf:reifies <<( ?shop bs:founded ?year )>> is a triple term pattern, and SPARQL 1.2 can match it. It requires the subject of a founding-year claim to be a bookshop, which all of them are, and the information rows list what each claim is about. Reifiers here are blank nodes, so the report shows them by label; the SPARQL course's q64 explains why that is what the annotation syntax produces.

Diagram

   _:c1  rdf:reifies  <<( bt:shop-ex-libris bs:founded "1919"^^xsd:gYear )>> ;
         bs:claimedBy bt:source-national-register ;
         bs:confidence 0.99 .

   sh:targetSubjectsOf bs:claimedBy      ->  _:c1 and every other claim is a focus node

   SELECT $this ?value WHERE { $this rdf:reifies <<( ?value bs:founded ?year )>> }
        a triple term pattern: ?value binds to the shop inside the statement

What to take away

  • A reifier can be targeted and constrained directly. sh:targetSubjectsOf on the annotation property finds them all.
  • A SPARQL constraint can take a triple term apart with <<( s p o )>> in the pattern.
  • Blank-node reifiers appear in the report under the engine's labels. Name reifiers with ~ if you want stable identifiers (RDF 1.2 Turtle).

The report

Does not conform · 1 violation, 0 warnings, 17 info · 5 shapes

SeverityFocus nodePathValueMessageShape
Violation_:1_b92bs:claimedBythe managerthe manager is not a bs:Source.bt:ClaimShape › property 2
Info_:1_b59bt:shop-ex-libris_:1_b59 is a claim about https://example.org/bookshop-trail/shop-ex-libris.bt:ClaimShape
Info_:1_b60bt:shop-ex-libris_:1_b60 is a claim about https://example.org/bookshop-trail/shop-ex-libris.bt:ClaimShape
Info_:1_b61bt:shop-endpapers_:1_b61 is a claim about https://example.org/bookshop-trail/shop-endpapers.bt:ClaimShape
Info_:1_b62bt:shop-endpapers_:1_b62 is a claim about https://example.org/bookshop-trail/shop-endpapers.bt:ClaimShape
Info_:1_b63bt:shop-candlemas_:1_b63 is a claim about https://example.org/bookshop-trail/shop-candlemas.bt:ClaimShape
Info_:1_b64bt:shop-candlemas_:1_b64 is a claim about https://example.org/bookshop-trail/shop-candlemas.bt:ClaimShape
Info_:1_b65bt:shop-gutter-gilt_:1_b65 is a claim about https://example.org/bookshop-trail/shop-gutter-gilt.bt:ClaimShape
Info_:1_b66bt:shop-gutter-gilt_:1_b66 is a claim about https://example.org/bookshop-trail/shop-gutter-gilt.bt:ClaimShape
Info_:1_b67bt:shop-castle-steps_:1_b67 is a claim about https://example.org/bookshop-trail/shop-castle-steps.bt:ClaimShape
Info_:1_b68bt:shop-castle-steps_:1_b68 is a claim about https://example.org/bookshop-trail/shop-castle-steps.bt:ClaimShape
Info_:1_b69bt:event-ex-libris-2025-01-18_:1_b69 is a claim about https://example.org/bookshop-trail/event-ex-libris-2025-01-18.bt:ClaimShape

and 6 more

Defined in

S55

The same fact modelled twice

The RDF 1.1 stock records and the RDF 1.2 stock annotations agree on every count.

Data: bookshop-trail-faulty-1.2.ttl

@prefix bt:  <https://example.org/bookshop-trail/> .
@prefix bs:  <https://example.org/bookshop-trail/schema#> .
@prefix sh:  <http://www.w3.org/ns/shacl#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

# Prefixes for the SPARQL inside this file. @prefix lines are Turtle syntax
# and do not reach the query engine; sh:declare does.
bt:prefixes
    a  owl:Ontology ;
    sh:declare [ sh:prefix "bt" ;   sh:namespace "https://example.org/bookshop-trail/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "bs" ;   sh:namespace "https://example.org/bookshop-trail/schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdf" ;  sh:namespace "http://www.w3.org/1999/02/22-rdf-syntax-ns#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdfs" ; sh:namespace "http://www.w3.org/2000/01/rdf-schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "xsd" ;  sh:namespace "http://www.w3.org/2001/XMLSchema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "skos" ; sh:namespace "http://www.w3.org/2004/02/skos/core#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "dct" ;  sh:namespace "http://purl.org/dc/terms/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "geo" ;  sh:namespace "http://www.opengis.net/ont/geosparql#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "schema" ; sh:namespace "https://schema.org/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "sh" ;   sh:namespace "http://www.w3.org/ns/shacl#"^^xsd:anyURI ] .

bt:StockAgreement
    a               sh:NodeShape ;
    sh:targetClass  bs:StockRecord ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:message   "The annotation says {$value} copies; the record disagrees." ;
        sh:select    """
            SELECT $this ?value WHERE {
              $this bs:atShop ?shop ; bs:ofWork ?work ; bs:copies ?n .
              ?claim rdf:reifies <<( ?shop bs:stocks ?work )>> ;
                     bs:copies ?value .
              FILTER ( ?value != ?n )
            }
        """ ;
    ] .

How it works

The 1.2 edition carries the stock twice: as bs:StockRecord nodes with bs:atShop, bs:ofWork and bs:copies, and as annotations on shop bs:stocks work. The SPARQL course's q68 shows the two side by side; this constraint checks that they agree. For each record it finds the annotation on the same shop and work through a triple term pattern and compares the counts. One annotation in the faulty data says thirteen copies where the record says twelve (F35). A check like this is what keeps a migration from RDF 1.1 to RDF 1.2 (q86, q121 in the SPARQL course) from drifting.

Diagram

   RDF 1.1     bt:stock-inkwell--the-book-town  a bs:StockRecord ;
                   bs:atShop bt:shop-inkwell ; bs:ofWork bt:book-the-book-town ; bs:copies 12 .
   RDF 1.2     bt:shop-inkwell bs:stocks bt:book-the-book-town {| bs:copies 13 |} .

   SELECT $this ?value WHERE {
     $this bs:atShop ?shop ; bs:ofWork ?work ; bs:copies ?n .
     ?claim rdf:reifies <<( ?shop bs:stocks ?work )>> ; bs:copies ?value .
     FILTER ( ?value != ?n ) }

What to take away

  • When the same fact is modelled two ways, a constraint that joins them is the only thing that keeps them equal.
  • A triple term pattern joins an annotation to the statement it annotates; the variables inside it bind like any others.
  • Write the consistency check before the migration, run it after.

The report

Does not conform · 1 violation, 0 warnings, 0 info · 1 shapes

SeverityFocus nodePathValueMessageShape
Violationbt:stock-inkwell--the-book-town13The annotation says 13 copies; the record disagrees.bt:StockAgreement

Defined in

S56

A node that names its own shape

A shop that says in the data which shape it should conform to, and a shape with no other target.

Data: bookshop-trail-faulty.ttl

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix sh:   <http://www.w3.org/ns/shacl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd:  <http://www.w3.org/2001/XMLSchema#> .

bt:NewShopShape
    a  sh:NodeShape ;
    sh:property [ sh:path rdfs:label ;   sh:minCount 1 ; sh:message "A new shop needs a name: {$this}." ] ;
    sh:property [ sh:path bs:locatedIn ; sh:minCount 1 ; sh:class bs:Settlement ] ;
    sh:property [ sh:path bs:founded ;   sh:minCount 1 ; sh:datatype xsd:gYear ] .

How it works

SHACL 1.2 adds sh:shape as a target that lives in the data graph: a triple n sh:shape S makes n a focus node of S. bt:NewShopShape has no target of its own. Fault F32 adds bt:shop-halfmoon sh:shape bt:NewShopShape to the data, and the shape now checks that one shop and reports its missing name. This is the opposite direction from every other target: the shapes graph normally decides what to check, and here the data volunteers. It suits records that carry their own profile, and it is the third 1.2 target this course has used, after sh:targetWhere in s33 and sh:ShapeClass in s08.

Diagram

   shapes graph                         data graph
   bt:NewShopShape  a sh:NodeShape       bt:shop-halfmoon  sh:shape  bt:NewShopShape .
     (no sh:target*)                          ^ this triple is the target

   focus nodes of bt:NewShopShape = { bt:shop-halfmoon }

What to take away

  • sh:shape in the data graph is a target. It is the only target the data can set for itself.
  • The 1.2 targets so far: sh:targetWhere (a shape), sh:ShapeClass (a class), sh:shape (the data). All run in this engine.
  • sh:targetNode in the shapes graph and sh:shape in the data graph name the same relationship from opposite ends.

The report

Does not conform · 2 violations, 0 warnings, 0 info · 4 shapes

SeverityFocus nodePathValueMessageShape
Violationbt:shop-halfmoonbs:foundedDoes not satisfy sh:minCountbt:NewShopShape › property 3
Violationbt:shop-halfmoonrdfs:labelA new shop needs a name: bt:shop-halfmoon.bt:NewShopShape › property 1

Defined in

S57

New in 1.2 Core, and what this build does with it

Notices stay on one line; the vocabulary's union classes are lists of classes; and the 1.2 features this engine reads but does not enforce.

Data: bookshop-trail-faulty-1.2.ttl

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix sh:   <http://www.w3.org/ns/shacl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

bt:NoticeShape
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:property [
        sh:path        skos:note ;
        sh:singleLine  true ;
        sh:message     "A shop notice is one line: {$value}" ;
    ] .

bt:UnionClassShape
    a                    sh:NodeShape ;
    sh:targetSubjectsOf  owl:unionOf ;
    sh:property [
        sh:path           owl:unionOf ;
        sh:memberShape    [ sh:class owl:Class ] ;
        sh:minListLength  2 ;
        sh:message        "A union class lists at least two classes." ;
    ] .

# Read by this build, not enforced: the result is a Violation, not a Warning.
# The property shape is named rather than written as [ ... ] because the
# editor's Turtle parser wants an annotation to be the last thing inside a
# blank node's brackets; a named subject has no such restriction.
bt:AnnotatedSeverity
    a               sh:NodeShape ;
    sh:targetNode   bt:shop-halfmoon ;
    sh:property     bt:AnnotatedSeverity-label .

bt:AnnotatedSeverity-label
    a            sh:PropertyShape ;
    sh:path      rdfs:label ;
    sh:message   "Written as a Warning on the constraint; reported by this build as a Violation." ;
    sh:minCount  1 {| sh:severity sh:Warning |} .

How it works

Two of the SHACL 1.2 Core additions run here. sh:singleLine true rejects a string with a line break, and the notice on Verso that runs to two lines (F36) is reported. The list constraints check RDF collections: the vocabulary, which is part of the data, declares two classes as owl:unionOf lists, and sh:memberShape [ sh:class owl:Class ] with sh:minListLength 2 checks each list's members and length. Three more 1.2 features are accepted by this build without effect, and are recorded here so that nobody relies on them: sh:severity written as an annotation on a single constraint, sh:reificationRequired (s53), and sh:uniqueValuesFor. Module 12 has the full table. Where a feature is read and ignored, the report looks the same as if the feature had passed, which is the case for a canary shape (s09). One note on syntax: the editor's Turtle parser accepts an annotation inside [ ... ] only as the last item, so the annotated constraint is written on a named property shape.

Diagram

   sh:singleLine true                  "Translated fiction ...\nAsk at the counter."   ->  violation

   owl:unionOf ( bs:Bookshop bs:Publisher )
   sh:property [ sh:path owl:unionOf ; sh:memberShape [ sh:class owl:Class ] ; sh:minListLength 2 ]
        each member checked, length checked                                     ->  passes

   read but not enforced by this build:
   sh:minCount 1 {| sh:severity sh:Warning |}      the result is still a Violation
   sh:reificationRequired true                     unannotated values pass
   sh:uniqueValuesFor bs:Work                      duplicates pass

What to take away

  • sh:singleLine and the list constraints are the 1.2 Core additions this engine runs today.
  • A feature an engine parses and ignores fails silently. Test each 1.2 feature with data that should fail before trusting it.
  • Module 12 is the reference for what runs, what errors, and what is silent.

The report

Does not conform · 1 violation, 1 warning, 0 info · 8 shapes

SeverityFocus nodePathValueMessageShape
Violationbt:shop-versoskos:noteTranslated fiction from twenty languages. Ask at the counter.A shop notice is one line: Translated fiction from twenty languages. Ask at the counter.bt:NoticeShape › property 1
Warningbt:shop-halfmoonrdfs:labelWritten as a Warning on the constraint; reported by this build as a Violation.bt:AnnotatedSeverity-label

Defined in

Module 09

Validating with inference

SHACL follows rdfs:subClassOf when it works out what a class covers, and nothing else. The editor can materialise the RDFS closure first (subclass, subproperty, domain and range), which changes the report in both directions: it finds nodes nobody typed, and it makes some errors disappear. Two lessons, and a pointer to the SPARQL course's module on reasoning.

In the standards
S58

The event nobody typed

Under RDFS inference, an event with no rdf:type is still an event, because bs:heldAt has a domain.

Data: bookshop-trail-faulty.ttl · Inference rdfs

@prefix bt:  <https://example.org/bookshop-trail/> .
@prefix bs:  <https://example.org/bookshop-trail/schema#> .
@prefix sh:  <http://www.w3.org/ns/shacl#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

# Prefixes for the SPARQL inside this file. @prefix lines are Turtle syntax
# and do not reach the query engine; sh:declare does.
bt:prefixes
    a  owl:Ontology ;
    sh:declare [ sh:prefix "bt" ;   sh:namespace "https://example.org/bookshop-trail/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "bs" ;   sh:namespace "https://example.org/bookshop-trail/schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdf" ;  sh:namespace "http://www.w3.org/1999/02/22-rdf-syntax-ns#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdfs" ; sh:namespace "http://www.w3.org/2000/01/rdf-schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "xsd" ;  sh:namespace "http://www.w3.org/2001/XMLSchema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "skos" ; sh:namespace "http://www.w3.org/2004/02/skos/core#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "dct" ;  sh:namespace "http://purl.org/dc/terms/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "geo" ;  sh:namespace "http://www.opengis.net/ont/geosparql#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "schema" ; sh:namespace "https://schema.org/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "sh" ;   sh:namespace "http://www.w3.org/ns/shacl#"^^xsd:anyURI ] .

bt:EventShape
    a               sh:NodeShape ;
    sh:targetClass  bs:Event ;
    sh:property [ sh:path bs:featuring ; sh:minCount 1 ] ;
    sh:property [ sh:path bs:eventDate ; sh:minCount 1 ; sh:maxCount 1 ] .

# Lists every focus node, so the headline shows how many there are.
bt:EventProbe
    a               sh:NodeShape ;
    sh:targetClass  bs:Event ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:severity  sh:Info ;
        sh:message   "{$this} is a focus node of the event shape." ;
        sh:select    "SELECT $this WHERE { }" ;
    ] .

How it works

SHACL follows rdfs:subClassOf when it decides what a class covers, and nothing else in RDFS. The vocabulary is part of the data and says bs:heldAt has rdfs:domain bs:Event; without inference that is documentation, and the event nobody typed (F21) is invisible to sh:targetClass bs:Event -- s07 needed sh:targetSubjectsOf to reach it. With the Inference dropdown at RDFS the engine materialises the closure first: subclass, subproperty, domain and range. The event acquires rdf:type bs:Event, and bt:EventShape now has 61 focus nodes instead of 60. The probe lists them all as information so the count is visible in the headline, and the constraint finds nothing wrong with the untyped event beyond its missing type -- which the shape cannot report, because under inference the type is there.

Diagram

   data:        bt:event-inkwell-2025-06-01  bs:heldAt bt:shop-inkwell .       (no rdf:type)
   vocabulary:  bs:heldAt  rdfs:domain  bs:Event .

   Inference: none    sh:targetClass bs:Event  ->  60 focus nodes
   Inference: RDFS    rdfs2: ?s bs:heldAt ?o  =>  ?s rdf:type bs:Event
                      sh:targetClass bs:Event  ->  61 focus nodes

   rules the closure applies: rdfs2, rdfs3, rdfs5, rdfs7, rdfs9, rdfs11  (domain, range, both hierarchies)

What to take away

  • Without inference, only asserted types and rdfs:subClassOf count. Domain and range are advice.
  • The RDFS mode makes the vocabulary's domains and ranges part of the data for the run, and targets grow accordingly.
  • A node that was invisible becomes checkable, and its missing type is no longer something a shape can see.

The report

Does not conform · 1 violation, 0 warnings, 61 info · 5 shapes · inference rdfs

SeverityFocus nodePathValueMessageShape
Violationbt:event-foxed-page-2025-05-01bs:featuringDoes not satisfy sh:minCountbt:EventShape › property 1
Infobt:event-bookbarrow-2025-09-06bt:event-bookbarrow-2025-09-06https://example.org/bookshop-trail/event-bookbarrow-2025-09-06 is a focus node of the event shape.bt:EventProbe
Infobt:event-bookwyrm-2025-05-03bt:event-bookwyrm-2025-05-03https://example.org/bookshop-trail/event-bookwyrm-2025-05-03 is a focus node of the event shape.bt:EventProbe
Infobt:event-bookwyrm-2025-08-16bt:event-bookwyrm-2025-08-16https://example.org/bookshop-trail/event-bookwyrm-2025-08-16 is a focus node of the event shape.bt:EventProbe
Infobt:event-borderprint-2025-02-28bt:event-borderprint-2025-02-28https://example.org/bookshop-trail/event-borderprint-2025-02-28 is a focus node of the event shape.bt:EventProbe
Infobt:event-broads-bindery-2025-05-31bt:event-broads-bindery-2025-05-31https://example.org/bookshop-trail/event-broads-bindery-2025-05-31 is a focus node of the event shape.bt:EventProbe
Infobt:event-broken-spine-2025-07-05bt:event-broken-spine-2025-07-05https://example.org/bookshop-trail/event-broken-spine-2025-07-05 is a focus node of the event shape.bt:EventProbe
Infobt:event-candlemas-2025-02-22bt:event-candlemas-2025-02-22https://example.org/bookshop-trail/event-candlemas-2025-02-22 is a focus node of the event shape.bt:EventProbe
Infobt:event-candlemas-2025-07-26bt:event-candlemas-2025-07-26https://example.org/bookshop-trail/event-candlemas-2025-07-26 is a focus node of the event shape.bt:EventProbe
Infobt:event-castle-steps-2025-05-26bt:event-castle-steps-2025-05-26https://example.org/bookshop-trail/event-castle-steps-2025-05-26 is a focus node of the event shape.bt:EventProbe
Infobt:event-castle-steps-2025-08-30bt:event-castle-steps-2025-08-30https://example.org/bookshop-trail/event-castle-steps-2025-08-30 is a focus node of the event shape.bt:EventProbe
Infobt:event-chapter-verse-2025-03-01bt:event-chapter-verse-2025-03-01https://example.org/bookshop-trail/event-chapter-verse-2025-03-01 is a focus node of the event shape.bt:EventProbe

and 50 more

Defined in

S59

The inference that hides an error

The same class constraints as s12, under RDFS: two of the errors disappear and two new ones appear.

Data: bookshop-trail-faulty.ttl · Inference rdfs

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix sh:   <http://www.w3.org/ns/shacl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

bt:PersonLinks
    a               sh:NodeShape ;
    sh:targetClass  bs:Person ;
    sh:property [ sh:path bs:basedIn ; sh:class bs:Settlement ;
                  sh:message "{$this} is based in {$value}, which is not a settlement." ] .

bt:PublisherLinks
    a               sh:NodeShape ;
    sh:targetClass  bs:Publisher ;
    sh:property [ sh:path bs:locatedIn ; sh:class bs:Settlement ;
                  sh:message "{$this} is located in {$value}, which is not a settlement." ] .

bt:SettlementLinks
    a               sh:NodeShape ;
    sh:targetClass  bs:Settlement ;
    sh:property [ sh:path bs:within ; sh:class bs:CouncilArea ;
                  sh:message "{$this} sits directly in {$value}, which is not a council area." ] .

bt:PlaceKind
    a               sh:NodeShape ;
    sh:targetClass  bs:Place ;
    sh:xone (
        [ sh:class bs:Country ]
        [ sh:class bs:Region ]
        [ sh:class bs:CouncilArea ]
        [ sh:class bs:Settlement ]
        [ sh:property [ sh:path skos:notation ; sh:hasValue "GB" ] ]
    ) ;
    sh:message  "{$this} is not exactly one kind of place." .

How it works

rdfs:range works the other way round from what a validator wants. bs:basedIn has range bs:Settlement, so under RDFS inference the author based in Wales (F16) makes Wales a settlement, and the publisher located in Wales (F19) does the same through the range of bs:locatedIn. Both sh:class bs:Settlement constraints now pass; the wrong data has typed the country to suit itself. Brecon, placed directly inside Wales (F26), still fails, because the range of bs:within is bs:Place and the constraint asks for a council area. Wales, now a settlement, fails that same constraint, because it sits directly inside Great Britain. And bt:PlaceKind from s26 -- a place is exactly one kind -- reports Wales again, a country and, since the inference, a settlement too. The SPARQL course's q143 calls this the inference nobody wanted. It is the reason inference is off by default here, and the reason to validate before you reason.

Diagram

   bt:author-owen-harker  bs:basedIn  bt:place-wales .
   bs:basedIn  rdfs:range  bs:Settlement .

   none    sh:class bs:Settlement on bs:basedIn      Wales is a Country     ->  violation
   RDFS    rdfs3:  ?o is a bs:Settlement             Wales is now both     ->  passes
           bt:PlaceKind (sh:xone of the four kinds)  Wales is two kinds    ->  violation

   two errors hidden, two new ones surfaced, none of the data changed

What to take away

  • Range inference types the object to fit the property. A constraint that checks the object's class is then checking the inference, not the data.
  • Validate the asserted data first. Inference is for what the data implies, not for what it should have said.
  • The same shapes graph gives different reports under different inference settings. The report headline says which was used.

The report

Does not conform · 3 violations, 0 warnings, 0 info · 13 shapes · inference rdfs

SeverityFocus nodePathValueMessageShape
Violationbt:place-breconbs:withinbt:place-walesbt:place-brecon sits directly in bt:place-wales, which is not a council area.bt:SettlementLinks › property 1
Violationbt:place-walesbt:place-walesbt:place-wales is not exactly one kind of place.bt:PlaceKind
Violationbt:place-walesbs:withinbt:place-gbbt:place-wales sits directly in bt:place-gb, which is not a council area.bt:SettlementLinks › property 1

Defined in

Module 10

Debugging shapes

'Conforms' is what a validator says when the data is right, and also what it says when it checked nothing. This module collects the ways a shape goes wrong without an error: a target that matches nothing, a path in the wrong direction, a datatype or language tag that stops a match, a severity that does not inherit. Each comes with the check that catches it.

In the standards
S60

The shape that finds nothing

Four shapes that should each report something on the faulty data, and do not -- and the canary that shows validation ran.

Data: bookshop-trail-faulty.ttl

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix sh:   <http://www.w3.org/ns/shacl#> .
@prefix rdf:  <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix xsd:  <http://www.w3.org/2001/XMLSchema#> .

# Prefixes for the SPARQL inside this file. @prefix lines are Turtle syntax
# and do not reach the query engine; sh:declare does.
bt:prefixes
    a  owl:Ontology ;
    sh:declare [ sh:prefix "bt" ;   sh:namespace "https://example.org/bookshop-trail/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "bs" ;   sh:namespace "https://example.org/bookshop-trail/schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdf" ;  sh:namespace "http://www.w3.org/1999/02/22-rdf-syntax-ns#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdfs" ; sh:namespace "http://www.w3.org/2000/01/rdf-schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "xsd" ;  sh:namespace "http://www.w3.org/2001/XMLSchema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "skos" ; sh:namespace "http://www.w3.org/2004/02/skos/core#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "dct" ;  sh:namespace "http://purl.org/dc/terms/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "geo" ;  sh:namespace "http://www.opengis.net/ont/geosparql#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "schema" ; sh:namespace "https://schema.org/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "sh" ;   sh:namespace "http://www.w3.org/ns/shacl#"^^xsd:anyURI ] .

bt:WrongClass
    a               sh:NodeShape ;
    sh:targetClass  bs:BookShop ;
    sh:property [ sh:path rdfs:label ; sh:minCount 1 ] .

bt:WrongDirection
    a               sh:NodeShape ;
    sh:targetClass  bs:Event ;
    sh:property [
        sh:path     [ sh:inversePath bs:heldAt ] ;
        sh:class    bs:Bookshop ;
        sh:message  "Meant: the place an event is held at is a bookshop." ;
    ] .

bt:WrongTag
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:message   "Meant: flag the shop called The Inkwell." ;
        sh:select    """
            SELECT $this WHERE { $this rdfs:label ?l . FILTER ( ?l = "The Inkwell" ) }
        """ ;
    ] .

bt:WrongPredicate
    a                    sh:NodeShape ;
    sh:targetSubjectsOf  bs:heldat ;
    sh:class             bs:Event .

bt:Canary
    a              sh:NodeShape ;
    sh:targetNode  bt:shop-inkwell ;
    sh:property [ sh:path rdf:type ; sh:maxCount 0 ;
                  sh:message "The canary: validation ran." ] .

How it works

Each shape here has one mistake that makes it select or match nothing, and none of them produces an error. bt:WrongClass targets bs:BookShop with a capital S: no such class, no focus nodes. bt:WrongDirection puts [ sh:inversePath bs:heldAt ] on events: nothing points at an event with bs:heldAt, so there are no value nodes, and sh:class on no values passes -- the event held at a publisher (F20) goes unreported. bt:WrongTag looks for shops labelled "The Inkwell" with no language tag: the data says "The Inkwell"@en, a different term, so the query never matches (the SPARQL course's q95). bt:WrongPredicate targets the subjects of bs:heldat, lower case: none. The canary from s09 is the only row. The shapes count of eight says all of them compiled; the single result says four of them did nothing.

Diagram

   symptom: Conforms, or fewer rows than expected, and no error

   check                                   this file
   is the target's class or predicate spelled as the data spells it?    bs:BookShop, bs:heldat
   is the path in the direction the data uses?                          ^bs:heldAt on an event
   do literals in the shape carry the same tag and datatype as the data?  "The Inkwell" vs "The Inkwell"@en
   does a canary that must fail, fail?                                  yes: validation ran

What to take away

  • A constraint on a path with no values passes. Missing values are a minCount question, and nothing else will notice them.
  • Term equality is exact: language tag and datatype included. Copy the literal from the data, do not retype it.
  • When a shape reports nothing, first prove that the target selects something: swap in sh:targetNode with a node you know.

Try it

Fix one mistake at a time and validate after each: BookShop to Bookshop, remove the sh:inversePath, add @en to the label, correct bs:heldat. A row should appear each time.

The report

Does not conform · 1 violation, 0 warnings, 0 info · 8 shapes

SeverityFocus nodePathValueMessageShape
Violationbt:shop-inkwellrdf:typeThe canary: validation ran.bt:Canary › property 1

Defined in

S61

The shape that flags everything

Four shapes that report every node they look at, on clean data, each for a different reason.

Data: bookshop-trail-1.1.ttl

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix sh:   <http://www.w3.org/ns/shacl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd:  <http://www.w3.org/2001/XMLSchema#> .

bt:GYearOrder
    a               sh:NodeShape ;
    sh:targetClass  bs:Author ;
    sh:property [ sh:path bs:born ; sh:lessThan bs:died ;
                  sh:message "Reported for every author with both dates: gYear does not compare." ] .

bt:StringLabels
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:property [ sh:path rdfs:label ; sh:datatype xsd:string ;
                  sh:message "Reported for every shop: a tagged label is an rdf:langString." ] .

bt:HeldAtForwards
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:property [ sh:path bs:heldAt ; sh:minCount 1 ;
                  sh:message "Reported for every shop: events point at shops, not the other way." ] .

# The hierarchy is declared here, in the shapes graph. SHACL reads it from the data graph.
bs:Bookshop  rdfs:subClassOf  bs:Retailer .

bt:RetailerClass
    a               sh:NodeShape ;
    sh:targetClass  bs:StockRecord ;
    sh:property [ sh:path bs:atShop ; sh:class bs:Retailer ;
                  sh:message "Reported for every record: the subclass axiom is in the wrong graph." ] .

How it works

The opposite failure is a report full of rows that are all wrong. bt:GYearOrder is the SPARQL course's sh:lessThan between bs:born and bs:died: gYears do not compare on this engine, so every author with both dates is reported (s14). bt:StringLabels asks for xsd:string on labels that are all rdf:langString (s11): 33 shops. bt:HeldAtForwards puts bs:heldAt on shops, where it should be the inverse: no shop has one, 33 minCount rows. bt:RetailerClass declares in the shapes graph that a bookshop is a bs:Retailer and asks every stock record's shop to be one: rdfs:subClassOf is read from the data graph, not the shapes graph, so none is, and 95 rows follow. When every focus node fails, the shape is wrong before the data is, and the count in the headline shows it first.

Diagram

   symptom: a row for every focus node

   cause                                       here
   a comparison the engine cannot make          sh:lessThan on xsd:gYear          8 rows
   a datatype the data never uses               xsd:string on "..."@en labels     33 rows
   a path in the wrong direction                bs:heldAt on a shop                33 rows
   a class hierarchy in the wrong graph         rdfs:subClassOf in the shapes file 95 rows

What to take away

  • A row for every node is a property of the shape, not the data. Read the shape again before touching the data.
  • rdfs:subClassOf is followed in the data graph. If the hierarchy lives with the shapes, sh:class will not see it (SHACL 1.2 Core 6.3 discusses this).
  • Datatype and direction mistakes fail every node the same way. Look at the first row's value and ask why it should have passed.

The report

Does not conform · 169 violations, 0 warnings, 0 info · 8 shapes

SeverityFocus nodePathValueMessageShape
Violationbt:author-callum-strachanbs:born1922Reported for every author with both dates: gYear does not compare.bt:GYearOrder › property 1
Violationbt:author-dermot-lislebs:born1933Reported for every author with both dates: gYear does not compare.bt:GYearOrder › property 1
Violationbt:author-gerard-tynebs:born1912Reported for every author with both dates: gYear does not compare.bt:GYearOrder › property 1
Violationbt:author-iolo-vaughanbs:born1908Reported for every author with both dates: gYear does not compare.bt:GYearOrder › property 1
Violationbt:author-maud-ellerybs:born1918Reported for every author with both dates: gYear does not compare.bt:GYearOrder › property 1
Violationbt:author-nesta-hywelbs:born1930Reported for every author with both dates: gYear does not compare.bt:GYearOrder › property 1
Violationbt:author-perrin-oakesbs:born1925Reported for every author with both dates: gYear does not compare.bt:GYearOrder › property 1
Violationbt:author-rhona-blackwoodbs:born1901Reported for every author with both dates: gYear does not compare.bt:GYearOrder › property 1
Violationbt:shop-bookbarrowbs:heldAtReported for every shop: events point at shops, not the other way.bt:HeldAtForwards › property 1
Violationbt:shop-bookbarrowrdfs:labelBookbarrowReported for every shop: a tagged label is an rdf:langString.bt:StringLabels › property 1
Violationbt:shop-bookwyrmbs:heldAtReported for every shop: events point at shops, not the other way.bt:HeldAtForwards › property 1
Violationbt:shop-bookwyrmrdfs:labelThe BookwyrmReported for every shop: a tagged label is an rdf:langString.bt:StringLabels › property 1

and 157 more

Defined in

S62

Naming your shapes

The same constraints as s38, with every property shape named and described, so the report says which rule fired.

Data: bookshop-trail-faulty.ttl

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix sh:   <http://www.w3.org/ns/shacl#> .
@prefix rdf:  <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd:  <http://www.w3.org/2001/XMLSchema#> .

bt:BookshopShape
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:name         "Bookshop" ;
    sh:description  "An independent bookshop on the trail." ;
    sh:property     bt:BookshopShape-label, bt:BookshopShape-locatedIn, bt:BookshopShape-founded,
                    bt:BookshopShape-staffCount, bt:BookshopShape-website .

bt:IdentityGroup
    a           sh:PropertyGroup ;
    rdfs:label  "Identity" ;
    sh:order    0 .

bt:BookshopShape-label
    a               sh:PropertyShape ;
    sh:path         rdfs:label ;
    sh:name         "name" ;
    sh:description  "The shop's name, with a language tag." ;
    sh:group        bt:IdentityGroup ;
    sh:order        1 ;
    sh:minCount     1 ;
    sh:datatype     rdf:langString .

bt:BookshopShape-locatedIn
    a               sh:PropertyShape ;
    sh:path         bs:locatedIn ;
    sh:name         "town" ;
    sh:description  "The settlement the shop is in. Exactly one." ;
    sh:group        bt:IdentityGroup ;
    sh:order        2 ;
    sh:minCount     1 ;
    sh:maxCount     1 ;
    sh:class        bs:Settlement .

bt:BookshopShape-founded
    a               sh:PropertyShape ;
    sh:path         bs:founded ;
    sh:name         "founded" ;
    sh:description  "The year the shop opened, as an xsd:gYear." ;
    sh:order        3 ;
    sh:minCount     1 ;
    sh:maxCount     1 ;
    sh:datatype     xsd:gYear .

bt:BookshopShape-staffCount
    a               sh:PropertyShape ;
    sh:path         bs:staffCount ;
    sh:name         "staff" ;
    sh:order        4 ;
    sh:datatype     xsd:integer ;
    sh:minInclusive 1 .

bt:BookshopShape-website
    a               sh:PropertyShape ;
    sh:path         bs:website ;
    sh:name         "website" ;
    sh:description  "Optional; at most one." ;
    sh:order        5 ;
    sh:maxCount     1 ;
    sh:datatype     xsd:anyURI ;
    sh:severity     sh:Warning .

How it works

A property shape written as [ ... ] is a blank node, and the report can only call it 'bt:BookshopShape > property 3'. Give it an IRI and the report names it; add sh:name and sh:description and a form builder or a documentation tool can use them too. These are the non-validating characteristics of 2.3.2: they change nothing about what is checked. The query below groups the report by sh:sourceShape, which is only readable once the shapes have names. The convention here is the node shape's name, a hyphen, and the property's local name.

Diagram

   before    sh:sourceShape  _:b12          shown as  bt:BookshopShape > property 3
   after     sh:sourceShape  bt:BookshopShape-founded

   bt:BookshopShape-founded
       a               sh:PropertyShape ;
       sh:path         bs:founded ;
       sh:name         "founded" ;                <- non-validating
       sh:description  "The year the shop opened, as an xsd:gYear." ;
       sh:minCount 1 ; sh:maxCount 1 ; sh:datatype xsd:gYear .

What to take away

  • Name property shapes. sh:sourceShape is the fastest route from a report row to the rule that produced it.
  • sh:name, sh:description, sh:order and sh:group are for people and tools; validation ignores them.
  • A named shapes graph is documentation of the data model. Write it as if it will be read.

The report

Does not conform · 8 violations, 2 warnings, 0 info · 6 shapes

SeverityFocus nodePathValueMessageShape
Violationbt:shop-foxed-pagebs:founded1985Does not satisfy sh:datatypebt:BookshopShape-founded
Violationbt:shop-foxed-pagebs:locatedInKendalDoes not satisfy sh:classbt:BookshopShape-locatedIn
Violationbt:shop-foxed-pagebs:staffCount3.5Does not satisfy sh:datatypebt:BookshopShape-staffCount
Violationbt:shop-foxed-pagerdfs:labelThe Foxed PageDoes not satisfy sh:datatypebt:BookshopShape-label
Violationbt:shop-halfmoonbs:foundedDoes not satisfy sh:minCountbt:BookshopShape-founded
Violationbt:shop-halfmoonbs:staffCount0Does not satisfy sh:minInclusivebt:BookshopShape-staffCount
Violationbt:shop-halfmoonrdfs:labelDoes not satisfy sh:minCountbt:BookshopShape-label
Violationbt:shop-inkwellbs:locatedInDoes not satisfy sh:maxCountbt:BookshopShape-locatedIn
Warningbt:shop-foxed-pagebs:websiteDoes not satisfy sh:maxCountbt:BookshopShape-website
Warningbt:shop-foxed-pagebs:websitewww.foxed-page.exampleDoes not satisfy sh:datatypebt:BookshopShape-website

Afterwards, in the SPARQL panel

Results by named shape (on the report)
PREFIX sh: <http://www.w3.org/ns/shacl#>
SELECT ?shape (COUNT(?r) AS ?results)
WHERE { ?r a sh:ValidationResult ; sh:sourceShape ?shape }
GROUP BY ?shape
ORDER BY DESC(?results)

Defined in

S63

Shapes that check nothing

Three ways to write a constraint the engine accepts and never applies.

Data: bookshop-trail-faulty.ttl

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix sh:   <http://www.w3.org/ns/shacl#> .
@prefix rdf:  <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .

bt:NoTarget
    a  sh:NodeShape ;
    sh:property [ sh:path rdfs:label ; sh:minCount 1 ] .

# Ill-formed: sh:minCount without a property shape. Ignored by this engine.
bt:CountOnNodeShape
    a              sh:NodeShape ;
    sh:targetNode  bt:shop-halfmoon ;
    sh:minCount    1 ;
    sh:message     "Never reported: there is no sh:path for the count to apply to." .

# Works without rdf:type sh:NodeShape: the target and the property shape make it a shape.
bt:UntypedButTargeted
    sh:targetNode  bt:shop-halfmoon ;
    sh:property [ sh:path rdfs:label ; sh:minCount 1 ;
                  sh:message "{$this} has no name (reported by a shape with no rdf:type)." ] .

bt:Canary
    a              sh:NodeShape ;
    sh:targetNode  bt:shop-inkwell ;
    sh:property [ sh:path rdf:type ; sh:maxCount 0 ; sh:message "The canary: validation ran." ] .

How it works

bt:NoTarget is a well-formed node shape with constraints and no target: it compiles, it counts, it checks nothing, and a rule on it would never fire (s43). bt:CountOnNodeShape puts sh:minCount 1 on the node shape itself, where it belongs to no path; the specification calls that ill-formed, and this engine ignores it without a word. bt:UntypedButTargeted is the case that does work, for contrast: a node with a target and property shapes is a shape whether or not it is typed sh:NodeShape. The canary reports, and the shape count of seven is the only sign the first two are there. s09 and s60 cover the target that matches nothing; this lesson is the constraint that is never reached.

Diagram

   bt:NoTarget            a sh:NodeShape ; sh:property [ ... ]            no focus nodes
   bt:CountOnNodeShape    a sh:NodeShape ; sh:targetNode ... ; sh:minCount 1   ignored: no sh:path
   bt:UntypedButTargeted  sh:targetNode ... ; sh:property [ ... ]        works: a shape by use

   shapes compiled: 7     results: 2   (one from the untyped shape, one canary)

What to take away

  • A shape with no target is a definition. It checks nothing unless another shape reaches it with sh:node, sh:not or a qualified shape.
  • Cardinality, datatype and the other property constraints belong on property shapes. On a node shape they are silently dropped by this engine.
  • Compare the shapes count with what you wrote, and keep a canary while you are unsure.

The report

Does not conform · 2 violations, 0 warnings, 0 info · 7 shapes

SeverityFocus nodePathValueMessageShape
Violationbt:shop-halfmoonrdfs:labelbt:shop-halfmoon has no name (reported by a shape with no rdf:type).bt:UntypedButTargeted › property 1
Violationbt:shop-inkwellrdf:typeThe canary: validation ran.bt:Canary › property 1

Defined in

Module 11

Putting it together

A complete shapes graph for the Bookshop Trail, run on the clean data and then on the faulty edition as its answer key; shapes used as questions rather than rules; a report turned into a list of issues in the dataset's own vocabulary; and a rule set that enriches the graph before checking it.

In the standards
S64

The whole trail in one shapes graph

A shapes graph for every class in the dataset, run on the faulty edition. Every numbered fault in data/faults.ttl appears in the report.

Data: bookshop-trail-faulty.ttl

@prefix bt:    <https://example.org/bookshop-trail/> .
@prefix bs:    <https://example.org/bookshop-trail/schema#> .
@prefix sh:    <http://www.w3.org/ns/shacl#> .
@prefix rdf:   <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs:  <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:   <http://www.w3.org/2002/07/owl#> .
@prefix xsd:   <http://www.w3.org/2001/XMLSchema#> .
@prefix skos:  <http://www.w3.org/2004/02/skos/core#> .
@prefix dct:   <http://purl.org/dc/terms/> .
@prefix geo:   <http://www.opengis.net/ont/geosparql#> .
@prefix wgs84: <http://www.w3.org/2003/01/geo/wgs84_pos#> .

# Prefixes for the SPARQL inside this file. @prefix lines are Turtle syntax
# and do not reach the query engine; sh:declare does.
bt:prefixes
    a  owl:Ontology ;
    sh:declare [ sh:prefix "bt" ;   sh:namespace "https://example.org/bookshop-trail/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "bs" ;   sh:namespace "https://example.org/bookshop-trail/schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdf" ;  sh:namespace "http://www.w3.org/1999/02/22-rdf-syntax-ns#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdfs" ; sh:namespace "http://www.w3.org/2000/01/rdf-schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "xsd" ;  sh:namespace "http://www.w3.org/2001/XMLSchema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "skos" ; sh:namespace "http://www.w3.org/2004/02/skos/core#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "dct" ;  sh:namespace "http://purl.org/dc/terms/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "geo" ;  sh:namespace "http://www.opengis.net/ont/geosparql#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "schema" ; sh:namespace "https://schema.org/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "sh" ;   sh:namespace "http://www.w3.org/ns/shacl#"^^xsd:anyURI ] .

# ---------------------------------------------------------------- bookshops

bt:BookshopShape
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:not          [ sh:class bs:Publisher ] ;
    sh:message      "{$this} is typed as both a bookshop and a publisher." ;
    sh:property     bt:BookshopShape-label, bt:BookshopShape-locatedIn, bt:BookshopShape-founded,
                    bt:BookshopShape-staffCount, bt:BookshopShape-floorArea, bt:BookshopShape-hasCafe,
                    bt:BookshopShape-website, bt:BookshopShape-specialises, bt:BookshopShape-country ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:message   "{$this} opened in {$value}, before 1800." ;
        sh:select    "SELECT $this ?value WHERE { $this bs:founded ?value . FILTER ( xsd:integer(STR(?value)) < 1800 ) }" ;
    ] ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:severity  sh:Warning ;
        sh:message   "{$this} cannot be reached on foot from The Inkwell. Two shops cannot, by design." ;
        sh:select    "SELECT $this WHERE { FILTER NOT EXISTS { bt:shop-inkwell (bs:connectsTo|^bs:connectsTo)+ $this } }" ;
    ] .

bt:BookshopShape-label       a sh:PropertyShape ; sh:path rdfs:label ;
    sh:minCount 1 ; sh:datatype rdf:langString ; sh:languageIn ( "en" "cy" "gd" ) ;
    sh:message "A shop has a name with a language tag." .
bt:BookshopShape-locatedIn   a sh:PropertyShape ; sh:path bs:locatedIn ;
    sh:minCount 1 ; sh:maxCount 1 ; sh:nodeKind sh:IRI ; sh:class bs:Settlement ;
    sh:message "A shop is in exactly one settlement." .
bt:BookshopShape-founded     a sh:PropertyShape ; sh:path bs:founded ;
    sh:minCount 1 ; sh:maxCount 1 ; sh:datatype xsd:gYear ;
    sh:message "A shop records the one year it opened, as an xsd:gYear." .
bt:BookshopShape-staffCount  a sh:PropertyShape ; sh:path bs:staffCount ;
    sh:maxCount 1 ; sh:datatype xsd:integer ; sh:minInclusive 1 ;
    sh:message "Staff count is a positive integer: {$value}." .
bt:BookshopShape-floorArea   a sh:PropertyShape ; sh:path bs:floorArea ;
    sh:maxCount 1 ; sh:datatype xsd:decimal ; sh:minExclusive 0 ;
    sh:message "Floor area is a positive number of square metres: {$value}." .
bt:BookshopShape-hasCafe     a sh:PropertyShape ; sh:path bs:hasCafe ;
    sh:maxCount 1 ; sh:datatype xsd:boolean .
bt:BookshopShape-website     a sh:PropertyShape ; sh:path bs:website ;
    sh:maxCount 1 ; sh:datatype xsd:anyURI ; sh:pattern "^https?://" ;
    sh:message "At most one website, with a scheme: {$value}." .
bt:BookshopShape-specialises a sh:PropertyShape ; sh:path bs:specialises ;
    sh:maxCount 1 ; sh:class skos:Concept ;
    sh:message "A specialism is a concept from the genre scheme, not {$value}." .
bt:BookshopShape-country     a sh:PropertyShape ;
    sh:path ( bs:locatedIn [ sh:oneOrMorePath bs:within ] ) ;
    sh:qualifiedValueShape [ sh:class bs:Country ] ; sh:qualifiedMinCount 1 ; sh:qualifiedMaxCount 1 ;
    sh:message "{$this} is in some number of countries other than one." .

bt:BookshopClosed
    a                     sh:NodeShape ;
    sh:targetClass        bs:Bookshop ;
    sh:closed             true ;
    sh:ignoredProperties  ( rdf:type sh:shape ) ;
    sh:message            "{$this} has a property the model does not know: {$path}." ;
    sh:property [ sh:path rdfs:label ] ; sh:property [ sh:path bs:locatedIn ] ;
    sh:property [ sh:path bs:founded ] ; sh:property [ sh:path bs:floorArea ] ;
    sh:property [ sh:path bs:staffCount ] ; sh:property [ sh:path bs:specialises ] ;
    sh:property [ sh:path bs:sellsSecondHand ] ; sh:property [ sh:path bs:hasCafe ] ;
    sh:property [ sh:path bs:website ] ; sh:property [ sh:path bs:connectsTo ] ;
    sh:property [ sh:path bs:stocks ] ; sh:property [ sh:path wgs84:lat ] ;
    sh:property [ sh:path wgs84:long ] ; sh:property [ sh:path geo:hasGeometry ] ;
    sh:property [ sh:path geo:hasDefaultGeometry ] ; sh:property [ sh:path skos:note ] .

# ---------------------------------------------------------------- places

bt:PlaceShape
    a               sh:NodeShape ;
    sh:targetClass  bs:Place ;
    sh:property     bt:PlaceShape-label, bt:PlaceShape-lat, bt:PlaceShape-long, bt:PlaceShape-reachesGB ;
    sh:xone (
        [ sh:class bs:Country ] [ sh:class bs:Region ] [ sh:class bs:CouncilArea ] [ sh:class bs:Settlement ]
        [ sh:property [ sh:path skos:notation ; sh:hasValue "GB" ] ]
    ) ;
    sh:message  "{$this} is not exactly one kind of place." ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:message   "{$this} contains itself." ;
        sh:select    "SELECT $this WHERE { $this bs:within+ $this }" ;
    ] .

bt:PlaceShape-label      a sh:PropertyShape ; sh:path rdfs:label ;
    sh:minCount 1 ; sh:languageIn ( "en" "cy" "gd" ) ; sh:uniqueLang true ;
    sh:message "Place names are in English, Welsh or Gaelic, one per language." .
bt:PlaceShape-lat        a sh:PropertyShape ; sh:path wgs84:lat ;
    sh:maxCount 1 ; sh:minInclusive -90 ; sh:maxInclusive 90 ;
    sh:message "Latitude {$value} is off the globe." .
bt:PlaceShape-long       a sh:PropertyShape ; sh:path wgs84:long ;
    sh:maxCount 1 ; sh:minInclusive -180 ; sh:maxInclusive 180 .
bt:PlaceShape-reachesGB  a sh:PropertyShape ; sh:path [ sh:zeroOrMorePath bs:within ] ;
    sh:hasValue bt:place-gb ;
    sh:message "{$this} is not inside Great Britain at any depth." .

bt:SettlementShape
    a               sh:NodeShape ;
    sh:targetClass  bs:Settlement ;
    sh:property     bt:SettlementShape-within, bt:SettlementShape-population, bt:SettlementShape-geometry .

bt:SettlementShape-within      a sh:PropertyShape ; sh:path bs:within ;
    sh:minCount 1 ; sh:maxCount 1 ; sh:class bs:CouncilArea ;
    sh:message "A settlement sits directly inside one council area; {$value} is not one." .
bt:SettlementShape-population  a sh:PropertyShape ; sh:path bs:population ;
    sh:maxCount 1 ; sh:datatype xsd:integer ; sh:minInclusive 0 .
bt:SettlementShape-geometry    a sh:PropertyShape ; sh:path geo:hasDefaultGeometry ;
    sh:minCount 1 ; sh:maxCount 1 ;
    sh:message "{$this} has no default geometry." .

bt:CouncilAreaShape
    a               sh:NodeShape ;
    sh:targetClass  bs:CouncilArea ;
    sh:property [
        sh:path                 [ sh:inversePath bs:within ] ;
        sh:qualifiedValueShape  [ sh:property [ sh:path bs:isBookTown ; sh:hasValue true ] ] ;
        sh:qualifiedMaxCount    1 ;
        sh:message              "{$this} contains more than one book town." ;
    ] .

# ---------------------------------------------------------------- works

bt:ISBN13ConstraintComponent
    a  sh:ConstraintComponent ;
    sh:parameter [ sh:path bt:isbn13 ; sh:datatype xsd:boolean ] ;
    sh:validator [
        a  sh:SPARQLAskValidator ;
        sh:prefixes  bt:prefixes ;
        sh:message   "The check digit of {$value} is wrong." ;
        sh:ask  """
            ASK {
              BIND ( STR($value) AS ?s )
              BIND (   xsd:integer(SUBSTR(?s, 1, 1))  + 3 * xsd:integer(SUBSTR(?s, 2, 1))
                     + xsd:integer(SUBSTR(?s, 3, 1))  + 3 * xsd:integer(SUBSTR(?s, 4, 1))
                     + xsd:integer(SUBSTR(?s, 5, 1))  + 3 * xsd:integer(SUBSTR(?s, 6, 1))
                     + xsd:integer(SUBSTR(?s, 7, 1))  + 3 * xsd:integer(SUBSTR(?s, 8, 1))
                     + xsd:integer(SUBSTR(?s, 9, 1))  + 3 * xsd:integer(SUBSTR(?s, 10, 1))
                     + xsd:integer(SUBSTR(?s, 11, 1)) + 3 * xsd:integer(SUBSTR(?s, 12, 1)) AS ?sum )
              BIND ( 10 - (?sum - 10 * FLOOR(?sum / 10)) AS ?c )
              BIND ( ?c - 10 * FLOOR(?c / 10) AS ?check )
              FILTER ( !REGEX(?s, "^97[89][0-9]{10}$") || ?check = xsd:integer(SUBSTR(?s, 13, 1)) )
            }
        """ ;
    ] .

bt:WorkShape
    a               sh:NodeShape ;
    sh:targetClass  bs:Work ;
    sh:property     bt:WorkShape-label, bt:WorkShape-author, bt:WorkShape-publishedBy, bt:WorkShape-year,
                    bt:WorkShape-pages, bt:WorkShape-rrp, bt:WorkShape-isbn, bt:WorkShape-genre ;
    sh:or (
        [ sh:property [ sh:path bs:isbn ; sh:minCount 1 ] ]
        [ sh:property [ sh:path bs:publicationYear ; sh:pattern "^(1[0-8][0-9][0-9]|19[0-6][0-9])$" ] ]
    ) ;
    sh:message  "{$this} has no ISBN and was published after 1969." ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:message   "{$this} was published in {$value}, before its author was born." ;
        sh:select    """
            SELECT $this ?value WHERE {
              $this bs:publicationYear ?value ; bs:author/bs:born ?born .
              FILTER ( xsd:integer(STR(?value)) < xsd:integer(STR(?born)) )
            }
        """ ;
    ] .

bt:WorkShape-label        a sh:PropertyShape ; sh:path rdfs:label ; sh:minCount 1 ; sh:equals dct:title ;
    sh:message "A work's rdfs:label and dct:title agree." .
bt:WorkShape-author       a sh:PropertyShape ; sh:path bs:author ; sh:minCount 1 ; sh:class bs:Author .
bt:WorkShape-publishedBy  a sh:PropertyShape ; sh:path bs:publishedBy ; sh:minCount 1 ; sh:maxCount 1 ;
    sh:node bt:PublisherShape ; sh:message "{$value} is not a well-formed publisher." .
bt:WorkShape-year         a sh:PropertyShape ; sh:path bs:publicationYear ; sh:minCount 1 ; sh:maxCount 1 ;
    sh:datatype xsd:gYear .
bt:WorkShape-pages        a sh:PropertyShape ; sh:path bs:pages ; sh:maxCount 1 ; sh:datatype xsd:integer ;
    sh:minInclusive 1 ; sh:message "{$value} pages is not a book." .
bt:WorkShape-rrp          a sh:PropertyShape ; sh:path bs:rrp ; sh:maxCount 1 ; sh:datatype xsd:decimal ;
    sh:minInclusive 0 ; sh:message "A negative price: {$value}." .
bt:WorkShape-isbn         a sh:PropertyShape ; sh:path bs:isbn ; sh:maxCount 1 ;
    sh:pattern "^97[89][0-9]{10}$" ; bt:isbn13 true ;
    sh:message "An ISBN-13 is thirteen digits starting 978 or 979: {$value}." .
bt:WorkShape-genre        a sh:PropertyShape ; sh:path ( bs:genre [ sh:zeroOrMorePath skos:broader ] ) ;
    sh:hasValue bt:genre-literature ;
    sh:message "{$this} is filed under a genre that is not in the scheme." .

bt:TranslationShape
    a               sh:NodeShape ;
    sh:targetClass  bs:Translation ;
    sh:property [ sh:path bs:translationOf ; sh:minCount 1 ; sh:maxCount 1 ; sh:class bs:Work ] ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:message   "{$this} was published in {$value}, before the work it translates." ;
        sh:select    """
            SELECT $this ?value WHERE {
              $this bs:publicationYear ?value ; bs:translationOf/bs:publicationYear ?original .
              FILTER ( xsd:integer(STR(?value)) < xsd:integer(STR(?original)) )
            }
        """ ;
    ] .

bt:GenreShape
    a               sh:NodeShape ;
    sh:targetClass  skos:Concept ;
    sh:property [ sh:path skos:inScheme ; sh:hasValue bt:genre-scheme ; sh:message "{$this} is in no scheme." ] ;
    sh:property [ sh:path [ sh:zeroOrMorePath skos:broader ] ; sh:hasValue bt:genre-literature ;
                  sh:message "{$this} does not lead up to the top of the scheme." ] .

# ---------------------------------------------------------------- people and publishers

bt:PersonShape
    a               sh:NodeShape ;
    sh:targetClass  bs:Person ;
    sh:property [ sh:path rdfs:label ; sh:minCount 1 ] ;
    sh:property [ sh:path bs:born ; sh:minCount 1 ; sh:maxCount 1 ; sh:datatype xsd:gYear ] ;
    sh:property [ sh:path bs:died ; sh:maxCount 1 ; sh:datatype xsd:gYear ] ;
    sh:property [ sh:path bs:basedIn ; sh:maxCount 1 ; sh:class bs:Settlement ;
                  sh:message "{$this} is based in {$value}, which is not a settlement." ] ;
    sh:property [ sh:path bs:writesIn ; sh:in ( "en" "cy" "gd" "ar" ) ;
                  sh:message "{$value} is not one of the dataset's writing languages." ] ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:message   "{$this} died in {$value}, before being born." ;
        sh:select    """
            SELECT $this ?value WHERE {
              $this bs:born ?born ; bs:died ?value .
              FILTER ( xsd:integer(STR(?value)) < xsd:integer(STR(?born)) )
            }
        """ ;
    ] ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:severity  sh:Warning ;
        sh:message   "{$this} is in a cycle of influence. Three authors are, by design (q34)." ;
        sh:select    "SELECT $this WHERE { $this bs:influencedBy+ $this }" ;
    ] .

bt:PublisherShape
    a               sh:NodeShape ;
    sh:targetClass  bs:Publisher ;
    sh:property [ sh:path rdfs:label ; sh:minCount 1 ] ;
    sh:property [ sh:path bs:locatedIn ; sh:maxCount 1 ; sh:class bs:Settlement ] ;
    sh:property [ sh:path bs:imprintOf ; sh:maxCount 1 ; sh:class bs:Publisher ] ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:message   "{$this} is an imprint of itself." ;
        sh:select    "SELECT $this WHERE { $this bs:imprintOf+ $this }" ;
    ] .

# ---------------------------------------------------------------- events

bt:EventShape
    a                    sh:NodeShape ;
    sh:targetClass       bs:Event ;
    sh:targetSubjectsOf  bs:heldAt ;
    sh:class             bs:Event ;
    sh:message           "{$this} is not typed as an event." ;
    sh:property [ sh:path bs:heldAt ; sh:minCount 1 ; sh:maxCount 1 ; sh:class bs:Bookshop ;
                  sh:message "Events are held at bookshops; {$value} is not one." ] ;
    sh:property [ sh:path bs:eventDate ; sh:minCount 1 ; sh:maxCount 1 ; sh:datatype xsd:date ;
                  sh:message "{$value} is not a well-formed date." ] ;
    sh:property [ sh:path bs:eventKind ; sh:minCount 1 ;
                  sh:in ( "Reading" "Signing" "Panel" "Workshop" "Launch" "Lecture" "Book Club" ) ;
                  sh:message "{$value} is not a kind of event this trail runs." ] ;
    sh:property [ sh:path bs:featuring ; sh:minCount 1 ; sh:class bs:Author ;
                  sh:message "An event features at least one author." ] ;
    sh:property [ sh:path bs:attendance ; sh:datatype xsd:integer ; sh:minInclusive 0 ] ;
    sh:property [ sh:path bs:ticketPrice ; sh:maxCount 1 ; sh:datatype xsd:decimal ; sh:minInclusive 0 ;
                  sh:message "Ticket price {$value} is not a non-negative decimal." ] ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:severity  sh:Warning ;
        sh:message   "{$this} features {$value}, who had died by then. A memorial, or a mistake?" ;
        sh:select    """
            SELECT $this ?value WHERE {
              $this bs:featuring ?value ; bs:eventDate ?date . ?value bs:died ?died .
              FILTER ( xsd:integer(SUBSTR(STR(?date), 1, 4)) > xsd:integer(STR(?died)) )
            }
        """ ;
    ] .

# ---------------------------------------------------------------- trail and stock

bt:TrailSegmentShape
    a               sh:NodeShape ;
    sh:targetClass  bs:TrailSegment ;
    sh:property [ sh:path bs:segmentFrom ; sh:minCount 1 ; sh:maxCount 1 ; sh:class bs:Bookshop ;
                  sh:disjoint bs:segmentTo ; sh:message "{$this} starts and ends at {$value}." ] ;
    sh:property [ sh:path bs:segmentTo ; sh:minCount 1 ; sh:maxCount 1 ; sh:class bs:Bookshop ] ;
    sh:property [ sh:path bs:distanceKm ; sh:minCount 1 ; sh:maxCount 1 ; sh:datatype xsd:decimal ;
                  sh:minExclusive 0 ; sh:message "A segment of {$value} km goes nowhere." ] .

bt:StockRecordShape
    a               sh:NodeShape ;
    sh:targetClass  bs:StockRecord ;
    sh:property [ sh:path bs:atShop ; sh:minCount 1 ; sh:maxCount 1 ; sh:class bs:Bookshop ;
                  sh:message "{$value} is not typed as a bookshop." ] ;
    sh:property [ sh:path bs:ofWork ; sh:minCount 1 ; sh:maxCount 1 ; sh:class bs:Work ] ;
    sh:property [ sh:path bs:copies ; sh:minCount 1 ; sh:maxCount 1 ; sh:datatype xsd:integer ; sh:minInclusive 0 ;
                  sh:message "{$value} copies." ] ;
    sh:property [
        sh:path bs:shelfPrice ; sh:minCount 1 ; sh:maxCount 1 ; sh:datatype xsd:decimal ; sh:minInclusive 0 ;
        sh:sparql [
            sh:prefixes  bt:prefixes ;
            sh:message   "{$value} is more than twice the recommended price." ;
            sh:select    "SELECT $this ?value WHERE { $this $PATH ?value ; bs:ofWork/bs:rrp ?rrp . FILTER ( ?value > 2 * ?rrp ) }" ;
        ] ;
    ] .

bt:SourceShape
    a               sh:NodeShape ;
    sh:targetClass  bs:Source ;
    sh:property [ sh:path rdfs:label ; sh:minCount 1 ] ;
    sh:property [ sh:path bs:sourceKind ; sh:in ( "guidebook" "survey" "self-reported" "register" "newspaper" ) ;
                  sh:message "{$value} is not a recognised kind of source." ] ;
    sh:property [ sh:path bs:confidence ; sh:datatype xsd:decimal ; sh:minInclusive 0 ; sh:maxInclusive 1 ;
                  sh:message "Confidence {$value} is not between 0 and 1." ] .

How it works

This is the course's shapes in one file, organised the way a real shapes graph is: one node shape per class, every property shape named, a message on everything, and severities that say what kind of finding each is. The Core constraints come from modules 01 to 04, the SPARQL constraints from 05, the ISBN check digit from 06. Nothing in it compares xsd:gYear values with a Core component; the year comparisons are SPARQL with the STR cast. On the clean data the graph reports no violations and six warnings, all of them known features of the dataset: the two unreachable shops, the three authors in the cycle of influence, and the lecture given by an author who had died. On the faulty data it reports every fault from F01 to F31, and the checker that builds this course confirms each one is there. Read the report as an answer key: each row's focus node is a resource the faults file describes.

Diagram

   one node shape per class            bt:BookshopShape, bt:SettlementShape, bt:WorkShape, ...
   named property shapes               bt:BookshopShape-founded, ...
   severities                          Violation for errors, Warning for known exceptions, Info for facts
   SPARQL where Core cannot            cycles, joins, year comparisons
   a constraint component              the ISBN check digit

   clean data:   0 violations, 6 warnings
   faulty data:  every fault F01 to F31, by focus node

What to take away

  • A shapes graph is a document about the data model. Name the shapes, write the messages, and it reads as one.
  • Severity is how the graph records what is an error and what is a known feature. A report with warnings and no violations is a clean run.
  • Keep a faulty edition of the data beside the shapes and run both. A shape that finds nothing in the faulty data is not doing its job.

The report

Does not conform · 67 violations, 9 warnings, 0 info · 100 shapes

SeverityFocus nodePathValueMessageShape
Violationbt:author-owen-harker1965https://example.org/bookshop-trail/author-owen-harker died in 1965, before being born.bt:PersonShape
Violationbt:author-owen-harkerbs:basedInbt:place-walesbt:author-owen-harker is based in bt:place-wales, which is not a settlement.bt:PersonShape › property 4
Violationbt:author-owen-harkerbs:writesInfrfr is not one of the dataset's writing languages.bt:PersonShape › property 5
Violationbt:book-precocious1990https://example.org/bookshop-trail/book-precocious was published in 1990, before its author was born.bt:WorkShape
Violationbt:book-precociousbs:isbn9780141187762The check digit of 9780141187762 is wrong.bt:WorkShape-isbn
Violationbt:book-the-dark-sea-fr2010https://example.org/bookshop-trail/book-the-dark-sea-fr was published in 2010, before the work it translates.bt:TranslationShape
Violationbt:book-the-margin-notesbs:isbn978-0-14-118776-1An ISBN-13 is thirteen digits starting 978 or 979: 978-0-14-118776-1.bt:WorkShape-isbn
Violationbt:book-the-margin-notesbs:pages00 pages is not a book.bt:WorkShape-pages
Violationbt:book-the-margin-notesbs:rrp-4.99A negative price: -4.99.bt:WorkShape-rrp
Violationbt:book-unnumberedbt:book-unnumberedbt:book-unnumbered has no ISBN and was published after 1969.bt:WorkShape
Violationbt:book-unnumbered_:0_b46bt:book-unnumbered is filed under a genre that is not in the scheme.bt:WorkShape-genre
Violationbt:book-unnumberedbs:publishedBybt:pub-orbitbt:pub-orbit is not a well-formed publisher.bt:WorkShape-publishedBy

and 64 more

Defined in

S65

Shapes as questions

Six facts about the clean data, asked as shapes and answered as information.

Data: bookshop-trail-1.1.ttl

@prefix bt:  <https://example.org/bookshop-trail/> .
@prefix bs:  <https://example.org/bookshop-trail/schema#> .
@prefix sh:  <http://www.w3.org/ns/shacl#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

# Prefixes for the SPARQL inside this file. @prefix lines are Turtle syntax
# and do not reach the query engine; sh:declare does.
bt:prefixes
    a  owl:Ontology ;
    sh:declare [ sh:prefix "bt" ;   sh:namespace "https://example.org/bookshop-trail/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "bs" ;   sh:namespace "https://example.org/bookshop-trail/schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdf" ;  sh:namespace "http://www.w3.org/1999/02/22-rdf-syntax-ns#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdfs" ; sh:namespace "http://www.w3.org/2000/01/rdf-schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "xsd" ;  sh:namespace "http://www.w3.org/2001/XMLSchema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "skos" ; sh:namespace "http://www.w3.org/2004/02/skos/core#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "dct" ;  sh:namespace "http://purl.org/dc/terms/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "geo" ;  sh:namespace "http://www.opengis.net/ont/geosparql#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "schema" ; sh:namespace "https://schema.org/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "sh" ;   sh:namespace "http://www.w3.org/ns/shacl#"^^xsd:anyURI ] .

bt:NoWebsite
    a sh:NodeShape ; sh:targetClass bs:Bookshop ;
    sh:property [ sh:path bs:website ; sh:minCount 1 ; sh:severity sh:Info ;
                  sh:message "{$this} has no website on record." ] .

bt:NoISBN
    a sh:NodeShape ; sh:targetClass bs:Work ;
    sh:property [ sh:path bs:isbn ; sh:minCount 1 ; sh:severity sh:Info ;
                  sh:message "{$this} has no ISBN." ] .

bt:TownWithoutShop
    a sh:NodeShape ; sh:targetClass bs:Settlement ;
    sh:property [ sh:path [ sh:inversePath bs:locatedIn ] ; sh:minCount 1 ; sh:severity sh:Info ;
                  sh:message "{$this} has no bookshop." ] .

bt:Unstocked
    a sh:NodeShape ; sh:targetClass bs:Work ;
    sh:property [ sh:path [ sh:inversePath bs:ofWork ] ; sh:minCount 1 ; sh:severity sh:Info ;
                  sh:message "No shop stocks {$this}." ] .

bt:Junction
    a sh:NodeShape ; sh:targetClass bs:Bookshop ;
    sh:property [ sh:path [ sh:alternativePath ( bs:connectsTo [ sh:inversePath bs:connectsTo ] ) ] ;
                  sh:maxCount 2 ; sh:severity sh:Info ;
                  sh:message "{$this} is a junction on the trail." ] .

bt:OffSpecialism
    a sh:NodeShape ; sh:targetClass bs:Bookshop ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:severity  sh:Info ;
        sh:message   "{$this} stocks nothing filed under its own specialism." ;
        sh:select    """
            SELECT $this WHERE {
              $this bs:specialises ?genre .
              FILTER NOT EXISTS { $this bs:stocks/bs:genre/skos:broader* ?genre }
            }
        """ ;
    ] .

How it works

Nothing in this file is an error. Each shape asks a question the SPARQL course asks with a query, and the answer is a set of Info rows: the six shops without a website (q14), the eleven works with no ISBN (q15), the four towns with no bookshop (q16), the eight works no shop stocks, the three junctions on the trail, and the shops that stock nothing in their own specialism. A validation report at Info severity is a profile of the data, and a shapes file like this one can be kept and run whenever the data changes. It is the SPARQL course's q05 and q06 -- what is in here, what can I ask -- with the questions written down.

Diagram

   sh:severity sh:Info      a row per answer, no effect on conformance

   question                                 asked with
   shops without a website                  sh:minCount on bs:website
   works with no ISBN                       sh:minCount on bs:isbn
   towns with no bookshop                   sh:minCount on ^bs:locatedIn
   works nobody stocks                      sh:minCount on ^bs:ofWork
   junctions on the trail                   sh:maxCount 2 on (bs:connectsTo | ^bs:connectsTo)
   shops stocking nothing in their specialism   a SPARQL constraint

What to take away

  • A shape with Info severity is a question. The report is the answer, one row per node.
  • Profiles written as shapes are repeatable. Run the same file on next month's data and diff the reports.
  • Every question here has a query in the SPARQL course. Choose the form by what you want back: a table, or a report.

The report

Does not conform · 0 violations, 0 warnings, 36 info · 12 shapes

SeverityFocus nodePathValueMessageShape
Infobt:book-afon-hirbs:isbnbt:book-afon-hir has no ISBN.bt:NoISBN › property 1
Infobt:book-cold-harbourbs:isbnbt:book-cold-harbour has no ISBN.bt:NoISBN › property 1
Infobt:book-evensong-for-a-thiefbs:isbnbt:book-evensong-for-a-thief has no ISBN.bt:NoISBN › property 1
Infobt:book-hedgerow-alphabetbs:isbnbt:book-hedgerow-alphabet has no ISBN.bt:NoISBN › property 1
Infobt:book-herring-and-hempbs:isbnbt:book-herring-and-hemp has no ISBN.bt:NoISBN › property 1
Infobt:book-high-water^bs:ofWorkNo shop stocks bt:book-high-water.bt:Unstocked › property 1
Infobt:book-llyfr-y-mynyddbs:isbnbt:book-llyfr-y-mynydd has no ISBN.bt:NoISBN › property 1
Infobt:book-nine-winters^bs:ofWorkNo shop stocks bt:book-nine-winters.bt:Unstocked › property 1
Infobt:book-north-of-the-tweedbs:isbnbt:book-north-of-the-tweed has no ISBN.bt:NoISBN › property 1
Infobt:book-the-govan-inheritance^bs:ofWorkNo shop stocks bt:book-the-govan-inheritance.bt:Unstocked › property 1
Infobt:book-the-lamp-room^bs:ofWorkNo shop stocks bt:book-the-lamp-room.bt:Unstocked › property 1
Infobt:book-the-lamp-roombs:isbnbt:book-the-lamp-room has no ISBN.bt:NoISBN › property 1

and 24 more

Defined in

S66

From report to issues

A report turned into bs:DataIssue records -- the vocabulary already has a class for them.

Data: bookshop-trail-faulty.ttl

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix sh:   <http://www.w3.org/ns/shacl#> .
@prefix rdf:  <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd:  <http://www.w3.org/2001/XMLSchema#> .

bt:BookshopShape
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:property [ sh:path rdfs:label ;    sh:minCount 1 ; sh:datatype rdf:langString ;
                  sh:message "A shop has a name with a language tag." ] ;
    sh:property [ sh:path bs:locatedIn ;  sh:minCount 1 ; sh:maxCount 1 ; sh:class bs:Settlement ;
                  sh:message "A shop is in exactly one settlement." ] ;
    sh:property [ sh:path bs:founded ;    sh:datatype xsd:gYear ;
                  sh:message "A founding year is an xsd:gYear." ] ;
    sh:property [ sh:path bs:staffCount ; sh:datatype xsd:integer ; sh:minInclusive 1 ;
                  sh:message "Staff count is a positive integer." ] ;
    sh:property [ sh:path bs:website ;    sh:maxCount 1 ; sh:severity sh:Warning ;
                  sh:message "At most one website." ] .

bt:EventShape
    a               sh:NodeShape ;
    sh:targetClass  bs:Event ;
    sh:property [ sh:path bs:heldAt ;    sh:class bs:Bookshop ; sh:message "Held somewhere that is not a bookshop." ] ;
    sh:property [ sh:path bs:featuring ; sh:minCount 1 ;       sh:message "Nobody is featured." ] ;
    sh:property [ sh:path bs:eventDate ; sh:datatype xsd:date ; sh:message "Not a well-formed date." ] .

How it works

The vocabulary declares bs:DataIssue, with bs:about and bs:message, and the clean data never uses them. They are what a validation result becomes once someone has to act on it. Validate with the shapes here, open the report as a tab, and run the CONSTRUCT: one issue per result, pointing at the focus node, carrying the message and the severity, and naming the constraint component so the issue can be sorted by kind. The result opens as a new tab, in the dataset's own vocabulary, ready to be saved next to the data or loaded into a tracker. The second query is the same list as a table. The SPARQL course's module 12 is about queries that hand a graph back; this is one of them.

Diagram

   report                                        issues
   _:r  a sh:ValidationResult ;                  _:i  a bs:DataIssue ;
        sh:focusNode  bt:shop-foxed-page ;   ->       bs:about    bt:shop-foxed-page ;
        sh:resultMessage "..." ;                      bs:message  "..." ;
        sh:resultSeverity sh:Violation ;              sh:resultSeverity sh:Violation ;
        sh:sourceConstraintComponent ... .            dct:type    sh:DatatypeConstraintComponent .

What to take away

  • A report is input as well as output. CONSTRUCT turns it into whatever the next process needs.
  • Keep the focus node, the message, the severity and the component: they are what a person needs to act.
  • The dataset's own vocabulary is the right target when the issues will live beside the data.

The report

Does not conform · 10 violations, 1 warning, 0 info · 10 shapes

SeverityFocus nodePathValueMessageShape
Violationbt:event-foxed-page-2025-05-01bs:eventDate2025-13-01Not a well-formed date.bt:EventShape › property 3
Violationbt:event-foxed-page-2025-05-01bs:featuringNobody is featured.bt:EventShape › property 2
Violationbt:event-foxed-page-2025-05-01bs:heldAtbt:pub-picaHeld somewhere that is not a bookshop.bt:EventShape › property 1
Violationbt:shop-foxed-pagebs:founded1985A founding year is an xsd:gYear.bt:BookshopShape › property 3
Violationbt:shop-foxed-pagebs:locatedInKendalA shop is in exactly one settlement.bt:BookshopShape › property 2
Violationbt:shop-foxed-pagebs:staffCount3.5Staff count is a positive integer.bt:BookshopShape › property 4
Violationbt:shop-foxed-pagerdfs:labelThe Foxed PageA shop has a name with a language tag.bt:BookshopShape › property 1
Violationbt:shop-halfmoonbs:staffCount0Staff count is a positive integer.bt:BookshopShape › property 4
Violationbt:shop-halfmoonrdfs:labelA shop has a name with a language tag.bt:BookshopShape › property 1
Violationbt:shop-inkwellbs:locatedInA shop is in exactly one settlement.bt:BookshopShape › property 2
Warningbt:shop-foxed-pagebs:websiteAt most one website.bt:BookshopShape › property 5

Afterwards, in the SPARQL panel

Issues, as RDF in the dataset's vocabulary (on the report)
PREFIX sh:  <http://www.w3.org/ns/shacl#>
PREFIX bs:  <https://example.org/bookshop-trail/schema#>
PREFIX dct: <http://purl.org/dc/terms/>
CONSTRUCT {
  [] a bs:DataIssue ;
     bs:about ?focus ;
     bs:message ?message ;
     sh:resultSeverity ?severity ;
     dct:type ?component .
}
WHERE {
  ?r a sh:ValidationResult ;
     sh:focusNode ?focus ;
     sh:resultSeverity ?severity ;
     sh:sourceConstraintComponent ?component .
  OPTIONAL { ?r sh:resultMessage ?message }
}
The same list as a table, worst first (on the report)
PREFIX sh: <http://www.w3.org/ns/shacl#>
SELECT ?severity ?focus ?path ?message
WHERE {
  ?r a sh:ValidationResult ;
     sh:focusNode ?focus ;
     sh:resultSeverity ?severity .
  OPTIONAL { ?r sh:resultPath ?path }
  OPTIONAL { ?r sh:resultMessage ?message }
}
ORDER BY ?severity ?focus

Defined in

S67

A rule set for the trail

Infer each shop's country and each record's value, then check what only the inferred data can answer.

Data: bookshop-trail-1.1.ttl · Inference rules

@prefix bt:  <https://example.org/bookshop-trail/> .
@prefix bs:  <https://example.org/bookshop-trail/schema#> .
@prefix sh:  <http://www.w3.org/ns/shacl#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

# Prefixes for the SPARQL inside this file. @prefix lines are Turtle syntax
# and do not reach the query engine; sh:declare does.
bt:prefixes
    a  owl:Ontology ;
    sh:declare [ sh:prefix "bt" ;   sh:namespace "https://example.org/bookshop-trail/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "bs" ;   sh:namespace "https://example.org/bookshop-trail/schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdf" ;  sh:namespace "http://www.w3.org/1999/02/22-rdf-syntax-ns#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdfs" ; sh:namespace "http://www.w3.org/2000/01/rdf-schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "xsd" ;  sh:namespace "http://www.w3.org/2001/XMLSchema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "skos" ; sh:namespace "http://www.w3.org/2004/02/skos/core#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "dct" ;  sh:namespace "http://purl.org/dc/terms/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "geo" ;  sh:namespace "http://www.opengis.net/ont/geosparql#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "schema" ; sh:namespace "https://schema.org/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "sh" ;   sh:namespace "http://www.w3.org/ns/shacl#"^^xsd:anyURI ] .

bt:ShopRules
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:rule [
        a  sh:TripleRule ; sh:subject sh:this ; sh:predicate bs:inCountry ;
        sh:object [ sh:filterShape [ sh:class bs:Country ] ;
                    sh:nodes [ sh:path ( bs:locatedIn [ sh:oneOrMorePath bs:within ] ) ] ] ;
    ] .

bt:StockRules
    a               sh:NodeShape ;
    sh:targetClass  bs:StockRecord ;
    sh:rule [
        a  sh:SPARQLRule ; sh:prefixes bt:prefixes ;
        sh:construct "CONSTRUCT { $this bs:stockValue ?v } WHERE { $this bs:copies ?c ; bs:shelfPrice ?p . BIND ( ?c * ?p AS ?v ) }" ;
    ] .

bt:WorkRules
    a               sh:NodeShape ;
    sh:targetClass  bs:Work ;
    sh:rule [
        a  sh:TripleRule ; sh:subject sh:this ; sh:predicate bs:contributor ;
        sh:object [ sh:union ( [ sh:path bs:author ] [ sh:path bs:translatedBy ] ) ] ;
    ] .

bt:WelshStock
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:severity  sh:Warning ;
        sh:message   "{$this} is in Wales and stocks nothing written in Welsh." ;
        sh:select    """
            SELECT $this WHERE {
              $this bs:inCountry bt:place-wales .
              FILTER NOT EXISTS { $this bs:stocks/bs:originalLanguage "cy" }
            }
        """ ;
    ] .

bt:HighValueStock
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:severity  sh:Info ;
        sh:message   "{$this} holds more than five hundred pounds of stock at shelf prices." ;
        sh:select    """
            SELECT $this WHERE {
              { SELECT $this (SUM(?v) AS ?total)
                WHERE { ?record bs:atShop $this ; bs:stockValue ?v }
                GROUP BY $this }
              FILTER ( ?total > 500 )
            }
        """ ;
    ] .

How it works

Three rules run before the checks: bs:inCountry from module 07's filter shape expression, bs:stockValue from its SPARQL rule, and bs:contributor from its union. Two constraints then use them. bt:WelshStock asks whether every shop in Wales stocks at least one work written in Welsh; one does not, and is reported as a warning. bt:HighValueStock sums the inferred values per shop and reports the shops holding more than five hundred pounds of stock as information. Neither question is askable of the asserted data without repeating the inference inside the query; with the rules in place the constraints are short, and the inferences are available to every shape in the file.

Diagram

   order 0   rules      shop  bs:inCountry  country
                        record bs:stockValue copies x price
                        work  bs:contributor author, translator

   then      shapes     shops in Wales with no Welsh-language stock          warning
                        shops whose bs:stockValue sums to more than 500      info

What to take away

  • A rule set is a small ontology of derived properties. Write it once, and every shape in the file can use them.
  • Constraints over inferred properties read like constraints over asserted ones, which is the reason to infer them.
  • Keep the rules and the shapes that depend on them in one file, with the Inference mode in the LOAD IT link, so a reader cannot run one without the other.

The report

Does not conform · 0 violations, 1 warning, 6 info · 11 shapes · inference rules

SeverityFocus nodePathValueMessageShape
Warningbt:shop-clock-towerbt:shop-clock-towerhttps://example.org/bookshop-trail/shop-clock-tower is in Wales and stocks nothing written in Welsh.bt:WelshStock
Infobt:shop-chapter-versebt:shop-chapter-versehttps://example.org/bookshop-trail/shop-chapter-verse holds more than five hundred pounds of stock at shelf prices.bt:HighValueStock
Infobt:shop-colophonbt:shop-colophonhttps://example.org/bookshop-trail/shop-colophon holds more than five hundred pounds of stock at shelf prices.bt:HighValueStock
Infobt:shop-cotton-quartobt:shop-cotton-quartohttps://example.org/bookshop-trail/shop-cotton-quarto holds more than five hundred pounds of stock at shelf prices.bt:HighValueStock
Infobt:shop-endpapersbt:shop-endpapershttps://example.org/bookshop-trail/shop-endpapers holds more than five hundred pounds of stock at shelf prices.bt:HighValueStock
Infobt:shop-ex-librisbt:shop-ex-librishttps://example.org/bookshop-trail/shop-ex-libris holds more than five hundred pounds of stock at shelf prices.bt:HighValueStock
Infobt:shop-taff-marginbt:shop-taff-marginhttps://example.org/bookshop-trail/shop-taff-margin holds more than five hundred pounds of stock at shelf prices.bt:HighValueStock

Defined in

S68

Challenge: the walking tour

Four questions about the trail that each need two or three techniques at once. Try them before reading the shapes.

Data: bookshop-trail-1.1.ttl · Inference rules

@prefix bt:  <https://example.org/bookshop-trail/> .
@prefix bs:  <https://example.org/bookshop-trail/schema#> .
@prefix sh:  <http://www.w3.org/ns/shacl#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

# Prefixes for the SPARQL inside this file. @prefix lines are Turtle syntax
# and do not reach the query engine; sh:declare does.
bt:prefixes
    a  owl:Ontology ;
    sh:declare [ sh:prefix "bt" ;   sh:namespace "https://example.org/bookshop-trail/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "bs" ;   sh:namespace "https://example.org/bookshop-trail/schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdf" ;  sh:namespace "http://www.w3.org/1999/02/22-rdf-syntax-ns#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdfs" ; sh:namespace "http://www.w3.org/2000/01/rdf-schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "xsd" ;  sh:namespace "http://www.w3.org/2001/XMLSchema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "skos" ; sh:namespace "http://www.w3.org/2004/02/skos/core#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "dct" ;  sh:namespace "http://purl.org/dc/terms/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "geo" ;  sh:namespace "http://www.opengis.net/ont/geosparql#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "schema" ; sh:namespace "https://schema.org/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "sh" ;   sh:namespace "http://www.w3.org/ns/shacl#"^^xsd:anyURI ] .

bt:BookTownShape
    a  sh:NodeShape ;
    sh:target [ a sh:SPARQLTarget ; sh:prefixes bt:prefixes ;
                sh:select "SELECT ?this WHERE { ?this bs:isBookTown true }" ] ;
    sh:property [ sh:path [ sh:inversePath bs:locatedIn ] ; sh:minCount 2 ;
                  sh:message "{$this} is a book town with fewer than two shops." ] .

bt:SegmentLinked
    a               sh:NodeShape ;
    sh:targetClass  bs:TrailSegment ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:message   "The two ends of {$this} are not linked by bs:connectsTo." ;
        sh:select    """
            SELECT $this WHERE {
              $this bs:segmentFrom ?f ; bs:segmentTo ?t .
              FILTER NOT EXISTS { { ?f bs:connectsTo ?t } UNION { ?t bs:connectsTo ?f } }
            }
        """ ;
    ] ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:message   "{$this} duplicates {$value}." ;
        sh:select    """
            SELECT $this ?value WHERE {
              $this bs:segmentFrom ?f ; bs:segmentTo ?t .
              ?value bs:segmentFrom ?f ; bs:segmentTo ?t .
              FILTER ( ?value != $this )
            }
        """ ;
    ] .

bt:ShopCountry
    a               sh:NodeShape ;
    sh:targetClass  bs:Bookshop ;
    sh:rule [
        a  sh:TripleRule ; sh:subject sh:this ; sh:predicate bs:inCountry ;
        sh:object [ sh:filterShape [ sh:class bs:Country ] ;
                    sh:nodes [ sh:path ( bs:locatedIn [ sh:oneOrMorePath bs:within ] ) ] ] ;
    ] .

bt:BorderCrossing
    a               sh:NodeShape ;
    sh:targetClass  bs:TrailSegment ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:severity  sh:Info ;
        sh:message   "{$this} crosses a border into {$value}." ;
        sh:select    """
            SELECT $this ?value WHERE {
              $this bs:segmentFrom ?f ; bs:segmentTo ?t .
              ?f bs:inCountry ?from . ?t bs:inCountry ?value .
              FILTER ( ?from != ?value )
            }
        """ ;
    ] .

How it works

Every book town has at least two shops: a SPARQL target and an inverse path with sh:minCount 2 (all three pass). Every segment joins two shops that bs:connectsTo links in one direction or the other: a SPARQL constraint with a UNION (all pass). No two segments join the same pair of shops: a SPARQL constraint with a second segment variable (all pass). And which segments cross a border: a rule infers each shop's country first, then a constraint compares the two ends and reports the three that differ as information. The SPARQL course's module 14 sets challenges in the same spirit; these are the validation versions.

Diagram

   book towns have two shops        sh:target [ SPARQL ]  +  sh:path [ sh:inversePath bs:locatedIn ] ; sh:minCount 2
   segment ends are linked          sh:sparql with { ?f bs:connectsTo ?t } UNION { ?t bs:connectsTo ?f }
   no duplicate segments            sh:sparql joining a second segment on the same two ends
   border crossings                 a rule for bs:inCountry, then a constraint comparing the ends

What to take away

  • Most real constraints are two techniques: a target that says who, and a path or a query that says what.
  • A rule can do the join once so that several constraints stay simple.
  • Passing constraints are worth keeping. They are the ones that will catch the next edit.

The report

Does not conform · 0 violations, 0 warnings, 3 info · 8 shapes · inference rules

SeverityFocus nodePathValueMessageShape
Infobt:seg-dog-eared-castle-stepsbt:place-waleshttps://example.org/bookshop-trail/seg-dog-eared-castle-steps crosses a border into https://example.org/bookshop-trail/place-wales.bt:BorderCrossing
Infobt:seg-gutter-gilt-borderprintbt:place-englandhttps://example.org/bookshop-trail/seg-gutter-gilt-borderprint crosses a border into https://example.org/bookshop-trail/place-england.bt:BorderCrossing
Infobt:seg-taff-margin-crescentbt:place-englandhttps://example.org/bookshop-trail/seg-taff-margin-crescent crosses a border into https://example.org/bookshop-trail/place-england.bt:BorderCrossing

Defined in

S69

A function you cannot call

A SHACL function that turns a gYear into an integer -- declared, refused by this engine, and replaced by the same expression inline.

Data: bookshop-trail-1.1.ttl

@prefix bt:  <https://example.org/bookshop-trail/> .
@prefix bs:  <https://example.org/bookshop-trail/schema#> .
@prefix sh:  <http://www.w3.org/ns/shacl#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

# Prefixes for the SPARQL inside this file. @prefix lines are Turtle syntax
# and do not reach the query engine; sh:declare does.
bt:prefixes
    a  owl:Ontology ;
    sh:declare [ sh:prefix "bt" ;   sh:namespace "https://example.org/bookshop-trail/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "bs" ;   sh:namespace "https://example.org/bookshop-trail/schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdf" ;  sh:namespace "http://www.w3.org/1999/02/22-rdf-syntax-ns#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdfs" ; sh:namespace "http://www.w3.org/2000/01/rdf-schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "xsd" ;  sh:namespace "http://www.w3.org/2001/XMLSchema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "skos" ; sh:namespace "http://www.w3.org/2004/02/skos/core#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "dct" ;  sh:namespace "http://purl.org/dc/terms/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "geo" ;  sh:namespace "http://www.opengis.net/ont/geosparql#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "schema" ; sh:namespace "https://schema.org/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "sh" ;   sh:namespace "http://www.w3.org/ns/shacl#"^^xsd:anyURI ] .

bt:yearOf
    a  sh:SPARQLFunction ;
    sh:parameter   [ sh:path bt:year ] ;
    sh:returnType  xsd:integer ;
    sh:prefixes    bt:prefixes ;
    sh:select      "SELECT ( xsd:integer(STR($year)) AS ?result ) WHERE { }" .

# Calls the function. Deactivated so that this file runs on this engine.
bt:BornBefore1920
    a               sh:NodeShape ;
    sh:deactivated  true ;
    sh:targetClass  bs:Author ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:severity  sh:Info ;
        sh:message   "{$this} was born before 1920 (via bt:yearOf)." ;
        sh:select    "SELECT $this WHERE { $this bs:born ?born . FILTER ( bt:yearOf(?born) < 1920 ) }" ;
    ] .

# The same test, inline.
bt:BornBefore1920Inline
    a               sh:NodeShape ;
    sh:targetClass  bs:Author ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:severity  sh:Info ;
        sh:message   "{$this} was born in {$value}, before 1920." ;
        sh:select    "SELECT $this ?value WHERE { $this bs:born ?value . FILTER ( xsd:integer(STR(?value)) < 1920 ) }" ;
    ] .

How it works

SHACL-AF lets a shapes graph declare a sh:SPARQLFunction: a name, parameters, and a SELECT that computes ?result, callable from any SPARQL in the file. bt:yearOf would make the STR cast of s14 a one-word call. This engine does not implement SHACL functions, and says so: a constraint that calls one stops the run with 'The custom function ... is not supported'. That is the right kind of failure. The calling shape here is deactivated so the file runs; take the sh:deactivated line off to see the message. bt:BornBefore1920Inline is the same constraint with the cast written out, and it lists the authors born before 1920 as information. A function is a convenience that ties the shapes to the validators that have it; the inline form runs everywhere.

Diagram

   bt:yearOf  a sh:SPARQLFunction ;
       sh:parameter [ sh:path bt:year ] ;
       sh:returnType xsd:integer ;
       sh:select "SELECT (xsd:integer(STR($year)) AS ?result) WHERE {}" .

   FILTER ( bt:yearOf(?born) < 1920 )          this engine: error, "custom function ... is not supported"
   FILTER ( xsd:integer(STR(?born)) < 1920 )   every engine

What to take away

  • SHACL functions are SHACL-AF, optional, and not in this engine. The refusal is an error rather than silence.
  • Anything a function would compute can be written inline. Do that when the shapes have to travel.
  • pySHACL and TopBraid implement SHACL functions; Jena does not. Check before depending on one.

Try it

Remove sh:deactivated true from bt:BornBefore1920 and validate. The engine refuses the run and names the function.

The report

Does not conform · 0 violations, 0 warnings, 4 info · 5 shapes

SeverityFocus nodePathValueMessageShape
Infobt:author-gerard-tyne1912https://example.org/bookshop-trail/author-gerard-tyne was born in 1912, before 1920.bt:BornBefore1920Inline
Infobt:author-iolo-vaughan1908https://example.org/bookshop-trail/author-iolo-vaughan was born in 1908, before 1920.bt:BornBefore1920Inline
Infobt:author-maud-ellery1918https://example.org/bookshop-trail/author-maud-ellery was born in 1918, before 1920.bt:BornBefore1920Inline
Infobt:author-rhona-blackwood1901https://example.org/bookshop-trail/author-rhona-blackwood was born in 1901, before 1920.bt:BornBefore1920Inline

Defined in

S70

Targets the engine ignores

A SPARQL target type with a parameter, and sh:uniqueValuesFor: two features this build reads and does nothing with.

Data: bookshop-trail-1.1.ttl

@prefix bt:   <https://example.org/bookshop-trail/> .
@prefix bs:   <https://example.org/bookshop-trail/schema#> .
@prefix sh:   <http://www.w3.org/ns/shacl#> .
@prefix rdf:  <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix xsd:  <http://www.w3.org/2001/XMLSchema#> .

# Prefixes for the SPARQL inside this file. @prefix lines are Turtle syntax
# and do not reach the query engine; sh:declare does.
bt:prefixes
    a  owl:Ontology ;
    sh:declare [ sh:prefix "bt" ;   sh:namespace "https://example.org/bookshop-trail/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "bs" ;   sh:namespace "https://example.org/bookshop-trail/schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdf" ;  sh:namespace "http://www.w3.org/1999/02/22-rdf-syntax-ns#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "rdfs" ; sh:namespace "http://www.w3.org/2000/01/rdf-schema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "xsd" ;  sh:namespace "http://www.w3.org/2001/XMLSchema#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "skos" ; sh:namespace "http://www.w3.org/2004/02/skos/core#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "dct" ;  sh:namespace "http://purl.org/dc/terms/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "geo" ;  sh:namespace "http://www.opengis.net/ont/geosparql#"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "schema" ; sh:namespace "https://schema.org/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "sh" ;   sh:namespace "http://www.w3.org/ns/shacl#"^^xsd:anyURI ] .

bt:FoundedBefore
    a               sh:SPARQLTargetType ;
    rdfs:subClassOf sh:Target ;
    sh:parameter    [ sh:path bt:year ] ;
    sh:prefixes     bt:prefixes ;
    sh:select       "SELECT ?this WHERE { ?this bs:founded ?y . FILTER ( xsd:integer(STR(?y)) < $year ) }" .

# Never gets a focus node on this engine.
bt:OldShopShape
    a          sh:NodeShape ;
    sh:target  [ a bt:FoundedBefore ; bt:year 1950 ] ;
    sh:property [ sh:path bs:website ; sh:minCount 1 ; sh:severity sh:Info ;
                  sh:message "Never reported here: the target type is not implemented." ] .

# Read, not enforced.
bt:UniqueByComponent
    a               sh:NodeShape ;
    sh:targetClass  bs:Work ;
    sh:property [ sh:path bs:isbn ; sh:uniqueValuesFor bs:Work ;
                  sh:message "Never reported here: sh:uniqueValuesFor is not enforced." ] .

# The portable form.
bt:UniqueISBN
    a               sh:NodeShape ;
    sh:targetClass  bs:Work ;
    sh:sparql [
        sh:prefixes  bt:prefixes ;
        sh:message   "{$this} shares its ISBN {$value} with another work." ;
        sh:select    """
            SELECT $this ?value WHERE {
              $this bs:isbn ?value . ?other bs:isbn ?value . FILTER ( ?other != $this )
            }
        """ ;
    ] .

bt:Canary
    a              sh:NodeShape ;
    sh:targetNode  bt:shop-inkwell ;
    sh:property [ sh:path rdf:type ; sh:maxCount 0 ; sh:message "The canary: validation ran." ] .

How it works

SHACL-AF's sh:SPARQLTargetType declares a reusable, parameterised target: bt:FoundedBefore takes a year and selects the shops opened before it. This engine compiles the shape, counts it, and gives it no focus nodes, so bt:OldShopShape checks nothing and the report says nothing about it. sh:uniqueValuesFor, from SHACL 1.2 Core, is the same story: read, counted, not enforced, so two works with the same ISBN would pass. Neither produces an error, and that is the hazard the whole of module 10 is about. bt:UniqueISBN is the portable form of the second: a SPARQL constraint that finds a second work with the same ISBN. The canary is the one result. The table in this module's README lists every feature by what this build does with it.

Diagram

   feature                      this build        symptom
   sh:SPARQLTargetType          silent            no focus nodes, no error, shape counted
   sh:uniqueValuesFor           silent            duplicates pass
   sh:SPARQLFunction            error             "custom function ... is not supported" (s69)
   sh:resultAnnotation          silent            results appear without the annotation
   ?message in a constraint     silent            sh:message is used instead (s32)

   enforced since engine 0.3.0, and silent before it:
   sh:severity as annotation    enforced          the annotated severity is used
   sh:reificationRequired       enforced          unannotated values are reported

   canary  ->  1 row, so the run happened

What to take away

  • A feature that is read and ignored looks like conformance. Test each one with data that must fail.
  • For a parameterised target, a sh:SPARQLTarget with the value written in, or sh:targetWhere, does the same job here.
  • For uniqueness, a SPARQL constraint that looks for a second node with the same value runs on every engine.

Try it

Give two works the same ISBN in the data tab and validate. bt:UniqueISBN reports both; bt:UniqueByComponent reports neither.

The report

Does not conform · 1 violation, 0 warnings, 0 info · 8 shapes

SeverityFocus nodePathValueMessageShape
Violationbt:shop-inkwellrdf:typeThe canary: validation ran.bt:Canary › property 1

Defined in

Reference

The faults

Thirty-six deliberate mistakes, appended to the clean data to make bookshop-trail-faulty.ttl. Each says which lesson catches it, and scripts/check_faults.py confirms that it does. The clean files are the SPARQL course's, byte for byte.

FaultWhat is wrongResourceCaught by
F01A shop with no rdfs:label and no bs:foundedbt:shop-halfmoons02 s04
F02Its bs:staffCount is zerobt:shop-halfmoons13
F03It is also typed as a publisherbt:shop-halfmoons25
F04The Inkwell is placed in a second townbt:shop-inkwells04
F05A predicate with a typo. bs:foundedIn is not in the vocabulary,bt:shop-inkwells27
F06A shop where nearly every value is the wrong kind of thingbt:shop-foxed-pages04 s10 s11 s12 s13 s15
F07A shop that is described but never typed. sh:targetClass cannot seebt:shop-ghosts12
F08An ISBN written with hyphensbt:book-the-margin-notess15
F09Zero pages, and a negative pricebt:book-the-margin-notess13
F10An ISBN of the right shape with the wrong check digit.bt:book-precociouss39
F11Published in 1990 by an author born in 1995bt:book-precociouss31
F12A work from 1998 with no ISBN. Before 1970 that is allowed;bt:book-unnumbereds26
F13Published by bt:pub-orbit, which is not well formedbt:book-unnumbereds18 s24
F14A translation published six years before the originalbt:book-the-dark-sea-frs18
F15Died before being bornbt:author-owen-harkers14
F16Based in a country rather than a settlementbt:author-owen-harkers12
F17Writes in a language the dataset does not usebt:author-owen-harkers17
F18Influenced by himselfbt:author-owen-harkers30
F19A publisher located in a country, and an imprint of itselfbt:pub-orbits24 s30
F20An event with nearly everything wrongbt:event-foxed-page-2025-05-01s07 s10 s12 s13 s17
F21An event nobody typed. Invisible to sh:targetClass; found bybt:event-inkwell-2025-06-01s07 s58
F22A segment from The Inkwell to The Inkwell, of no lengthbt:seg-inkwell-inkwells13 s18
F23Negative copies, at three times the recommended pricebt:stock-foxed-page--the-book-towns13 s31
F24A record at the shop nobody typed (F07)bt:stock-ghost--the-book-towns12
F25Newtown, Powys -- a real town, with faults in every other columnbt:place-newtowns10 s13 s16 s28 s33 s64
F26Brecon, placed directly inside Wales with no council area betweenbt:place-brecons12
F27Two council areas each inside the other. Nothing in Core can saybt:place-loop-as22 s30
F28A French label on Kendalbt:place-kendals16
F29A concept in no scheme, under no broader conceptbt:genre-strays22
F30Two concepts, each broader than the otherbt:genre-loop-as22 s30
F31A source of a kind the list does not have, more than fully confidentbt:source-rumours13 s17
F32sh:shape in the data graph is a target in SHACL 1.2bt:shop-halfmoons56
F33A founding claim with no source and a confidence above onebt:shop-marginalias53
F34An attendance claimed by a string rather than a sourcebt:event-ex-libris-2025-01-18s54
F35A stock annotation that disagrees with its RDF 1.1 recordbt:shop-inkwells55
F36A shop notice that runs to two linesbt:shop-versos57
Reference

What this engine does with each feature

Measured against shacl-wasm-node 0.3.2, the build the editor ships, while the course was written. runs means a lesson shows it; error means the engine refuses and says why; silent means the feature is read and ignored, and the report looks the same as if it had passed. Module 12's README has the comparison with pySHACL.

FeatureDefined inThis buildLessonNote
SHACL Core constraint componentsSHACL section 4runss01-s29All of them, including sh:closed, sh:qualifiedValueShape and the property pair components.
Comparisons on xsd:gYearSHACL 4.3, 4.5runs, with a differences14 s18 s61Range and pair comparisons cannot compare gYear values and report every one. xsd:date compares as expected. Cast through STR() in a SPARQL constraint.
Recursive shapesSHACL 3.4.3runs, with a differences29Undefined by the specification. A check already in progress for the same node and shape is treated as passing, so a cycle in the data is not detected by recursion.
sh:severity on a node shapeSHACL 2.1.4runss05Applies to constraints on the node shape only; property shapes take their own. As specified.
Warnings, infos and sh:conformsSHACL 3.6.1.1runs, with a differences03Only sh:Violation counts against conformance in this build. The specification's default disallows Warning and Info too; engine 0.3.0 follows it.
Several sh:message valuesSHACL 2.1.5runs, with a differences05Joined into one string rather than reported as separate values.
sh:sparql constraints, $this, $PATH, ?value, ?pathSHACL section 5runss30-s38
?message in a SPARQL constraintSHACL 5.3.2silents32Ignored; sh:message is used. {?var} templates are not filled either.
$shapesGraph, $currentShapeSHACL 5.3.1runss37
sh:severity on a sh:sparql constraintSHACL 1.2 SPARQL 3.2runss32A 1.2 addition. SHACL 1.0 allows severity on shapes only, and a 1.0 validator such as pySHACL reports such rows as violations.
MINUS, VALUES, SERVICE under pre-bindingSHACL Appendix Aerrors34Refused with a message naming the construct. SERVICE is refused in constraints, targets and rules alike; it remains allowed in queries run from the SPARQL panel.
A SPARQL target whose query does not parseSHACL-AF 3.1silents33Selects nothing. The same query in sh:sparql is an error.
An aggregate projected as ?valueSHACL 5.3.2silents35The row is reported; sh:value is not set.
Custom constraint components, ASK and SELECT validatorsSHACL section 6runss39-s42
SELECT property validatorsSHACL 6.2.3.1runs, with a differences41 s42Run once per value node rather than once per focus node, so a validator that counts or tests for absence does not run when the path has no values. Use a node validator to count.
Parameters in sh:message ({$param})SHACL 6.2.2silents40Left as written.
sh:labelTemplateSHACL 6.2.2silents40Accepted; nothing in the report uses it.
SPARQL-based targets, sh:SPARQLTargetSHACL-AF 3.1runss33
SPARQL-based target types, sh:SPARQLTargetTypeSHACL-AF 3.2silents70Compiled and counted; gives the shape no focus nodes.
Result annotations, sh:resultAnnotationSHACL-AF section 4silentResults appear without the annotation.
SHACL functions, sh:SPARQLFunctionSHACL-AF section 5errors69'The custom function ... is not supported'.
Node expressions: sh:this, constants, [ sh:path ], filter shapes, sh:union, sh:intersectionSHACL-AF section 6runss43-s46Inside rules. Anything else is an error rather than an empty result.
Expression constraints, sh:expressionSHACL-AF section 7error'unsupported node expression' for every form but sh:this.
Triple rules, SPARQL rules, sh:condition, sh:order, sh:deactivatedSHACL-AF section 8runss43-s52One pass as specified; 'rules, iterated' repeats to a fixpoint, up to ten rounds.
RDFS inference before validationSHACL 1.5runss58 s59rdfs2, rdfs3, rdfs5, rdfs7, rdfs9, rdfs11. Opt-in.
sh:ShapeClassSHACL 1.2 Core 3.1.3.3runss08
sh:targetWhereSHACL 1.2 Core 3.1.3.6runss33
sh:shape in the data graphSHACL 1.2 Core 3.1.3.7runss56
sh:severity as an annotation on one constraintSHACL 1.2 Core 3.1.4silents57The result takes the shape's severity. Engine 0.3.0 honours it.
sh:select as a target or node expressionSHACL 1.2 SPARQL 6.1runss33As a target. sh:values with sh:select is not enforced in this build.
sh:reifierShapeSHACL 1.2 Core 7.8.5runss53
sh:reificationRequiredSHACL 1.2 Core 7.8.5silents53Parsed, not enforced. Engine 0.3.0 enforces it.
sh:nodeKind sh:TripleTermSHACL 1.2 Core 7.1.3error'not a known node kind'.
sh:singleLineSHACL 1.2 Core 7.4.4runss57
List constraints: sh:memberShape, sh:minListLength, sh:maxListLengthSHACL 1.2 Core 7.5runss57
sh:subsetOfSHACL 1.2 Core 7.6.3runsMeasured on a small example; the dataset has no natural pair of properties for it.
sh:uniqueValuesForSHACL 1.2 Core 7.9.5silents70Duplicates pass.
sh:values, sh:defaultValue (derived values)SHACL 1.2 Node ExpressionssilentThe property shape's other constraints see only asserted values.
The shnex: node expression librarySHACL 1.2 Node Expressions section 4errorNot implemented; the vocabulary changed between drafts in 2026.
SHACL 1.2 Rules (SPARQL-RL)SHACL 1.2 Ruleserrors52A different language from SHACL-AF rules; not implemented. s52 reads it for the stratification it adds.
SHACL-JS, the compact syntaxSHACL-JS, SHACL Compact SyntaxerrorNot implemented.
Named graphs in TriG or N-Quads dataSHACL 3.2runs, with a differenceMerged into one data graph. A result does not say which graph its focus node came from.
Annotations after ';' inside [ ... ]RDF 1.2 Turtleruns, with a differences57The editor's Turtle parser wants an annotation to be the last item inside a blank node's brackets. Write the annotated constraint on a named shape.
Reference

Every term, and where it is shown

Generated from the shapes themselves, so it cannot disagree with them. The same index is FEATURES.md in the repository.

Shapes and targets

TermDefined inLessons
sh:NodeShapeSHACL 2.2 Node Shapess01 s02 s03 s04 s05 s06 s07 s08 s09 s10 s11 s12 s13 s14 s15 s16 s17 s18 s19 s20 s21 s22 s23 s24 s25 s26 s27 s28 s29 s30 s31 s32 s33 s34 s35 s36 s37 s38 s39 s40 s41 s42 s43 s44 s45 s46 s47 s48 s49 s50 s51 s52 s53 s54 s55 s56 s57 s58 s59 s60 s61 s62 s63 s64 s65 s66 s67 s68 s69 s70
sh:PropertyShapeSHACL 2.3 Property Shapess57 s62 s64
sh:propertySHACL 4.7.2 sh:propertys01 s02 s03 s04 s05 s06 s07 s08 s09 s10 s11 s12 s13 s14 s15 s16 s17 s18 s19 s20 s21 s22 s23 s24 s25 s26 s27 s28 s29 s31 s33 s38 s39 s40 s41 s42 s43 s44 s45 s48 s50 s51 s52 s53 s54 s56 s57 s58 s59 s60 s61 s62 s63 s64 s65 s66 s68 s70
sh:pathSHACL 2.3 Property Shapess01 s02 s03 s04 s05 s06 s07 s08 s09 s10 s11 s12 s13 s14 s15 s16 s17 s18 s19 s20 s21 s22 s23 s24 s25 s26 s27 s28 s29 s31 s33 s38 s39 s40 s41 s42 s43 s44 s45 s46 s48 s49 s50 s51 s52 s53 s54 s56 s57 s58 s59 s60 s61 s62 s63 s64 s65 s66 s67 s68 s69 s70
sh:targetClassSHACL 2.1.3.2 sh:targetClasss01 s02 s03 s04 s05 s06 s07 s09 s10 s11 s12 s13 s14 s15 s16 s17 s18 s19 s20 s21 s22 s23 s24 s25 s26 s27 s28 s30 s31 s32 s34 s35 s36 s37 s38 s39 s40 s42 s43 s44 s45 s46 s47 s48 s49 s50 s51 s52 s53 s55 s57 s58 s59 s60 s61 s62 s64 s65 s66 s67 s68 s69 s70
sh:targetNodeSHACL 2.1.3.1 sh:targetNodes06 s07 s09 s14 s29 s57 s60 s63 s70
sh:targetSubjectsOfSHACL 2.1.3.4 sh:targetSubjectsOfs07 s54 s57 s60 s64
sh:targetObjectsOfSHACL 2.1.3.5 sh:targetObjectsOfs07
sh:targetSHACL-AF section 3 Custom Targetss33 s41 s68 s70
sh:SPARQLTargetSHACL-AF 3.1 SPARQL-based Targetss33 s41 s68
sh:SPARQLTargetTypeSHACL-AF 3.2 SPARQL-based Target Typess70
sh:targetWhereSHACL 1.2 Core 3.1.3.6 Where Targets (sh:targetWhere)s33
sh:shapeSHACL 1.2 Core 3.1.3.7 Explicit shape targets (sh:shape)s27 s64
sh:ShapeClassSHACL 1.2 Core 3.1.3.3 Implicit Class Targets and sh:ShapeClasss08
sh:severitySHACL 2.1.4 Declaring the Severity of a Shapes02 s03 s05 s18 s21 s23 s30 s31 s32 s33 s34 s35 s36 s38 s41 s42 s43 s44 s45 s46 s47 s48 s49 s50 s51 s52 s53 s54 s57 s58 s62 s64 s65 s66 s67 s68 s69 s70
sh:ViolationSHACL 2.1.4 Declaring the Severity of a Shapes02
sh:WarningSHACL 2.1.4 Declaring the Severity of a Shapes03 s05 s21 s30 s31 s35 s36 s38 s41 s49 s50 s51 s52 s53 s57 s62 s64 s66 s67
sh:InfoSHACL 2.1.4 Declaring the Severity of a Shapes05 s18 s21 s23 s32 s33 s34 s35 s42 s43 s44 s45 s46 s47 s48 s49 s50 s51 s54 s58 s65 s67 s68 s69 s70
sh:messageSHACL 2.1.5 Declaring Messages for a Shapes02 s03 s04 s05 s06 s07 s08 s09 s10 s11 s12 s13 s14 s15 s16 s17 s18 s19 s20 s21 s22 s23 s24 s25 s26 s27 s28 s29 s30 s31 s32 s33 s34 s35 s36 s37 s39 s40 s41 s42 s43 s44 s45 s46 s47 s48 s49 s50 s51 s52 s53 s54 s55 s56 s57 s58 s59 s60 s61 s63 s64 s65 s66 s67 s68 s69 s70
sh:deactivatedSHACL 2.1.6 Deactivating a Shapes06 s48 s69

Property paths

Value type, cardinality, range

Strings and languages

Property pairs

Logic and shape-based

SPARQL-based constraints and components

TermDefined inLessons
sh:sparqlSHACL section 5 SPARQL-based Constraintss14 s30 s31 s32 s34 s35 s36 s37 s43 s44 s45 s46 s47 s48 s49 s50 s51 s52 s54 s55 s58 s60 s64 s65 s67 s68 s69 s70
sh:selectSHACL 5.2 Syntax of SPARQL-based Constraintss14 s30 s31 s32 s33 s34 s35 s36 s37 s40 s41 s42 s43 s44 s45 s46 s47 s48 s49 s50 s51 s52 s54 s55 s58 s60 s64 s65 s67 s68 s69 s70
sh:askSHACL 6.2.3.2 ASK-based Validatorss39 s42 s64
sh:prefixesSHACL 5.2.1 Prefix Declarations for SPARQL Queriess14 s30 s31 s32 s33 s34 s35 s36 s37 s39 s40 s41 s42 s43 s44 s45 s46 s47 s48 s49 s50 s51 s52 s54 s55 s58 s60 s64 s65 s67 s68 s69 s70
sh:declareSHACL 5.2.1 Prefix Declarations for SPARQL Queriess14 s30 s31 s32 s33 s34 s35 s36 s37 s39 s40 s41 s42 s43 s44 s45 s46 s47 s48 s49 s50 s51 s52 s54 s55 s58 s60 s64 s65 s67 s68 s69 s70
sh:prefixSHACL 5.2.1 Prefix Declarations for SPARQL Queriess14 s30 s31 s32 s33 s34 s35 s36 s37 s39 s40 s41 s42 s43 s44 s45 s46 s47 s48 s49 s50 s51 s52 s54 s55 s58 s60 s64 s65 s67 s68 s69 s70
sh:namespaceSHACL 5.2.1 Prefix Declarations for SPARQL Queriess14 s30 s31 s32 s33 s34 s35 s36 s37 s39 s40 s41 s42 s43 s44 s45 s46 s47 s48 s49 s50 s51 s52 s54 s55 s58 s60 s64 s65 s67 s68 s69 s70
sh:ConstraintComponentSHACL section 6 SPARQL-based Constraint Componentss39 s40 s41 s42 s64
sh:parameterSHACL 6.2.1 Parameter Declarations (sh:parameter)s39 s40 s41 s42 s64 s69 s70
sh:optionalSHACL 6.2.1 Parameter Declarations (sh:parameter)s41
sh:labelTemplateSHACL 6.2.2 Label Templates (sh:labelTemplate)s40
sh:validatorSHACL 6.2.3 Validatorss39 s42 s64
sh:nodeValidatorSHACL 6.2.3 Validatorss42
sh:propertyValidatorSHACL 6.2.3 Validatorss40 s41
sh:SPARQLAskValidatorSHACL 6.2.3.2 ASK-based Validatorss39 s42 s64
sh:SPARQLSelectValidatorSHACL 6.2.3.1 SELECT-based Validatorss40 s41 s42
sh:SPARQLFunctionSHACL-AF 5.4 SPARQL-based Functionss69
sh:returnTypeSHACL-AF section 5 SHACL Functionss69

Rules and node expressions

Non-validating and report vocabulary

Reference

The standards

Everything this course teaches is defined in one of these, usually in one short section, and each lesson's header names the sections it is defined by. python scripts/check_links.py fetches every document and confirms each anchor still lands on its heading.

The specifications

  • Shapes Constraint Language (SHACL) — The W3C Recommendation of 2017. Modules 01 to 06 and 10 are defined here; section 4 is the constraint component reference you will open most often, and Appendix D shows each Core component as the SPARQL it is equivalent to.
  • SHACL Advanced Features — A Working Group Note rather than a Recommendation, and the definition of SPARQL-based targets, node expressions and SHACL rules. Module 07 is defined here.
  • SHACL 1.2 Core — A Working Draft, still changing. The reification constraints, sh:targetWhere, sh:ShapeClass, per-constraint severities and the new string and list components are here. Module 08 says which of them the engine runs.
  • SHACL 1.2 SPARQL Extensions — The 1.2 edition of SHACL-SPARQL: constraints, constraint components, prefix declarations, and SPARQL-based node expressions.
  • SHACL 1.2 Node Expressions — A library of node expression functions with a namespace of its own. Not implemented by the engine in the editor; module 12 explains where it is heading.
  • SHACL 1.2 Rules (SPARQL-RL) — A rule language with stratified negation, different in design from SHACL-AF rules. Not implemented by the engine; the last lesson of module 07 reads it for what it fixes.

The data model and the query language

  • SPARQL 1.2 Query Language — Everything inside sh:select, sh:ask and sh:construct is SPARQL. The SPARQL course covers it in full; this course points at it where a constraint needs it.
  • RDF 1.2 Concepts and Abstract Syntax — Triple terms and reifiers, which module 08 validates.
  • RDF 1.2 Turtle — The syntax of every file here, including the {| ... |} annotations in the 1.2 edition of the data.