Skip to content

verifiedPattern — Load → Validate → Query ​

Check a graph against SHACL shapes and act on the verdict: gate a load, raise an alert, or query only what conforms. This page walks the whole journey on real data, with the real report.

When to use it ​

You want a conformance gate: refuse data that violates your shapes, or surface the violations as data. Put Reason before Validate when the shapes constrain entailed facts, and skip it when they constrain only asserted triples. Both steps run in one session over one graph, so the graph has to fit your hardware.

A worked scenario — required fields ​

Every person must have a name and an age. We load two records, one complete and one missing its age, and watch validation catch exactly the incomplete one.

Step 1 — load the data ​

sql
SELECT pgrdf.add_graph('http://example.org/staff');   -- → 1
SELECT pgrdf.parse_turtle('
@prefix ex:   <http://example.org/> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix xsd:  <http://www.w3.org/2001/XMLSchema#> .

ex:alice a foaf:Person ; foaf:name "Alice" .
ex:bob   a foaf:Person ; foaf:name "Bob" ; ex:age "30"^^xsd:integer .
', pgrdf.graph_id('http://example.org/staff'));      -- → 5

Alice has a name but no age; Bob has both.

Step 2 — load the shapes into their own graph ​

sql
SELECT pgrdf.add_graph('http://example.org/staff-shapes');   -- → 2
SELECT pgrdf.parse_turtle('
@prefix ex:   <http://example.org/> .
@prefix sh:   <http://www.w3.org/ns/shacl#> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix xsd:  <http://www.w3.org/2001/XMLSchema#> .

ex:PersonShape a sh:NodeShape ;
    sh:targetClass foaf:Person ;
    sh:property [ sh:path foaf:name ; sh:minCount 1 ; sh:datatype xsd:string  ] ;
    sh:property [ sh:path ex:age   ; sh:minCount 1 ; sh:datatype xsd:integer ] .
', pgrdf.graph_id('http://example.org/staff-shapes'));      -- → 10

Every foaf:Person must carry at least one string name and one integer age.

Step 3 — validate ​

sql
SELECT pgrdf.validate(pgrdf.graph_id('http://example.org/staff'),
                      pgrdf.graph_id('http://example.org/staff-shapes'));

pgRDF returns the validation report as JSONB:

json
{
  "conforms": false,
  "results": [
    {
      "focusNode": "http://example.org/alice",
      "resultPath": "http://example.org/age",
      "value": null,
      "sourceShape": "_:f25130e0…",
      "sourceConstraintComponent": "http://www.w3.org/ns/shacl#MinCountConstraintComponent",
      "resultSeverity": "sh:Violation",
      "resultMessage": "MinCount(1) not satisfied"
    }
  ],
  "mode": "native",
  "data_triples": 5,
  "shapes_triples": 10,
  "data_graph_id": 1,
  "shapes_graph_id": 2,
  "elapsed_ms": 1.421415
}

Reading the report ​

  • conforms is false: at least one node violates the shapes.
  • One result, and it's Alice: on ex:age, a sh:Violation from the minCount constraint. She's missing the required age. value is null because there's no value to report.
  • sourceShape is the property shape that fired. It's written with [ … ], so it's a blank node with a generated label.
  • Bob isn't in results: he conforms, so a gate built on this report lets him through and stops Alice.

Acting on the report ​

The report is JSONB, so the gate is plain SQL:

sql
SELECT (pgrdf.validate(pgrdf.graph_id('http://example.org/staff'),
                       pgrdf.graph_id('http://example.org/staff-shapes')) ->> 'conforms')::boolean AS ok;
--  → f

Branch on ok to accept or reject the load, or unfold the violations to act on each one:

sql
SELECT res ->> 'focusNode'     AS node,
       res ->> 'resultPath'    AS path,
       res ->> 'resultMessage' AS message
FROM jsonb_array_elements(
       pgrdf.validate(pgrdf.graph_id('http://example.org/staff'),
                      pgrdf.graph_id('http://example.org/staff-shapes')) -> 'results') AS res;
--            node           |          path          |          message
-- --------------------------+------------------------+---------------------------
--  http://example.org/alice | http://example.org/age | MinCount(1) not satisfied

Fix the data and validate again:

sql
SELECT * FROM pgrdf.sparql(
  'PREFIX ex:  <http://example.org/>
   PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
   INSERT DATA { GRAPH <http://example.org/staff> { ex:alice ex:age "41"^^xsd:integer } }');

SELECT pgrdf.validate(pgrdf.graph_id('http://example.org/staff'),
                      pgrdf.graph_id('http://example.org/staff-shapes')) ->> 'conforms' AS conforms;
--  → true

See Report as data for more ways to query reports.

A wrong shapes graph is refused, not passed

By default validate is strict. Pass the wrong graph id for the shapes (here 0, the empty default graph) and you get an error, not a hollow conforms: true:

ERROR:  validate: shapes graph 0 declares no SHACL target (0 triples). Nothing would be
        selected, so a verdict would be vacuous — ...

Variation — validate the entailed graph ​

Validation checks the triples that are in the graph. It doesn't apply rdfs:domain or rdfs:subClassOf by itself. Add a person who is a person only by implication:

sql
SELECT pgrdf.parse_turtle('
@prefix ex:   <http://example.org/> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .

ex:worksOn rdfs:domain foaf:Person .
ex:dave ex:worksOn ex:apollo ; foaf:name "Dave" .
', pgrdf.graph_id('http://example.org/staff'));

SELECT pgrdf.validate(pgrdf.graph_id('http://example.org/staff'),
                      pgrdf.graph_id('http://example.org/staff-shapes')) ->> 'conforms' AS conforms;
--  → true   (dave isn't a foaf:Person yet, so the shape doesn't target him)

Reason first, so validation sees the closure:

sql
SELECT pgrdf.materialize(pgrdf.graph_id('http://example.org/staff'));

SELECT res ->> 'focusNode' AS node, res ->> 'resultPath' AS path
FROM jsonb_array_elements(
       pgrdf.validate(pgrdf.graph_id('http://example.org/staff'),
                      pgrdf.graph_id('http://example.org/staff-shapes')) -> 'results') AS res;
--           node           |          path
-- -------------------------+------------------------
--  http://example.org/dave | http://example.org/age

The reasoner typed dave as a foaf:Person through the domain of ex:worksOn, so the shape now checks him and finds no age. This is the amber Reason → Validate segment of the chain above.

Next step ​

For a source larger than your hardware can reason over, carve a slice first with Ingest → Carve → Reason, then validate the slice. For SPARQL-based constraints, see Validate.

pgRDF is released under the MIT license. Documentation built with VitePress, served via GitHub Pages.