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.
| Path | Written | Matches |
|---|---|---|
| Inverse | ?s ^p ?o | the edge ?o p ?s, walked backwards |
| One or more | ?s p+ ?o | one or more p steps; the start node is not included |
| Zero or more | ?s p* ?o | like p+, plus the start node itself |
| Zero or one | ?s p? ?o | the start node, or one p step |
| Alternatives | ?s (p1|p2) ?o | one 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?pand exclude it withFILTER(?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.
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'));
-- 10When 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:
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:
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:
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:
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:
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:
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:
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:
| Value | Effect |
|---|---|
warn (default) | Return the partial result with the WARNING above. |
error | Refuse the query with SQLSTATE 54000. |
count | Return the partial result without a warning; it is still counted in last_call_stats(). |
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:
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 classesEXPLAIN of that SQL no longer shows a recursive CTE; see Inspect a query.
See also
- Reasoning: compute the closure once with
materialize. - GRAPH: scope a path to one graph.
- Not supported.