Inference
Reasoning is forward chaining: pgrdf.materialize(graph_id, profile) computes the entailed triples of one graph and stores them beside the asserted ones. Queries then read inferred triples like any others, with no reasoning at query time. The code is in src/inference/reasonable.rs. The user guide is Reasoning.
pgrdf.materialize(graph_id BIGINT, profile TEXT DEFAULT 'owl-rl') → JSONBFlow
SELECT pgrdf.materialize(g)
│ lock check (a locked graph refuses with 55P03)
▼
DELETE the graph's rows WHERE is_inferred → previous_inferred_dropped
▼
read the asserted rows (quads joined to the → base_triples, load_ms
dictionary) and rebuild them as oxrdf triples
▼
reason: 'owl-rl' → reasonable::Reasoner → reason_ms
'rdfs' → pgRDF's RDFS closure
▼
set difference: entailed minus asserted → diff_ms
▼
resolve the new terms in one batch, then INSERT → inferred_triples_written,
… SELECT FROM unnest(…) with is_inferred = true write_ms
▼
record last_materialize_at and materialized_base_count on _pgrdf_graphs
▼
ANALYZE pgrdf._pgrdf_quads (when rows were written → auto_analyzed, analyze_ms
and pgrdf.auto_analyze is on)Each run starts from scratch: the previous inferred rows are deleted first, so two runs in a row write the same rows, and the second run's previous_inferred_dropped equals the first run's inferred_triples_written. A run after the asserted triples change picks up the new entailments.
The automatic ANALYZE matters because a materialised closure can change the row count and value distribution of the quad table substantially. Stale statistics then lead the planner to poor join orders for the queries that follow.
Profiles
| Profile | Engine | Rules |
|---|---|---|
'owl-rl' (default) | the reasonable crate | OWL 2 RL: class and property hierarchies, domain and range, owl:inverseOf, symmetric, transitive, functional and inverse-functional properties, owl:sameAs, property chains within RL, … |
'rdfs' | pgRDF's own rdfs_closure | rdfs2 (domain), rdfs3 (range), rdfs5 (subPropertyOf transitivity), rdfs7 (subPropertyOf application), rdfs9 (subClassOf application), rdfs11 (subClassOf transitivity) |
Any other profile string is refused before any work is done:
ERROR: materialize: unknown profile "bogus" (supported: 'owl-rl', 'rdfs')The reasonable fork pgRDF builds against exposes one fused OWL 2 RL fixpoint and no way to select a subset of rules. The RDFS profile is therefore computed in pgRDF's own code. It is restricted to the six productive RDFS rules above, iterated to a fixpoint (sub-property propagation feeds domain and range, whose types feed subClassOf application), so it is a strict subset of what 'owl-rl' derives.
reasonable is an OWL 2 RL reasoner. It does not implement OWL 2 EL or QL, full OWL 2 DL, or custom Datalog rules. OWL 2 RL also adds a small set of axiomatic triples (for example owl:Thing typing), so even a graph with no schema gains a few inferred rows.
reasonable comes from a fork (styk-tv/reasonable, branch rdf12-passthrough), applied through [patch.crates-io] in Cargo.toml. The fork adds a pass-through RDF 1.2 feature so it can share one oxrdf build with the shacl crate, which enables that feature unconditionally.
Example
SELECT pgrdf.add_graph('http://example.com/onto');
SELECT pgrdf.parse_turtle('
@prefix ex: <http://example.com/> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
ex:Student rdfs:subClassOf ex:Person .
ex:Person rdfs:subClassOf ex:Agent .
ex:knows rdfs:domain ex:Person .
ex:alice a ex:Student .
ex:bob ex:knows ex:carol .
', pgrdf.graph_id('http://example.com/onto'));
SELECT pgrdf.materialize(pgrdf.graph_id('http://example.com/onto'), 'rdfs');
-- {"profile": "rdfs", "base_triples": 5, "inferred_triples_written": 5,
-- "previous_inferred_dropped": 0, "reasoner_errors": [], "auto_analyzed": true,
-- "elapsed_ms": …, "load_ms": …, "reason_ms": …, "diff_ms": …,
-- "write_ms": …, "analyze_ms": …}
SELECT pgrdf.materialize(pgrdf.graph_id('http://example.com/onto'));
-- {"profile": "owl-rl", "base_triples": 5, "inferred_triples_written": 14,
-- "previous_inferred_dropped": 5, …}The RDFS run derives Student ⊑ Agent, alice a Person, alice a Agent, bob a Person and bob a Agent. The OWL 2 RL run adds axiomatic and RL-specific triples on top.
| Field | Meaning |
|---|---|
profile | the profile that ran |
base_triples | asserted triples handed to the reasoner |
inferred_triples_written | entailed triples that were not already asserted |
previous_inferred_dropped | inferred rows deleted before the run |
reasoner_errors | errors reported by reasonable, as strings (always [] for 'rdfs') |
auto_analyzed | whether the closing ANALYZE ran |
elapsed_ms, load_ms, reason_ms, diff_ms, write_ms, analyze_ms | wall time overall and per step |
Where inferred triples go
Inferred rows sit in the graph's own partition with is_inferred = true:
- Read by
sparql,construct,describeandvalidate, so a validation aftermaterializechecks the entailed closure. - Ignored by
export_graph,graph_digest,structural_digestandgraph_manifest, which describe asserted content only. - Carried by
copy_graphandmove_graph. A copy's freshness then readsunknown, because the destination has inferred rows but no run record. - Guarded by
drop_graph(g, cascade => false), which refuses (2BP01) while inferred rows exist. - Used by the query translator's materialised-closure shortcut: when inferred
rdfs:subClassOf,rdfs:subPropertyOforowl:sameAsrows exist, ap+orp*path over that predicate becomes a direct match instead of a recursive walk. See Query.
graph_inventory() reports whether a graph's materialisation is never, current, stale (the asserted count changed since the run) or unknown; see Storage.
Performance notes
- The graph is read back in one scan joined to the dictionary.
- The reasoner runs in process and holds the graph's asserted triples in memory for the duration of the run. It is CPU-bound.
- New terms are resolved in one batch, and inferred quads are written with the same batched
INSERT … unneststatement the loader uses. - Reasoning at larger scale is described on Reasoning at scale.
Settings
| Setting | Default | Effect |
|---|---|---|
pgrdf.auto_analyze | on | run ANALYZE pgrdf._pgrdf_quads after a materialize that wrote rows |
Tests
60-materialize-owl-rl.sql: core OWL 2 RL entailments, repeat runs,owl:inverseOf.61-materialize-then-sparql.sql: inferred triples visible to SPARQL.62-materialize-empty.sql: an empty graph.117-materialize-rdfs.sql: the'rdfs'profile as a subset of'owl-rl', and the unknown-profile refusal.#[pg_test]s inreasonable.rs, including a check that the asserted triples survive a run unchanged.