Datalog Reasoning

pg_ripple includes a built-in Datalog reasoning engine (v0.10.0+) that runs entirely inside PostgreSQL. Rules are parsed from a Turtle-flavoured syntax, stratified for evaluation order, and compiled to native SQL — no external reasoner needed.

Derived triples are written back into VP storage with source = 1, so explicit and inferred triples are always distinguishable.


Quick start

-- Load two custom rules
SELECT pg_ripple.load_rules('
  ?x <http://example.org/grandparent> ?z :-
    ?x <http://example.org/parent> ?y ,
    ?y <http://example.org/parent> ?z .
', 'family');

-- Insert some data
SELECT pg_ripple.load_ntriples('
<http://example.org/alice> <http://example.org/parent> <http://example.org/bob> .
<http://example.org/bob>   <http://example.org/parent> <http://example.org/carol> .
');

-- Run inference — inserts alice grandparent carol
SELECT pg_ripple.infer('family');
-- Returns: 1 (one derived triple)

-- Query the result
SELECT * FROM pg_ripple.sparql('
  SELECT ?gp WHERE {
    <http://example.org/alice> <http://example.org/grandparent> ?gp
  }
');

Rule syntax

Rules use a Turtle-flavoured Datalog syntax:

head :- body .
  • Variables are written as ?x, ?y, etc.
  • IRIs use angle brackets: <http://example.org/knows>
  • Prefixed IRIs use prefix:local form (if prefixes are registered)
  • Literals use quoted strings: "hello", "42"^^<xsd:integer>
  • Body atoms are separated by commas
  • Negation uses NOT: NOT ?x <http://example.org/blocked> ?y

Example rules

-- Transitive closure of knows
?x <http://example.org/knowsTransitive> ?z :-
  ?x <http://example.org/knows> ?y ,
  ?y <http://example.org/knowsTransitive> ?z .

?x <http://example.org/knowsTransitive> ?y :-
  ?x <http://example.org/knows> ?y .

-- Constraint (empty head): every Person must have a name
:- ?x <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://example.org/Person> ,
   NOT ?x <http://example.org/name> ?n .

load_rules

pg_ripple.load_rules(rules TEXT, rule_set TEXT DEFAULT 'custom') → BIGINT

Parses Turtle-flavoured Datalog rules, stratifies them (checks for negation cycles), compiles each stratum to SQL, and stores them in _pg_ripple.rules. Returns the number of rules loaded.

SELECT pg_ripple.load_rules('
  ?x <http://example.org/sibling> ?y :-
    ?x <http://example.org/parent> ?z ,
    ?y <http://example.org/parent> ?z .
', 'family');
-- Returns: 1

Rules are grouped by rule set name. You can load multiple rule sets independently and run inference on each one separately.


load_rules_builtin

pg_ripple.load_rules_builtin(name TEXT) → BIGINT

Loads a pre-defined rule set by name. Returns the number of rules loaded.

NameRulesDescription
'rdfs'13Full RDFS entailment (rdfs2–rdfs12, subclass, domain, range)
'owl-rl'~20Core OWL RL: class hierarchy, property chains, inverse, symmetric, transitive
-- Load RDFS entailment rules
SELECT pg_ripple.load_rules_builtin('rdfs');
-- Returns: 13

-- Load OWL RL rules
SELECT pg_ripple.load_rules_builtin('owl-rl');

What RDFS entailment does

If your data contains rdfs:subClassOf, rdfs:domain, rdfs:range, and similar RDFS vocabulary, running RDFS inference materializes all implied class memberships and property assignments. For example, if ex:Student rdfs:subClassOf ex:Person and ex:alice rdf:type ex:Student, then ex:alice rdf:type ex:Person is derived.

What OWL RL reasoning does

OWL RL handles richer ontology constructs: owl:inverseOf (if ex:knows is the inverse of ex:knownBy, both directions are materialized), owl:TransitiveProperty (transitive closure), owl:SymmetricProperty, owl:propertyChainAxiom, and class hierarchy axioms.


infer

pg_ripple.infer(rule_set TEXT DEFAULT 'custom') → BIGINT

Runs all strata in the named rule set and inserts derived triples with source = 1. Returns the number of new triples inserted. Safe to call repeatedly — duplicate triples are ignored.

SELECT pg_ripple.infer('rdfs');
-- Returns: 42 (the number of new derived triples)

Non-recursive strata use INSERT … SELECT … ON CONFLICT DO NOTHING. Recursive strata use WITH RECURSIVE … CYCLE (PostgreSQL 18 native cycle detection).


infer_with_stats

pg_ripple.infer_with_stats(rule_set TEXT) → JSONB

Runs semi-naive fixpoint evaluation on the named rule set and returns a JSONB object with the number of derived triples and the number of fixpoint iterations taken (v0.24.0+).

SELECT pg_ripple.infer_with_stats('rdfs');
-- Returns: {"derived": 42, "iterations": 3, "eliminated_rules": []}

Why use this instead of infer()? For large ontologies, semi-naive evaluation is significantly faster because each fixpoint iteration only re-evaluates rules against new triples derived in the previous iteration (the ΔR delta), rather than rescanning the entire derived relation. The iterations counter tells you how many iterations the engine needed to reach the fixpoint — bounded by the longest derivation chain, not the size of the dataset.

The eliminated_rules array (v0.29.0+) lists any rules that were removed by subsumption checking before evaluation: a rule R2 is subsumed by R1 when R1's body is a multiset-subset of R2's body (R2 would only derive a subset of what R1 derives). Eliminating subsumed rules reduces the number of SQL statements executed per fixpoint iteration.

Semi-naive evaluation mechanics

The engine maintains, for each derived relation R, a delta table ΔR containing only the rows derived in the most recent iteration. Each iteration:

  1. For every rule in the current stratum, re-evaluate the rule body against ΔR (the delta of its input relations).
  2. Insert any new rows into ΔR_new with ON CONFLICT DO NOTHING.
  3. After all rules are processed: ΔR ← ΔR_new, then continue if ΔR is non-empty.

This means iteration cost scales with the frontier of new derivations, not the total size of the relation. On RDFS closure over a dataset where the longest subClassOf chain has depth 5, the engine converges in 5 iterations regardless of how many triples there are.

Stratified evaluation order is preserved: each stratum is fully converged before the next stratum begins. Semi-naive is applied within each stratum.

OWL RL coverage

The built-in owl-rl rule set implements the following OWL 2 RL axioms:

OWL RL RuleAxiomStatus
rdfs2domain inference
rdfs3range inference
rdfs4a/4bResource membership
rdfs5subPropertyOf transitivity
rdfs7 / prp-spo1subPropertyOf propagation
rdfs9 / cax-scosubClassOf type propagation
rdfs11subClassOf transitivity
cls-avfallValuesFrom chaining
prp-ifpInverseFunctionalProperty
prp-symSymmetricProperty
prp-trpTransitiveProperty
prp-inv1/2inverseOf
prp-fpFunctionalProperty
cax-eqc1equivalentClass
prp-eqp1equivalentProperty
prp-chmpropertyChainAxiom (2-link)
cls-hv1hasValue restriction
cls-int1intersectionOf membership
eq-symsameAs symmetry
eq-transsameAs transitivity
eq-rep-csameAs class propagation
owl:onProperty + allValuesFromcls-avf full form

Rules that require decidable enumeration (e.g. owl:oneOf, cls-oo) or second-order patterns are outside the OWL RL profile and are not implemented.


infer_goal

pg_ripple.infer_goal(rule_set TEXT, goal TEXT) → JSONB

Runs goal-directed inference using a simplified magic sets transformation (v0.29.0+). Instead of deriving every possible fact, only the facts relevant to the specified goal triple pattern are materialized. Returns a JSONB object with three fields:

FieldTypeDescription
derivedbigintTotal triples inserted by the inference
iterationsintegerNumber of fixpoint iterations
matchingbigintTriples that match the goal pattern after inference
-- How many rdfs:type triples can we derive with type foaf:Person?
SELECT pg_ripple.infer_goal('rdfs', '?x <http://xmlns.com/foaf/0.1/type> <http://xmlns.com/foaf/0.1/Person>');
-- Returns: {"derived": 14, "iterations": 2, "matching": 5}

-- Fully open goal (equivalent to infer_with_stats but goal-directed machinery still prunes internally)
SELECT pg_ripple.infer_goal('rdfs', '?x ?p ?y');

Goal syntax

A goal is a triple pattern string. Variables are written as ?name. IRIs use angle-bracket notation:

  • ?x rdf:type ex:Person — find all persons (prefix form — uses registered prefix map)
  • ?x <http://example.org/knows> ?y — all knows triples
  • <http://example.org/alice> ?p ?o — all triples about Alice
  • ?x ?p ?y — fully open (all triples)

Magic sets strategy

For each bound term in the goal, the engine identifies which rules can derive triples matching that pattern and restricts the fixpoint evaluation to those rules. Magic temp tables (_magic_{rule_set}_{pred}) hold the demanded binding set and are automatically dropped at the end of inference.

Set pg_ripple.magic_sets = false to disable the transformation and fall back to full bottom-up evaluation (useful for debugging).


check_constraints

pg_ripple.check_constraints(rule_set TEXT DEFAULT NULL) → JSONB

Evaluates all integrity constraints (rules with empty heads) and returns violations as a JSONB array. Pass NULL to check all rule sets, or a specific name to check one.

SELECT pg_ripple.check_constraints();
-- [
--   {"rule_set": "family", "rule_index": 3, "bindings": {"x": "<http://example.org/alice>"}},
--   ...
-- ]

An empty array means no violations.


list_rules

pg_ripple.list_rules() → JSONB

Returns all active rules as a JSONB array. Each element includes the rule set name, stratum, head, body, and compiled SQL.

SELECT pg_ripple.list_rules();

drop_rules

pg_ripple.drop_rules(rule_set TEXT) → BIGINT

Deletes all rules in a named rule set. Returns the number of rules deleted.

SELECT pg_ripple.drop_rules('family');

Note: This does not delete triples that were already derived by those rules. To remove derived triples, delete rows where source = 1 from the relevant VP tables, or use vacuum_dictionary() after clearing them.


enable_rule_set / disable_rule_set

pg_ripple.enable_rule_set(name TEXT) → VOID
pg_ripple.disable_rule_set(name TEXT) → VOID

Toggle a rule set between active and inactive. Disabled rule sets are skipped by infer() and check_constraints() but remain in the catalog.

-- Temporarily disable OWL RL reasoning
SELECT pg_ripple.disable_rule_set('owl-rl');

-- Re-enable later
SELECT pg_ripple.enable_rule_set('owl-rl');

prewarm_dictionary_hot

pg_ripple.prewarm_dictionary_hot() → BIGINT

Loads frequently-used IRIs (≤ 512 bytes) into an UNLOGGED hot table (_pg_ripple.dictionary_hot) for sub-microsecond lookups during inference. Returns the number of rows loaded.

The hot table survives connection pooling but not a database restart. It is automatically populated at _PG_init when pg_ripple.inference_mode != 'off'.

SELECT pg_ripple.prewarm_dictionary_hot();
-- Returns: 1024

SHACL-AF bridge

When shapes loaded via load_shacl() contain sh:rule properties, pg_ripple detects them and registers placeholder entries in the Datalog rules catalog. This bridges SHACL Advanced Features (SHACL-AF) rule definitions with the Datalog engine.


Aggregate rules (Datalog^agg, v0.30.0)

pg_ripple v0.30.0 adds aggregate literals to the Datalog engine, allowing rules to derive facts that depend on computed aggregates (COUNT, SUM, MIN, MAX, AVG) over the triple store. This unlocks graph analytics and metrics directly from inference rules — for example, "count the number of friends each person has" or "find the maximum salary in each department".

Aggregate rule syntax

An aggregate literal appears in the rule body and uses the following form:

FUNC(?aggVar WHERE subject pred object) = ?resultVar

Where:

  • FUNC is one of COUNT, SUM, MIN, MAX, AVG
  • ?aggVar is the variable to aggregate over (must appear in the atom's subject or object position)
  • subject pred object is the atom pattern (each can be a variable or IRI constant)
  • ?resultVar must appear in the rule head

Example — count friends:

SELECT pg_ripple.load_rules(
  '?x <https://example.org/friendCount> ?n :-
     COUNT(?y WHERE ?x <https://xmlns.com/foaf/0.1/knows> ?y) = ?n .',
  'social'
);

-- Insert data
SELECT pg_ripple.insert_triple(
  '<https://example.org/Alice>',
  '<https://xmlns.com/foaf/0.1/knows>',
  '<https://example.org/Bob>'
);
SELECT pg_ripple.insert_triple(
  '<https://example.org/Alice>',
  '<https://xmlns.com/foaf/0.1/knows>',
  '<https://example.org/Carol>'
);

-- Run aggregate inference
SELECT pg_ripple.infer_agg('social');
-- Returns: {"derived": 0, "aggregate_derived": 1, "iterations": 0}

-- Query result: Alice has 2 friends
SELECT * FROM pg_ripple.find_triples(
  '<https://example.org/Alice>',
  '<https://example.org/friendCount>',
  NULL
);

pg_ripple.infer_agg(rule_set TEXT DEFAULT 'custom') RETURNS JSONB

Run Datalog^agg inference for a rule set. Non-aggregate rules are evaluated first via semi-naive fixpoint; aggregate rules are evaluated in a single GROUP BY pass afterwards.

JSON keyTypeDescription
derivedbigintTotal triples derived (non-aggregate + aggregate)
aggregate_derivedbigintTriples derived from aggregate rules
iterationsintSemi-naive fixpoint iterations for non-aggregate rules
SELECT pg_ripple.infer_agg('social');
-- {"derived": 1, "aggregate_derived": 1, "iterations": 0}

Aggregation stratification (PT510)

Aggregate rules must be stratified: no derived predicate may appear in the body of a rule that aggregates over a predicate which also depends on that derived predicate. If a stratification violation is detected, pg_ripple emits WARNING PT510 and skips the aggregate rules (falling back to running only non-aggregate rules safely).

-- This rule pair creates a cycle through aggregation — PT510 will be emitted:
SELECT pg_ripple.load_rules(
  '?x <ex:a> ?y :- ?x <ex:b> ?y .', 'bad');
SELECT pg_ripple.load_rules(
  '?x <ex:b> ?n :- COUNT(?y WHERE ?x <ex:a> ?y) = ?n .', 'bad');

SELECT pg_ripple.infer_agg('bad');
-- WARNING:  infer_agg: aggregation stratification violation (PT510): …

Rule plan cache (v0.30.0)

The compiled SQL for each rule set is cached in a process-local LRU so that repeated infer() / infer_agg() calls on the same rule set skip the parse + compile step.

The cache is automatically invalidated when load_rules() or drop_rules() is called for that rule set.

pg_ripple.rule_plan_cache_stats() RETURNS TABLE(...)

Returns statistics from the plan cache.

ColumnTypeDescription
rule_settextName of the rule set
hitsbigintNumber of times the cached SQL was used
missesbigintNumber of cache misses (SQL was compiled from scratch)
entriesintTotal number of rule sets currently in the cache
-- After two calls to infer_agg():
SELECT * FROM pg_ripple.rule_plan_cache_stats();
-- rule_set | hits | misses | entries
-- ---------+------+--------+---------
-- social   |    1 |      1 |       1

Entity Resolution & Demand Transformation (v0.31.0)

owl:sameAs entity canonicalization

When pg_ripple.sameas_reasoning = on (default), the inference engine automatically handles owl:sameAs triples. Before each fixpoint iteration, it computes equivalence classes from all owl:sameAs triples in the store, then rewrites rule-body constants to their canonical (lowest dictionary-ID) representative.

This means that if your knowledge graph contains:

ex:Alice owl:sameAs ex:A.Smith .
ex:Alice ex:name "Alice" .

then rules that would derive new facts for ex:A.Smith (e.g., from patterns that match ex:name) will correctly produce results for the canonical ex:Alice entity. SPARQL queries referencing ex:A.Smith are transparently redirected to ex:Alice.

GUC: pg_ripple.sameas_reasoning (bool, default true) — set to false to disable the pre-pass.

pg_ripple.infer_demand(rule_set TEXT DEFAULT 'custom', demands JSONB) RETURNS JSONB

Goal-directed inference restricted to the subset of rules needed to derive the specified demands. This is a generalisation of infer_goal() (single goal, single predicate) that supports multiple goal patterns at once.

demands is a JSONB array of goal patterns. Each element is an object with optional "s", "p", "o" keys:

SELECT pg_ripple.infer_demand('rdfs', '[{"p": "<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>"}]');
-- Derives only triples needed to answer rdf:type queries.

When demands is an empty array ('[]'), runs full inference identically to infer().

Return value (JSONB):

KeyTypeDescription
derivednumberTotal triples derived
iterationsnumberFixpoint iteration count
demand_predicatesarrayPredicate IRIs used as demand seeds (decoded)
-- Derive only "descendantOf" and its dependencies, ignoring unrelated rules.
SELECT pg_ripple.infer_demand('hierarchy', '[{"p": "<https://ex.org/descendantOf>"}]');
-- {"derived": 5, "iterations": 2, "demand_predicates": ["https://ex.org/descendantOf"]}

GUC: pg_ripple.demand_transform (bool, default true) — when true, create_datalog_view() automatically applies demand transformation when multiple goal patterns are specified.


Configuration

GUCDefaultDescription
pg_ripple.inference_mode'on_demand''off' disables the engine entirely; 'on_demand' evaluates via CTEs when infer() is called; 'materialized' uses pg_trickle stream tables for automatic refresh
pg_ripple.enforce_constraints'warn''off' silences constraint violations; 'warn' logs them; 'error' raises an exception
pg_ripple.rule_graph_scope'default''default' applies rules to the default graph only; 'all' applies rules across all named graphs
pg_ripple.magic_setstrueMaster switch for goal-directed magic sets inference (v0.29.0+)
pg_ripple.datalog_cost_reordertrueSort Datalog body atoms by VP-table cardinality at compile time (v0.29.0+)
pg_ripple.datalog_antijoin_threshold1000Minimum VP-table row count for using LEFT JOIN … IS NULL anti-join form for negation (v0.29.0+)
pg_ripple.delta_index_threshold500Minimum delta-table row count before creating a B-tree index on (s, o) (v0.29.0+)
pg_ripple.rule_plan_cachetrueMaster switch for the Datalog rule plan cache (v0.30.0+)
pg_ripple.rule_plan_cache_size64Maximum number of rule sets kept in the plan cache; oldest entries evicted on overflow (v0.30.0+)
pg_ripple.sameas_reasoningtrueEnable owl:sameAs canonicalization pre-pass before each inference run (v0.31.0+)
pg_ripple.demand_transformtrueAuto-apply demand transformation in create_datalog_view() with multiple goals (v0.31.0+)
-- Enable strict constraint enforcement
SET pg_ripple.enforce_constraints = 'error';

-- Apply rules across all graphs
SET pg_ripple.rule_graph_scope = 'all';

-- Disable magic sets for debugging goal-directed inference
SET pg_ripple.magic_sets = false;

-- Force anti-join form for all negated atoms (even small tables)
SET pg_ripple.datalog_antijoin_threshold = 1;

-- Disable the rule plan cache (useful for testing)
SET pg_ripple.rule_plan_cache = false;

-- Set a smaller plan cache (saves memory on servers with many rule sets)
SET pg_ripple.rule_plan_cache_size = 16;

Internal tables

TableDescription
_pg_ripple.rulesStores each parsed rule with its set name, stratum, head, body, and compiled SQL
_pg_ripple.rule_setsTracks named rule sets with their active/inactive flag
_pg_ripple.dictionary_hotUNLOGGED hot cache for frequently-used IRIs

Derived triples are stored in the same VP tables as explicit triples, distinguished by the source column: 0 = explicit, 1 = derived.


Well-founded semantics (v0.32.0)

For programs with cyclic negation (rules that are mutually recursive through NOT), stratification fails and infer() cannot be used. infer_wfs() handles these programs using the alternating-fixpoint algorithm (Van Gelder et al., 1991), which assigns a third truth value — unknown — to facts that are neither provably true nor false.

-- Load a non-stratifiable rule set (mutual negation)
SELECT pg_ripple.load_rules('
  ?x <https://ex.org/trusted>   <https://ex.org/yes> :-
    ?x <https://ex.org/person>    <https://ex.org/yes> ,
    NOT ?x <https://ex.org/untrusted> <https://ex.org/yes> .
  ?x <https://ex.org/untrusted> <https://ex.org/yes> :-
    ?x <https://ex.org/person>    <https://ex.org/yes> ,
    NOT ?x <https://ex.org/trusted>   <https://ex.org/yes> .
', 'trust');

SELECT pg_ripple.infer_wfs('trust');
-- Returns: {"certain": 0, "unknown": 2, "derived": 2, "iterations": 2, "stratifiable": false}

For stratifiable programs, infer_wfs() falls through to run_inference_seminaive and returns "stratifiable": true with "unknown": 0.

pg_ripple.infer_wfs(rule_set TEXT DEFAULT 'custom') RETURNS JSONB

KeyTypeDescription
certainintegerFacts concluded with certainty (true)
unknownintegerFacts that are neither true nor false
derivedintegerTotal derived facts (certain + unknown)
iterationsintegerNumber of alternating fixpoint rounds
stratifiablebooleanWhether the program was stratifiable (fast path)

Tabling cache integration

infer_wfs() results are automatically cached in _pg_ripple.tabling_cache (when pg_ripple.tabling = on). Repeated calls with the same rule set return the cached result without re-running the fixpoint. The cache is invalidated by insert_triple(), delete_triple(), drop_rules(), and load_rules().


Tabling / memoisation (v0.32.0)

The tabling cache stores results of infer_wfs() calls, keyed by an XXH3-64 hash of the goal string, so repeated identical calls pay zero computation cost within the TTL.

pg_ripple.tabling_stats() RETURNS TABLE(...)

SELECT * FROM pg_ripple.tabling_stats();
-- goal_hash | hits | computed_ms | cached_at
ColumnTypeDescription
goal_hashBIGINTXXH3-64 hash of the cached goal key
hitsBIGINTNumber of cache hits for this entry
computed_msFLOAT8Wall-clock time (ms) of the original computation
cached_atTEXTISO-8601 timestamp of when the entry was written

Tabling GUCs

GUCTypeDefaultDescription
pg_ripple.tablingbooltrueEnable/disable the tabling cache
pg_ripple.tabling_ttlinteger300Cache TTL in seconds; 0 = never expire
pg_ripple.wfs_max_iterationsinteger100Safety cap on WFS fixpoint rounds; emits WARNING PT520 if exceeded

Well-Founded Semantics limits (v0.45.0)

When a program contains cyclic negation that prevents the alternating fixpoint from converging, infer_wfs() stops after pg_ripple.wfs_max_iterations rounds and returns a partial result with "stratifiable": false. A PostgreSQL WARNING with code PT520 is emitted to the client.

SET pg_ripple.wfs_max_iterations = 10;
SELECT pg_ripple.infer_wfs('cyclic_rules');
-- Returns: {"certain": 3, "unknown": 6, "derived": 9, "iterations": 10, "stratifiable": false}
-- WARNING:  PT520: WFS fixpoint cap reached (10 iterations)

Detecting the cap: check "stratifiable": false in the result and "iterations" = pg_ripple.wfs_max_iterations.

Recovering: increase the cap, or restructure the program to break the negation cycle. For programs where some atoms are genuinely indeterminate, the partial result is valid — atoms in the "unknown" bucket are three-valued unknown.


Bounded-Depth Termination & Incremental Retraction (v0.34.0)

pg_ripple.datalog_max_depth GUC

GUCTypeDefaultDescription
pg_ripple.datalog_max_depthinteger0Maximum derivation depth for recursive rules; 0 = unlimited

When set to a positive integer d, any recursive Datalog rule compiled by load_rules() emits a WITH RECURSIVE (s, o, g, depth) CTE that adds a depth counter column. The base case starts at depth = 0; each recursive step increments depth and the recursion terminates when depth >= d. Non-recursive rules are unaffected.

-- Limit inference to 3 hops
SET pg_ripple.datalog_max_depth = 3;
SELECT pg_ripple.load_rules('...', 'my_rules');
SELECT pg_ripple.infer('my_rules');
-- Reset to unlimited
SET pg_ripple.datalog_max_depth = 0;

pg_ripple.add_rule(rule_set TEXT, rule_text TEXT) RETURNS BIGINT

Adds a single rule to an existing rule set without discarding previous materialization. Returns the new rule's ID.

SELECT pg_ripple.add_rule(
  'my_rules',
  '?x <https://ex.org/knows2> ?z :- ?x <https://ex.org/knows> ?y , ?y <https://ex.org/knows> ?z .'
);

After insertion, one additional semi-naive pass is triggered for the affected stratum only. All previously derived facts are preserved.

pg_ripple.remove_rule(rule_id BIGINT) RETURNS BIGINT

Removes a rule by its ID (as returned by list_rules() or add_rule()). Returns the ID of the removed rule, or 0 if no matching rule was found.

SELECT pg_ripple.remove_rule(42);

Derived facts that were solely supported by the removed rule are retracted via the DRed algorithm (see below), provided pg_ripple.dred_enabled = true. Facts supported by alternative derivation paths are preserved.

pg_ripple.dred_on_delete(pred_id BIGINT, s BIGINT, o BIGINT, g BIGINT) RETURNS BIGINT

Low-level entry point for the Delete-Rederive (DRed) algorithm. Normally invoked automatically by the CDC delete path. Returns the number of derived triples retracted.

ParameterDescription
pred_idEncoded predicate ID (from the dictionary)
sEncoded subject ID
oEncoded object ID
gEncoded graph ID

DRed GUCs

GUCTypeDefaultDescription
pg_ripple.dred_enabledbooltrueEnable DRed incremental retraction; false falls back to full recompute
pg_ripple.dred_batch_sizeinteger1000Maximum base triples to process in a single DRed transaction

Parallel Stratum Evaluation (v0.35.0)

Parallel evaluation GUCs

Within a single stratum, rules that derive different predicates with no shared body dependencies can execute independently. pg_ripple analyses this dependency graph and partitions rules into groups. The infer_with_stats() function reports the number of groups detected.

GUCTypeDefaultDescription
pg_ripple.datalog_parallel_workersinteger4Maximum parallel worker count for stratum evaluation; 1 = serial
pg_ripple.datalog_parallel_thresholdinteger10000Minimum estimated total row count before parallel analysis is applied
-- Enable maximum parallelism for a large OWL RL closure.
SET pg_ripple.datalog_parallel_workers = 8;
SET pg_ripple.datalog_parallel_threshold = 0;
SELECT pg_ripple.infer_with_stats('owl-rl');

infer_with_stats() parallel fields

The infer_with_stats() function now includes two additional fields in its JSONB output (v0.35.0):

FieldTypeDescription
parallel_groupsintegerNumber of independent rule groups detected in the rule set
max_concurrentintegerEffective worker count: min(parallel_groups, datalog_parallel_workers)
SELECT pg_ripple.infer_with_stats('owl-rl');
-- {
--   "derived": 1240,
--   "iterations": 4,
--   "eliminated_rules": [],
--   "parallel_groups": 3,
--   "max_concurrent": 3
-- }

A parallel_groups value of 1 means all rules form a single dependency chain and no parallelism is possible. Values > 1 indicate that independent groups exist.


Lattice-Based Datalog — Datalog^L (v0.36.0)

Standard Datalog^agg requires aggregation-stratification — aggregates can only appear in a strictly higher stratum than their inputs. Lattice-Based Datalog relaxes this requirement by demanding only that the aggregation is monotone with respect to a user-specified partial order (a lattice).

What is a lattice?

A lattice is an algebraic structure (L, ⊔) where:

  • L is the value domain (numbers, sets, intervals, etc.)
  • ⊔ is the join operation — commutative, associative, idempotent
  • There is a bottom element ⊥ such that ⊥ ⊔ x = x for all x

Fixpoint computation over a lattice is guaranteed to terminate when the ascending chain condition holds (no infinite strictly ascending chains).

Built-in lattices

NameJoinBottomTypical use
minMIN+∞ (i64::MAX)Trust propagation, shortest paths
maxMAX−∞ (i64::MIN)Longest paths, reachability
setUNION{} (empty set)Set-valued annotations
intervalinterval hullempty intervalTemporal/numeric ranges

New GUC parameters (v0.36.0)

GUCDefaultDescription
pg_ripple.lattice_max_iterations1000Maximum fixpoint iterations before PT540 warning

Functions

pg_ripple.create_lattice(name, join_fn, bottom)boolean

Register a user-defined lattice.

  • name — unique lattice identifier
  • join_fn — a PostgreSQL aggregate function (must be commutative and associative)
  • bottom — bottom element as text

Returns true if newly registered, false if it already existed.

-- Trust scores over [0, 100] — min-lattice (lower is more trusted).
SELECT pg_ripple.create_lattice('trust', 'min', '100');

pg_ripple.list_lattices()jsonb

List all registered lattice types.

SELECT jsonb_pretty(pg_ripple.list_lattices());

Example output:

[
  {"name": "min",   "join_fn": "min",       "bottom": "9223372036854775807", "builtin": true},
  {"name": "max",   "join_fn": "max",       "bottom": "-9223372036854775808", "builtin": true},
  {"name": "set",   "join_fn": "array_agg", "bottom": "{}", "builtin": true},
  {"name": "trust", "join_fn": "min",       "bottom": "100", "builtin": false}
]

pg_ripple.infer_lattice(rule_set, lattice_name)jsonb

Run a lattice-based Datalog fixpoint for all rules in rule_set using the specified lattice.

-- Run trust propagation using the MinLattice.
SELECT pg_ripple.infer_lattice('trust_rules', 'min');
-- {
--   "derived": 42,
--   "iterations": 5,
--   "lattice": "min",
--   "rule_set": "trust_rules"
-- }

Returns JSONB with fields:

  • derived — number of new lattice values written
  • iterations — fixpoint iterations performed
  • lattice — lattice type used
  • rule_set — rule set evaluated

Trust propagation example

-- 1. Register a MinLattice for trust scores.
SELECT pg_ripple.create_lattice('trust', 'min', '100');

-- 2. Insert direct trust edges (score 0–100, lower = more trusted).
SELECT pg_ripple.load_ntriples('
  <https://example.org/alice> <https://example.org/directTrust> "80"^^<xsd:integer> .
  <https://example.org/bob>   <https://example.org/directTrust> "70"^^<xsd:integer> .
  <https://example.org/alice> <https://example.org/knows>       <https://example.org/bob> .
');

-- 3. Run lattice fixpoint to propagate transitive trust.
SELECT pg_ripple.infer_lattice('trust_rules', 'trust');

-- 4. Query propagated trust values.
SELECT * FROM pg_ripple.sparql('
  SELECT ?x ?t WHERE { ?x <https://example.org/directTrust> ?t }
');

Error code PT540

When pg_ripple.lattice_max_iterations is exceeded, the engine emits a WARNING:

WARNING:  PT540: lattice fixpoint did not converge after 1000 iterations; returning partial results.

If you see this warning, either:

  • Increase pg_ripple.lattice_max_iterations, or
  • Verify that your lattice join function is truly monotone and the value domain is finite.