Skip to content

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.

sql
pgrdf.materialize(graph_id BIGINT, profile TEXT DEFAULT 'owl-rl') → JSONB

Flow

text
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

ProfileEngineRules
'owl-rl' (default)the reasonable crateOWL 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_closurerdfs2 (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:

text
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

sql
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.

FieldMeaning
profilethe profile that ran
base_triplesasserted triples handed to the reasoner
inferred_triples_writtenentailed triples that were not already asserted
previous_inferred_droppedinferred rows deleted before the run
reasoner_errorserrors reported by reasonable, as strings (always [] for 'rdfs')
auto_analyzedwhether the closing ANALYZE ran
elapsed_ms, load_ms, reason_ms, diff_ms, write_ms, analyze_mswall 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, describe and validate, so a validation after materialize checks the entailed closure.
  • Ignored by export_graph, graph_digest, structural_digest and graph_manifest, which describe asserted content only.
  • Carried by copy_graph and move_graph. A copy's freshness then reads unknown, 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:subPropertyOf or owl:sameAs rows exist, a p+ or p* 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 … unnest statement the loader uses.
  • Reasoning at larger scale is described on Reasoning at scale.

Settings

SettingDefaultEffect
pgrdf.auto_analyzeonrun ANALYZE pgrdf._pgrdf_quads after a materialize that wrote rows

Tests

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