Skip to content

Property paths ​

A property path matches a route through the graph instead of a single edge: "everyone Alice reports to, directly or indirectly" is one pattern.

PathWrittenMatches
Inverse?s ^p ?othe edge ?o p ?s, walked backwards
One or more?s p+ ?oone or more p steps; the start node is not included
Zero or more?s p* ?olike p+, plus the start node itself
Zero or one?s p? ?othe start node, or one p step
Alternatives?s (p1|p2) ?oone step along either predicate

They combine: ^p+ and (p1|p2)+ work. A path can go wherever a triple pattern can: joined with other patterns, inside GRAPH and OPTIONAL, in ASK and pgrdf.construct(), and in the WHERE of a SPARQL UPDATE.

Not supported (refused with an error):

  • Sequence paths p1/p2. Write two patterns joined on a variable: ?s p1 ?mid . ?mid p2 ?o.
  • Negated property sets !(p). Match ?p and exclude it with FILTER(?p != …) (see FILTER).
  • Nested forms such as (p1/p2)+ or (p1/p2|p3).

Example data ​

These examples use their own graph: a small class hierarchy and a reporting chain.

sql
SELECT pgrdf.add_graph('http://example.org/org');
SELECT pgrdf.parse_turtle($$
@prefix ex:   <http://example.org/> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
ex:Engineer rdfs:subClassOf ex:Employee .
ex:Manager  rdfs:subClassOf ex:Employee .
ex:Employee rdfs:subClassOf ex:Person .
ex:Person   rdfs:subClassOf ex:Agent .
ex:Employee rdfs:label "Employee" .
ex:alice ex:reportsTo ex:bob .
ex:bob   ex:reportsTo ex:dana .
ex:dana  ex:reportsTo ex:erin .
ex:carol ex:mentors  ex:alice .
ex:alice a ex:Engineer .
$$, pgrdf.graph_id('http://example.org/org'));
--  10

When you're done, remove it with SELECT pgrdf.drop_graph('http://example.org/org'); so it doesn't show up in other examples.

+ — one or more steps ​

Everything that is a subclass of ex:Agent, directly or indirectly:

sql
SELECT * FROM pgrdf.sparql($$
  PREFIX ex:   <http://example.org/>
  PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
  SELECT ?c WHERE { ?c rdfs:subClassOf+ ex:Agent } ORDER BY ?c
$$);
--  {"c": "http://example.org/Employee"}
--  {"c": "http://example.org/Engineer"}
--  {"c": "http://example.org/Manager"}
--  {"c": "http://example.org/Person"}

Everyone above Alice in the reporting chain:

sql
SELECT * FROM pgrdf.sparql($$
  PREFIX ex: <http://example.org/>
  SELECT ?boss WHERE { ex:alice ex:reportsTo+ ?boss } ORDER BY ?boss
$$);
--  {"boss": "http://example.org/bob"}
--  {"boss": "http://example.org/dana"}
--  {"boss": "http://example.org/erin"}

Cycles are safe: if the chain loops back to Alice, the walk still ends, and each node is returned once.

* and ? — including the start node ​

* adds the start node to the + result; ? is the start node plus one step:

sql
SELECT * FROM pgrdf.sparql($$
  PREFIX ex:   <http://example.org/>
  PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
  SELECT ?c WHERE { ex:Engineer rdfs:subClassOf* ?c } ORDER BY ?c
$$);
--  {"c": "http://example.org/Agent"}
--  {"c": "http://example.org/Employee"}
--  {"c": "http://example.org/Engineer"}
--  {"c": "http://example.org/Person"}

SELECT * FROM pgrdf.sparql($$
  PREFIX ex:   <http://example.org/>
  PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
  SELECT ?c WHERE { ex:Engineer rdfs:subClassOf? ?c } ORDER BY ?c
$$);
--  {"c": "http://example.org/Employee"}
--  {"c": "http://example.org/Engineer"}

The start node counts even when it appears nowhere in the data: ex:nobody ex:reportsTo* ?x returns ex:nobody.

^ — inverse ​

Who reports to Bob, and everyone below Erin:

sql
SELECT * FROM pgrdf.sparql($$
  PREFIX ex: <http://example.org/>
  SELECT ?who WHERE { ex:bob ^ex:reportsTo ?who }
$$);
--  {"who": "http://example.org/alice"}

SELECT * FROM pgrdf.sparql($$
  PREFIX ex: <http://example.org/>
  SELECT ?who WHERE { ex:erin ^ex:reportsTo+ ?who } ORDER BY ?who
$$);
--  {"who": "http://example.org/alice"}
--  {"who": "http://example.org/bob"}
--  {"who": "http://example.org/dana"}

| — alternatives ​

Follow either relation, as many steps as needed:

sql
SELECT * FROM pgrdf.sparql($$
  PREFIX ex: <http://example.org/>
  SELECT ?y WHERE { ex:carol (ex:reportsTo|ex:mentors)+ ?y } ORDER BY ?y
$$);
--  {"y": "http://example.org/alice"}
--  {"y": "http://example.org/bob"}
--  {"y": "http://example.org/dana"}
--  {"y": "http://example.org/erin"}

Combining with other patterns ​

A path joins like any triple pattern. Here it runs inside a GRAPH clause, next to a plain pattern, with an OPTIONAL label:

sql
SELECT * FROM pgrdf.sparql($$
  PREFIX ex:   <http://example.org/>
  PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
  SELECT ?person ?class ?label WHERE {
    GRAPH <http://example.org/org> {
      ?person a ?type .
      ?type rdfs:subClassOf* ?class
    }
    OPTIONAL { ?class rdfs:label ?label }
  } ORDER BY ?class
$$);
--  {"class": "http://example.org/Agent", "label": null, "person": "http://example.org/alice"}
--  {"class": "http://example.org/Employee", "label": "Employee", "person": "http://example.org/alice"}
--  {"class": "http://example.org/Engineer", "label": null, "person": "http://example.org/alice"}
--  {"class": "http://example.org/Person", "label": null, "person": "http://example.org/alice"}

Depth limit ​

A + or * walk stops after pgrdf.path_max_depth steps: 64 by default, settable per session from 1 to 1024. When a walk is cut short, some answers are missing, and pgRDF tells you:

sql
SET pgrdf.path_max_depth = 2;
SELECT * FROM pgrdf.sparql($$
  PREFIX ex: <http://example.org/>
  SELECT ?boss WHERE { ex:alice ex:reportsTo+ ?boss } ORDER BY ?boss
$$);
-- WARNING:  sparql: property path truncated at pgrdf.path_max_depth=2 — longer paths are
--           missing from this result; raise pgrdf.path_max_depth, or
--           SET pgrdf.on_path_truncation = 'error' to forbid partial results
--  {"boss": "http://example.org/bob"}
--  {"boss": "http://example.org/dana"}

SELECT pgrdf.last_call_stats();
--  {"filter_clauses_dropped": 0, "path_depth_truncations": 1}

pgrdf.last_call_stats() describes the last sparql, construct or describe call in your session. Both numbers at zero means the answer was complete.

pgrdf.on_path_truncation decides what a cut-short walk does:

ValueEffect
warn (default)Return the partial result with the WARNING above.
errorRefuse the query with SQLSTATE 54000.
countReturn the partial result without a warning; it is still counted in last_call_stats().
sql
SET pgrdf.on_path_truncation = 'error';
-- the same query now fails:
-- ERROR:  54000: sparql: property path truncated at pgrdf.path_max_depth=2
--         (pgrdf.on_path_truncation=error forbids partial results)
RESET pgrdf.on_path_truncation;
RESET pgrdf.path_max_depth;

pgrdf.stats()->'path_depth_truncations' keeps a server-wide running total. See also Was the answer complete? and Settings.

Faster after materialize ​

After pgrdf.materialize() has computed a graph's inferred triples, a + or * path over rdfs:subClassOf or owl:sameAs reads those triples directly instead of walking the chain step by step. The answer is the same, and the query is a plain index lookup.

pgrdf.sparql_sql() shows the SQL a query will run, so you can see the change:

sql
SELECT pgrdf.sparql_sql($$
  PREFIX ex:   <http://example.org/>
  PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
  SELECT ?c WHERE { ?c rdfs:subClassOf+ ex:Agent }
$$) ~* 'recursive' AS walks_the_chain;
--  walks_the_chain
-- -----------------
--  t

SELECT pgrdf.materialize(pgrdf.graph_id('http://example.org/org'), 'rdfs');

-- the same check now returns f; the query still returns the same four classes

EXPLAIN of that SQL no longer shows a recursive CTE; see Inspect a query.

See also ​

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