← every query · class 09 · examples/07-shacl

Check what comes in

Conforms: True  data/shapes.ttl over data/domain.ttl

The graph every query on this site runs over. Nothing to report: that is what a passing pipeline looks like (exit code 0).

No violations. The data says what the shapes require.

The same data under the OWL 2 RL reasoner: 0 inconsistencies. OWL infers; SHACL validates — they are complementary, not alternatives.

The files

data/shapes.ttl
# shapes.ttl — the SHACL shapes of the catalogue/orders domain.
#
# OWL infers; SHACL is what says "this is wrong". The messages are part of the
# shape: a report without readable messages is not actionable by whoever
# produces the data. They are in English, like the rest of the domain; the
# @en tag lets a second language be added next to them later.

@prefix sh:     <http://www.w3.org/ns/shacl#> .
@prefix rdf:    <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix xsd:    <http://www.w3.org/2001/XMLSchema#> .
@prefix schema: <https://schema.org/> .
@prefix onto:   <https://data.csoares.me/onto#> .
@prefix shapes: <https://data.csoares.me/shapes#> .

shapes:ProductShape a sh:NodeShape ;
    sh:targetClass onto:Product ;
    sh:property [ sh:path schema:name ;
        sh:minCount 1 ;
        sh:datatype rdf:langString ;   # name with a language tag
        sh:uniqueLang true ;           # one per language, not just one
        sh:minLength 2 ;
        sh:message "Every product has at least one name, one per language."@en ] ;
    sh:property [ sh:path schema:sku ;
        sh:minCount 1 ; sh:maxCount 1 ; sh:datatype xsd:string ;
        sh:pattern "^[A-Z]{2}-[0-9]{4}$" ;
        sh:message "The SKU is required and has the shape XX-0000."@en ] ;
    sh:property [ sh:path schema:offers ;   # the price lives in the offer
        sh:minCount 1 ; sh:node shapes:OfferShape ;
        sh:message "Every product has at least one offer, with a price and a currency."@en ] ;
    sh:property [ sh:path onto:manufacturedBy ;
        sh:minCount 1 ; sh:class onto:Manufacturer ;
        sh:message "The manufacturer is an entity of class Manufacturer, not text."@en ] .

shapes:OfferShape a sh:NodeShape ;
    sh:property [ sh:path schema:price ; sh:minCount 1 ; sh:maxCount 1 ;
        sh:datatype xsd:decimal ; sh:minInclusive 0 ;
        sh:message "Price required, decimal and not negative."@en ] ;
    sh:property [ sh:path schema:priceCurrency ;
        sh:minCount 1 ; sh:maxCount 1 ; sh:pattern "^[A-Z]{3}$" ;
        sh:message "Currency required, as a three-letter ISO code."@en ] .

shapes:ManufacturerShape a sh:NodeShape ;
    sh:targetClass onto:Manufacturer ;
    sh:property [
        sh:path schema:name ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:message "The manufacturer has exactly one name."@en ] ;
    sh:property [
        sh:path onto:country ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:class onto:Country ;
        sh:message "The country is required and is an entity, not text."@en ] .

shapes:OrderShape a sh:NodeShape ;
    sh:targetClass onto:Order ;
    sh:property [
        sh:path onto:hasCustomer ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:class onto:Customer ;
        sh:message "The order has exactly one customer. This is integrity, not inference: in OWL, two customers would give owl:sameAs."@en ] ;
    sh:property [
        sh:path onto:date ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:datatype xsd:date ;
        sh:message "The date of the order is required and is an xsd:date."@en ] ;
    sh:property [
        sh:path onto:containsProduct ;
        sh:minCount 1 ;
        sh:class onto:Product ;
        sh:message "The order has at least one product."@en ] ;
    sh:property [
        sh:path onto:totalCents ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:datatype xsd:integer ;
        sh:minInclusive 0 ;
        sh:message "The total in cents is required, an integer and not negative."@en ] .

shapes:SupplyShape a sh:NodeShape ;
    sh:targetClass onto:Supply ;
    sh:property [
        sh:path onto:supplier ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:class onto:Manufacturer ;
        sh:message "The supply has exactly one supplier."@en ] ;
    sh:property [
        sh:path onto:quantity ;
        sh:minCount 1 ;
        sh:datatype xsd:integer ;
        sh:minExclusive 0 ;
        sh:message "The quantity supplied is a positive integer."@en ] .
data/domain.ttl
# domain.ttl — the demonstration domain graph: catalogue, orders and
# suppliers. The same file is used in classes 06, 08 and 09.
#
# URI convention (slide "Designing the URIs"):
#   https://data.csoares.me/<collection>/<identifier>
# One prefix is declared per collection because in Turtle 1.1 the slash is not
# allowed in a local name without escaping: product:17 is exactly the URI
# https://data.csoares.me/product/17, which is how the slides write it.

@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/> .
@prefix dcterms:      <http://purl.org/dc/terms/> .
@prefix skos:         <http://www.w3.org/2004/02/skos/core#> .
@prefix onto:         <https://data.csoares.me/onto#> .
@prefix product:      <https://data.csoares.me/product/> .
@prefix manufacturer: <https://data.csoares.me/manufacturer/> .
@prefix customer:     <https://data.csoares.me/customer/> .
@prefix order:        <https://data.csoares.me/order/> .
@prefix component:    <https://data.csoares.me/component/> .
@prefix category:     <https://data.csoares.me/category/> .
@prefix country:      <https://data.csoares.me/country/> .
@prefix batch:        <https://data.csoares.me/batch/> .
@prefix container:    <https://data.csoares.me/container/> .
@prefix supply:       <https://data.csoares.me/supply/> .

## Countries — entities, not literals, so that they can link outwards
country:PT a onto:Country ;
    rdfs:label "Portugal"@en ;
    owl:sameAs <http://www.wikidata.org/entity/Q45> .

country:DE a onto:Country ;
    rdfs:label "Germany"@en ;
    owl:sameAs <http://www.wikidata.org/entity/Q183> .

## Manufacturers
manufacturer:9 a onto:Manufacturer ;
    schema:name "Acme Lda" ;
    onto:country country:PT .

manufacturer:12 a onto:Manufacturer ;
    schema:name "Nordwind GmbH" ;
    onto:country country:DE .

## Categories — reuses SKOS
category:computing a skos:Concept ;
    skos:prefLabel "Computing"@en .

category:peripherals a skos:Concept ;
    skos:prefLabel "Peripherals"@en ;
    skos:broader category:computing .

category:monitors a skos:Concept ;
    skos:prefLabel "Monitors"@en ;
    skos:broader category:computing .

## Products
product:17 a onto:Product ;
    schema:name "Teclado mecanico"@pt ;
    schema:name "Mechanical keyboard"@en ;
    schema:sku "TC-0017" ;
    dcterms:created "2026-09-01"^^xsd:date ;
    schema:category category:peripherals ;
    schema:offers [
        a schema:Offer ;
        schema:price "49.90"^^xsd:decimal ;
        schema:priceCurrency "EUR"
    ] ;
    onto:manufacturedBy manufacturer:9 ;
    onto:hasPart component:blue-switch , component:usb-c-cable ;
    onto:partOf batch:A ;
    # a kind of thing, not the same thing: Q250 is the concept «keyboard», not
    # this product -- so not owl:sameAs (see class 10, «Alignment predicates»)
    schema:additionalType <http://www.wikidata.org/entity/Q250> .

product:23 a onto:Product , onto:DiscontinuedProduct ;
    schema:name "Optical mouse"@en ;
    schema:sku "RT-0023" ;
    dcterms:created "2025-03-14"^^xsd:date ;
    schema:category category:peripherals ;
    schema:offers [ a schema:Offer ; schema:price "19.90"^^xsd:decimal ; schema:priceCurrency "EUR" ] ;
    onto:manufacturedBy manufacturer:9 ;
    onto:hasPart component:blue-switch .

product:31 a onto:Product ;
    schema:name "Wireless optical mouse"@en ;
    schema:sku "RT-0031" ;
    dcterms:created "2026-05-06"^^xsd:date ;
    schema:category category:peripherals ;
    schema:offers [ a schema:Offer ; schema:price "29.90"^^xsd:decimal ; schema:priceCurrency "EUR" ] ;
    onto:manufacturedBy manufacturer:9 ;
    onto:replaces product:23 .

product:88 a onto:Product ;
    schema:name "27-inch monitor"@en ;
    schema:sku "MN-0088" ;
    dcterms:created "2026-02-11"^^xsd:date ;
    schema:category category:monitors ;
    schema:offers [ a schema:Offer ; schema:price "249.00"^^xsd:decimal ; schema:priceCurrency "EUR" ] ;
    onto:manufacturedBy manufacturer:12 ;
    onto:hasPart component:usb-c-cable .

product:52 a onto:DigitalProduct ;
    schema:name "Firmware licence"@en ;
    schema:sku "LF-0052" ;
    dcterms:created "2026-06-30"^^xsd:date ;
    schema:category category:computing ;
    schema:offers [ a schema:Offer ; schema:price "12.00"^^xsd:decimal ; schema:priceCurrency "EUR" ] ;
    onto:manufacturedBy manufacturer:12 .

## Components and physical groupings
component:blue-switch a onto:Component ;
    rdfs:label "Blue switch"@en ;
    onto:hasPart component:spring .

component:usb-c-cable a onto:Component ;
    rdfs:label "USB-C cable"@en .

component:spring a onto:Component ;
    rdfs:label "Spring"@en .

batch:A a onto:Batch ;
    rdfs:label "Batch A"@en ;
    onto:partOf container:7 .

container:7 a onto:Container ;
    rdfs:label "Container 7"@en .

## Customers
customer:4 a onto:Customer ;
    schema:name "Papelaria Central" .

customer:5 a onto:Customer ;
    schema:name "Escola Secundaria do Douro" .

## Orders
order:1001 a onto:Order ;
    onto:hasCustomer customer:4 ;
    onto:date "2026-10-12"^^xsd:date ;
    onto:containsProduct product:17 , product:88 ;
    onto:totalCents "52000"^^xsd:integer .

order:1002 a onto:Order ;
    onto:hasCustomer customer:5 ;
    onto:date "2026-09-20"^^xsd:date ;
    onto:containsProduct product:23 ;
    onto:totalCents "1990"^^xsd:integer .

## N-ary relation: the fact has an identity of its own (slide "N-ary relations")
supply:2026-10-01\/17 a onto:Supply ;
    onto:supplier manufacturer:9 ;
    onto:product product:17 ;
    onto:quantity "50"^^xsd:integer ;
    onto:unitPriceCents "490"^^xsd:integer ;
    onto:date "2026-10-01"^^xsd:date .