What Is pg_ripple?
pg_ripple turns your PostgreSQL database into a knowledge graph store — and into the foundation for AI applications that need verifiable, structured, traceable answers rather than hallucinated ones.
You can build a chatbot that answers questions from your own knowledge base, deduplicate customer records across systems, validate data quality with formal rules, run OWL reasoning, and answer SPARQL queries over billions of triples — all inside the PostgreSQL you already operate, with no extra infrastructure.
-- Ask a natural-language question against your knowledge graph.
SELECT pg_ripple.rag_context('Which drugs interact with metformin?', k := 8);
-- Returns structured context ready to pass to an LLM.
-- Or query directly in SPARQL.
SELECT * FROM pg_ripple.sparql($$
PREFIX ex: <https://example.org/>
SELECT ?name WHERE {
ex:alice <http://xmlns.com/foaf/0.1/knows>+ ?person .
?person <http://xmlns.com/foaf/0.1/name> ?name .
}
$$);
-- Follows the foaf:knows relationship through any number of hops.
The three problems pg_ripple solves
1. AI that knows what it knows — and can prove it
LLM responses fabricate facts because the model has no reliable source of truth. rag_context() retrieves structured graph context for any question in a single SQL call, grounding the LLM's answer in your data. Every retrieved fact links back to its source triple, PROV-O provenance, and audit log entry.
→ AI Overview & Decision Tree | Chatbot Recipe
2. Record linkage and deduplication at database speed
Deduplicate customer records across CRM and ERP, align external ontologies, or merge research entity mentions — all inside one PostgreSQL transaction. Knowledge-graph embeddings generate candidates; SHACL hard rules block unsafe merges; owl:sameAs canonicalization makes the merged view transparent to every downstream query.
→ Record Linkage | Dedup Recipe
3. Knowledge graph + relational + vector in one transaction
No separate graph store, no separate vector index, no separate schema registry. One pg_dump captures everything. One ACID transaction spans triple writes, vector index updates, and ordinary table updates together.
→ Architecture at a Glance | Comparison vs Alternatives
Key capabilities
| Capability | What it does |
|---|---|
| SPARQL 1.1 | W3C standard graph query — full conformance, < 10 ms typical |
| SHACL validation | Define and enforce data-quality rules — reject bad data on insert |
| Datalog reasoning | Derive new facts from RDFS, OWL RL/EL/QL, or custom rules |
| Vector + graph hybrid | SPARQL traversal combined with pgvector HNSW similarity search |
| RAG pipeline | rag_context() — one call retrieves structured LLM context |
| NL → SPARQL | sparql_from_nl() — auto-generate SPARQL from natural-language questions |
| Record linkage | KGE embeddings + SHACL gates + owl:sameAs canonicalization |
| GraphRAG | Microsoft GraphRAG ingest, enrich, validate, export pipeline |
| Federation | Query Wikidata, DBpedia, and other SPARQL endpoints alongside local data |
| R2RML | Generate an RDF graph from any existing PostgreSQL schema |
| JSON-LD framing | Export nested JSON documents shaped to your API contract |
| SPARQL Protocol | Standard HTTP endpoint via pg_ripple_http |
Key numbers
| Metric | Value |
|---|---|
| Bulk load throughput | > 100 K triples/sec (commodity hardware) |
| SPARQL query latency | < 10 ms for typical star patterns |
| W3C SPARQL 1.1 | 100 % conformance |
| W3C SHACL Core | 100 % conformance |
| W3C OWL 2 RL | 100 % conformance |
| PostgreSQL version | 18 |
Architecture at a glance
┌─────────────────────────────────────────────────┐
│ PostgreSQL 18 │
│ ┌───────────────────────────────────────────┐ │
│ │ pg_ripple extension │ │
│ │ ┌─────────┐ ┌────────┐ ┌───────────┐ │ │
│ │ │Dictionary│ │ SPARQL │ │ Datalog │ │ │
│ │ │ Encoder │ │ Engine │ │ Engine │ │ │
│ │ └────┬─────┘ └───┬────┘ └─────┬─────┘ │ │
│ │ │ │ │ │ │
│ │ ┌────┴─────────────┴─────────────┴─────┐ │ │
│ │ │ VP Tables (one per predicate) │ │ │
│ │ │ HTAP: delta + main + merge worker │ │ │
│ │ └──────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
▲ ▲
│ SQL │ HTTP
Application pg_ripple_http
Every IRI, literal, and blank node is mapped to a compact integer ID by the dictionary encoder. Data is stored in Vertical Partitioning (VP) tables — one table per unique predicate — with integer-only joins for fast query execution. The HTAP architecture separates read and write paths so that heavy loads do not block queries.
Start here — pick your path
| Your role | Start with | Then explore | Go deeper |
|---|---|---|---|
| PostgreSQL DBA — you know PostgreSQL, new to RDF | Installation → Hello World | Key Concepts (RDF for PG users) | Operations, Tuning |
| Data / AI Engineer — building RAG or knowledge pipelines | AI Overview → Grounded Chatbot | Vector + Hybrid Search | GraphRAG, NL-to-SPARQL |
| Semantic Web / RDF Engineer — you know SPARQL and OWL | SPARQL Features → Datalog | SHACL Validation | Federation, OWL Profiles |
Next steps
- Evaluating? When to Use pg_ripple, Comparison vs Alternatives, and Performance Results have what you need.
- Ready to install? Start with Installation then Hello World in Five Minutes.
- Building a RAG or AI application? Read the AI Overview and the Grounded Chatbot recipe.
- Migrating from a relational database or Neo4j? See R2RML (relational → RDF) and LPG/Cypher Mapping.
- Deduplicating records? See Record Linkage and the Dedup recipe.
- Want the full picture? The Guided Tutorial takes you from loading data to inference in 30 minutes.
- Want to contribute? See Contributing.
When to Use pg_ripple
pg_ripple is a PostgreSQL extension that turns your database into a knowledge graph store. This page helps you decide whether it fits your architecture.
Decision flowchart
Ask yourself these questions in order:
- Do you already run PostgreSQL? If yes, pg_ripple integrates with zero additional infrastructure for the data store. If you run a different database, evaluate the migration cost.
- Do you need to model complex relationships? If your data is primarily tabular with few joins, standard SQL may be simpler. If you have deeply nested, many-to-many, or hierarchical relationships, a graph model helps.
- Do you need a standard query language? SPARQL is a W3C standard with broad tool support. If you prefer a property-graph query language (Cypher/GQL), consider Neo4j or Amazon Neptune.
- Do you need reasoning or validation? pg_ripple includes SHACL validation and Datalog reasoning. Standalone triple stores like Virtuoso or Blazegraph may not.
- Do you need graph context for LLM prompts? pg_ripple combines SPARQL graph traversal with pgvector similarity search in a single query — something pure vector databases cannot do.
Comparison matrix
| Criterion | pg_ripple | Plain SQL | Virtuoso / Blazegraph | Neo4j | Pure vector DB |
|---|---|---|---|---|---|
| Deployment | PostgreSQL extension | Any RDBMS | Standalone JVM | Standalone | Standalone |
| Query language | SPARQL 1.1 | SQL | SPARQL 1.1 | Cypher | Proprietary |
| Data model | RDF (triples) | Relational | RDF (triples) | Property graph | Vectors + metadata |
| Schema validation | SHACL | CHECK / triggers | Varies | Constraints | None |
| Reasoning | Datalog (RDFS, OWL RL) | Manual SQL | RDFS / OWL (varies) | None built-in | None |
| Vector search | pgvector integration | pgvector | Not built-in | Limited | Native |
| Hybrid graph+vector | Yes (single query) | Manual joins | No | No | No |
| HTTP API | pg_ripple_http | Build your own | Built-in | Built-in | Built-in |
| Transactions | Full PostgreSQL ACID | Full ACID | Varies | ACID | Varies |
| Backup/restore | pg_dump/pg_restore | Standard | Custom tools | Custom tools | Custom tools |
| Operational complexity | Low (PostgreSQL) | Low | Medium–High | Medium | Medium |
When pg_ripple is a good fit
- You already operate PostgreSQL and want to avoid managing a separate graph database
- Your data has rich, interconnected relationships (ontologies, catalogs, supply chains)
- You need SPARQL 1.1 compliance for interoperability with W3C-standard tools
- You need to validate data quality against formal rules (SHACL)
- You need to derive new facts from existing data (Datalog reasoning, OWL RL, RDFS)
- You want to combine graph traversal with vector similarity for RAG pipelines
- You need full ACID transactions on graph data
When pg_ripple is not the best fit
- Graph datasets exceeding ~1 billion triples: pg_ripple has been tested to 100M triples. For very large datasets, consider distributed solutions.
- Property graph with Cypher/GQL: if your team already uses Cypher and Neo4j, migrating to SPARQL has a learning curve. pg_ripple speaks SPARQL, not Cypher.
- Pure vector search workload: if you only need approximate nearest neighbor search without graph traversal, pgvector alone is simpler.
- Real-time streaming graphs: pg_ripple processes data in transactions, not continuous streams. For streaming graph analytics, consider Apache Flink with a graph library.
- No PostgreSQL in your stack: if you run MySQL, MongoDB, or a managed NoSQL service and have no plans to adopt PostgreSQL, introducing it solely for pg_ripple adds operational overhead.
AI/LLM comparison: when does graph context outperform flat vector retrieval?
Graph-augmented retrieval helps when:
- The query requires multi-hop reasoning — "find papers by co-authors of Alice's co-authors" cannot be answered by vector similarity alone
- Entity deduplication matters —
owl:sameAscanonicalization ensures the same entity is not embedded multiple times with different IRIs - Structured output is needed — JSON-LD framing produces token-efficient, structured context that flat top-k results cannot provide
- Provenance matters — graph traversal can trace why a fact is relevant, not just that it is similar
Pure vector search (Qdrant, Weaviate, pgvector-only) is sufficient when:
- The query is a simple "find similar documents" without relationship constraints
- Your corpus is unstructured text without entity-level structure
- Latency requirements are sub-millisecond at millions of vectors
Next steps
- Installation — get pg_ripple running
- Hello World — load and query data in five minutes
Architecture at a Glance
A two-minute overview of how pg_ripple is built. The full architectural reference lives at Reference → Architecture; this page is the summary you can hand to an architect during evaluation.
Where pg_ripple sits
┌────────────────────────────────────────────────────────────────────┐
│ Your application │
│ │
│ psql / asyncpg / JDBC HTTP / SPARQL Protocol │
└──────────────┬───────────────────────────┬─────────────────────────┘
│ SQL │ HTTP
▼ ▼
┌────────────────────────┐ ┌────────────────────────┐
│ PostgreSQL 18 │ │ pg_ripple_http │
│ │◄───┤ (companion service) │
│ ┌──────────────────┐ │ │ Rust + Axum + deadpool│
│ │ pg_ripple │ │ └────────┬───────────────┘
│ │ extension │ │ │
│ │ (Rust + pgrx) │ │ │ pg_ripple.sparql(...)
│ └──────────────────┘ │ │
└────────────────────────┘ │
│ │
▼ │
┌────────────────────────────────────────┘
│ Storage layer (PostgreSQL tables, all integer-encoded)
│
│ _pg_ripple.dictionary — IRI / blank / literal → BIGINT
│ _pg_ripple.vp_<predicate_id> — one VP table per predicate
│ _pg_ripple.vp_rare — long-tail predicates
│ _pg_ripple.embeddings — pgvector vectors, HNSW indexed
│ _pg_ripple.kge_embeddings — graph-structural embeddings
│ _pg_ripple.audit_log — every SPARQL UPDATE
│ …
Everything is plain PostgreSQL. There is no separate graph store, no separate vector store, no separate cache. A single pg_dump captures the whole thing.
The five subsystems
| Subsystem | What it owns | Implemented in |
|---|---|---|
| Dictionary | Encoding every IRI / blank node / literal as a stable BIGINT | src/dictionary/ |
| Storage | VP tables, HTAP delta + main split, the merge background worker | src/storage/ |
| SPARQL engine | SPARQL text → algebra → SQL → SPI execution → decode | src/sparql/ |
| Datalog engine | Rule parser, stratifier, SQL compiler, OWL RL/EL/QL profiles | src/datalog/ |
| SHACL validator | Shapes → DDL constraints + async validation pipeline | src/shacl/ |
All five live inside the same PostgreSQL process and the same SQL transaction. This is the property that makes hybrid retrieval, atomic record-linkage, and audit-grade provenance possible.
Three architectural choices that shape everything
1. Vertical Partitioning (VP) — one table per predicate
Every unique predicate gets its own table. vp_<id>(s, o, g, i, source) — all integers. Star patterns (same subject, multiple predicates) collapse into one self-join over a single subject value. There is no triples(s, p, o, g) mega-table.
The result: SPARQL star-pattern queries match the speed of a hand-written SQL query against an analogously-shaped relational schema. Often faster, because every join is integer-equality.
2. Dictionary encoding — BIGINTs everywhere
Every triple, even at the parser boundary, is converted to a tuple of BIGINT IDs before it touches storage. Joins are integer equality. Filters are integer comparison. Decoding to text happens only at query output. The dictionary uses XXH3-128 for collision-free hashing and an LRU shared-memory cache for hot terms.
3. HTAP storage — delta + main + merge worker
Heavy ingest goes into per-predicate delta tables (regular B-tree heap). Read queries see (main EXCEPT tombstones) UNION ALL delta. A background worker merges delta into BRIN-indexed main asynchronously. Writers never block readers; readers never see partial loads.
What you do not see in the diagram
These are deliberately invisible to the user but have outsized impact:
- Statement-ID timeline — every triple carries a globally-unique SID from a shared sequence. Powers
point_in_time()queries. - Predicate catalog —
_pg_ripple.predicates(id, table_oid, triple_count). Cached in shared memory; survives extension reloads. - Plan cache — translated SPARQL → SQL plans are cached by query text hash, with invalidation on schema change.
- Background workers — merge worker, embedding worker (when
auto_embed = on), CDC publisher.
How it scales
| Axis | Mechanism |
|---|---|
| More CPU on one machine | Parallel merge workers, parallel Datalog stratum evaluation, PostgreSQL parallel scans on VP tables |
| More disk on one machine | BRIN indexes on vp_<id>_main, dictionary cache sized by GUC |
| Many small concurrent queries | PostgreSQL connection pool + pg_ripple_http deadpool pool |
| Read replicas | pg_ripple.read_replica_dsn routes read queries automatically |
| Across many machines | Citus integration: VP tables become distributed tables; bound-subject SPARQL patterns are pruned to one shard |
For deep coverage of each axis see Operations → Scaling and Operations → Citus integration.
Where to read more
- Reference → Architecture — full, code-linked architecture description.
- Operations → Architecture overview — the operations-team view of the same architecture.
- Plans → Implementation plan — the authoritative description of the eventual target architecture.
Comparison: pg_ripple vs Alternatives
A dispassionate side-by-side. Pick the technology that fits your team and constraints, not the one with the loudest blog.
At a glance
| pg_ripple | Plain PostgreSQL | Virtuoso / Blazegraph / GraphDB | Neo4j / Memgraph | Pinecone / Weaviate / Qdrant | |
|---|---|---|---|---|---|
| Deployment | PostgreSQL extension | RDBMS you already run | Standalone (JVM/native) | Standalone | Standalone / SaaS |
| Query language | SPARQL 1.1 | SQL | SPARQL 1.1 | Cypher / GQL | Proprietary |
| Data model | RDF (triples) | Relational | RDF (triples) | Property graph | Vectors + metadata |
| Validation | SHACL (full Core + SPARQL) | CHECK / triggers | Varies | Constraints | None |
| Reasoning | Datalog (RDFS, OWL RL/EL/QL, lattices, WFS, magic sets) | Manual SQL or none | RDFS / OWL (varies) | None built-in | None |
| Vector search | pgvector + KGE | pgvector | Plug-in or none | Limited | Native, only purpose |
| Hybrid graph + vector | One SQL query | Manual joins | Plug-in workarounds | Limited | Not possible |
| ACID | Full PostgreSQL ACID | Full ACID | Varies (often eventual) | ACID | Varies |
| Backup / restore | pg_dump / pg_restore | Standard | Custom tooling | Custom tooling | Custom tooling |
| HTTP / SPARQL Protocol | pg_ripple_http | None | Built-in | Built-in (Cypher over HTTP) | REST |
| Federation | SPARQL SERVICE + vector federation | None | SPARQL SERVICE | None | Some |
| Operational expertise | PostgreSQL DBA skills transfer | PostgreSQL DBA | Specialised triple-store ops | Specialised graph ops | Vendor-specific |
| Conformance | W3C SPARQL 1.1, SHACL Core, OWL 2 RL: 100 % | n/a | Varies | n/a | n/a |
When pg_ripple is the obvious choice
- You already operate PostgreSQL and want to avoid running a second database.
- Your data has rich, interconnected relationships — ontologies, supply chains, organisational hierarchies, citation networks.
- You need SPARQL 1.1 for interoperability with W3C-standard tooling.
- You need to validate data quality against formal rules (SHACL).
- You need to derive new facts from existing data (Datalog, OWL RL).
- You want to combine graph traversal with vector similarity for RAG, recommendations, or record linkage.
- You need transactional guarantees spanning graph data, vector data, and ordinary relational data.
When pg_ripple is not the right answer
| Situation | Better fit |
|---|---|
| > 1 B triples, single instance | Distributed triple stores (or pg_ripple + Citus, see Scaling) |
| Existing Cypher / GQL codebase, no plans to learn SPARQL | Neo4j / Memgraph |
| Pure vector search, no graph traversal | pgvector by itself, or Pinecone/Qdrant if you need a managed service |
| Streaming graph analytics over append-only event firehose | Apache Flink + a graph library |
| You do not run PostgreSQL anywhere and have no plans to | Pick a tool native to your stack |
| Need SQL-only, allergic to RDF | Plain PostgreSQL with thoughtful schema design |
A specific comparison: hybrid retrieval for RAG
This is the comparison most teams care about today.
| pg_ripple | Vector DB only | Graph DB only | |
|---|---|---|---|
| Free-text question → similar entities | Native | Native | Manual |
| Multi-hop relationship walk | Native (SPARQL property paths) | Not possible | Native (Cypher) |
| Combined hybrid query | One SQL call | Glue code in app | Glue code in app |
| Atomic write of triple + embedding | Yes (one transaction) | No | No |
| Audit + provenance | PROV-O + audit log | None | Custom |
| Multi-tenant isolation | Graph RLS + quota | Per-namespace, per-tier | Per-database |
| Operational footprint | One PostgreSQL | Two systems | Two systems |
| Cost of an extra vector store you no longer need | $0 | $$$$ | $$$$ |
The dominant trade-off: vector DBs are simpler when your only job is "find similar passages". Once you also need precise relationships, provenance, multi-hop reasoning, or transactional consistency, the cost of stitching two systems together quickly exceeds the cost of running pg_ripple.
See also
Performance and Conformance Results
A summary of every benchmark and conformance test pg_ripple runs in CI, with links to the full result pages.
W3C standards conformance
| Suite | Result | Reference |
|---|---|---|
| W3C SPARQL 1.1 | 100 % (smoke gate; full suite runs informationally) | SPARQL Compliance Matrix |
| W3C SHACL Core | 100 % | W3C Conformance |
| W3C OWL 2 RL | 100 % | OWL 2 RL Results |
| Apache Jena edge cases (~1,000 tests) | Tracked in CI; informational until ≥95 % | W3C Conformance |
The first three are blocking CI gates — a release cannot ship if any drops below 100 %. The Apache Jena suite stays informational until pg_ripple confidently passes ≥ 95 %; reporting is honest about the current state.
Performance benchmarks
| Benchmark | What it measures | Result |
|---|---|---|
| WatDiv 10 M / 100 M | SPARQL correctness + latency across 100 query templates (star, chain, snowflake, complex) | 100 % correctness; latency competitive with Virtuoso open-source on the same hardware. See WatDiv Results. |
| LUBM | OWL RL inference correctness across 14 canonical queries | 14 / 14 pass. See LUBM Results. |
| BSBM | E-commerce-style mixed query workload | Regression gate in CI; numbers tracked per commit. |
| Bulk load | Triples per second on commodity hardware | > 100 K triples/sec on a 4-core / 16 GB machine. |
| SPARQL latency | Typical star pattern p50 | < 10 ms on a 100 M-triple store with warm cache. |
Why the results matter
100 % conformance is the floor, not the ceiling
A triple store that gets 95 % of W3C tests right has a 5 % chance of returning a wrong result for your query. That is not an academic concern — it is the difference between "always correct" and "occasionally surprising". pg_ripple's CI fails the build before merging anything that drops below 100 %.
Performance numbers come from real machines, not marketing
Every benchmark on this page is run in GitHub Actions on a known instance type. The configuration, dataset, and harness are reproducible from the benchmarks/ directory. If you cannot reproduce a number, that is a bug — file an issue.
What we have not benchmarked
- Distributed (Citus) clusters at production scale. CI runs a small four-worker cluster; production-scale numbers are pending.
- Federation latency to remote SPARQL endpoints. The variance is dominated by the remote endpoint, not by pg_ripple.
- LLM end-to-end latency for
rag_context()+ chat completion. The LLM dominates; pg_ripple's contribution is sub-100 ms.
Running the benchmarks yourself
# WatDiv (10 M triples)
cd benchmarks/watdiv && ./run.sh
# LUBM (14 queries)
cd benchmarks && ./lubm.sh
# Bulk load
cd benchmarks && bash ci_benchmark.sh insert_throughput.sql
# Vector index comparison
cd benchmarks && bash ci_benchmark.sh vector_index_compare.sql
Most benchmarks run in under five minutes on a developer laptop; the full WatDiv 100 M takes ~30 minutes.
See also
- Reference → SPARQL compliance matrix
- Reference → WatDiv results
- Reference → LUBM results
- Reference → OWL 2 RL results
Installation
pg_ripple is a PostgreSQL 18 extension written in Rust. Choose the installation method that fits your environment.
Docker (recommended)
docker compose up -d gets you a working pg_ripple instance in under a minute. No Rust compiler, no PostgreSQL development headers — just Docker.
The fastest path to a working pg_ripple instance. No build tools required.
# Start pg_ripple with Docker Compose
docker compose up -d
# Connect
psql -h localhost -p 5432 -U postgres -d pg_ripple
The docker-compose.yml in the repository root starts PostgreSQL 18 with pg_ripple pre-installed and the extension created in the default database.
Verify the installation
SELECT pg_ripple.triple_count();
The result should be 0 — the extension is installed and ready.
From source (cargo pgrx)
Build and install directly into a local PostgreSQL 18 instance.
Prerequisites
- Rust (stable, edition 2024)
- PostgreSQL 18 development headers
cargo-pgrx0.18
# Install cargo-pgrx
cargo install cargo-pgrx --version 0.18 --locked
# Initialize pgrx with PostgreSQL 18
cargo pgrx init --pg18 $(which pg_config)
# Build and install
cargo pgrx install --release --pg-config $(which pg_config)
Create the extension
Connect to your database and run:
CREATE EXTENSION pg_ripple;
Verify
SELECT pg_ripple.triple_count();
Configuration
pg_ripple works out of the box with default settings. For production deployments, you may want to adjust GUC parameters — see Configuration and Tuning.
For HTAP storage (background merge worker) and shared-memory dictionary cache, add pg_ripple to shared_preload_libraries in postgresql.conf:
shared_preload_libraries = 'pg_ripple'
Restart PostgreSQL after this change.
Troubleshooting
Wrong PostgreSQL version
pg_ripple requires PostgreSQL 18. Check your version:
pg_config --version
Missing shared_preload_libraries
If you see errors about shared memory or the merge worker not starting, ensure pg_ripple is in shared_preload_libraries and PostgreSQL has been restarted.
pgrx version mismatch
pg_ripple requires cargo-pgrx 0.18. If you have an older version:
cargo install cargo-pgrx --version 0.18 --locked --force
Extension not found after install
If CREATE EXTENSION pg_ripple fails with "extension not found", verify that the extension files were installed to the correct PostgreSQL directory:
pg_config --sharedir
ls $(pg_config --sharedir)/extension/pg_ripple*
Docker container fails to start
Check logs:
docker compose logs pg_ripple
Common causes: port 5432 already in use (change the port mapping), insufficient memory (pg_ripple recommends at least 512MB).
Next steps
- Hello World — Five-Minute Walkthrough — load and query your first triples
- Guided Tutorial — build a knowledge graph in 30 minutes
Hello World — Five-Minute Walkthrough
This walkthrough takes you from an empty database to working SPARQL queries in five minutes. You will load ten triples about people and movies, then run three queries of increasing complexity.
Prerequisites
pg_ripple is installed and you are connected to a PostgreSQL database with the extension created. See Installation if you have not done this yet.
Step 1: Register prefixes
Prefixes are shortcuts for long IRIs. Register a few common ones:
SELECT pg_ripple.register_prefix('ex', 'http://example.org/');
SELECT pg_ripple.register_prefix('foaf', 'http://xmlns.com/foaf/0.1/');
SELECT pg_ripple.register_prefix('schema', 'http://schema.org/');
Step 2: Load data
Load ten triples about people and the movies they directed or acted in:
SELECT pg_ripple.load_turtle('
@prefix ex: <http://example.org/> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix schema: <http://schema.org/> .
ex:alice foaf:name "Alice" .
ex:alice schema:knows ex:bob .
ex:bob foaf:name "Bob" .
ex:bob schema:knows ex:carol .
ex:carol foaf:name "Carol" .
ex:movie1 schema:name "The Graph" .
ex:movie1 schema:director ex:alice .
ex:movie1 schema:actor ex:bob .
ex:movie2 schema:name "Linked Data" .
ex:movie2 schema:director ex:bob .
');
The function returns the number of triples loaded (10).
Step 3: Query — basic pattern
Find all movies and their directors:
SELECT * FROM pg_ripple.sparql('
PREFIX schema: <http://schema.org/>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
SELECT ?movieName ?directorName WHERE {
?movie schema:director ?person .
?movie schema:name ?movieName .
?person foaf:name ?directorName .
}
');
Each row in the result is a JSONB object with the variable bindings. You should see "The Graph" directed by "Alice" and "Linked Data" directed by "Bob".
Step 4: Query — OPTIONAL
Find all movies with their directors, and actors if they have any:
SELECT * FROM pg_ripple.sparql('
PREFIX schema: <http://schema.org/>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
SELECT ?movieName ?directorName ?actorName WHERE {
?movie schema:name ?movieName .
?movie schema:director ?director .
?director foaf:name ?directorName .
OPTIONAL {
?movie schema:actor ?actor .
?actor foaf:name ?actorName .
}
}
');
"The Graph" has an actor (Bob), while "Linked Data" does not — the actorName column is null for that row. The OPTIONAL keyword works like a SQL LEFT JOIN.
Step 5: Query — property path
Find everyone Alice is connected to, directly or indirectly, through schema:knows links:
SELECT * FROM pg_ripple.sparql('
PREFIX ex: <http://example.org/>
PREFIX schema: <http://schema.org/>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
SELECT ?name WHERE {
ex:alice schema:knows+ ?person .
?person foaf:name ?name .
}
');
The + operator follows the schema:knows relationship one or more times. Alice knows Bob directly, and Bob knows Carol, so the query returns both "Bob" and "Carol".
What you just learned
- Triples are facts with three parts: subject, predicate, object
- Prefixes are shortcuts for long IRIs
load_turtle()loads data in Turtle formatsparql()runs SPARQL queries and returns results as JSONB- OPTIONAL is like a SQL
LEFT JOIN - Property paths (
+,*) follow chains of relationships
Next steps
- Guided Tutorial — build a complete knowledge graph with validation and reasoning
- Key Concepts — understand RDF concepts using PostgreSQL analogies
- Querying with SPARQL — the full SPARQL feature set
Guided Tutorial — Build a Knowledge Graph in 30 Minutes
This tutorial picks up where the Hello World walkthrough ends. You will build a bibliographic knowledge graph with papers, authors, institutions, and citations — then validate it, reason over it, and export it as JSON-LD.
The tutorial is organized in four independent segments. Each takes under ten minutes and leaves you with a working, progressively richer knowledge graph. You can stop after any segment.
This tutorial uses an academic bibliographic dataset. The patterns — entity relationships, typed literals, named graphs, inference, validation — apply equally to product catalogs, supply chains, organizational hierarchies, or any domain with interconnected data.
Prerequisites
pg_ripple is installed and you are connected to a PostgreSQL database with the extension created. See Installation.
Segment 1: Load and Explore (10 min)
- How to register prefixes and load Turtle data into pg_ripple
- How to write SPARQL queries with filters, aggregates, and graph traversals
- How data is organized as triples (subject–predicate–object)
Register prefixes
SELECT pg_ripple.register_prefix('bib', 'http://example.org/bib/');
SELECT pg_ripple.register_prefix('foaf', 'http://xmlns.com/foaf/0.1/');
SELECT pg_ripple.register_prefix('dc', 'http://purl.org/dc/elements/1.1/');
SELECT pg_ripple.register_prefix('dcterms', 'http://purl.org/dc/terms/');
SELECT pg_ripple.register_prefix('schema', 'http://schema.org/');
SELECT pg_ripple.register_prefix('skos', 'http://www.w3.org/2004/02/skos/core#');
Load the bibliographic dataset
SELECT pg_ripple.load_turtle('
@prefix bib: <http://example.org/bib/> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix dc: <http://purl.org/dc/elements/1.1/> .
@prefix dcterms: <http://purl.org/dc/terms/> .
@prefix schema: <http://schema.org/> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .
bib:mit a schema:Organization ; schema:name "MIT" .
bib:stanford a schema:Organization ; schema:name "Stanford University" .
bib:oxford a schema:Organization ; schema:name "University of Oxford" .
bib:alice a foaf:Person ; foaf:name "Alice Chen" ;
schema:affiliation bib:mit .
bib:bob a foaf:Person ; foaf:name "Bob Smith" ;
schema:affiliation bib:stanford .
bib:carol a foaf:Person ; foaf:name "Carol Martinez" ;
schema:affiliation bib:oxford .
bib:paper1 a schema:ScholarlyArticle ;
dc:title "Knowledge Graphs in Practice" ;
dc:creator bib:alice ; dc:creator bib:bob ;
dcterms:issued "2024-01-15"^^xsd:date ;
schema:about <http://example.org/bib/kg> .
bib:paper2 a schema:ScholarlyArticle ;
dc:title "Efficient SPARQL Query Processing" ;
dc:creator bib:bob ; dc:creator bib:carol ;
dcterms:issued "2024-03-22"^^xsd:date .
bib:paper3 a schema:ScholarlyArticle ;
dc:title "Graph-Enhanced Retrieval for LLMs" ;
dc:creator bib:alice ;
dcterms:issued "2024-06-10"^^xsd:date .
bib:paper2 dcterms:references bib:paper1 .
bib:paper3 dcterms:references bib:paper1 .
bib:paper3 dcterms:references bib:paper2 .
bib:alice foaf:knows bib:bob .
bib:bob foaf:knows bib:carol .
');
Explore: find all papers by Alice
SELECT * FROM pg_ripple.sparql('
PREFIX dc: <http://purl.org/dc/elements/1.1/>
PREFIX bib: <http://example.org/bib/>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
SELECT ?title WHERE {
?paper dc:creator bib:alice .
?paper dc:title ?title .
}
');
Explore: citation chains
Find papers that cite papers Alice authored:
SELECT * FROM pg_ripple.sparql('
PREFIX dc: <http://purl.org/dc/elements/1.1/>
PREFIX dcterms: <http://purl.org/dc/terms/>
PREFIX bib: <http://example.org/bib/>
SELECT ?citingTitle ?citedTitle WHERE {
?citing dcterms:references ?cited .
?cited dc:creator bib:alice .
?citing dc:title ?citingTitle .
?cited dc:title ?citedTitle .
}
');
Explore: count papers per author
SELECT * FROM pg_ripple.sparql('
PREFIX dc: <http://purl.org/dc/elements/1.1/>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
SELECT ?name (COUNT(?paper) AS ?papers) WHERE {
?paper dc:creator ?author .
?author foaf:name ?name .
}
GROUP BY ?name
ORDER BY DESC(?papers)
');
Segment 2: Validate (10 min)
- How to define data quality rules using SHACL shapes
- How to validate your knowledge graph and catch constraint violations
- How SHACL shapes act like CHECK constraints for graph data
SHACL (Shapes Constraint Language) lets you define data quality rules. You will create a shape that requires every ScholarlyArticle to have a title and at least one creator.
Load a SHACL shape
SELECT pg_ripple.load_shacl('
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix schema: <http://schema.org/> .
@prefix dc: <http://purl.org/dc/elements/1.1/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
<http://example.org/shapes/ArticleShape>
a sh:NodeShape ;
sh:targetClass schema:ScholarlyArticle ;
sh:property [
sh:path dc:title ;
sh:minCount 1 ;
sh:maxCount 1 ;
sh:datatype xsd:string ;
sh:message "Every article must have exactly one title" ;
] ;
sh:property [
sh:path dc:creator ;
sh:minCount 1 ;
sh:message "Every article must have at least one creator" ;
] .
');
Validate the dataset
SELECT pg_ripple.validate();
The result is a JSONB validation report. If all articles conform, the report shows zero violations. Now insert a bad article to see validation catch it:
SELECT pg_ripple.insert_triple(
'http://example.org/bib/bad_paper',
'http://www.w3.org/1999/02/22-rdf-syntax-ns#type',
'http://schema.org/ScholarlyArticle'
);
SELECT pg_ripple.validate();
The report now shows a violation: the article has no title and no creator.
Segment 3: Reason (10 min)
- How to write Datalog rules that derive new facts from existing data
- How transitive inference works (if A connects to B and B connects to C, then A connects to C)
- How inference compares to SQL materialized views
Datalog rules let you derive new facts. You will write a rule that infers transitive co-authorship: if Alice co-authored a paper with Bob, and Bob co-authored with Carol, then Alice and Carol are indirectly connected.
Write and load a rule
SELECT pg_ripple.load_rules('
coauthor(?a, ?b) :- <http://purl.org/dc/elements/1.1/creator>(?paper, ?a),
<http://purl.org/dc/elements/1.1/creator>(?paper, ?b),
?a != ?b.
connected(?a, ?b) :- coauthor(?a, ?b).
connected(?a, ?b) :- connected(?a, ?c), coauthor(?c, ?b), ?a != ?b.
', 'coauthorship');
Run inference
SELECT pg_ripple.infer('coauthorship');
This returns the number of new facts derived.
Query the derived facts
SELECT * FROM pg_ripple.sparql('
PREFIX bib: <http://example.org/bib/>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
SELECT ?name WHERE {
bib:alice <http://example.org/bib/connected> ?person .
?person foaf:name ?name .
}
');
Alice is now connected to Bob (direct co-author on paper1), Carol (through Bob on paper2), and potentially others through the transitive chain.
Segment 4: Export (10 min)
- How to export your knowledge graph in Turtle and JSON-LD formats
- How SPARQL CONSTRUCT queries shape output for API consumers
- How JSON-LD framing produces nested, API-ready JSON documents
Export your knowledge graph as JSON-LD, shaped for an API using a frame template.
Export as Turtle
SELECT pg_ripple.export_turtle();
This returns all triples in human-readable Turtle format.
Export as JSON-LD with framing
SELECT pg_ripple.sparql_construct_jsonld('
PREFIX dc: <http://purl.org/dc/elements/1.1/>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
PREFIX schema: <http://schema.org/>
CONSTRUCT {
?paper dc:title ?title .
?paper dc:creator ?author .
?author foaf:name ?name .
?author schema:affiliation ?org .
?org schema:name ?orgName .
}
WHERE {
?paper a schema:ScholarlyArticle .
?paper dc:title ?title .
?paper dc:creator ?author .
?author foaf:name ?name .
OPTIONAL {
?author schema:affiliation ?org .
?org schema:name ?orgName .
}
}
');
The result is a nested JSON-LD document with papers, their authors, and institutional affiliations — ready to serve from a REST API.
What you built
In 30 minutes, you created a knowledge graph with:
- Structured data — papers, authors, institutions, and citations as RDF triples
- Quality rules — SHACL shapes that catch incomplete articles
- Derived knowledge — Datalog rules that infer transitive co-authorship
- API-ready export — JSON-LD output shaped for downstream consumers
Next steps
- Storing Knowledge — data modeling deep dive
- Querying with SPARQL — the full query language
- Validating Data Quality — advanced SHACL patterns
- Reasoning and Inference — Datalog, RDFS, OWL RL
Key Concepts — RDF for PostgreSQL Users
If you know PostgreSQL, you already understand most of what you need to work with pg_ripple. This page maps RDF concepts to their PostgreSQL equivalents.
Triples
A triple is the atomic unit of data in RDF. It has three parts:
| Part | What it is | PostgreSQL analogy |
|---|---|---|
| Subject | The entity being described | A row's primary key |
| Predicate | The relationship or attribute | A column name |
| Object | The value or related entity | A cell value or foreign key |
For example, the fact "Alice knows Bob" is the triple:
<http://example.org/alice> <http://xmlns.com/foaf/0.1/knows> <http://example.org/bob> .
In pg_ripple, this triple is stored in a VP table named after the predicate (foaf:knows), with integer-encoded subject and object columns.
IRIs
An IRI (Internationalized Resource Identifier) is a globally unique identifier for an entity or relationship. Think of it as a namespaced primary key that is guaranteed unique across all datasets in the world.
http://example.org/alice -- an entity
http://xmlns.com/foaf/0.1/knows -- a relationship
Prefixes are shortcuts to avoid writing full IRIs repeatedly:
SELECT pg_ripple.register_prefix('ex', 'http://example.org/');
-- Now ex:alice means http://example.org/alice
Blank nodes
A blank node is an anonymous entity — like a row with no primary key. It exists only within the document where it was created.
ex:alice foaf:address [ foaf:city "Boston" ; foaf:country "US" ] .
The address has no IRI. It is a blank node, identified internally by a system-generated label. Blank nodes from different load_turtle() calls are always distinct entities, even if they share the same label.
Blank nodes cannot be referenced from outside their originating load call. If you need to reference an entity from multiple places, give it an IRI.
Literals
A literal is a data value — a string, number, date, or boolean. Literals can have a datatype or a language tag.
| Literal | Type | PostgreSQL equivalent |
|---|---|---|
"Alice" | Plain string | TEXT |
"42"^^xsd:integer | Typed integer | INTEGER |
"2024-01-15"^^xsd:date | Typed date | DATE |
"Bonjour"@fr | Language-tagged string | No direct equivalent |
In pg_ripple, all literals are dictionary-encoded to compact integer IDs for storage. The original string representation is preserved and decoded on query output.
Predicates and VP tables
In a relational database, a table groups all attributes of a single entity type. In pg_ripple, data is organized by predicate — each unique predicate gets its own table (a Vertical Partitioning, or VP, table).
Relational: persons(id, name, email, knows_id)
pg_ripple: vp_foaf_name(s, o) -- subject → name
vp_foaf_knows(s, o) -- subject → object
vp_schema_email(s, o) -- subject → email
This structure makes join-heavy SPARQL queries fast because each predicate's data is co-located and indexed.
Named graphs
A named graph is a labeled collection of triples — like a PostgreSQL schema that groups related tables.
-- Create a named graph
SELECT pg_ripple.create_graph('http://example.org/publications');
-- Load data into it
SELECT pg_ripple.load_turtle_into_graph(
'<http://example.org/paper1> <http://purl.org/dc/elements/1.1/title> "My Paper" .',
'http://example.org/publications'
);
Named graphs are useful for:
- Multi-source data: keep data from different sources separate
- Access control: grant read access to specific graphs per role
- Versioning: load new data into a fresh graph, validate, then swap
All triples without an explicit graph belong to the default graph (graph ID = 0).
RDF-star
Standard RDF says "Alice knows Bob." But what if you want to say when Alice met Bob, or who recorded that fact? RDF-star lets you make statements about statements:
<< ex:alice foaf:knows ex:bob >> ex:since "2020"^^xsd:gYear .
This says: "The fact that Alice knows Bob has been true since 2020." In pg_ripple, each triple has a statement identifier (SID) that can be used as the subject or object of other triples, enabling edge properties similar to labeled property graphs.
SPARQL
SPARQL is the standard query language for RDF data — the equivalent of SQL for relational databases. Where SQL queries tables, SPARQL queries graph patterns.
| SQL | SPARQL |
|---|---|
SELECT name FROM persons WHERE id = 1 | SELECT ?name WHERE { ex:person1 foaf:name ?name } |
JOIN | Graph pattern matching (implicit) |
LEFT JOIN | OPTIONAL { } |
WHERE x IN (...) | VALUES (?x) { ... } |
GROUP BY ... HAVING | GROUP BY ... HAVING |
WITH RECURSIVE | Property paths (foaf:knows+) |
In pg_ripple, SPARQL queries are compiled to SQL and executed via PostgreSQL's query engine. You call them through pg_ripple.sparql():
SELECT * FROM pg_ripple.sparql('
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
SELECT ?name WHERE { ?person foaf:name ?name }
');
Dictionary encoding
pg_ripple does not store raw strings in its data tables. Every IRI, blank node, and literal is mapped to a compact BIGINT (i64) by the dictionary encoder. VP tables contain only integer columns, making joins and comparisons fast.
You never need to interact with dictionary IDs directly — sparql() and find_triples() handle encoding and decoding automatically. For advanced use cases, encode_term() and decode_id() are available.
Summary of analogies
| RDF concept | PostgreSQL analogy |
|---|---|
| Triple | Row in a table |
| Subject | Primary key value |
| Predicate | Column name / table name (VP) |
| Object | Cell value or foreign key |
| IRI | Globally unique identifier |
| Blank node | Row with system-generated ID |
| Literal | Typed column value |
| Named graph | Schema |
| SPARQL | SQL |
| SHACL shape | CHECK constraint / trigger |
| Datalog rule | Materialized view definition |
Further reading
- Blog: Why RDF Inside PostgreSQL? — the design philosophy behind pg_ripple
- Blog: Dictionary Encoding and Integer Joins — how pg_ripple achieves fast query performance
- Blog: Vertical Partitioning Explained — why one table per predicate works
Next steps
- Storing Knowledge — data modeling with triples
- Loading Data — all import formats and methods
- Querying with SPARQL — the full query language
Playground
The quickest way to try pg_ripple is with Docker. No PostgreSQL installation required.
Start the sandbox
docker run --rm -p 5432:5432 \
-e POSTGRES_PASSWORD=ripple \
ghcr.io/trickle-labs/pg-ripple:latest
Note: The sandbox container is configured for development/testing and uses trust authentication for external TCP connections. For production use, see Installation and Security.
Connect with any PostgreSQL client (no password required):
psql -h localhost -U postgres -d postgres
Pre-loaded example dataset
The sandbox image includes a small FOAF-style dataset pre-loaded in the examples database:
psql -h localhost -U postgres -d examples
-- Who does Alice know?
SELECT * FROM pg_ripple.sparql('
SELECT ?name WHERE {
<https://example.org/alice> <https://xmlns.com/foaf/0.1/knows> ?person .
?person <https://xmlns.com/foaf/0.1/name> ?name
}
');
-- Transitive: everyone reachable from Alice through knows+
SELECT * FROM pg_ripple.sparql('
SELECT ?target WHERE {
<https://example.org/alice>
<https://xmlns.com/foaf/0.1/knows>+
?target
}
');
-- Count people by organisation
SELECT * FROM pg_ripple.sparql('
SELECT ?org (COUNT(?person) AS ?headcount) WHERE {
?person <https://xmlns.com/foaf/0.1/member> ?org
} GROUP BY ?org ORDER BY DESC(?headcount)
');
Try your own data
-- Load your own N-Triples
SELECT pg_ripple.load_ntriples('
<https://my.example/a> <https://my.example/p> <https://my.example/b> .
<https://my.example/b> <https://my.example/p> <https://my.example/c> .
');
-- Run a path query
SELECT * FROM pg_ripple.sparql('
SELECT ?target WHERE {
<https://my.example/a> <https://my.example/p>+ ?target
}
');
Building locally
To build the Docker image yourself:
git clone https://github.com/trickle-labs/pg-ripple.git
cd pg-ripple
docker build -t pg-ripple:local .
docker run --rm -p 5432:5432 -e POSTGRES_PASSWORD=ripple pg-ripple:local
Next steps
- Installation — install pg_ripple into your own PostgreSQL instance
- Getting Started — five-minute tutorial
- SPARQL Queries — full SPARQL reference
§2.1 Storing Knowledge
What and Why
pg_ripple stores data as RDF triples — the W3C standard for representing knowledge. Every fact is a three-part statement: a subject, a predicate, and an object. This structure is deceptively simple but powerful enough to model any domain — from bibliographic records and biomedical ontologies to enterprise knowledge graphs.
Why triples instead of tables?
- Schema-free evolution: add new predicates without ALTER TABLE.
- Natural linking: every entity is an IRI — links across datasets are free.
- Standards-based: SPARQL, SHACL, OWL, and thousands of public vocabularies work out of the box.
- Provenance-ready: RDF-star lets you annotate individual facts with confidence scores, sources, and timestamps.
pg_ripple stores triples inside PostgreSQL using Vertical Partitioning (VP) — one
internal table per predicate, with all values dictionary-encoded as BIGINT. You never
see this machinery directly; you interact through insert_triple(), load_turtle(),
and SPARQL.
How It Works
The Triple Model
Every RDF triple has the form:
<subject> <predicate> <object> .
- Subject: the thing you are describing (always an IRI or blank node).
- Predicate: the relationship or property (always an IRI).
- Object: the value — an IRI (another entity), a literal (string, number, date), or a blank node.
IRIs (Internationalized Resource Identifiers) look like URLs but are identifiers, not
necessarily web addresses. <https://example.org/paper/42> identifies a paper —
it does not need to resolve to a web page.
Named Graphs
Triples can be grouped into named graphs — logical partitions identified by an IRI. This is useful for:
- Tracking provenance: "these triples came from PubMed"
- Multi-tenancy: one graph per customer
- Inference output: derived triples go into a separate graph
pg_ripple uses graph ID 0 for the default graph (triples with no explicit graph).
Named graphs get a positive integer ID via dictionary encoding.
Blank Nodes
Blank nodes are anonymous identifiers — they represent "something exists" without
giving it a global IRI. pg_ripple encodes blank nodes with a _: prefix:
SELECT pg_ripple.insert_triple(
'_:review1',
'<https://schema.org/author>',
'<https://example.org/person/alice>'
);
Blank nodes are document-scoped. Two separate load_turtle() calls that both use
_:x will create two different internal identifiers. If you need stable cross-document
identity, use IRIs instead.
RDF-Star (Quoted Triples)
RDF-star lets you make statements about other statements. This is essential for provenance, confidence scores, and temporal annotations.
A quoted triple wraps << subject predicate object >> and can appear as a subject
or object in another triple:
-- "The fact that Paper42 was authored by Alice has confidence 0.95"
SELECT pg_ripple.insert_triple(
'<< <https://example.org/paper/42> <https://purl.org/dc/terms/creator> <https://example.org/person/alice> >>',
'<https://example.org/confidence>',
'"0.95"^^<http://www.w3.org/2001/XMLSchema#decimal>'
);
Dictionary Encoding
Every IRI, blank node, and literal is mapped to a BIGINT (i64) via XXH3-128 hashing
before storage. VP tables contain only integers — this makes joins fast and storage
compact. You never need to think about encoding; pg_ripple handles it transparently.
HTAP Storage Architecture
pg_ripple uses a Hybrid Transactional/Analytical Processing (HTAP) split to serve fast writes and analytical reads concurrently:
flowchart LR
subgraph Write Path
A[INSERT triple] --> B[vp_{id}_delta\nheap + B-tree]
end
subgraph Read Path
C[SPARQL query] --> D["(main EXCEPT tombstones)\nUNION ALL delta"]
end
subgraph Background Merge Worker
E[vp_{id}_main\nBRIN index] --> F[Fresh vp_{id}_main]
B --> F
G[vp_{id}_tombstones] --> F
end
B --> D
E --> D
G --> D
F --> E
- Delta table (
vp_{id}_delta): receives all new writes via heap + B-tree. - Main table (
vp_{id}_main): historical BRIN-indexed partition, read-optimized. - Tombstones (
vp_{id}_tombstones): deleted main-resident triples are recorded here. - Merge worker: periodically combines main + delta (minus tombstones) into a new main, then drops the old delta.
Reads always see the full consistent view: (main EXCEPT tombstones) UNION ALL delta.
Worked Examples
The examples in this chapter use a bibliographic dataset: papers, authors, institutions, journals, and citations.
Setting Up Prefixes
Register namespace prefixes so SPARQL queries are readable:
SELECT pg_ripple.register_prefix('ex', 'https://example.org/');
SELECT pg_ripple.register_prefix('dct', 'http://purl.org/dc/terms/');
SELECT pg_ripple.register_prefix('foaf', 'http://xmlns.com/foaf/0.1/');
SELECT pg_ripple.register_prefix('bibo', 'http://purl.org/ontology/bibo/');
SELECT pg_ripple.register_prefix('schema','https://schema.org/');
SELECT pg_ripple.register_prefix('xsd', 'http://www.w3.org/2001/XMLSchema#');
Inserting Individual Triples
-- Create a paper
SELECT pg_ripple.insert_triple(
'<https://example.org/paper/42>',
'<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>',
'<http://purl.org/ontology/bibo/AcademicArticle>'
);
-- Add a title
SELECT pg_ripple.insert_triple(
'<https://example.org/paper/42>',
'<http://purl.org/dc/terms/title>',
'"Knowledge Graphs in Practice"'
);
-- Add an author
SELECT pg_ripple.insert_triple(
'<https://example.org/paper/42>',
'<http://purl.org/dc/terms/creator>',
'<https://example.org/person/alice>'
);
-- Author metadata
SELECT pg_ripple.insert_triple(
'<https://example.org/person/alice>',
'<http://xmlns.com/foaf/0.1/name>',
'"Alice Johnson"'
);
SELECT pg_ripple.insert_triple(
'<https://example.org/person/alice>',
'<https://schema.org/affiliation>',
'<https://example.org/institution/mit>'
);
-- Institution metadata
SELECT pg_ripple.insert_triple(
'<https://example.org/institution/mit>',
'<http://xmlns.com/foaf/0.1/name>',
'"Massachusetts Institute of Technology"'
);
Loading a Full Dataset with Turtle
For bulk data, Turtle format is more natural:
SELECT pg_ripple.load_turtle('
@prefix ex: <https://example.org/> .
@prefix dct: <http://purl.org/dc/terms/> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix bibo: <http://purl.org/ontology/bibo/> .
@prefix schema: <https://schema.org/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
ex:paper/42 a bibo:AcademicArticle ;
dct:title "Knowledge Graphs in Practice" ;
dct:creator ex:person/alice, ex:person/bob ;
dct:date "2024-03-15"^^xsd:date ;
bibo:citedBy ex:paper/99 ;
schema:keywords "knowledge graph", "RDF", "SPARQL" .
ex:paper/99 a bibo:AcademicArticle ;
dct:title "Graph Neural Networks for Entity Resolution" ;
dct:creator ex:person/carol ;
bibo:cites ex:paper/42 .
ex:person/alice foaf:name "Alice Johnson" ;
schema:affiliation ex:institution/mit .
ex:person/bob foaf:name "Bob Smith" ;
schema:affiliation ex:institution/stanford .
ex:person/carol foaf:name "Carol Williams" ;
schema:affiliation ex:institution/mit .
ex:institution/mit foaf:name "Massachusetts Institute of Technology" .
ex:institution/stanford foaf:name "Stanford University" .
');
Using Named Graphs
Store triples from different sources in separate graphs:
-- Create named graphs for different data sources
SELECT pg_ripple.create_graph('https://example.org/graph/pubmed');
SELECT pg_ripple.create_graph('https://example.org/graph/arxiv');
-- Load PubMed data into its graph
SELECT pg_ripple.load_turtle_into_graph('
@prefix ex: <https://example.org/> .
@prefix dct: <http://purl.org/dc/terms/> .
@prefix bibo: <http://purl.org/ontology/bibo/> .
ex:paper/100 a bibo:AcademicArticle ;
dct:title "Drug Interaction Networks" ;
dct:creator ex:person/dave .
', 'https://example.org/graph/pubmed');
-- Load arXiv data into its graph
SELECT pg_ripple.load_turtle_into_graph('
@prefix ex: <https://example.org/> .
@prefix dct: <http://purl.org/dc/terms/> .
@prefix bibo: <http://purl.org/ontology/bibo/> .
ex:paper/200 a bibo:AcademicArticle ;
dct:title "Transformer Architectures for NLP" ;
dct:creator ex:person/eve .
', 'https://example.org/graph/arxiv');
-- List all named graphs
SELECT * FROM pg_ripple.list_graphs();
RDF-Star for Provenance and Confidence
Annotate citations with provenance metadata:
-- Record that Paper 42 cites Paper 99 (the base fact)
SELECT pg_ripple.insert_triple(
'<https://example.org/paper/42>',
'<http://purl.org/ontology/bibo/cites>',
'<https://example.org/paper/99>'
);
-- Annotate this citation with a confidence score
SELECT pg_ripple.insert_triple(
'<< <https://example.org/paper/42> <http://purl.org/ontology/bibo/cites> <https://example.org/paper/99> >>',
'<https://example.org/confidence>',
'"0.92"^^<http://www.w3.org/2001/XMLSchema#decimal>'
);
-- Record who asserted this citation
SELECT pg_ripple.insert_triple(
'<< <https://example.org/paper/42> <http://purl.org/ontology/bibo/cites> <https://example.org/paper/99> >>',
'<http://purl.org/dc/terms/source>',
'<https://example.org/system/citation-extractor>'
);
Translating a Relational Schema to RDF
Suppose you have a relational database with tables papers, authors, and affiliations:
| papers.id | papers.title | papers.year |
|---|---|---|
| 42 | Knowledge Graphs in Practice | 2024 |
| authors.id | authors.name | authors.institution_id |
|---|---|---|
| 1 | Alice Johnson | 10 |
The mapping pattern:
- Each row becomes a subject IRI:
<https://example.org/paper/{id}> - Each column becomes a predicate: use a standard vocabulary (Dublin Core, Schema.org, FOAF)
- Foreign keys become object IRIs:
authors.institution_id = 10→<https://example.org/institution/10> - Scalar values become literals:
papers.title→"Knowledge Graphs in Practice"
-- Row from papers table → triples
SELECT pg_ripple.insert_triple(
'<https://example.org/paper/42>',
'<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>',
'<http://purl.org/ontology/bibo/AcademicArticle>'
);
SELECT pg_ripple.insert_triple(
'<https://example.org/paper/42>',
'<http://purl.org/dc/terms/title>',
'"Knowledge Graphs in Practice"'
);
SELECT pg_ripple.insert_triple(
'<https://example.org/paper/42>',
'<http://purl.org/dc/terms/date>',
'"2024"^^<http://www.w3.org/2001/XMLSchema#gYear>'
);
-- Foreign key → IRI link
SELECT pg_ripple.insert_triple(
'<https://example.org/person/1>',
'<https://schema.org/affiliation>',
'<https://example.org/institution/10>'
);
Common Patterns
Pattern: Type Hierarchies
Use rdf:type and rdfs:subClassOf to create type hierarchies:
SELECT pg_ripple.load_turtle('
@prefix ex: <https://example.org/> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix bibo: <http://purl.org/ontology/bibo/> .
bibo:AcademicArticle rdfs:subClassOf bibo:Article .
bibo:Article rdfs:subClassOf bibo:Document .
ex:paper/42 a bibo:AcademicArticle .
');
With RDFS inference enabled (see §2.5),
pg_ripple can automatically derive that ex:paper/42 is also a bibo:Article and
a bibo:Document.
Pattern: Multi-Valued Properties
Unlike relational columns, RDF predicates are naturally multi-valued:
-- A paper can have multiple authors — just insert multiple triples
SELECT pg_ripple.insert_triple(
'<https://example.org/paper/42>',
'<http://purl.org/dc/terms/creator>',
'<https://example.org/person/alice>'
);
SELECT pg_ripple.insert_triple(
'<https://example.org/paper/42>',
'<http://purl.org/dc/terms/creator>',
'<https://example.org/person/bob>'
);
Pattern: Typed and Language-Tagged Literals
-- Typed literal (date)
SELECT pg_ripple.insert_triple(
'<https://example.org/paper/42>',
'<http://purl.org/dc/terms/date>',
'"2024-03-15"^^<http://www.w3.org/2001/XMLSchema#date>'
);
-- Language-tagged string
SELECT pg_ripple.insert_triple(
'<https://example.org/paper/42>',
'<http://purl.org/dc/terms/title>',
'"Knowledge Graphs in Practice"@en'
);
SELECT pg_ripple.insert_triple(
'<https://example.org/paper/42>',
'<http://purl.org/dc/terms/title>',
'"Wissensgraphen in der Praxis"@de'
);
Pattern: Reification with RDF-Star vs Named Graphs
Two approaches for tracking who said what:
RDF-star — annotate individual triples:
SELECT pg_ripple.insert_triple(
'<< <https://example.org/paper/42> <http://purl.org/dc/terms/creator> <https://example.org/person/alice> >>',
'<http://purl.org/dc/terms/source>',
'<https://example.org/dataset/pubmed>'
);
Named graphs — group triples by source:
SELECT pg_ripple.load_turtle_into_graph('
@prefix ex: <https://example.org/> .
@prefix dct: <http://purl.org/dc/terms/> .
ex:paper/42 dct:creator ex:person/alice .
', 'https://example.org/dataset/pubmed');
Use RDF-star when different triples about the same entity have different provenance. Use named graphs when entire batches share the same source.
Performance and Trade-offs
| Approach | Insert rate | Query flexibility | Storage overhead |
|---|---|---|---|
insert_triple() | ~5,000 triples/s | Full | Highest (per-call overhead) |
load_turtle() | ~50,000 triples/s | Full | Low (batch dictionary encoding) |
load_turtle_file() | ~100,000 triples/s | Full | Lowest (server-side streaming) |
- Dictionary cache: frequently used IRIs (predicates, common types) stay in the
shared-memory LRU cache. Check hit rates with
SELECT pg_ripple.cache_stats(). - VP table promotion: predicates with fewer than 1,000 triples share the
vp_rareconsolidation table. Once a predicate crosses the threshold, it gets its own dedicated VP table with dual B-tree indexes. - Named graph overhead: the
gcolumn adds 8 bytes per triple. If you do not need named graphs, using the default graph (the default) avoids the cost of graph-ID lookups.
After large bulk loads, run ANALYZE on the internal tables to update PostgreSQL planner statistics:
SELECT pg_ripple.vacuum();
</div>
</div>
Gotchas and Debugging
IRI formatting: All IRIs must be wrapped in angle brackets (<...>) in function calls.
Forgetting the brackets is the most common error:
-- WRONG: will be treated as a plain literal
SELECT pg_ripple.insert_triple(
'https://example.org/paper/42',
'http://purl.org/dc/terms/title',
'"Hello"'
);
-- CORRECT: angle brackets around IRIs
SELECT pg_ripple.insert_triple(
'<https://example.org/paper/42>',
'<http://purl.org/dc/terms/title>',
'"Hello"'
);
Blank node scoping: Blank nodes from separate load_turtle() calls are independent.
Two calls using _:x create two different entities.
Literal quoting: Literals must be wrapped in double quotes within the single-quoted SQL
string. Typed literals use ^^<datatype> suffix:
-- Plain string
'"Hello"'
-- Typed integer
'"42"^^<http://www.w3.org/2001/XMLSchema#integer>'
-- Language-tagged string
'"Bonjour"@fr'
Checking what is stored: Use find_triples() with wildcards to inspect data:
-- All triples about Paper 42
SELECT * FROM pg_ripple.find_triples(
'<https://example.org/paper/42>', NULL, NULL
);
-- All triples with the dct:creator predicate
SELECT * FROM pg_ripple.find_triples(
NULL, '<http://purl.org/dc/terms/creator>', NULL
);
-- Total triple count
SELECT pg_ripple.triple_count();
Duplicate triples: Inserting the same (s, p, o, g) twice is idempotent — the second
insert returns the existing SID. Use deduplicate_all() to clean up historical duplicates.
Next Steps
- §2.2 Loading Data — bulk loading in all RDF formats with performance tuning.
- §2.3 Querying with SPARQL — query the triples you stored.
- §2.4 Validating Data Quality — enforce schema constraints with SHACL.
Further reading
- Blog: RDF-star — Statements About Statements — edge properties and provenance on triples
- Blog: Vertical Partitioning Explained — why pg_ripple stores one table per predicate
§2.2 Loading Data
What and Why
Getting data into pg_ripple is the first step in building a knowledge graph. pg_ripple supports every major RDF serialization format and offers three loading strategies tuned for different scenarios: inline string loading, server-side file loading, and single-triple insertion.
Choosing the right format and loading mode matters. A 10-million-triple dataset loaded
via insert_triple() in a loop takes hours; the same dataset loaded from a server-side
N-Triples file via load_ntriples_file() finishes in minutes.
How It Works
Supported Formats
| Format | Function (string) | Function (file) | Named graphs | Notes |
|---|---|---|---|---|
| Turtle | load_turtle() | load_turtle_file() | No (use load_turtle_into_graph()) | Human-readable; supports prefixes, RDF-star |
| N-Triples | load_ntriples() | load_ntriples_file() | No (use load_ntriples_into_graph()) | One triple per line; fastest to parse |
| N-Quads | load_nquads() | load_nquads_file() | Yes (inline) | N-Triples + fourth graph column |
| TriG | load_trig() | load_trig_file() | Yes (inline) | Turtle + named graph blocks |
| RDF/XML | load_rdfxml() | load_rdfxml_file() | No (use load_rdfxml_into_graph()) | Legacy XML format; widely supported |
Three Loading Modes
Mode 1: String loading — pass RDF text as a SQL string parameter. Best for small-to-medium datasets (up to a few MB) and interactive use:
SELECT pg_ripple.load_turtle('
@prefix ex: <https://example.org/> .
ex:paper/1 ex:title "Hello World" .
');
Mode 2: Server-side file loading — read from a file on the PostgreSQL server's filesystem. Best for large datasets. Requires superuser privileges:
SELECT pg_ripple.load_turtle_file('/data/papers.ttl');
Mode 3: Single-triple insertion — insert one triple at a time. Best for real-time ingestion from application code:
SELECT pg_ripple.insert_triple(
'<https://example.org/paper/1>',
'<https://example.org/title>',
'"Hello World"'
);
The Loading Pipeline
Regardless of format, every loader follows the same internal pipeline:
- Parse — deserialize the RDF serialization into (subject, predicate, object, graph) quads.
- Encode — dictionary-encode each IRI, blank node, and literal to a
BIGINTID using batchON CONFLICT DO NOTHING ... RETURNING. - Route — look up the predicate in
_pg_ripple.predicatesto find the target VP table (orvp_rare). - Insert — batch-insert encoded
(s, o, g)rows into the appropriate VP delta table.
String loaders process the entire input in a single transaction. If any triple fails
to parse with strict = true, the entire load is rolled back. With strict = false
(the default), malformed triples are skipped and a WARNING is emitted.
Worked Examples
Loading Turtle
The most common format for hand-authored data:
SELECT pg_ripple.load_turtle('
@prefix ex: <https://example.org/> .
@prefix dct: <http://purl.org/dc/terms/> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix bibo: <http://purl.org/ontology/bibo/> .
@prefix schema: <https://schema.org/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
ex:paper/42 a bibo:AcademicArticle ;
dct:title "Knowledge Graphs in Practice"@en ;
dct:creator ex:person/alice, ex:person/bob ;
dct:date "2024-03-15"^^xsd:date ;
bibo:citedBy ex:paper/99 ;
schema:keywords "knowledge graph", "RDF", "SPARQL" .
ex:paper/99 a bibo:AcademicArticle ;
dct:title "Graph Neural Networks for Entity Resolution" ;
dct:creator ex:person/carol .
ex:person/alice foaf:name "Alice Johnson" ;
schema:affiliation ex:institution/mit .
ex:person/bob foaf:name "Bob Smith" ;
schema:affiliation ex:institution/stanford .
ex:person/carol foaf:name "Carol Williams" ;
schema:affiliation ex:institution/mit .
ex:institution/mit foaf:name "Massachusetts Institute of Technology" .
ex:institution/stanford foaf:name "Stanford University" .
');
The function returns the number of triples loaded:
-- Returns: 15
Loading N-Triples
N-Triples is one triple per line with no abbreviations — optimal for machine-generated data:
SELECT pg_ripple.load_ntriples('
<https://example.org/paper/42> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://purl.org/ontology/bibo/AcademicArticle> .
<https://example.org/paper/42> <http://purl.org/dc/terms/title> "Knowledge Graphs in Practice" .
<https://example.org/paper/42> <http://purl.org/dc/terms/creator> <https://example.org/person/alice> .
<https://example.org/paper/42> <http://purl.org/dc/terms/creator> <https://example.org/person/bob> .
');
Loading N-Quads (with Named Graphs)
N-Quads extend N-Triples with a fourth field for the graph IRI:
SELECT pg_ripple.load_nquads('
<https://example.org/paper/42> <http://purl.org/dc/terms/title> "Knowledge Graphs in Practice" <https://example.org/graph/pubmed> .
<https://example.org/paper/99> <http://purl.org/dc/terms/title> "Graph Neural Networks" <https://example.org/graph/arxiv> .
<https://example.org/paper/42> <http://purl.org/dc/terms/creator> <https://example.org/person/alice> <https://example.org/graph/pubmed> .
');
Loading TriG (Turtle with Named Graphs)
TriG wraps Turtle blocks in GRAPH { } sections:
SELECT pg_ripple.load_trig('
@prefix ex: <https://example.org/> .
@prefix dct: <http://purl.org/dc/terms/> .
@prefix bibo: <http://purl.org/ontology/bibo/> .
GRAPH ex:graph/pubmed {
ex:paper/100 a bibo:AcademicArticle ;
dct:title "Drug Interaction Networks" ;
dct:creator ex:person/dave .
}
GRAPH ex:graph/arxiv {
ex:paper/200 a bibo:AcademicArticle ;
dct:title "Transformer Architectures for NLP" ;
dct:creator ex:person/eve .
}
');
Loading RDF/XML
The original XML serialization of RDF — common in older datasets and OWL ontologies:
SELECT pg_ripple.load_rdfxml('
<?xml version="1.0" encoding="UTF-8"?>
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:dct="http://purl.org/dc/terms/"
xmlns:bibo="http://purl.org/ontology/bibo/">
<bibo:AcademicArticle rdf:about="https://example.org/paper/42">
<dct:title>Knowledge Graphs in Practice</dct:title>
<dct:creator rdf:resource="https://example.org/person/alice"/>
</bibo:AcademicArticle>
</rdf:RDF>
');
Loading from Server-Side Files
For large datasets, server-side file loading avoids transferring data through the SQL protocol:
-- Load a large N-Triples dump (superuser required)
SELECT pg_ripple.load_ntriples_file('/data/exports/papers.nt');
-- Load Turtle with strict parsing (abort on any error)
SELECT pg_ripple.load_turtle_file('/data/exports/ontology.ttl', true);
-- Load into a specific named graph
SELECT pg_ripple.load_turtle_file_into_graph(
'/data/exports/pubmed.ttl',
'https://example.org/graph/pubmed'
);
File loading functions read from the PostgreSQL server's filesystem, not the client's.
The path must be accessible to the postgres OS user. These functions require superuser
privileges for security reasons.
Loading Turtle-Star (RDF-Star)
pg_ripple's Turtle parser supports RDF-star quoted triples natively:
SELECT pg_ripple.load_turtle('
@prefix ex: <https://example.org/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
<< ex:paper/42 ex:cites ex:paper/99 >> ex:confidence "0.92"^^xsd:decimal .
<< ex:paper/42 ex:cites ex:paper/99 >> ex:source ex:system/citation-extractor .
');
Loading into Named Graphs
Load data into a specific graph without the TriG/N-Quads format:
-- Create the graph first (optional — auto-created on load)
SELECT pg_ripple.create_graph('https://example.org/graph/2024');
-- Load Turtle into the named graph
SELECT pg_ripple.load_turtle_into_graph('
@prefix ex: <https://example.org/> .
@prefix dct: <http://purl.org/dc/terms/> .
@prefix bibo: <http://purl.org/ontology/bibo/> .
ex:paper/300 a bibo:AcademicArticle ;
dct:title "New Findings in Graph Theory" ;
dct:creator ex:person/frank .
', 'https://example.org/graph/2024');
Using SPARQL Update for Loading
SPARQL INSERT DATA is another way to add triples:
SELECT pg_ripple.sparql_update('
PREFIX ex: <https://example.org/>
PREFIX dct: <http://purl.org/dc/terms/>
PREFIX bibo: <http://purl.org/ontology/bibo/>
INSERT DATA {
ex:paper/500 a bibo:AcademicArticle ;
dct:title "SPARQL Performance Tuning" ;
dct:creator ex:person/alice .
}
');
Common Patterns
Pattern: ETL Pipeline
A typical ETL pipeline loads data in stages:
-- Step 1: Load the ontology
SELECT pg_ripple.load_turtle_file('/data/ontology.ttl');
-- Step 2: Load reference data
SELECT pg_ripple.load_ntriples_file('/data/institutions.nt');
-- Step 3: Load the main dataset
SELECT pg_ripple.load_ntriples_file('/data/papers.nt');
-- Step 4: Load supplementary data into a named graph
SELECT pg_ripple.load_nquads_file('/data/citations.nq');
-- Step 5: Update statistics
SELECT pg_ripple.vacuum();
-- Step 6: Verify the load
SELECT pg_ripple.triple_count();
SELECT pg_ripple.stats();
Pattern: Incremental Loading
For streaming data ingestion, use insert_triple() inside application code:
-- Application inserts triples as events arrive
SELECT pg_ripple.insert_triple(
'<https://example.org/paper/new123>',
'<http://purl.org/dc/terms/title>',
'"Just Published: A New Study"'
);
-- Periodically compact HTAP tables
SELECT pg_ripple.compact();
Pattern: Strict vs Lenient Parsing
-- Lenient (default): skip bad triples, emit WARNINGs
SELECT pg_ripple.load_turtle('
@prefix ex: <https://example.org/> .
ex:good ex:rel ex:target .
ex:bad ex:rel "unclosed literal .
ex:also_good ex:rel ex:other .
', false);
-- Returns: 2 (skipped the bad triple)
-- Strict: abort on any parse error
SELECT pg_ripple.load_turtle('
@prefix ex: <https://example.org/> .
ex:good ex:rel ex:target .
ex:bad ex:rel "unclosed literal .
', true);
-- ERROR: Turtle parse error at line 4
Pattern: Loading OWL Ontologies
-- Auto-detects format from file extension (.ttl, .nt, .xml, .rdf, .owl)
SELECT pg_ripple.load_owl_ontology('/data/ontologies/foaf.rdf');
-- Or load explicitly as RDF/XML
SELECT pg_ripple.load_rdfxml_file('/data/ontologies/dublin_core.rdf');
Performance and Trade-offs
Throughput by Loading Mode
| Mode | Approximate throughput | Use case |
|---|---|---|
insert_triple() | 3,000–8,000 triples/s | Real-time ingestion, single-triple updates |
load_turtle() / load_ntriples() | 30,000–80,000 triples/s | Interactive bulk loads up to a few MB |
load_ntriples_file() | 80,000–200,000 triples/s | Large server-side files |
load_turtle_file() | 60,000–150,000 triples/s | Large server-side Turtle files |
N-Triples is consistently faster than Turtle because it requires no prefix expansion
or abbreviation handling. For maximum throughput on large datasets, convert to N-Triples
first: rapper -i turtle -o ntriples data.ttl > data.nt
Format Selection Guide
| Scenario | Recommended format |
|---|---|
| Hand-authored data | Turtle (readable, supports prefixes) |
| Machine-generated export | N-Triples (fastest parsing, one line per triple) |
| Data with named graphs | N-Quads or TriG |
| Legacy XML datasets | RDF/XML |
| Maximum load speed | N-Triples via load_ntriples_file() |
ANALYZE After Loads
After loading significant amounts of data, update PostgreSQL planner statistics:
-- Run ANALYZE on all VP tables
SELECT pg_ripple.vacuum();
This ensures the query planner has accurate row-count estimates for join ordering.
Batch Size Considerations
For string-based loaders, the entire input is processed in one transaction. Very large strings (hundreds of MB) can cause memory pressure. For datasets over 50 MB, prefer file-based loading:
-- Instead of a huge string literal:
-- SELECT pg_ripple.load_ntriples('... 100 million lines ...');
-- Use file loading:
SELECT pg_ripple.load_ntriples_file('/data/huge_dataset.nt');
Gotchas and Debugging
Blank Node Scoping
Each load_turtle() call creates a fresh blank-node scope. Two separate calls using
_:x produce two different internal IDs:
-- Call 1: _:x maps to internal ID 12345
SELECT pg_ripple.load_turtle('
@prefix ex: <https://example.org/> .
_:x ex:name "Alice" .
ex:paper/1 ex:author _:x .
');
-- Call 2: _:x maps to internal ID 67890 (different!)
SELECT pg_ripple.load_turtle('
@prefix ex: <https://example.org/> .
_:x ex:name "Bob" .
ex:paper/2 ex:author _:x .
');
If you need the same anonymous node across loads, use a stable IRI instead:
SELECT pg_ripple.insert_triple(
'<https://example.org/anon/shared-node>',
'<https://example.org/name>',
'"Shared Entity"'
);
Character Encoding
All loaders expect UTF-8 input. Non-UTF-8 data causes parse errors:
-- If your file is Latin-1, convert first:
-- iconv -f ISO-8859-1 -t UTF-8 data.nt > data_utf8.nt
SELECT pg_ripple.load_ntriples_file('/data/data_utf8.nt');
Verifying Loaded Data
After loading, verify with find_triples() or triple_count():
-- Check total triples
SELECT pg_ripple.triple_count();
-- Inspect specific triples
SELECT * FROM pg_ripple.find_triples(
'<https://example.org/paper/42>', NULL, NULL
);
-- Check per-predicate statistics
SELECT pg_ripple.stats();
File Path Errors
File loaders read from the server filesystem. Common errors:
-- ERROR: could not open file "/data/papers.nt": No such file or directory
-- Fix: ensure the file exists and is readable by the postgres OS user
-- ERROR: permission denied for function load_turtle_file
-- Fix: file loaders require superuser; use string loaders for non-superusers
Duplicate Handling
Loading the same data twice does not create duplicates — VP tables use ON CONFLICT DO NOTHING:
SELECT pg_ripple.load_turtle('
@prefix ex: <https://example.org/> .
ex:a ex:rel ex:b .
');
-- Returns: 1
SELECT pg_ripple.load_turtle('
@prefix ex: <https://example.org/> .
ex:a ex:rel ex:b .
');
-- Returns: 0 (already exists)
Next Steps
- §2.1 Storing Knowledge — understand the triple model and named graphs.
- §2.3 Querying with SPARQL — query the data you loaded.
- §2.6 Exporting and Sharing — export data in various formats.
§2.3 Querying with SPARQL
What and Why
SPARQL is the W3C standard query language for RDF data — the SQL of the knowledge graph world. pg_ripple translates SPARQL queries into optimized PostgreSQL SQL behind the scenes, so you get the expressiveness of SPARQL with the performance of a mature relational engine.
Why SPARQL instead of raw SQL against VP tables?
- Graph pattern matching: find paths, cycles, and subgraph shapes naturally.
- Property paths: traverse variable-length relationships with
+,*,?. - Federation: query remote SPARQL endpoints alongside local data.
- Standards compliance: queries are portable across triple stores.
- Update support:
INSERT DATAandDELETE DATAfor programmatic modifications.
pg_ripple supports all four SPARQL query forms (SELECT, CONSTRUCT, DESCRIBE, ASK) and
SPARQL Update (INSERT DATA, DELETE DATA, DELETE/INSERT WHERE).
How It Works
The SPARQL Pipeline
- Parse —
spargebraparses the SPARQL text into an algebra tree. - Optimize —
sparoptapplies algebraic optimizations (filter pushdown, join reordering). - Translate — pg_ripple's SQL generator converts the algebra to PostgreSQL SQL with integer-only VP table joins.
- Cache — the plan cache stores translated SQL keyed by SPARQL text hash.
- Execute — SPI executes the SQL; results are batch-decoded from integer IDs back to IRIs and literals.
- Return — each result row is returned as a JSONB object.
Key Functions
| Function | Purpose |
|---|---|
sparql(query) | Execute SELECT or ASK; returns JSONB rows |
sparql_ask(query) | Execute ASK; returns boolean |
sparql_construct(query) | Execute CONSTRUCT; returns triple JSONB rows |
sparql_construct_turtle(query) | CONSTRUCT → Turtle text |
sparql_construct_jsonld(query) | CONSTRUCT → JSON-LD JSONB |
sparql_describe(query) | DESCRIBE with CBD; returns triple JSONB rows |
sparql_describe_turtle(query) | DESCRIBE → Turtle text |
sparql_update(query) | INSERT DATA / DELETE DATA; returns affected count |
sparql_explain(query, analyze) | Show generated SQL or EXPLAIN ANALYZE output |
explain_sparql(query, format) | Extended explain with SQL, text, JSON, or algebra output |
Worked Examples
All examples assume the bibliographic dataset from §2.1 and §2.2 has been loaded.
Basic Triple Patterns
Find all papers and their titles:
SELECT * FROM pg_ripple.sparql('
PREFIX dct: <http://purl.org/dc/terms/>
PREFIX bibo: <http://purl.org/ontology/bibo/>
SELECT ?paper ?title
WHERE {
?paper a bibo:AcademicArticle .
?paper dct:title ?title .
}
');
Each row is a JSONB object like {"paper": "<https://example.org/paper/42>", "title": "\"Knowledge Graphs in Practice\""}.
Filtering Results
Find papers published after 2023:
SELECT * FROM pg_ripple.sparql('
PREFIX dct: <http://purl.org/dc/terms/>
PREFIX bibo: <http://purl.org/ontology/bibo/>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
SELECT ?paper ?title ?date
WHERE {
?paper a bibo:AcademicArticle ;
dct:title ?title ;
dct:date ?date .
FILTER (?date > "2023-01-01"^^xsd:date)
}
');
OPTIONAL Patterns
Include authors even if they have no affiliation:
SELECT * FROM pg_ripple.sparql('
PREFIX dct: <http://purl.org/dc/terms/>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
PREFIX schema: <https://schema.org/>
SELECT ?paper ?authorName ?instName
WHERE {
?paper dct:creator ?author .
?author foaf:name ?authorName .
OPTIONAL {
?author schema:affiliation ?inst .
?inst foaf:name ?instName .
}
}
');
UNION
Find entities that are either papers or people:
SELECT * FROM pg_ripple.sparql('
PREFIX bibo: <http://purl.org/ontology/bibo/>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
PREFIX dct: <http://purl.org/dc/terms/>
SELECT ?entity ?label
WHERE {
{
?entity a bibo:AcademicArticle .
?entity dct:title ?label .
}
UNION
{
?entity a foaf:Person .
?entity foaf:name ?label .
}
}
');
MINUS
Find papers that have no citations:
SELECT * FROM pg_ripple.sparql('
PREFIX bibo: <http://purl.org/ontology/bibo/>
PREFIX dct: <http://purl.org/dc/terms/>
SELECT ?paper ?title
WHERE {
?paper a bibo:AcademicArticle ;
dct:title ?title .
MINUS {
?paper bibo:citedBy ?other .
}
}
');
Aggregation
Count papers per institution:
SELECT * FROM pg_ripple.sparql('
PREFIX dct: <http://purl.org/dc/terms/>
PREFIX schema: <https://schema.org/>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
SELECT ?instName (COUNT(DISTINCT ?paper) AS ?paperCount)
WHERE {
?paper dct:creator ?author .
?author schema:affiliation ?inst .
?inst foaf:name ?instName .
}
GROUP BY ?instName
ORDER BY DESC(?paperCount)
');
Subqueries
Find the most prolific author and all their papers:
SELECT * FROM pg_ripple.sparql('
PREFIX dct: <http://purl.org/dc/terms/>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
SELECT ?authorName ?paper ?title
WHERE {
{
SELECT ?author (COUNT(?p) AS ?count)
WHERE {
?p dct:creator ?author .
}
GROUP BY ?author
ORDER BY DESC(?count)
LIMIT 1
}
?author foaf:name ?authorName .
?paper dct:creator ?author ;
dct:title ?title .
}
');
Property Paths
Property paths let you traverse variable-length relationships.
Transitive closure (+) — find all classes an entity belongs to through the subclass hierarchy:
SELECT * FROM pg_ripple.sparql('
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
SELECT ?entity ?superClass
WHERE {
?entity rdf:type/rdfs:subClassOf+ ?superClass .
}
');
Zero-or-more (*) — include the starting node:
SELECT * FROM pg_ripple.sparql('
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?class ?ancestor
WHERE {
?class rdfs:subClassOf* ?ancestor .
}
');
Optional step (?) — zero or one hops:
SELECT * FROM pg_ripple.sparql('
PREFIX schema: <https://schema.org/>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
SELECT ?person ?nameOrInst
WHERE {
?person schema:affiliation? ?target .
?target foaf:name ?nameOrInst .
}
');
Sequence path (/) — chain properties:
SELECT * FROM pg_ripple.sparql('
PREFIX dct: <http://purl.org/dc/terms/>
PREFIX schema: <https://schema.org/>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
SELECT ?paper ?instName
WHERE {
?paper dct:creator/schema:affiliation/foaf:name ?instName .
}
');
Alternative path (|) — match either property:
SELECT * FROM pg_ripple.sparql('
PREFIX dct: <http://purl.org/dc/terms/>
PREFIX schema: <https://schema.org/>
SELECT ?entity ?label
WHERE {
?entity (dct:title | schema:name) ?label .
}
');
Inverse path (^) — traverse in reverse:
SELECT * FROM pg_ripple.sparql('
PREFIX dct: <http://purl.org/dc/terms/>
SELECT ?author ?paper
WHERE {
?author ^dct:creator ?paper .
}
');
GRAPH Patterns
Query data in specific named graphs:
SELECT * FROM pg_ripple.sparql('
PREFIX dct: <http://purl.org/dc/terms/>
SELECT ?paper ?title ?graph
WHERE {
GRAPH ?graph {
?paper dct:title ?title .
}
}
');
Query a specific named graph:
SELECT * FROM pg_ripple.sparql('
PREFIX dct: <http://purl.org/dc/terms/>
SELECT ?paper ?title
WHERE {
GRAPH <https://example.org/graph/pubmed> {
?paper dct:title ?title .
}
}
');
ASK Queries
Check if something exists:
SELECT pg_ripple.sparql_ask('
PREFIX dct: <http://purl.org/dc/terms/>
PREFIX bibo: <http://purl.org/ontology/bibo/>
ASK {
?paper a bibo:AcademicArticle ;
dct:title "Knowledge Graphs in Practice" .
}
');
-- Returns: true
CONSTRUCT Queries
Build new triples from query results:
SELECT * FROM pg_ripple.sparql_construct('
PREFIX dct: <http://purl.org/dc/terms/>
PREFIX schema: <https://schema.org/>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
PREFIX ex: <https://example.org/>
CONSTRUCT {
?author ex:worksOn ?paper .
?paper ex:authoredAt ?inst .
}
WHERE {
?paper dct:creator ?author .
?author schema:affiliation ?inst .
}
');
Get CONSTRUCT results as Turtle:
SELECT pg_ripple.sparql_construct_turtle('
PREFIX dct: <http://purl.org/dc/terms/>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
PREFIX ex: <https://example.org/>
CONSTRUCT {
?author ex:wrote ?paper .
}
WHERE {
?paper dct:creator ?author .
}
');
Get CONSTRUCT results as JSON-LD:
SELECT pg_ripple.sparql_construct_jsonld('
PREFIX dct: <http://purl.org/dc/terms/>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
PREFIX ex: <https://example.org/>
CONSTRUCT {
?author ex:wrote ?paper .
}
WHERE {
?paper dct:creator ?author .
}
');
DESCRIBE Queries
Get everything about an entity using Concise Bounded Description:
SELECT * FROM pg_ripple.sparql_describe('
DESCRIBE <https://example.org/paper/42>
');
Get the description as Turtle:
SELECT pg_ripple.sparql_describe_turtle('
DESCRIBE <https://example.org/person/alice>
');
Choose the describe strategy:
-- Symmetric CBD: include triples where the entity is the object too
SELECT * FROM pg_ripple.sparql_describe(
'DESCRIBE <https://example.org/person/alice>',
'scbd'
);
SPARQL Update
Insert new triples:
SELECT pg_ripple.sparql_update('
PREFIX ex: <https://example.org/>
PREFIX dct: <http://purl.org/dc/terms/>
INSERT DATA {
ex:paper/600 a <http://purl.org/ontology/bibo/AcademicArticle> ;
dct:title "Emerging Trends in Knowledge Graphs" ;
dct:creator ex:person/alice .
}
');
-- Returns: 3
Delete specific triples:
SELECT pg_ripple.sparql_update('
PREFIX ex: <https://example.org/>
PREFIX dct: <http://purl.org/dc/terms/>
DELETE DATA {
ex:paper/600 dct:title "Emerging Trends in Knowledge Graphs" .
}
');
-- Returns: 1
Query Debugging with EXPLAIN
View the generated SQL without executing:
SELECT pg_ripple.sparql_explain('
PREFIX dct: <http://purl.org/dc/terms/>
PREFIX bibo: <http://purl.org/ontology/bibo/>
SELECT ?paper ?title
WHERE {
?paper a bibo:AcademicArticle ;
dct:title ?title .
}
', false);
Run EXPLAIN ANALYZE to see execution times:
SELECT pg_ripple.sparql_explain('
PREFIX dct: <http://purl.org/dc/terms/>
PREFIX bibo: <http://purl.org/ontology/bibo/>
SELECT ?paper ?title
WHERE {
?paper a bibo:AcademicArticle ;
dct:title ?title .
}
', true);
Use the extended explain with format options:
-- Show just the generated SQL
SELECT pg_ripple.explain_sparql('
PREFIX dct: <http://purl.org/dc/terms/>
SELECT ?paper ?title
WHERE { ?paper dct:title ?title }
', 'sql');
-- Show EXPLAIN ANALYZE as JSON (for programmatic consumption)
SELECT pg_ripple.explain_sparql('
PREFIX dct: <http://purl.org/dc/terms/>
SELECT ?paper ?title
WHERE { ?paper dct:title ?title }
', 'json');
-- Show the spargebra algebra tree
SELECT pg_ripple.explain_sparql('
PREFIX dct: <http://purl.org/dc/terms/>
SELECT ?paper ?title
WHERE { ?paper dct:title ?title }
', 'sparql_algebra');
Common Patterns
Pattern: Star Queries (Multiple Predicates on the Same Subject)
The optimizer detects star patterns and collapses them into efficient multi-way joins:
SELECT * FROM pg_ripple.sparql('
PREFIX dct: <http://purl.org/dc/terms/>
PREFIX schema: <https://schema.org/>
PREFIX bibo: <http://purl.org/ontology/bibo/>
SELECT ?paper ?title ?date
WHERE {
?paper a bibo:AcademicArticle ;
dct:title ?title ;
dct:date ?date ;
schema:keywords ?kw .
FILTER (CONTAINS(?kw, "knowledge"))
}
');
Pattern: Existence Checks with FILTER EXISTS
SELECT * FROM pg_ripple.sparql('
PREFIX dct: <http://purl.org/dc/terms/>
PREFIX bibo: <http://purl.org/ontology/bibo/>
SELECT ?paper ?title
WHERE {
?paper a bibo:AcademicArticle ;
dct:title ?title .
FILTER EXISTS {
?paper bibo:citedBy ?other .
}
}
');
Pattern: VALUES Clause for Parameterized Queries
SELECT * FROM pg_ripple.sparql('
PREFIX dct: <http://purl.org/dc/terms/>
SELECT ?paper ?title
WHERE {
VALUES ?paper {
<https://example.org/paper/42>
<https://example.org/paper/99>
}
?paper dct:title ?title .
}
');
Pattern: BIND and Computed Values
SELECT * FROM pg_ripple.sparql('
PREFIX dct: <http://purl.org/dc/terms/>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
SELECT ?paper ?title ?yearLabel
WHERE {
?paper dct:title ?title ;
dct:date ?date .
BIND(YEAR(?date) AS ?year)
BIND(CONCAT("Published in ", STR(?year)) AS ?yearLabel)
}
');
Performance and Trade-offs
Plan Cache
pg_ripple caches translated SQL by SPARQL query hash. Repeated queries skip the parse and translate steps:
-- Check cache statistics
SELECT pg_ripple.plan_cache_stats();
-- Returns: {"hits": 42, "misses": 5, "size": 5, "capacity": 128, "hit_rate": 0.89}
-- Reset the cache (e.g., after schema changes)
SELECT pg_ripple.plan_cache_reset();
Filter Pushdown
SPARQL FILTERs on bound constants are encoded to integers before SQL generation. This means the database compares integers, not strings:
-- This FILTER is pushed down as an integer comparison:
SELECT * FROM pg_ripple.sparql('
PREFIX dct: <http://purl.org/dc/terms/>
SELECT ?paper WHERE {
?paper dct:creator <https://example.org/person/alice> .
}
');
Property Path Depth Limit
Recursive property paths (+, *) compile to WITH RECURSIVE ... CYCLE. The GUC
pg_ripple.max_path_depth (default: 50) prevents runaway recursion:
-- Increase depth for deep hierarchies
SET pg_ripple.max_path_depth = 100;
Setting max_path_depth very high on cyclic graphs can cause slow queries. pg_ripple
uses PostgreSQL 18's CYCLE clause for hash-based cycle detection, but wide graphs
still accumulate many intermediate rows.
Full-Text Search Integration
Create a GIN index for fast text search on specific predicates:
-- Index the dct:title predicate for full-text search
SELECT pg_ripple.fts_index('<http://purl.org/dc/terms/title>');
-- Then CONTAINS() and REGEX() filters on dct:title objects use the GIN index
SELECT * FROM pg_ripple.sparql('
PREFIX dct: <http://purl.org/dc/terms/>
SELECT ?paper ?title
WHERE {
?paper dct:title ?title .
FILTER (CONTAINS(?title, "Knowledge"))
}
');
Or use the direct full-text search function:
SELECT * FROM pg_ripple.fts_search(
'knowledge & graph',
'<http://purl.org/dc/terms/title>'
);
Gotchas and Debugging
SPARQL Syntax Errors
pg_ripple uses the spargebra parser, which gives precise error messages:
SELECT * FROM pg_ripple.sparql('
SELECT ?x WHERE { ?x ?p }
');
-- ERROR: SPARQL parse error: Expected '.' or '}' at line 2
Check the query compiles before running:
SELECT pg_ripple.explain_sparql('
PREFIX dct: <http://purl.org/dc/terms/>
SELECT ?paper WHERE { ?paper dct:title ?title }
', 'sql');
No Results When Expected
Common causes:
- Missing angle brackets:
dct:titlein SPARQL requires a PREFIX declaration. Without it, the parser treats it as a relative IRI. - Wrong literal format:
"42"is a string, not a number. Use"42"^^xsd:integer. - Case sensitivity: IRIs are case-sensitive.
<https://Example.org/X>and<https://example.org/x>are different.
Debug by checking what is stored:
-- Check if the predicate exists
SELECT * FROM pg_ripple.find_triples(
NULL, '<http://purl.org/dc/terms/title>', NULL
);
Slow Queries
- Check the generated SQL with
sparql_explain(). - Look for sequential scans on large VP tables — run
pg_ripple.vacuum()to update statistics. - For property paths, check
max_path_depth— lower it if the query is exploring too many paths. - Check the plan cache hit rate — a low hit rate means many unique queries are being parsed repeatedly.
-- Step 1: See the execution plan
SELECT pg_ripple.sparql_explain('
PREFIX dct: <http://purl.org/dc/terms/>
SELECT ?paper ?title
WHERE { ?paper dct:title ?title }
', true);
-- Step 2: Update statistics
SELECT pg_ripple.vacuum();
-- Step 3: Check plan cache
SELECT pg_ripple.plan_cache_stats();
SPARQL Update Limitations
sparql_update() supports INSERT DATA and DELETE DATA (ground triples only).
Pattern-based DELETE/INSERT WHERE with variables is also supported for flexible
graph modifications. Use delete_triple() for programmatic single-triple deletion.
Next Steps
- §2.4 Validating Data Quality — enforce constraints on your data with SHACL.
- §2.5 Reasoning and Inference — derive new facts with Datalog rules.
- §2.8 APIs and Integration — access SPARQL from application code via the HTTP endpoint.
Further reading
- Blog: SPARQL-to-SQL Translation — how pg_ripple compiles SPARQL into optimized PostgreSQL SQL
- Blog: Property Paths and Recursive CTEs — the implementation of
*and+path operators - Blog: Explain SPARQL Query Plans — understanding the SPARQL query debugger
§2.4 Validating Data Quality
Status: Available since v0.3.0 (SHACL-01)
Requires: No external dependencies.
SQL: pg_ripple.load_shacl_shapes(), pg_ripple.validate_shapes(), pg_ripple.list_shacl_violations()
What and Why
Storing knowledge is only half the battle — you also need to ensure it is correct. SHACL (Shapes Constraint Language) is the W3C standard for declaring and validating constraints on RDF data. It answers questions like:
- Does every paper have at least one author?
- Are all email addresses syntactically valid?
- Does every person have exactly one name?
- Are date values well-formed?
pg_ripple integrates SHACL validation directly into the database engine. You can validate on demand, enforce constraints synchronously on every insert, or queue triples for asynchronous background validation with violations routed to a dead-letter queue.
SHACL is to RDF what CHECK constraints and triggers are to relational databases — but SHACL shapes are declarative, composable, and standardized across all RDF systems.
How It Works
The SHACL Model
A SHACL shape declares constraints on a set of focus nodes (entities matching a target pattern). Each shape contains one or more property shapes that constrain the values of a specific predicate.
NodeShape (target: instances of bibo:AcademicArticle)
└─ PropertyShape (path: dct:title)
├─ sh:minCount 1 ← every paper must have at least one title
├─ sh:maxCount 1 ← at most one title
└─ sh:datatype xsd:string ← title must be a string
Validation Modes
| Mode | GUC setting | Behavior |
|---|---|---|
| Off (default) | pg_ripple.shacl_mode = 'off' | No automatic validation |
| Sync | pg_ripple.shacl_mode = 'sync' | Every insert_triple() is validated before commit; violations raise an ERROR |
| Async | pg_ripple.shacl_mode = 'async' | Triples are inserted immediately; a background worker validates and routes violations to the dead-letter queue |
Supported Constraints
| Constraint | Description |
|---|---|
sh:minCount | Minimum number of values |
sh:maxCount | Maximum number of values |
sh:datatype | Value must have a specific XSD datatype |
sh:class | Value must be an instance of a class |
sh:in | Value must be from an enumerated set |
sh:pattern | Value must match a regex |
sh:node | Value must conform to another shape |
sh:or | Value must conform to at least one of several shapes |
sh:and | Value must conform to all listed shapes |
sh:not | Value must NOT conform to a shape |
sh:qualifiedValueShape | Qualified cardinality constraints |
sh:hasValue | At least one value must equal the given term |
sh:nodeKind | Value must be IRI, blank node, or literal |
sh:languageIn | Language tag must be in the allowed list |
sh:uniqueLang | No duplicate language tags |
sh:lessThan / sh:greaterThan | Comparative constraints between properties |
sh:closed | Reject unknown predicates |
Worked Examples
Loading Simple Shapes
Define shapes for the bibliographic dataset:
SELECT pg_ripple.load_shacl('
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix ex: <https://example.org/> .
@prefix dct: <http://purl.org/dc/terms/> .
@prefix bibo: <http://purl.org/ontology/bibo/> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix schema: <https://schema.org/> .
ex:PaperShape a sh:NodeShape ;
sh:targetClass bibo:AcademicArticle ;
sh:property [
sh:path dct:title ;
sh:minCount 1 ;
sh:maxCount 1 ;
sh:datatype xsd:string ;
] ;
sh:property [
sh:path dct:creator ;
sh:minCount 1 ;
sh:class foaf:Person ;
] .
ex:PersonShape a sh:NodeShape ;
sh:targetClass foaf:Person ;
sh:property [
sh:path foaf:name ;
sh:minCount 1 ;
sh:maxCount 1 ;
sh:datatype xsd:string ;
] ;
sh:property [
sh:path schema:affiliation ;
sh:maxCount 1 ;
sh:nodeKind sh:IRI ;
] .
');
-- Returns: 2 (number of shapes loaded)
Running Validation
Validate the default graph against all active shapes:
SELECT pg_ripple.validate();
The result is a JSONB validation report:
{
"conforms": false,
"violations": [
{
"focusNode": "<https://example.org/paper/99>",
"shapeIRI": "<https://example.org/PaperShape>",
"path": "<http://purl.org/dc/terms/creator>",
"constraint": "sh:class",
"message": "value <https://example.org/person/carol> is not an instance of <http://xmlns.com/foaf/0.1/Person>",
"severity": "sh:Violation"
}
]
}
Validate a specific named graph:
SELECT pg_ripple.validate('https://example.org/graph/pubmed');
Validate all graphs at once:
SELECT pg_ripple.validate('*');
Synchronous Validation
Enable sync mode so invalid triples are rejected at insert time:
SET pg_ripple.shacl_mode = 'sync';
-- This succeeds (paper has a title)
SELECT pg_ripple.insert_triple(
'<https://example.org/paper/700>',
'<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>',
'<http://purl.org/ontology/bibo/AcademicArticle>'
);
-- This would fail if the shape requires dct:title and the paper doesn't have one yet
-- (sync validation checks per-triple, not transactionally)
Synchronous validation adds overhead to every insert_triple() call. Use it for
low-volume, high-integrity scenarios. For bulk loads, use 'off' mode and validate
after loading.
Asynchronous Validation with Dead-Letter Queue
Enable async mode for high-throughput pipelines:
SET pg_ripple.shacl_mode = 'async';
-- Triples are inserted immediately; validation happens in the background
SELECT pg_ripple.insert_triple(
'<https://example.org/paper/800>',
'<http://purl.org/dc/terms/title>',
'"A New Paper"'
);
-- Check the validation queue length
SELECT pg_ripple.validation_queue_length();
-- Manually process the queue (normally handled by background worker)
SELECT pg_ripple.process_validation_queue(1000);
-- Check for violations
SELECT pg_ripple.dead_letter_count();
-- View the full dead-letter queue
SELECT pg_ripple.dead_letter_queue();
Complex Shapes
Disjunctive constraints (sh:or):
SELECT pg_ripple.load_shacl('
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix ex: <https://example.org/> .
@prefix dct: <http://purl.org/dc/terms/> .
ex:DateShape a sh:NodeShape ;
sh:targetSubjectsOf dct:date ;
sh:property [
sh:path dct:date ;
sh:or (
[ sh:datatype xsd:date ]
[ sh:datatype xsd:gYear ]
[ sh:datatype xsd:dateTime ]
) ;
] .
');
Closed shapes (reject unknown predicates):
SELECT pg_ripple.load_shacl('
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <https://example.org/> .
@prefix dct: <http://purl.org/dc/terms/> .
@prefix bibo: <http://purl.org/ontology/bibo/> .
@prefix schema: <https://schema.org/> .
ex:StrictPaperShape a sh:NodeShape ;
sh:targetClass bibo:AcademicArticle ;
sh:closed true ;
sh:ignoredProperties (
<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>
) ;
sh:property [
sh:path dct:title ;
sh:minCount 1 ;
] ;
sh:property [
sh:path dct:creator ;
sh:minCount 1 ;
] ;
sh:property [
sh:path dct:date ;
sh:maxCount 1 ;
] ;
sh:property [
sh:path schema:keywords ;
] ;
sh:property [
sh:path bibo:cites ;
] ;
sh:property [
sh:path bibo:citedBy ;
] .
');
Qualified cardinality:
SELECT pg_ripple.load_shacl('
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <https://example.org/> .
@prefix dct: <http://purl.org/dc/terms/> .
@prefix bibo: <http://purl.org/ontology/bibo/> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
ex:CollabPaperShape a sh:NodeShape ;
sh:targetClass bibo:AcademicArticle ;
sh:property [
sh:path dct:creator ;
sh:qualifiedValueShape [
sh:class foaf:Person ;
] ;
sh:qualifiedMinCount 2 ;
] .
');
Managing Shapes
-- List all loaded shapes
SELECT * FROM pg_ripple.list_shapes();
-- Deactivate a shape without deleting it
SELECT pg_ripple.disable_rule_set('custom');
-- Remove a shape entirely
SELECT pg_ripple.drop_shape('https://example.org/StrictPaperShape');
SHACL DAG Monitors
For real-time violation detection, enable DAG monitors (requires pg_trickle):
-- Load shapes first
SELECT pg_ripple.load_shacl('...');
-- Enable per-shape violation stream tables
SELECT pg_ripple.enable_shacl_dag_monitors();
-- View the live violation summary
SELECT * FROM _pg_ripple.violation_summary_dag;
-- List active monitors
SELECT * FROM pg_ripple.list_shacl_dag_monitors();
-- Disable when no longer needed
SELECT pg_ripple.disable_shacl_dag_monitors();
Common Patterns
Pattern: Validate After Bulk Load
The most common workflow — load first, validate second:
-- Turn off validation during load
SET pg_ripple.shacl_mode = 'off';
-- Load data
SELECT pg_ripple.load_turtle_file('/data/papers.ttl');
-- Load shapes
SELECT pg_ripple.load_shacl('...');
-- Validate
SELECT pg_ripple.validate();
Pattern: Data Quality Dashboard
Use the dead-letter queue as a data quality monitor:
-- Enable async validation
SET pg_ripple.shacl_mode = 'async';
-- Periodically check violation counts
SELECT pg_ripple.dead_letter_count();
-- Get violation details
SELECT pg_ripple.dead_letter_queue();
-- With pg_trickle: enable violation summary stream table
SELECT pg_ripple.enable_shacl_monitors();
SELECT * FROM _pg_ripple.violation_summary;
Pattern: Embedding Completeness Check
Ensure all entities have vector embeddings (see §2.7):
SELECT pg_ripple.load_shacl('
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix ex: <https://example.org/> .
@prefix pg: <urn:pg_ripple:> .
@prefix bibo: <http://purl.org/ontology/bibo/> .
ex:EmbeddingCompletenessShape a sh:NodeShape ;
sh:targetClass bibo:AcademicArticle ;
sh:property [
sh:path pg:hasEmbedding ;
sh:minCount 1 ;
sh:hasValue "true"^^xsd:boolean ;
] .
');
-- Add embedding triples for entities that have been embedded
SELECT pg_ripple.add_embedding_triples();
-- Check completeness
SELECT pg_ripple.validate();
Pattern: Multi-Language Support
Ensure labels exist in required languages:
SELECT pg_ripple.load_shacl('
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <https://example.org/> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
ex:LabelShape a sh:NodeShape ;
sh:targetSubjectsOf rdfs:label ;
sh:property [
sh:path rdfs:label ;
sh:languageIn ( "en" "de" "fr" ) ;
sh:uniqueLang true ;
] .
');
Performance and Trade-offs
| Validation mode | Overhead | Data integrity | Use case |
|---|---|---|---|
off | None | Manual check with validate() | Bulk loads, development |
sync | High (per-triple check) | Immediate rejection | Low-volume critical data |
async | Low (background worker) | Eventual (violations in DLQ) | High-throughput pipelines |
- Shape count: validation time scales linearly with the number of active shapes and focus nodes. Deactivate shapes you do not need.
- DAG monitors: per-shape stream tables are
IMMEDIATEmode — violations are detected within the same transaction. But pg_trickle must be installed. - Dead-letter queue: grows without bound. Periodically review and clean it:
-- Remove violations older than 30 days DELETE FROM _pg_ripple.dead_letter_queue WHERE detected_at < NOW() - INTERVAL '30 days';
Shapes with sh:maxCount 1 allow the SPARQL query engine to omit DISTINCT on that
predicate's joins. Shapes with sh:minCount 1 allow downgrading LEFT JOIN to
INNER JOIN. Declaring accurate shapes improves both data quality and query performance.
Gotchas and Debugging
Shape Loading Errors
If load_shacl() returns 0, the Turtle may have syntax errors. Check for:
- Missing
@prefixdeclarations - Unclosed brackets in blank node property lists
- Missing semicolons between property shapes
Sync Mode and Transaction Boundaries
Sync validation checks individual triples, not entire transactions. A paper might pass
the dct:creator check (because the triple being inserted is the author link) but fail
the dct:title check because the title has not been inserted yet in the same transaction.
Solution: insert all triples for an entity, then validate explicitly:
SET pg_ripple.shacl_mode = 'off';
-- Insert all triples for the entity
SELECT pg_ripple.insert_triple('<https://example.org/paper/900>', '<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>', '<http://purl.org/ontology/bibo/AcademicArticle>');
SELECT pg_ripple.insert_triple('<https://example.org/paper/900>', '<http://purl.org/dc/terms/title>', '"My Paper"');
SELECT pg_ripple.insert_triple('<https://example.org/paper/900>', '<http://purl.org/dc/terms/creator>', '<https://example.org/person/alice>');
-- Then validate
SELECT pg_ripple.validate();
Viewing Shape Definitions
-- List all shapes and their active status
SELECT * FROM pg_ripple.list_shapes();
Validation Report Interpretation
The validation report JSONB has two top-level keys:
conforms:trueif no violations were foundviolations: array of violation objects, each withfocusNode,shapeIRI,path,constraint,message, andseverity
-- Extract just the violation messages
SELECT v->>'message'
FROM jsonb_array_elements(
(SELECT pg_ripple.validate()::jsonb -> 'violations')
) AS v;
Next Steps
- §2.5 Reasoning and Inference — derive new facts from rules; SHACL shapes interact with inference.
- §2.6 Exporting and Sharing — SHACL quality enforcement for GraphRAG exports.
- §2.7 AI Retrieval and GraphRAG — embedding completeness shapes.
Further reading
- Blog: SHACL Data Quality — a deep dive into how SHACL shapes protect your data
§2.5 Reasoning and Inference
Status: Available since v0.6.0 (RULE-01)
Requires: No external dependencies. Rule sets are compiled to SQL by the Datalog engine.
SQL: pg_ripple.add_rule(), pg_ripple.run_inference(), pg_ripple.enable_builtin_rules()
What and Why
Inference lets pg_ripple derive new facts from existing data using logical rules. If Alice works at MIT and MIT is located in Massachusetts, inference can conclude that Alice is located in Massachusetts — without anyone explicitly inserting that triple.
pg_ripple ships a full Datalog reasoning engine that supports:
- Built-in rule sets: RDFS and OWL RL entailment out of the box.
- Custom rules: domain-specific inference in a Turtle-flavoured Datalog syntax.
- Stratified negation: "flag people without an email address."
- Aggregation: COUNT, SUM, MIN, MAX, AVG over grouped triple patterns.
- Magic sets: goal-directed inference that only materialises relevant facts.
- Semi-naive evaluation: efficient fixpoint iteration that skips unchanged rows.
- Well-Founded Semantics: handle programs with cyclic negation (v0.32.0).
Derived triples are stored with source = 1 (inferred) alongside explicit triples
(source = 0), so you can always distinguish asserted from derived facts.
How It Works
The Datalog Pipeline
- Parse — rules are parsed from a Turtle-flavoured Datalog syntax into an internal Rule IR.
- Stratify — the dependency graph is analyzed; rules are grouped into strata such that negated predicates are fully computed in lower strata.
- Compile — each stratum is compiled to PostgreSQL SQL: non-recursive rules become
INSERT ... SELECT, recursive rules becomeWITH RECURSIVE ... CYCLE. - Execute — strata run bottom-up; each stratum's SQL is executed via SPI, inserting derived triples into VP delta tables.
- Fixpoint — recursive strata iterate until no new facts are derived (semi-naive evaluation).
Rule Syntax
Rules use a Prolog-like notation with RDF terms. The prefix registry from register_prefix() is available:
head_triple :- body_triple1 , body_triple2 .
Variables start with ?. Constants are IRIs (prefixed or full). Negation uses NOT.
Built-in Rule Sets
| Name | Rules | What it covers |
|---|---|---|
rdfs | ~12 rules | rdfs:subClassOf transitivity, rdfs:subPropertyOf transitivity, rdf:type propagation via subclass/subproperty, rdfs:domain/rdfs:range inference |
owl-rl | ~80 rules | OWL RL profile: symmetric/transitive/inverse properties, owl:equivalentClass, owl:sameAs, owl:unionOf, owl:intersectionOf, property chains, and more |
Worked Examples
Loading Built-in RDFS Rules
-- Load the RDFS entailment rules
SELECT pg_ripple.load_rules_builtin('rdfs');
-- Returns: 12 (number of rules)
-- Load some class hierarchy data
SELECT pg_ripple.load_turtle('
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix bibo: <http://purl.org/ontology/bibo/> .
@prefix ex: <https://example.org/> .
bibo:AcademicArticle rdfs:subClassOf bibo:Article .
bibo:Article rdfs:subClassOf bibo:Document .
bibo:Document rdfs:subClassOf rdfs:Resource .
ex:paper/42 rdf:type bibo:AcademicArticle .
');
-- Run inference
SELECT pg_ripple.infer('rdfs');
-- Now ex:paper/42 is also a bibo:Article, bibo:Document, and rdfs:Resource
SELECT * FROM pg_ripple.sparql('
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX bibo: <http://purl.org/ontology/bibo/>
SELECT ?type
WHERE {
<https://example.org/paper/42> rdf:type ?type .
}
');
Loading OWL RL Rules
-- Load the OWL RL entailment rules
SELECT pg_ripple.load_rules_builtin('owl-rl');
-- Load ontology with OWL constructs
SELECT pg_ripple.load_turtle('
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix ex: <https://example.org/> .
ex:cites owl:inverseOf ex:citedBy .
ex:collaboratesWith a owl:SymmetricProperty .
ex:influencedBy a owl:TransitiveProperty .
ex:paper/42 ex:cites ex:paper/99 .
ex:person/alice ex:collaboratesWith ex:person/bob .
ex:person/carol ex:influencedBy ex:person/alice .
ex:person/alice ex:influencedBy ex:person/dave .
');
-- Run OWL RL inference
SELECT pg_ripple.infer('owl-rl');
-- Derived: ex:paper/99 ex:citedBy ex:paper/42 (inverse)
-- Derived: ex:person/bob ex:collaboratesWith ex:person/alice (symmetric)
-- Derived: ex:person/carol ex:influencedBy ex:person/dave (transitive)
Writing Custom Rules
Define domain-specific rules for the bibliographic dataset:
SELECT pg_ripple.load_rules('
# Derive co-authorship: two people who authored the same paper
?a ex:coAuthor ?b :- ?paper dct:creator ?a , ?paper dct:creator ?b .
# Derive institutional collaboration
?inst1 ex:collaboratesWith ?inst2 :-
?paper dct:creator ?a ,
?paper dct:creator ?b ,
?a schema:affiliation ?inst1 ,
?b schema:affiliation ?inst2 .
# Derive prolific author (authored 5+ papers)
# Uses arithmetic guard: at least 5 papers
?author ex:isProlific "true"^^xsd:boolean :-
?paper1 dct:creator ?author ,
?paper2 dct:creator ?author ,
?paper3 dct:creator ?author ,
?paper4 dct:creator ?author ,
?paper5 dct:creator ?author .
', 'biblio');
-- Run the custom rule set
SELECT pg_ripple.infer('biblio');
Negation-as-Failure
Flag entities that are missing expected properties:
SELECT pg_ripple.load_rules('
# Flag papers without a date
?paper ex:missingDate "true"^^xsd:boolean :-
?paper rdf:type bibo:AcademicArticle ,
NOT ?paper dct:date ?_ .
# Flag people without an affiliation
?person ex:missingAffiliation "true"^^xsd:boolean :-
?person rdf:type foaf:Person ,
NOT ?person schema:affiliation ?_ .
', 'quality');
SELECT pg_ripple.infer('quality');
-- Query the derived quality flags
SELECT * FROM pg_ripple.sparql('
PREFIX ex: <https://example.org/>
SELECT ?paper WHERE { ?paper ex:missingDate "true"^^<http://www.w3.org/2001/XMLSchema#boolean> }
');
Named Graph Scoping
Write derived triples into a separate graph:
SELECT pg_ripple.load_rules('
# All RDFS inference goes into the "inferred" graph
GRAPH ex:graph/inferred { ?x rdf:type ?c } :-
?x rdf:type ?b , ?b rdfs:subClassOf ?c .
', 'scoped-rdfs');
SELECT pg_ripple.infer('scoped-rdfs');
-- Query only inferred types
SELECT * FROM pg_ripple.sparql('
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
SELECT ?x ?type WHERE {
GRAPH <https://example.org/graph/inferred> {
?x rdf:type ?type .
}
}
');
Semi-Naive Evaluation with Statistics
Get detailed inference statistics:
SELECT pg_ripple.infer_with_stats('rdfs');
Returns JSONB:
{
"derived": 156,
"iterations": 4,
"eliminated_rules": [
"?x rdf:type rdfs:Resource :- ?x ?p ?o ."
]
}
The eliminated_rules field shows rules removed by subsumption checking — rules
whose body is a superset of another rule's body.
Goal-Directed Inference with Magic Sets
When you only need a subset of derived facts, magic sets avoids materialising everything:
-- Only derive facts relevant to: "What types does paper/42 have?"
SELECT pg_ripple.infer_goal('rdfs', '?x rdf:type <http://xmlns.com/foaf/0.1/Person>');
Returns JSONB:
{
"derived": 12,
"iterations": 3,
"matching": 5
}
Compare with full inference:
-- Full materialization: derives ALL facts
SELECT pg_ripple.infer('rdfs');
-- derived: 156
-- Goal-directed: derives only what's needed for the goal
SELECT pg_ripple.infer_goal('rdfs', '?x rdf:type foaf:Person');
-- derived: 12 (much fewer)
Magic sets are controlled by the GUC pg_ripple.magic_sets. When set to false,
infer_goal() falls back to full materialization and filters post-hoc.
Demand-Filtered Inference
For multiple goals at once, use demand-filtered inference:
SELECT pg_ripple.infer_demand('rdfs', '[
{"p": "<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>"},
{"s": "<https://example.org/paper/42>"}
]'::jsonb);
Returns:
{
"derived": 45,
"iterations": 3,
"demand_predicates": [
"http://www.w3.org/1999/02/22-rdf-syntax-ns#type"
]
}
Aggregate Rules (Datalog^agg)
Derive facts using aggregate functions:
SELECT pg_ripple.load_rules('
# Count papers per author
?author ex:paperCount ?count :-
COUNT(?paper WHERE ?paper dct:creator ?author) = ?count .
# Sum citation counts per paper
?paper ex:totalCitations ?total :-
COUNT(?citing WHERE ?citing bibo:cites ?paper) = ?total .
', 'metrics');
-- Use the aggregate-aware inference function
SELECT pg_ripple.infer_agg('metrics');
Returns:
{
"derived": 25,
"aggregate_derived": 25,
"iterations": 1
}
Well-Founded Semantics (v0.32.0)
For programs with cyclic negation (where standard stratification fails):
SELECT pg_ripple.load_rules('
# Cyclic negation: a node is "in" if it is not "out", and vice versa
?x ex:in "true"^^xsd:boolean :- ?x rdf:type ex:Node , NOT ?x ex:out "true"^^xsd:boolean .
?x ex:out "true"^^xsd:boolean :- ?x rdf:type ex:Node , NOT ?x ex:in "true"^^xsd:boolean .
', 'wfs-demo');
-- Standard infer() would fail with "unstratifiable" error
-- WFS handles it gracefully
SELECT pg_ripple.infer_wfs('wfs-demo');
Returns:
{
"derived": 6,
"certain": 0,
"unknown": 6,
"iterations": 3,
"stratifiable": false
}
Facts with certainty = 'unknown' are reported but NOT materialised into VP tables.
Common Patterns
Pattern: Layered Inference
Run rule sets in order — base entailment first, then domain rules:
-- Layer 1: RDFS entailment
SELECT pg_ripple.load_rules_builtin('rdfs');
SELECT pg_ripple.infer('rdfs');
-- Layer 2: OWL RL (builds on RDFS-derived facts)
SELECT pg_ripple.load_rules_builtin('owl-rl');
SELECT pg_ripple.infer('owl-rl');
-- Layer 3: Custom domain rules
SELECT pg_ripple.load_rules('...', 'domain');
SELECT pg_ripple.infer('domain');
Pattern: Incremental Re-Inference
After adding new data, re-run inference. Semi-naive evaluation only derives new facts:
-- Load new data
SELECT pg_ripple.load_turtle('
@prefix ex: <https://example.org/> .
@prefix bibo: <http://purl.org/ontology/bibo/> .
ex:paper/newOne a bibo:AcademicArticle .
');
-- Re-run inference — only new derivations are computed
SELECT pg_ripple.infer_with_stats('rdfs');
Pattern: Explicit vs Inferred Triples
All VP tables have a source column: 0 = explicit, 1 = inferred. You can query
this distinction via SPARQL or check the full triple store:
-- Find all inferred type assertions
SELECT * FROM pg_ripple.find_triples(
NULL,
'<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>',
NULL
);
Pattern: owl:sameAs Canonicalization
When pg_ripple.sameas_reasoning = 'on' (default), owl:sameAs links are
canonicalized before inference. All mentions of equivalent entities are collapsed to
a single canonical ID, reducing redundant derivations.
-- Two IRIs refer to the same entity
SELECT pg_ripple.insert_triple(
'<https://example.org/person/alice>',
'<http://www.w3.org/2002/07/owl#sameAs>',
'<https://other.org/people/a-johnson>'
);
-- After inference, both IRIs are treated as identical
SELECT pg_ripple.infer('owl-rl');
Performance and Trade-offs
Full Materialization vs Goal-Directed
| Strategy | Pros | Cons |
|---|---|---|
Full (infer()) | Complete; all derived facts available | May derive millions of unneeded facts |
Goal-directed (infer_goal()) | Only derives relevant facts | Must specify the goal pattern |
Demand-filtered (infer_demand()) | Multiple goals; partial materialization | Slightly more setup |
| On-demand (query-time) | Zero materialization cost | Slower queries |
Semi-Naive Evaluation
Semi-naive evaluation tracks which facts are new in each iteration and only joins new facts with existing facts. This reduces the work per iteration from O(n^2) to O(n * delta), where delta is the number of new facts per round.
Subsumption Checking
When two rules have the same head and one rule's body is a subset of the other's, the subsumed rule is eliminated. This reduces the number of SQL statements per iteration.
Tabling / Memoisation (v0.32.0)
Goal-directed inference results and WFS results are cached in _pg_ripple.tabling_cache.
Cache entries are automatically invalidated when data changes (inserts or deletes).
-- Check tabling cache statistics
SELECT * FROM pg_ripple.tabling_stats();
Rule Set Management
-- List all rules and their metadata
SELECT pg_ripple.list_rules();
-- Enable/disable a rule set without deleting it
SELECT pg_ripple.enable_rule_set('rdfs');
SELECT pg_ripple.disable_rule_set('quality');
-- Drop all rules in a set
SELECT pg_ripple.drop_rules('quality');
Gotchas and Debugging
Unstratifiable Programs
If your rules contain cyclic negation, standard infer() will fail:
ERROR: unstratifiable rule set — negation cycle detected
DETAIL: ex:in negates ex:out, which depends on ex:in
HINT: remove the negation cycle or use infer_wfs() for well-founded semantics
Fix: either restructure the rules to eliminate the cycle, or use infer_wfs().
Prefix Registration
Rules use the prefix registry from register_prefix(). If a prefix is not registered,
the parser treats it as a parse error:
-- Register required prefixes BEFORE loading rules
SELECT pg_ripple.register_prefix('ex', 'https://example.org/');
SELECT pg_ripple.register_prefix('dct', 'http://purl.org/dc/terms/');
-- Now load rules that use these prefixes
SELECT pg_ripple.load_rules('?x ex:rel ?y :- ?x dct:creator ?y .', 'test');
Checking What Was Derived
After inference, check the statistics:
-- How many triples total?
SELECT pg_ripple.triple_count();
-- Detailed stats including inferred count
SELECT pg_ripple.stats();
-- Check constraint rules for violations
SELECT pg_ripple.check_constraints();
Performance Diagnosis
If inference is slow:
- Check iteration count with
infer_with_stats()— many iterations suggest deep recursive chains. - Use goal-directed inference (
infer_goal()) if you only need a subset. - Check for redundant rules with subsumption (
eliminated_rulesin stats output). - Run
pg_ripple.vacuum()after inference to update planner statistics.
-- Get inference diagnostics
SELECT pg_ripple.infer_with_stats('rdfs');
-- Check rule plan cache
SELECT * FROM pg_ripple.rule_plan_cache_stats();
Next Steps
- §2.4 Validating Data Quality — SHACL shapes interact with inference; validate derived facts.
- §2.6 Exporting and Sharing — export inferred facts; Datalog enrichment for GraphRAG.
- §2.3 Querying with SPARQL — query both explicit and inferred facts with SPARQL.
Further reading
- Blog: Datalog Inside PostgreSQL — how the Datalog engine works under the hood
- Blog: Built-in Reasoning Rules Explained — the RDFS and OWL RL rule sets
- Blog: Magic Sets for Goal-Directed Inference — how demand-driven evaluation speeds up queries
- Blog: Well-Founded Semantics — handling negation in cyclic rules
§2.6 Exporting and Sharing
What and Why
Data in pg_ripple needs to flow out — to other systems, to files for archival, to LLMs for RAG pipelines, or to Microsoft's GraphRAG framework via Parquet files. pg_ripple supports all standard RDF serialization formats plus JSON-LD framing for API-ready output and BYOG (Bring Your Own Graph) Parquet export for GraphRAG.
This chapter is the canonical reference for all export functionality, including the GraphRAG BYOG pipeline. Other chapters cross-reference here for GraphRAG details.
How It Works
Export Formats
| Format | Function | Streaming variant | Named graph support |
|---|---|---|---|
| N-Triples | export_ntriples() | — | Per-graph or default |
| N-Quads | export_nquads() | — | Yes (all graphs) |
| Turtle | export_turtle() | export_turtle_stream() | Per-graph or default |
| JSON-LD | export_jsonld() | export_jsonld_stream() | Per-graph or default |
| JSON-LD Framed | export_jsonld_framed() | export_jsonld_framed_stream() | Per-graph or default |
| SPARQL CONSTRUCT → Turtle | sparql_construct_turtle() | — | Via query |
| SPARQL CONSTRUCT → JSON-LD | sparql_construct_jsonld() | — | Via query |
| SPARQL DESCRIBE → Turtle | sparql_describe_turtle() | — | Via query |
| SPARQL DESCRIBE → JSON-LD | sparql_describe_jsonld() | — | Via query |
| Parquet (GraphRAG entities) | export_graphrag_entities() | — | Per-graph |
| Parquet (GraphRAG relationships) | export_graphrag_relationships() | — | Per-graph |
| Parquet (GraphRAG text units) | export_graphrag_text_units() | — | Per-graph |
Streaming Exports
For large graphs, streaming exports return one row per triple (or per subject for JSON-LD), avoiding buffering the entire document in memory:
-- Stream Turtle one line at a time
SELECT * FROM pg_ripple.export_turtle_stream();
-- Stream JSON-LD one subject at a time (NDJSON)
SELECT * FROM pg_ripple.export_jsonld_stream();
JSON-LD Framing
JSON-LD framing reshapes flat RDF into nested, application-friendly JSON. A frame is a JSON template that specifies the desired structure:
- pg_ripple translates the frame to a SPARQL CONSTRUCT query.
- The CONSTRUCT query executes against the triple store.
- The W3C embedding algorithm nests matched nodes per the frame.
- The result is compacted with the frame's
@context.
Worked Examples
Exporting as N-Triples
The simplest format — one triple per line:
-- Export the default graph
SELECT pg_ripple.export_ntriples(NULL);
Output:
<https://example.org/paper/42> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://purl.org/ontology/bibo/AcademicArticle> .
<https://example.org/paper/42> <http://purl.org/dc/terms/title> "Knowledge Graphs in Practice" .
<https://example.org/paper/42> <http://purl.org/dc/terms/creator> <https://example.org/person/alice> .
Export a specific named graph:
SELECT pg_ripple.export_ntriples('https://example.org/graph/pubmed');
Exporting as N-Quads
N-Quads include the graph IRI for each triple:
-- Export all graphs (pass NULL)
SELECT pg_ripple.export_nquads(NULL);
Exporting as Turtle
Compact, human-readable output with prefix declarations:
SELECT pg_ripple.export_turtle();
Output:
@prefix ex: <https://example.org/> .
@prefix dct: <http://purl.org/dc/terms/> .
@prefix bibo: <http://purl.org/ontology/bibo/> .
ex:paper/42 a bibo:AcademicArticle ;
dct:title "Knowledge Graphs in Practice" ;
dct:creator ex:person/alice, ex:person/bob .
Exporting as JSON-LD
SELECT pg_ripple.export_jsonld();
Returns a JSONB array of expanded node objects:
[
{
"@id": "https://example.org/paper/42",
"@type": ["http://purl.org/ontology/bibo/AcademicArticle"],
"http://purl.org/dc/terms/title": [{"@value": "Knowledge Graphs in Practice"}],
"http://purl.org/dc/terms/creator": [
{"@id": "https://example.org/person/alice"},
{"@id": "https://example.org/person/bob"}
]
}
]
JSON-LD Framing
Shape the output into the exact JSON structure your application expects:
SELECT pg_ripple.export_jsonld_framed('{
"@context": {
"dct": "http://purl.org/dc/terms/",
"foaf": "http://xmlns.com/foaf/0.1/",
"bibo": "http://purl.org/ontology/bibo/",
"schema": "https://schema.org/",
"title": "dct:title",
"creator": "dct:creator",
"name": "foaf:name",
"affiliation": "schema:affiliation"
},
"@type": "bibo:AcademicArticle",
"creator": {
"name": {},
"affiliation": {
"name": {}
}
}
}'::jsonb);
Returns nested JSON-LD:
{
"@context": {"dct": "http://purl.org/dc/terms/", "...": "..."},
"@graph": [
{
"@type": "bibo:AcademicArticle",
"title": "Knowledge Graphs in Practice",
"creator": [
{
"name": "Alice Johnson",
"affiliation": {
"name": "Massachusetts Institute of Technology"
}
},
{
"name": "Bob Smith",
"affiliation": {
"name": "Stanford University"
}
}
]
}
]
}
Debugging Frames
See the generated SPARQL CONSTRUCT without executing:
SELECT pg_ripple.jsonld_frame_to_sparql('{
"@context": {
"dct": "http://purl.org/dc/terms/",
"bibo": "http://purl.org/ontology/bibo/",
"title": "dct:title"
},
"@type": "bibo:AcademicArticle",
"title": {}
}'::jsonb);
CONSTRUCT-Based Exports
Use SPARQL CONSTRUCT for selective, transformed exports:
-- Export a citation graph as Turtle
SELECT pg_ripple.sparql_construct_turtle('
PREFIX bibo: <http://purl.org/ontology/bibo/>
PREFIX dct: <http://purl.org/dc/terms/>
PREFIX ex: <https://example.org/>
CONSTRUCT {
?paper ex:cites ?cited .
?paper dct:title ?title .
?cited dct:title ?citedTitle .
}
WHERE {
?paper bibo:cites ?cited ;
dct:title ?title .
?cited dct:title ?citedTitle .
}
');
-- Same as JSON-LD for REST APIs
SELECT pg_ripple.sparql_construct_jsonld('
PREFIX bibo: <http://purl.org/ontology/bibo/>
PREFIX dct: <http://purl.org/dc/terms/>
PREFIX ex: <https://example.org/>
CONSTRUCT {
?paper ex:cites ?cited .
?paper dct:title ?title .
}
WHERE {
?paper bibo:cites ?cited ;
dct:title ?title .
}
');
DESCRIBE-Based Exports
Export everything about specific entities:
-- Full description as Turtle
SELECT pg_ripple.sparql_describe_turtle('
DESCRIBE <https://example.org/paper/42>
');
-- Symmetric CBD (includes incoming links)
SELECT pg_ripple.sparql_describe_turtle(
'DESCRIBE <https://example.org/person/alice>',
'scbd'
);
-- As JSON-LD
SELECT pg_ripple.sparql_describe_jsonld(
'DESCRIBE <https://example.org/paper/42>'
);
GraphRAG BYOG Pipeline
pg_ripple is the canonical source for Microsoft GraphRAG's Bring Your Own Graph (BYOG) data. The pipeline uses three export functions to produce Parquet files compatible with GraphRAG's ingestion format.
This is the CANONICAL GraphRAG chapter. All other documentation that mentions GraphRAG should cross-reference this section.
Step 1: Model Entities and Relationships
GraphRAG requires entities, relationships, and text units modeled with the gr: prefix.
Load the GraphRAG ontology:
SELECT pg_ripple.load_turtle('
@prefix gr: <urn:graphrag:> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix ex: <https://example.org/> .
# Entity: a paper
ex:paper/42 a gr:Entity ;
gr:title "Knowledge Graphs in Practice" ;
gr:type "AcademicArticle" ;
gr:description "A comprehensive survey of knowledge graph technologies and applications." ;
gr:frequency 5 ;
gr:degree 3 .
# Entity: an author
ex:person/alice a gr:Entity ;
gr:title "Alice Johnson" ;
gr:type "Person" ;
gr:description "Researcher at MIT specializing in knowledge representation." ;
gr:frequency 8 ;
gr:degree 5 .
# Relationship
ex:rel/1 a gr:Relationship ;
gr:source ex:paper/42 ;
gr:target ex:person/alice ;
gr:description "authored by" ;
gr:weight "1.0"^^xsd:float ;
gr:combinedDegree 8 .
# Text unit
ex:text/1 a gr:TextUnit ;
gr:text "This paper surveys knowledge graph technologies..." ;
gr:nTokens 150 ;
gr:documentId "doc-001" .
');
Step 2: Enrich with Datalog Rules
Use Datalog rules to derive additional GraphRAG metadata:
SELECT pg_ripple.load_rules('
# Derive entity frequency from triple count
?e gr:frequency ?count :-
?e rdf:type gr:Entity ,
COUNT(?t WHERE ?t ?anyPred ?e) = ?count .
# Derive relationship combined degree
?r gr:combinedDegree ?deg :-
?r rdf:type gr:Relationship ,
?r gr:source ?s ,
?r gr:target ?t ,
COUNT(?p1 WHERE ?s ?p1 ?_) = ?sDeg ,
COUNT(?p2 WHERE ?t ?p2 ?_) = ?tDeg .
', 'graphrag-enrichment');
SELECT pg_ripple.infer_agg('graphrag-enrichment');
Step 3: Validate with SHACL
Ensure data quality before export:
SELECT pg_ripple.load_shacl('
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix gr: <urn:graphrag:> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
<urn:graphrag:EntityShape> a sh:NodeShape ;
sh:targetClass gr:Entity ;
sh:property [
sh:path gr:title ;
sh:minCount 1 ;
sh:datatype xsd:string ;
] ;
sh:property [
sh:path gr:type ;
sh:minCount 1 ;
] .
<urn:graphrag:RelationshipShape> a sh:NodeShape ;
sh:targetClass gr:Relationship ;
sh:property [
sh:path gr:source ;
sh:minCount 1 ;
sh:maxCount 1 ;
] ;
sh:property [
sh:path gr:target ;
sh:minCount 1 ;
sh:maxCount 1 ;
] .
');
-- Validate before export
SELECT pg_ripple.validate();
Step 4: Export to Parquet
-- Export entities (requires superuser)
SELECT pg_ripple.export_graphrag_entities('', '/data/graphrag/entities.parquet');
-- Export relationships
SELECT pg_ripple.export_graphrag_relationships('', '/data/graphrag/relationships.parquet');
-- Export text units
SELECT pg_ripple.export_graphrag_text_units('', '/data/graphrag/text_units.parquet');
Each function returns the number of rows written. The Parquet files are directly
compatible with pyarrow.parquet.read_table() and GraphRAG's BYOG configuration:
# GraphRAG settings.yaml
entity_table_path: /data/graphrag/entities.parquet
relationship_table_path: /data/graphrag/relationships.parquet
text_unit_table_path: /data/graphrag/text_units.parquet
Step 5: Export from a Named Graph
For multi-tenant or versioned exports:
-- Export only entities from the "production" graph
SELECT pg_ripple.export_graphrag_entities(
'https://example.org/graph/production',
'/data/graphrag/prod_entities.parquet'
);
SELECT pg_ripple.export_graphrag_relationships(
'https://example.org/graph/production',
'/data/graphrag/prod_relationships.parquet'
);
SELECT pg_ripple.export_graphrag_text_units(
'https://example.org/graph/production',
'/data/graphrag/prod_text_units.parquet'
);
Common Patterns
Pattern: API Response Formatting
Use JSON-LD framing to produce API-ready responses:
-- Papers endpoint: nested JSON with authors
SELECT pg_ripple.export_jsonld_framed('{
"@context": {
"title": "http://purl.org/dc/terms/title",
"creator": "http://purl.org/dc/terms/creator",
"name": "http://xmlns.com/foaf/0.1/name",
"type": "@type"
},
"@type": "http://purl.org/ontology/bibo/AcademicArticle",
"creator": { "name": {} }
}'::jsonb);
Pattern: Scheduled Exports via CONSTRUCT Views
For continuously updated exports, create a CONSTRUCT view (requires pg_trickle):
SELECT pg_ripple.create_construct_view(
'citation_graph',
'PREFIX bibo: <http://purl.org/ontology/bibo/>
PREFIX dct: <http://purl.org/dc/terms/>
CONSTRUCT { ?p bibo:cites ?c . ?p dct:title ?t . }
WHERE { ?p bibo:cites ?c ; dct:title ?t }',
'30s',
true
);
-- The view is automatically refreshed every 30 seconds
SELECT * FROM pg_ripple.construct_view_citation_graph_decoded;
Pattern: Streaming Export to File
For large graphs, use COPY with streaming exports:
COPY (SELECT * FROM pg_ripple.export_turtle_stream())
TO '/data/export/full_graph.ttl';
COPY (SELECT * FROM pg_ripple.export_jsonld_stream())
TO '/data/export/full_graph.ndjson';
Pattern: Selective Export with SPARQL
Export only a subset of the graph:
-- Export only papers from 2024
SELECT pg_ripple.sparql_construct_turtle('
PREFIX dct: <http://purl.org/dc/terms/>
PREFIX bibo: <http://purl.org/ontology/bibo/>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
CONSTRUCT { ?paper ?p ?o }
WHERE {
?paper a bibo:AcademicArticle ;
dct:date ?date ;
?p ?o .
FILTER (?date >= "2024-01-01"^^xsd:date)
}
');
Performance and Trade-offs
Buffered vs Streaming Exports
| Mode | Memory usage | Output format | Best for |
|---|---|---|---|
Buffered (export_turtle()) | Entire graph in memory | Complete document | Small-medium graphs |
Streaming (export_turtle_stream()) | One triple at a time | Row-per-triple | Large graphs (millions of triples) |
Parquet Export Performance
GraphRAG Parquet export scans the relevant VP tables once per entity type. Performance
depends on the number of gr:Entity, gr:Relationship, and gr:TextUnit nodes:
- ~100K entities: <5 seconds
- ~1M entities: ~30 seconds
- Write path requires superuser (writes to server filesystem)
JSON-LD Framing Cost
Framing involves executing a SPARQL CONSTRUCT query, then applying the W3C embedding algorithm. The cost is dominated by the CONSTRUCT query; the embedding step is linear in the number of matched nodes.
Use jsonld_frame_to_sparql() to inspect the generated CONSTRUCT query and verify
it is efficient before calling export_jsonld_framed().
Gotchas and Debugging
Empty Parquet Files
If export_graphrag_entities() returns 0, check that your data uses the correct gr:
prefix and that entities have rdf:type gr:Entity:
SELECT * FROM pg_ripple.find_triples(
NULL,
'<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>',
'<urn:graphrag:Entity>'
);
Framing Returns Empty Result
Ensure the frame's @type matches actual rdf:type values in the store. The type
must be a full IRI, not a prefixed name:
-- Check what types exist
SELECT * FROM pg_ripple.sparql('
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
SELECT DISTINCT ?type WHERE { ?x rdf:type ?type }
');
Server-Side File Permissions
Parquet export writes to the server filesystem. Ensure the postgres OS user has write
permission to the output directory:
sudo mkdir -p /data/graphrag
sudo chown postgres:postgres /data/graphrag
Large Export Memory
For graphs with millions of triples, buffered exports (export_turtle(), export_jsonld())
may use significant memory. Switch to streaming variants or COPY ... TO with streaming.
Next Steps
- §2.7 AI Retrieval and GraphRAG — vector embeddings and RAG retrieval pipelines.
- §2.8 APIs and Integration — serve exported data via the HTTP endpoint.
- §2.3 Querying with SPARQL — CONSTRUCT and DESCRIBE queries for selective export.
Further reading
- Blog: JSON-LD Framing for Nested JSON — shaping graph data into API-ready JSON documents
Live Views and Subscriptions
Status: Live views available since v0.12.0; CDC subscriptions available since v0.42.0
See also: Live SPARQL Subscriptions · CDC Subscriptions
Two features cover the push side of pg_ripple — getting data out as it changes, instead of polling for it.
| Feature | Best for | Backed by |
|---|---|---|
| Materialized SPARQL / Datalog views | Always-fresh dashboards, denormalised tables | pg_trickle |
| CDC subscriptions | Streaming events to applications, Kafka, WebSocket clients | PostgreSQL LISTEN/NOTIFY |
If you want a snapshot of what is true now, use a view. If you want a stream of what changed since I last looked, use CDC.
Materialized SPARQL and Datalog views
A SPARQL view compiles a SPARQL SELECT (or a Datalog goal) into a pg_trickle stream table that is incrementally maintained as triples change. The view always reflects the latest data without you running the query yourself.
Requires the optional
pg_trickleextension. pg_ripple loads and runs without it; view functions detect its absence at call time and return a clear error with an install hint.
-- Check availability before using.
SELECT pg_ripple.pg_trickle_available(); -- true / false
-- Create a view of all people and their names, refreshed every second.
-- The stream table stores BIGINT dictionary IDs for IVM correctness.
SELECT pg_ripple.create_sparql_view(
name := 'people_names',
sparql := 'SELECT ?p ?name WHERE { ?p <http://xmlns.com/foaf/0.1/name> ?name }',
schedule := '1s',
decode := true -- also creates pg_ripple.people_names_decoded with TEXT columns
);
-- Query the raw BIGINT view.
SELECT * FROM pg_ripple.people_names;
-- Or use the auto-created decoded companion view for human-readable TEXT output.
SELECT * FROM pg_ripple.people_names_decoded;
-- Drop when you are done (also drops the _decoded companion view if present).
SELECT pg_ripple.drop_sparql_view('people_names');
The decode flag controls whether a _{name}_decoded companion VIEW is created on top of the stream table. The stream table itself always stores raw BIGINT dictionary IDs — this keeps pg_trickle's incremental view maintenance (IVM) working correctly, since IVM diffs rows using the integer columns of the underlying VP tables. When decode = true, a thin SQL VIEW named {name}_decoded is created alongside the stream table; it performs the dictionary lookups at read time and exposes TEXT columns. This is the same pattern used by create_construct_view.
You can build the same kind of view from a Datalog goal:
SELECT pg_ripple.create_datalog_view(
name := 'indirect_managers',
goal := '?x <https://example.org/indirectManager> ?y',
schedule := '5s'
);
These views integrate with the materialised inference pipeline — the view stays correct after infer(), retract_rule(), and clear_graph().
CDC subscriptions
A subscription publishes a JSON message on a named PostgreSQL NOTIFY channel every time a triple is inserted or deleted that matches a SPARQL filter. Listeners receive changes with no polling.
Available since v0.42.0.
Create
-- Subscribe to all changes.
SELECT pg_ripple.create_subscription('all_changes');
-- Subscribe with a SPARQL pattern filter.
SELECT pg_ripple.create_subscription(
'person_changes',
filter_sparql := 'SELECT ?s ?p ?o WHERE { ?s a <https://schema.org/Person> ; ?p ?o }'
);
-- Subscribe with a SHACL-shape filter — only triples that *violate* the shape are published.
SELECT pg_ripple.create_subscription(
'shape_violations',
filter_shape := '<https://shapes.example.org/PersonShape>'
);
Listen
LISTEN pg_ripple_cdc_person_changes;
In your application (Python, Node.js, Go, …) connect, issue the same LISTEN, and read the notification stream:
import psycopg
import json
with psycopg.connect("...") as conn:
conn.set_isolation_level(0) # AUTOCOMMIT
with conn.cursor() as cur:
cur.execute("LISTEN pg_ripple_cdc_person_changes;")
for notify in conn.notifies():
event = json.loads(notify.payload)
print(event["op"], event["s"], event["p"], event["o"])
Payload
{
"op": "add",
"s": "<https://example.org/alice>",
"p": "<https://schema.org/name>",
"o": "\"Alice\"",
"g": ""
}
| Field | Meaning |
|---|---|
op | "add" or "remove" |
s / p / o | N-Triples-formatted subject / predicate / object |
g | Named-graph IRI, or empty string for the default graph |
Manage
SELECT name, has_filter, created_at FROM pg_ripple.list_subscriptions();
SELECT pg_ripple.drop_subscription('person_changes');
WebSocket access via pg_ripple_http
When the pg_ripple_http companion service is running, every subscription is automatically exposed as a WebSocket endpoint:
ws://<host>:8080/ws/subscriptions/{name}
The HTTP service handles backpressure, reconnection, and authentication — you point a browser-side EventSource or a server-side stream consumer at the URL.
Lifecycle telemetry
The _pg_ripple.cdc_lifecycle_events table records every subscription create / drop / error, with timestamps. Useful for alerting on dropped subscriptions in production.
Choosing between views and subscriptions
| If you want… | Use |
|---|---|
| A table that always shows the latest aggregate / projection | View |
| A push notification per change to drive an external system | Subscription |
| A WebSocket stream to a browser | Subscription + pg_ripple_http |
| A denormalised cache of derived facts | Datalog view |
| To trigger Kafka / SQS / SNS messages on change | Subscription + an outbox worker |
The two compose: build a view of what is currently true, and a subscription of what just changed, and let your application choose per use case.
See also
- APIs and Integration —
pg_ripple_httpand WebSocket access. - Cookbook: CDC → Kafka via JSON-LD outbox
Further reading
- Blog: CONSTRUCT Views — Live Transformations — incremental materialization of SPARQL views
- Blog: IVM with pg-trickle Integration — how pg_ripple keeps views up to date
Geospatial Queries (GeoSPARQL)
Status: Available since v0.36.0 (GEO-01)
Requires: PostGIS extension (CREATE EXTENSION postgis). Without PostGIS, loading succeeds but geof: filter functions return NULL.
SQL: pg_ripple.sparql_select() with geof: and geo: FILTER functions
Degraded: GeoSPARQL filter functions silently return NULL when PostGIS is absent — enable PostGIS before ingesting WKT data.
pg_ripple implements the GeoSPARQL 1.1 query function vocabulary for geographic data, delegating geometry computation to PostGIS. You store WKT literals as triple objects, and SPARQL filter functions like geof:sfWithin and geof:sfIntersects resolve them against PostGIS at query time — without any extra schema work on your part.
What you get
| Capability | Function family | Notes |
|---|---|---|
| Topological filters | geof:sfWithin, geof:sfIntersects, geof:sfContains, geof:sfTouches, … | Simple Features 1.x |
| Distance | geof:distance | Returns metres for geographic CRS |
| Constructive operations | geof:buffer, geof:convexHull, geof:envelope, geof:union, geof:intersection | Returns a geometry literal |
| Accessor functions | geof:asWKT, geof:srid | Inspection |
All of these compile to PostGIS function calls in the generated SQL. You inherit PostGIS's spatial index support automatically when you register a geometry index on the relevant VP table.
Storing geometries
Geometries live as Well-Known Text (WKT) literals on a geometry predicate of your choice. The conventional predicate is locn:geometry:
SELECT pg_ripple.load_turtle($TTL$
@prefix ex: <https://example.org/> .
@prefix locn: <https://www.w3.org/ns/locn#> .
ex:berlin locn:geometry "POINT(13.404954 52.520008)" .
ex:munich locn:geometry "POINT(11.5820 48.1351)" .
ex:bavaria locn:geometry "POLYGON((9.0 47.0, 13.8 47.0, 13.8 50.6, 9.0 50.6, 9.0 47.0))" .
$TTL$);
For better performance, create a PostGIS geometry index on the VP table for locn:geometry (one-time, per predicate):
SELECT pg_ripple.create_spatial_index('<https://www.w3.org/ns/locn#geometry>');
Querying
Find every point inside a polygon
PREFIX geof: <http://www.opengis.net/def/function/geosparql/>
PREFIX locn: <https://www.w3.org/ns/locn#>
SELECT ?city WHERE {
?city locn:geometry ?g .
FILTER(geof:sfWithin(?g,
"POLYGON((9.0 47.0, 13.8 47.0, 13.8 50.6, 9.0 50.6, 9.0 47.0))"))
}
Distance-bounded nearest-neighbour
SELECT ?city ?d WHERE {
?city locn:geometry ?g .
BIND(geof:distance(?g, "POINT(11.5820 48.1351)") AS ?d)
FILTER(?d < 200000) # within 200 km of Munich
}
ORDER BY ?d
Buffer + intersect (constructive)
SELECT ?city WHERE {
?city locn:geometry ?g .
FILTER(geof:sfIntersects(?g,
geof:buffer("POINT(11.5820 48.1351)", 50000))) # 50 km around Munich
}
Coordinate reference systems
GeoSPARQL literals can carry a CRS as an IRI suffix:
ex:berlin locn:geometry
"<http://www.opengis.net/def/crs/EPSG/0/4326> POINT(13.4049 52.5200)" .
When the CRS is omitted, pg_ripple uses WGS84 (EPSG:4326) as the default. This matches the GeoSPARQL 1.1 default.
What is not implemented
- Egenhofer (
geof:eh*) and RCC8 (geof:rcc8*) topological functions are not yet wired up. - The
gml:Geometryliteral datatype is not parsed (onlygeo:wktLiteral). - DE-9IM matrix queries are not exposed.
If you need any of these, file an issue — most are a thin wrapper over the corresponding PostGIS function.
See also
Further reading
- Blog: GeoSPARQL + PostGIS Spatial Queries — combining geographic and semantic queries
Full-Text Search
Sometimes the right query against a knowledge graph is not "find the entity with this exact label" but "find any entity whose label, description, or notes mention these words". pg_ripple uses PostgreSQL's built-in full-text search machinery — tsvector, tsquery, GIN indexes — for this, exposed both as a SQL function and as a SPARQL filter.
Setup
Full-text indexing is opt-in per predicate. Tell pg_ripple which literal-valued predicates are searchable:
SELECT pg_ripple.create_fts_index(
predicate := '<http://www.w3.org/2000/01/rdf-schema#label>',
config := 'english'
);
SELECT pg_ripple.create_fts_index('<http://purl.org/dc/elements/1.1/title>', 'english');
SELECT pg_ripple.create_fts_index('<https://schema.org/description>', 'english');
Behind the scenes pg_ripple maintains a generated tsvector column on the relevant VP table and a GIN index over it. Inserts and updates flow through automatically.
The config parameter is any PostgreSQL text-search configuration name (english, simple, spanish, …). Use simple for languages whose stemmer you do not have, or for proper-noun-heavy data.
Searching from SQL
-- All subjects whose label matches the query.
SELECT * FROM pg_ripple.fts_search(
predicate := '<http://www.w3.org/2000/01/rdf-schema#label>',
query := 'machine & learning'
);
The query argument follows PostgreSQL tsquery syntax: & for AND, | for OR, ! for NOT, <-> for adjacency. See the PostgreSQL FTS documentation.
Searching from SPARQL
The pg:fts() SPARQL filter function returns true when an entity's literal value matches the tsquery. It composes naturally with other graph patterns:
PREFIX pg: <http://pg-ripple.io/fn/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX schema:<https://schema.org/>
SELECT ?paper ?title WHERE {
?paper a <https://example.org/ScholarlyArticle> ;
schema:author <https://example.org/alice> ;
rdfs:label ?title .
FILTER(pg:fts(?title, "graph & neural & networks"))
}
pg:fts() only fires when the matched literal lives on a predicate that has been indexed. Otherwise it returns false (and emits a debug-level log entry).
Ranking
For ranked results, pg_ripple.fts_search_ranked() returns the FTS rank score per row:
SELECT subject, rank
FROM pg_ripple.fts_search_ranked(
predicate := '<https://schema.org/description>',
query := 'sustainable & supply & chain'
)
ORDER BY rank DESC
LIMIT 25;
The ranking is ts_rank_cd with default normalisation. To override the normalisation, use fts_search_ranked(predicate, query, normalisation := 32).
Combining FTS and vector search
FTS catches exact lexical matches; vector search catches paraphrase. Combining them improves recall:
SELECT entity_iri, fused_score
FROM pg_ripple.hybrid_search(
sparql := 'SELECT ?p WHERE { ?p a <https://example.org/Paper> .
FILTER(pg:fts(?p, "neural & networks")) }',
text := 'deep learning architectures',
k := 25,
alpha := 0.4
);
This pattern — FTS for must-include keywords, vector for semantic broadening — is one of the most useful tricks in RAG pipelines.
When not to use FTS
- The data is structured with controlled vocabularies (
skos:Concepttaxonomies, code lists) — use the exact SPARQL pattern instead. - Your dataset is small (< 10 K labels). Plain
LIKE '%keyword%'is fine. - You only ever query a single language. PostgreSQL's
simpleconfig is faster for that case than the language-aware ones.
See also
- Vector & Hybrid Search — the semantic counterpart to FTS.
- PostgreSQL Full-Text Search documentation
Temporal Queries and Provenance
Two of the most-asked compliance questions in any data system are:
- "What did the graph look like at 03:14 last Tuesday?" — a temporal question.
- "Where did this fact come from, and who introduced it?" — a provenance question.
pg_ripple answers both with first-class features that need no schema changes and no extra storage on the hot path.
| Capability | Function / GUC | Page section |
|---|---|---|
| Replay the graph as of a past timestamp | pg_ripple.point_in_time(ts) | Temporal queries |
Record a prov:Activity per bulk-load | pg_ripple.prov_enabled = on | PROV-O provenance |
| Capture every SPARQL UPDATE | pg_ripple.audit_log_enabled = on | Audit log |
| Per-fact confidence and source | RDF-star quoted triples | Storing Knowledge → RDF-star |
The four are designed to compose. A regulator-defensible audit trail typically uses all four.
Temporal queries
Every triple in pg_ripple carries a globally-unique statement ID (i BIGINT) drawn from a shared sequence. The sequence value monotonically increases with insertion time, so it acts as a logical timestamp. The _pg_ripple.statement_id_timeline table maps wall-clock timestamps to SID ranges.
point_in_time(ts) sets a session GUC that restricts every subsequent SPARQL and Datalog query to the SID range that existed at ts.
-- See the graph as it stood last Tuesday at 03:14.
SELECT pg_ripple.point_in_time('2026-04-21 03:14:00+00'::timestamptz);
-- All queries in this session are now scoped to that point in time.
SELECT * FROM pg_ripple.sparql('
SELECT ?paper ?title WHERE {
?paper <http://purl.org/dc/elements/1.1/title> ?title
}
');
-- Reset to "now".
SELECT pg_ripple.point_in_time(NULL);
What this is good for
- Audits: prove what your application would have shown a user at the time of an event.
- Reproducible analytics: re-run a report against the exact data the original report saw.
- Debugging: bisect a quality regression by walking back through time.
- Historical change-detection: combine with CDC subscriptions to see what changed between two points in time.
What this is not
- It is not a full bitemporal store. You cannot query "the version of fact X that was believed in March but valid for January". For that you need RDF-star plus your own valid-time predicate.
- It is not safe across
VACUUM FULLof_pg_ripple.statement_id_timeline. RoutineVACUUMis fine.
PROV-O provenance
PROV-O is the W3C standard for describing the origin of data. Set one GUC and every bulk-load operation is automatically annotated with prov:Activity and prov:Entity triples.
SET pg_ripple.prov_enabled = on;
-- Every load is recorded.
SELECT pg_ripple.load_turtle_file('/data/products.ttl');
SELECT pg_ripple.load_nquads_file('/data/customers.nq');
-- Inspect the recorded activities.
SELECT * FROM pg_ripple.prov_stats();
A typical recorded activity looks like:
_:act_42 a prov:Activity ;
prov:startedAtTime "2026-04-27T10:00:00Z"^^xsd:dateTime ;
prov:endedAtTime "2026-04-27T10:00:14Z"^^xsd:dateTime ;
prov:wasAssociatedWith <urn:postgres-role:loader_service> ;
pg:loadFunction "load_turtle_file" ;
pg:sourceFile "/data/products.ttl" ;
pg:tripleCount 128432 .
You can query provenance with SPARQL like any other data. PROV-O integrates cleanly with point_in_time() — "as of the close of business yesterday, which loader had touched this graph?" is a single query.
Audit log
While PROV-O captures bulk loads, the audit log captures every SPARQL UPDATE — INSERT DATA, DELETE DATA, INSERT { … } WHERE, MOVE, COPY, LOAD, etc. Set the GUC and entries land in _pg_ripple.audit_log:
SET pg_ripple.audit_log_enabled = on;
-- Every UPDATE is captured.
SELECT pg_ripple.sparql_update('
INSERT DATA { <https://example.org/x> <https://example.org/y> "z" }
');
-- Inspect.
SELECT ts, role, txid, operation, query
FROM _pg_ripple.audit_log
ORDER BY ts DESC
LIMIT 20;
-- Cleanup (e.g. nightly).
SELECT pg_ripple.purge_audit_log(before := now() - interval '90 days');
The audit log is a PostgreSQL table — you can ship it to your SIEM, partition it, or replicate it like any other table.
Per-fact provenance with RDF-star
For granular, per-triple provenance (rather than per-load or per-update), use RDF-star quoted triples. See Storing Knowledge → RDF-star for the syntax. The most common pattern:
<< <:alice> <:knows> <:bob> >>
:assertedBy <:dataset/foaf2024> ;
:confidence "0.95"^^xsd:decimal ;
:timestamp "2026-04-27"^^xsd:date .
These quoted triples are queryable with the same SPARQL patterns as ordinary triples.
Putting them together — a worked example
A regulator asks: "On 21 March, did your system tell the user that drug A interacts with drug B? If so, on what evidence?"
-- 1. Replay the state of the graph.
SELECT pg_ripple.point_in_time('2026-03-21 12:00:00+00');
-- 2. Re-ask the question.
SELECT * FROM pg_ripple.sparql('
ASK { <https://example.org/drugA> <https://example.org/interactsWith> <https://example.org/drugB> }
');
-- → true
-- 3. Find the evidence (RDF-star + PROV-O).
SELECT * FROM pg_ripple.sparql('
SELECT ?source ?confidence ?activity WHERE {
<< <https://example.org/drugA> <https://example.org/interactsWith> <https://example.org/drugB> >>
<https://example.org/source> ?source ;
<https://example.org/confidence> ?confidence .
?activity <http://www.w3.org/ns/prov#generated> ?source .
}
');
-- 4. Find the operator who loaded it.
SELECT * FROM pg_ripple.sparql('
SELECT ?role WHERE {
?activity <http://www.w3.org/ns/prov#wasAssociatedWith> ?role
}
');
That four-line chain is the kind of evidence a regulated industry needs and a black-box system cannot produce.
Performance and storage notes
point_in_time()is read-only and zero-cost on the write path.- The
_pg_ripple.statement_id_timelinetable is a small append-only log: ~24 bytes per timestamp boundary. A 24/7 store accumulates a few KB per day. prov_enabledadds ~5–10 triples per bulk load. Negligible for any non-trivial load.audit_log_enabledwrites one row per UPDATE statement. For OLTP-heavy workloads consider partitioning the table monthly.- All three features are off by default. Enable them per database according to your compliance posture.
See also
- CDC Subscriptions — push changes in real time, complementary to point-in-time replay.
- Multi-Tenant Graphs — pair the audit log with RLS to attribute every change to a tenant role.
- Cookbook: Audit trail with PROV-O and temporal queries
Further reading
- Blog: Temporal Time-Travel Queries — point-in-time replay of your knowledge graph
- Blog: Provenance Tracking with PROV-O — tracing where every fact came from
Multi-Tenant Graphs
Multi-tenant graph storage is one of the trickiest patterns in any database. You have one logical product but many customers, each of whose data must be invisible to the others, bounded in size, and measurable for billing and capacity planning. pg_ripple solves this with three composable building blocks:
| Building block | What it gives you |
|---|---|
| Named graphs | Logical partitioning — each tenant lives in their own graph IRI |
| Row-level security on graphs | A tenant role sees only their own graph(s); enforced by PostgreSQL itself |
| Tenant quotas | A configurable triple-count cap per tenant, enforced on insert |
Together these give you true tenant isolation backed by PostgreSQL's permission system — no application-level filtering, no risk of an SQL injection bypassing the wall.
When to use this
- A SaaS product where every customer has their own knowledge graph.
- A research platform where each project is sandboxed.
- A multi-team data platform where you want to share an instance without sharing data.
If you only need labelled partitioning without isolation (e.g. "this graph is from PubMed, this one from Crossref"), plain named graphs are enough. The features on this page are about enforced isolation.
The model
┌──────────────────────────────────────────────────────────────┐
│ pg_ripple instance │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ graph A │ │ graph B │ │ graph C │ │
│ │ (acme.com) │ │ (globex) │ │ (shared) │ │
│ │ 500 K trip. │ │ 120 K trip. │ │ 20 K trip. │ │
│ │ quota: 1M │ │ quota: 250K │ │ quota: ∞ │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ └────► role tenant_acme ◄─────────┤ │
│ role tenant_globex ◄────────┤ │
│ │ │
└──────────────────────────────────────────────┴──────────────┘
│
everyone reads "shared"
- A graph IRI is the unit of isolation: triples are tagged with their graph at insert time.
- A role is the unit of access control: PostgreSQL roles own grants on graphs.
- A tenant is the unit of quota: every named graph that has been registered as a tenant carries a triple-count cap.
Step 1 — Register tenants
create_tenant() registers a named graph as a tenant and assigns it a triple-count quota. After registration, every insert into that graph is checked against the quota; an over-quota insert raises PT530.
SELECT pg_ripple.create_tenant(
graph_iri := 'https://example.org/tenants/acme',
triple_quota := 1_000_000
);
SELECT pg_ripple.create_tenant(
graph_iri := 'https://example.org/tenants/globex',
triple_quota := 250_000
);
You can list current usage at any time:
SELECT graph_iri, triple_count, triple_quota,
round(100.0 * triple_count / triple_quota, 1) AS pct_used
FROM pg_ripple.tenant_stats()
ORDER BY pct_used DESC;
The view is built on a trigger-maintained counter, so it is O(1) — safe to call from a billing job.
Step 2 — Grant graphs to roles
grant_graph(role, graph_iri) is the analog of GRANT … ON … TO … for graphs. It registers a per-role visibility rule that pg_ripple enforces in every SPARQL query.
CREATE ROLE tenant_acme LOGIN PASSWORD 'pw_a';
CREATE ROLE tenant_globex LOGIN PASSWORD 'pw_b';
GRANT USAGE ON SCHEMA pg_ripple, _pg_ripple TO tenant_acme, tenant_globex;
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA pg_ripple TO tenant_acme, tenant_globex;
SELECT pg_ripple.grant_graph('tenant_acme', 'https://example.org/tenants/acme');
SELECT pg_ripple.grant_graph('tenant_globex', 'https://example.org/tenants/globex');
-- Both tenants share access to a public reference graph.
SELECT pg_ripple.grant_graph('tenant_acme', 'https://example.org/shared');
SELECT pg_ripple.grant_graph('tenant_globex', 'https://example.org/shared');
A SPARQL query run by tenant_acme now sees only triples in acme and shared. There is no application-level filter to forget; the rule is enforced at the storage layer.
Step 3 — (Optional) attribute every change
Pair tenants with the audit log for billing-grade attribution:
SET pg_ripple.audit_log_enabled = on;
-- Per-tenant write volume in the last 24h:
SELECT role, count(*) AS updates
FROM _pg_ripple.audit_log
WHERE ts > now() - interval '24 hours'
GROUP BY role
ORDER BY updates DESC;
For per-tenant read volume, capture pg_stat_statements data; pg_ripple integrates with it transparently.
Operational concerns
Quota enforcement
The trigger that enforces quotas runs in the same transaction as the insert. If a bulk-load would exceed the quota, the entire load is rolled back — there is no partial commit. Plan your loaders accordingly: chunk loads if you expect to flirt with the quota, or pre-check via tenant_stats().
Eviction
Quotas are caps, not LRU. pg_ripple does not automatically evict old triples when a tenant fills up. To shrink a tenant: delete triples (or call clear_graph()) and the counter updates immediately.
Renaming or splitting a tenant
rename_tenant(old_iri, new_iri) updates the registration and re-tags every triple in a single transaction. Use it sparingly — it touches every triple in the graph.
Backup and restore
Tenants are pure PostgreSQL objects. pg_dump --schema=pg_ripple --schema=_pg_ripple captures everything. To export a single tenant for legal-hold or migration:
COPY (SELECT * FROM pg_ripple.export_quads_for_graph('https://example.org/tenants/acme'))
TO '/tmp/acme.nq';
Failure modes and pitfalls
- Forgetting to grant the shared graph. A tenant without read access to your reference vocabularies will see types as opaque IRIs. Always grant
https://example.org/shared(or your equivalent) to every tenant. - Granting the default graph. The default graph (graph ID
0) is not tenant-scoped. Do not put tenant data into the default graph; it will be visible to everyone. - Using a single role for all tenants. Quota and RLS attach to the role. Sharing a role across tenants defeats both.
- Pre-emptive quotas vs reactive quotas. PT530 is raised before commit, not afterwards. Long-running bulk loads should split into batches and check
tenant_stats()after each batch.
See also
- Operations → Security — the deeper PostgreSQL RLS background.
- Temporal & Provenance — audit-log capture for tenant attribution.
- Cookbook: Audit trail with PROV-O and temporal queries
Further reading
- Blog: Multi-Tenant Knowledge Graphs — isolation, quotas, and RLS patterns for SaaS deployments
OWL 2 Profiles — RL, EL, QL
OWL 2 is too expressive to evaluate in full inside a database — full OWL is undecidable for some queries. The W3C therefore standardised three OWL 2 profiles that trade expressiveness for tractability. pg_ripple ships built-in rule sets and query rewriters for all three.
| Profile | Best for | Tractability | Loaded with |
|---|---|---|---|
| OWL 2 RL | General-purpose enterprise reasoning, RDFS-on-steroids | Polynomial-time materialisation | load_rules_builtin('owl-rl') |
| OWL 2 EL | Large terminological hierarchies (medical ontologies, SNOMED) | Polynomial-time classification | load_rules_builtin('owl-el') |
| OWL 2 QL | Read-heavy access over an ontology, query rewriting | Sub-polynomial query answering | load_rules_builtin('owl-ql') |
A single GUC selects the active profile for new connections:
ALTER SYSTEM SET pg_ripple.owl_profile = 'rl'; -- 'rl' | 'el' | 'ql' | 'off'
SELECT pg_reload_conf();
Setting owl_profile = 'off' disables ontology rewriting — useful when you want to inspect raw triples without inference smoothing.
OWL 2 RL — the workhorse
OWL 2 RL is the profile most users want. It covers:
- Class hierarchy:
rdfs:subClassOf,owl:equivalentClass,owl:disjointWith - Property hierarchy:
rdfs:subPropertyOf,owl:equivalentProperty,owl:propertyChainAxiom - Property characteristics:
owl:TransitiveProperty,owl:SymmetricProperty,owl:InverseOf,owl:FunctionalProperty,owl:InverseFunctionalProperty - Class constructors:
owl:unionOf,owl:intersectionOf,owl:hasValue,owl:someValuesFrom(in restricted positions) owl:sameAsandowl:differentFrom
Run it with:
SELECT pg_ripple.load_rules_builtin('owl-rl');
SELECT pg_ripple.infer('owl-rl');
Performance: the OWL 2 RL rule set has ~80 rules. On a 10 M-triple graph with a typical 1:1 T-Box / A-Box ratio, a full materialisation takes seconds with parallel stratum evaluation enabled.
pg_ripple is 100 % conformant with the W3C OWL 2 RL test suite — see OWL 2 RL Conformance Results.
OWL 2 EL — for large terminologies
EL was designed for ontologies with very large class hierarchies and few individuals — medical terminologies (SNOMED, NCIt), gene ontologies, product taxonomies. It supports:
- Class subsumption with existential restrictions:
owl:someValuesFrom - Property chains: e.g.
partOf ∘ partOf ⊑ partOf owl:hasSelf- Reflexive properties
EL classification (computing the full subclass hierarchy) is polynomial in the size of the ontology. pg_ripple's EL implementation uses a saturation-based algorithm and stores the closure in _pg_ripple.el_classified.
SELECT pg_ripple.load_rules_builtin('owl-el');
SELECT pg_ripple.infer('owl-el');
-- All classes that subsume :BacterialPneumonia.
SELECT * FROM pg_ripple.sparql('
SELECT ?super WHERE { <https://example.org/BacterialPneumonia>
<http://www.w3.org/2000/01/rdf-schema#subClassOf>+ ?super }
');
OWL 2 QL — for query rewriting
QL is the profile of choice when you want to answer SPARQL queries over an ontology without materialising inferences first. Instead of expanding the data, pg_ripple rewrites the query at translation time using the ontology axioms. This keeps the data store small and lets you change the ontology without re-materialising.
QL supports a deliberately small set of constructs — rdfs:subClassOf, rdfs:subPropertyOf, owl:inverseOf, owl:someValuesFrom (in object position only), owl:disjointWith. The trade-off is that everything is fast.
SELECT pg_ripple.load_rules_builtin('owl-ql');
SET pg_ripple.owl_profile = 'ql';
-- This SELECT is rewritten using subClassOf axioms before execution.
SELECT * FROM pg_ripple.sparql('
SELECT ?animal WHERE { ?animal a <https://example.org/Mammal> }
');
If Dog rdfs:subClassOf Mammal, the rewriter expands Mammal into (Mammal | Dog | Cat | …) automatically — without inserting any new triples.
SPARQL-DL — direct OWL axiom queries
When you want to query the T-Box itself (the ontology, not the data), pg_ripple exposes two SPARQL-DL helpers:
-- Direct subclasses of :Mammal.
SELECT * FROM pg_ripple.sparql_dl_subclasses('<https://example.org/Mammal>');
-- All superclasses of :Dog.
SELECT * FROM pg_ripple.sparql_dl_superclasses('<https://example.org/Dog>');
These route OWL vocabulary BGPs (owl:subClassOf, owl:equivalentClass, owl:disjointWith) directly to T-Box VP tables — no separate index required.
Choosing a profile
| Question | Profile |
|---|---|
| I just want SPARQL queries to "see" RDFS+ inference | RL (default) |
| I have a million-class taxonomy and need fast classification | EL |
| I have an ontology and a tiny data store, and want to skip materialisation | QL |
| I'm not sure | RL |
You can also load multiple profiles' rule sets at once and run them under different rule-set names — they are independent.
See also
- Reasoning & Inference — the Datalog engine that powers all three profiles.
- OWL 2 RL Conformance Results
- SPARQL Compliance Matrix
SHACL-SPARQL Rules and Constraints
Stock SHACL Core covers ~95 % of the constraints anyone needs in practice — cardinalities, datatypes, value ranges, property paths. The other 5 % is where SHACL Core runs out of expressiveness: cross-shape conditions, complex logical compositions, "this attribute must equal the sum of those attributes", and so on. SHACL-SPARQL (Advanced Features) closes the gap by letting you embed a SPARQL query inside a shape.
pg_ripple supports sh:SPARQLConstraint (validation), sh:TripleRule (inference), and sh:SPARQLRule (SPARQL CONSTRUCT-based inference, added in v0.79.0).
sh:SPARQLConstraint — custom validation
A sh:SPARQLConstraint runs an ASK or SELECT query for every focus node. If the query returns true (for ASK) or any rows (for SELECT), pg_ripple records a violation.
The classic example is "a person's birth date must be earlier than their death date" — not expressible in pure SHACL Core because it requires comparing two properties of the same node:
SELECT pg_ripple.load_shacl($TTL$
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <https://example.org/> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
ex:LifeSpanShape a sh:NodeShape ;
sh:targetClass foaf:Person ;
sh:sparql [
sh:message "Birth date must be earlier than death date" ;
sh:select """
SELECT $this WHERE {
$this <https://example.org/birthDate> ?b ;
<https://example.org/deathDate> ?d .
FILTER(?d <= ?b)
}
""" ;
] .
$TTL$);
-- Validate the whole store.
SELECT focus_node, message FROM pg_ripple.shacl_validate();
Inside the query, the special variable $this is bound to the focus node. The query is evaluated by the same SPARQL engine you would use for any other query, so anything you can write in SPARQL — property paths, FILTER, BIND, sub-SELECT — is fair game inside a constraint.
sh:TripleRule — inference from shapes
A sh:TripleRule adds triples to the store for every focus node that matches the shape. It is the recommended SHACL-AF inference primitive in pg_ripple because it compiles directly to a Datalog rule.
SELECT pg_ripple.load_shacl($TTL$
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <https://example.org/> .
ex:AdultRule a sh:NodeShape ;
sh:targetClass <https://schema.org/Person> ;
sh:rule [
a sh:TripleRule ;
sh:subject sh:this ;
sh:predicate ex:isAdult ;
sh:object "true"^^<http://www.w3.org/2001/XMLSchema#boolean> ;
] .
$TTL$);
-- Apply the rule.
SELECT pg_ripple.shacl_apply_rules();
Triples produced by sh:TripleRule are written with source = 1 (inferred) — they coexist with explicit triples but stay distinguishable.
sh:SPARQLRule — inference from validation shapes
A sh:SPARQLRule runs a CONSTRUCT query whose graph pattern is evaluated for every focus node, and the constructed triples are added to the store. This is essentially "Datalog spelled in SPARQL" — useful when your validation already lives in SHACL and you want to derive new facts on the same data without writing a separate Datalog rule set.
SELECT pg_ripple.load_shacl($TTL$
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <https://example.org/> .
ex:AdultRule a sh:NodeShape ;
sh:targetClass <https://schema.org/Person> ;
sh:rule [
a sh:SPARQLRule ;
sh:construct """
CONSTRUCT { $this a ex:Adult }
WHERE { $this <https://example.org/age> ?age .
FILTER(?age >= 18) }
""" ;
] .
$TTL$);
-- Apply the rule.
SELECT pg_ripple.shacl_apply_rules();
Triples constructed by sh:SPARQLRule are written with source = 1 (inferred) — they coexist with explicit triples but stay distinguishable.
When to use SHACL-SPARQL vs Datalog
Both can express custom validation and inference. Pick by audience:
| Concern | SHACL-SPARQL | Datalog |
|---|---|---|
| Audience | Data architects who already write SHACL | Engineers comfortable with logic programming |
| Tooling | Standard SHACL editors and validators | pg_ripple-specific .pl-style files |
| Expressiveness | Full SPARQL inside the shape | Recursion, magic sets, lattices, well-founded semantics |
| Performance | Each query runs once per focus node | Compiled to a single SQL INSERT … SELECT per stratum |
| Recursion | Limited — you can recurse manually with property paths | First-class — semi-naive evaluation, fixpoint |
| Negation | SPARQL FILTER NOT EXISTS | Stratified negation; well-founded semantics |
Rule of thumb: if the rule fits in one SHACL shape, write it in SHACL-SPARQL. If it is naturally recursive or needs negation, use Datalog.
Performance notes
sh:SPARQLConstraintis evaluated per focus node. For shapes whose target matches millions of nodes, pre-filter the target with a tightersh:targetClassorsh:targetSubjectsOf.sh:SPARQLRuleis reapplied on everyshacl_apply_rules()call. It is not incremental. For incremental inference, use the Datalog engine.- Both run in a single transaction; either everything succeeds or nothing changes.
See also
- Validating Data Quality — SHACL Core constraints.
- Reasoning & Inference — the Datalog alternative.
- SHACL Advanced Features (W3C)
R2RML — Map Relational Tables to RDF
Most enterprises already have their data in relational tables. Re-modelling that data as triples by hand is tedious and error-prone. R2RML (W3C Recommendation) is the standard mapping language that says, declaratively: "this column becomes this predicate, this row becomes this subject IRI". pg_ripple ships an R2RML executor that runs the mapping inside the same database — no ETL pipeline required.
Available since v0.55.0 via
pg_ripple.r2rml_load(mapping_ttl).
Why R2RML?
| Without R2RML | With R2RML |
|---|---|
| Bespoke loader scripts per table | One mapping document, version-controlled |
| Mapping logic spread across application code | Mapping logic is the schema, in standard syntax |
| Re-running requires a full export | Re-runs incrementally; only changed rows produce new triples |
| Ontology drift is invisible | Ontology drift surfaces as SHACL violations |
| Hard to reason about what gets exported | The R2RML document is the contract |
If you already have an RDF triple store and are migrating from PostgreSQL, R2RML lets you avoid touching the source schema. If you are growing an RDF graph alongside a relational schema, it lets the two stay in sync with one declarative artefact.
A worked example
Suppose you have a tiny relational schema:
CREATE TABLE customer (
id SERIAL PRIMARY KEY,
full_name TEXT NOT NULL,
email TEXT,
country CHAR(2)
);
CREATE TABLE purchase (
id SERIAL PRIMARY KEY,
customer_id INT REFERENCES customer(id),
sku TEXT,
amount_cents INT
);
INSERT INTO customer (full_name, email, country) VALUES
('Alice Chen', 'alice@example.org', 'US'),
('Bob Smith', 'bob@example.org', 'GB');
INSERT INTO purchase (customer_id, sku, amount_cents) VALUES
(1, 'WIDGET-1', 1999),
(2, 'WIDGET-2', 2999);
The R2RML mapping below turns each row into a triple cluster.
SELECT pg_ripple.r2rml_load($TTL$
@prefix rr: <http://www.w3.org/ns/r2rml#> .
@prefix rml: <http://semweb.mmlab.be/ns/rml#> .
@prefix ex: <https://example.org/> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix schema:<https://schema.org/> .
# Map customer rows to ex:customer/{id}
<#CustomerMap>
rr:logicalTable [ rr:tableName "customer" ] ;
rr:subjectMap [ rr:template "https://example.org/customer/{id}" ;
rr:class foaf:Person ] ;
rr:predicateObjectMap [ rr:predicate foaf:name ; rr:objectMap [ rr:column "full_name" ] ] ;
rr:predicateObjectMap [ rr:predicate foaf:mbox ; rr:objectMap [ rr:template "mailto:{email}" ;
rr:termType rr:IRI ] ] ;
rr:predicateObjectMap [ rr:predicate schema:addressCountry ; rr:objectMap [ rr:column "country" ] ] .
# Map purchase rows to ex:purchase/{id}, with a foreign-key reference to the customer
<#PurchaseMap>
rr:logicalTable [ rr:tableName "purchase" ] ;
rr:subjectMap [ rr:template "https://example.org/purchase/{id}" ;
rr:class schema:Order ] ;
rr:predicateObjectMap [ rr:predicate schema:customer ;
rr:objectMap [ rr:template "https://example.org/customer/{customer_id}" ;
rr:termType rr:IRI ] ] ;
rr:predicateObjectMap [ rr:predicate schema:sku ; rr:objectMap [ rr:column "sku" ] ] ;
rr:predicateObjectMap [ rr:predicate schema:price ; rr:objectMap [ rr:column "amount_cents" ;
rr:datatype <http://www.w3.org/2001/XMLSchema#integer> ] ] .
$TTL$);
-- Returns the count of triples produced.
After the call, your knowledge graph contains:
<https://example.org/customer/1>
a foaf:Person ;
foaf:name "Alice Chen" ;
foaf:mbox <mailto:alice@example.org> ;
schema:addressCountry "US" .
<https://example.org/purchase/1>
a schema:Order ;
schema:customer <https://example.org/customer/1> ;
schema:sku "WIDGET-1" ;
schema:price 1999 .
…and SPARQL queries work immediately:
SELECT * FROM pg_ripple.sparql($$
PREFIX schema: <https://schema.org/>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
SELECT ?name ?sku WHERE {
?order schema:customer ?c ; schema:sku ?sku .
?c foaf:name ?name .
}
$$);
What pg_ripple's R2RML supports
| R2RML feature | Status |
|---|---|
Subject maps with rr:template, rr:column, rr:constant | ✅ |
Predicate-object maps with rr:column, rr:template, rr:datatype, rr:language | ✅ |
rr:class shortcut on subject maps | ✅ |
rr:logicalTable with rr:tableName or rr:sqlQuery (R2RML view) | ✅ |
rr:joinCondition between two triples maps | ✅ |
rr:graphMap (assign triples to a named graph) | ✅ |
rr:termType (rr:IRI, rr:Literal, rr:BlankNode) | ✅ |
| RML extensions for non-SQL sources | ❌ — use a separate ETL step |
Patterns and recipes
Per-row provenance via a graph map
Route every triple from a table into its own named graph for downstream tenant isolation or audit:
<#CustomerMap>
rr:graphMap [ rr:template "https://example.org/source/customer-table" ] ;
...
Soft-delete handling
Restrict what gets exported with rr:sqlQuery:
<#ActiveCustomersMap>
rr:logicalTable [ rr:sqlQuery "SELECT * FROM customer WHERE deleted_at IS NULL" ] ;
...
Re-running incrementally
R2RML is idempotent: running it twice produces the same triples (dictionary IDs are deterministic from XXH3-128 hashes, so no duplicates accumulate). Schedule it as a cron job that runs after your relational ETL.
Validate the result with SHACL
Pair every R2RML mapping with a SHACL shape that encodes the intended shape of the output. The shape catches mapping bugs and source-data drift in a single check:
SELECT pg_ripple.shacl_validate();
Combining with FDW
rr:tableName accepts any table — including a foreign table provided by postgres_fdw. This lets you map a remote relational database into the local triple store without copying data.
When not to use R2RML
- The source data is already RDF (use
load_turtle()instead). - The mapping is one-shot and you will never re-run it (a hand-crafted INSERT is faster to write).
- You need bidirectional sync (R2RML is one-way: relational → RDF).
See also
- Loading Data — for direct RDF formats.
- Validating Data Quality — pair every R2RML run with SHACL.
- Cookbook: Knowledge graph from a relational catalogue
Further reading
- Blog: R2RML — Relational to Graph — mapping existing PostgreSQL tables to RDF
Lattice Datalog — When and Why
The Lattice Datalog reference documents every function and GUC. This page answers a different question: should you reach for a lattice at all? Most users never need one. This page helps you decide.
The short answer
Use a lattice when you need to propagate a value along a graph edge — not just whether a node is reachable, but how much something is worth at that node — and the propagation is recursive.
If your rules are not recursive, standard Datalog aggregates (COUNT, SUM, AVG over strata) are simpler.
A concrete intuition
Standard Datalog asks: can you get from A to B?
Lattice Datalog asks: what is the best way to get from A to B?
The same graph, two different questions:
A ──0.9──► B ──0.85──► C ──0.95──► D
| Question | Answer | Tool |
|---|---|---|
| Is D reachable from A? | yes | Standard Datalog |
| What is the maximum single-hop weight on any path to D? | 0.95 | Standard Datalog + MAX aggregate (non-recursive) |
| What is the minimum weight along the best path from A to D? | 0.85 (the bottleneck) | Lattice Datalog with min |
| What is the product of weights along the best path from A to D? | 0.726 | Custom lattice |
| Which intermediate nodes does every path from A to D pass through? | {B, C} | SetLattice |
The moment "best path" is recursive — you don't know in advance which direction is best — you need a lattice.
Choosing the right built-in lattice
min — weakest-link reasoning
The strength of a chain is the strength of its weakest link.
Use min when:
- Propagating trust / confidence through a network (the result is only as trustworthy as the least-trusted step).
- Computing shortest path where the path cost is the maximum edge weight.
- Finding bottleneck capacity in a flow network (the flow is limited by the narrowest pipe).
-- Load rules (trust propagates, bottlenecked by the weakest hop).
SELECT pg_ripple.load_rules($RULES$
?x ex:trusts ?y :- ?x ex:directlyTrusts ?y .
?x ex:trusts ?z :- ?x ex:directlyTrusts ?y, ?y ex:trusts ?z .
@lattice ex:trusts confidence min .
$RULES$, 'trust');
SELECT pg_ripple.infer_lattice('trust', 'min');
max — best-case reasoning
Use max when:
- Propagating reputation / endorsement scores where having one highly-rated connection is enough.
- Finding the longest path weight in a DAG.
- Any "optimistic" or "best evidence wins" scenario.
@lattice ex:endorses score max .
set — provenance and multi-valued reasoning
Use set when:
- You need to track which source triples justify a derived fact (provenance semiring).
- You need the union of all witnesses along all derivation paths, not just one.
- Each node collects contributions from multiple parents and you need all of them.
-- Collect the set of all papers that support a hypothesis.
@lattice ex:supports evidence set .
Note: set-lattice results can be large. Consider a maximum set size or a bloom-filter approximation for large graphs.
interval — when truth has a time range
Use interval when reasoning about temporal overlap: "A and B are both true during the period when both of their valid intervals overlap".
-- Derived fact is valid only during the intersection of the body's intervals.
@lattice ex:validDuring interval interval .
Picking between min and a custom multiplicative lattice
The difference matters when the graph has many hops:
| Hops | min result | Multiplicative result |
|---|---|---|
| A→B (0.9) | 0.9 | 0.9 |
| A→B→C (×0.85) | 0.85 | 0.765 |
| A→B→C→D (×0.95) | 0.85 | 0.726 |
min: "how reliable is my weakest source?" — appropriate for trust chains where one bad link invalidates everything.- Multiplicative: "what is the combined probability, assuming independence?" — appropriate for Bayesian-style confidence propagation.
For the multiplicative case, register a custom aggregate:
-- Step 1: create the multiplicative aggregate.
CREATE OR REPLACE FUNCTION prob_mul(state FLOAT8, val FLOAT8)
RETURNS FLOAT8 LANGUAGE sql IMMUTABLE AS $$ SELECT COALESCE(state, 1.0) * val $$;
CREATE AGGREGATE prob_product(FLOAT8) (SFUNC = prob_mul, STYPE = FLOAT8, INITCOND = '1.0');
-- Step 2: register the lattice.
SELECT pg_ripple.create_lattice('probability', 'prob_product', '0.0');
-- Step 3: run inference.
SELECT pg_ripple.infer_lattice('my_rules', 'probability');
Do I really need a lattice?
Before reaching for a lattice, check whether a simpler approach works:
| Scenario | Simpler alternative |
|---|---|
| Count edges reachable | Standard WITH RECURSIVE in SQL |
| Sum weights along a fixed-depth path | SPARQL SELECT with arithmetic |
| Aggregate over non-recursive rules | Standard Datalog with @agg directive |
| Confidence is additive, not multiplicative or min | SUM aggregate in a non-recursive rule |
Lattices add one piece of complexity: the fixpoint loop. If you can express the same computation as a single SQL WITH RECURSIVE query or a non-recursive Datalog rule, that is almost always simpler and faster.
Convergence and termination
Every lattice computation is guaranteed to terminate if the lattice has the ascending chain condition — no infinite strictly ascending chains. All four built-in lattices satisfy this:
minover bounded integers / floats: descending-only.maxover bounded integers / floats: ascending but bounded.setof a finite universe: a finite powerset, every chain terminates.intervalover bounded timestamps: bounded.
Custom lattices over unbounded domains can diverge. Set pg_ripple.lattice_max_iterations (default: 1000) as a safety cap. The engine emits a PT540 warning if the cap is hit and returns the current (partial) fixpoint.
See also
- Lattice Datalog Reference — all SQL functions, GUCs, and catalog tables.
- Cookbook: Probabilistic Rules — end-to-end worked example.
- Reasoning and Inference — the full Datalog story.
Advanced Inference: WCOJ, DRed, and Tabling
Three terms appear in the pg_ripple release notes — worst-case optimal joins, DRed (Decremental Re-evaluation), and tabling — that have no obvious meaning to a SQL or SPARQL practitioner. This page explains what each one does, when the engine uses it, and why it matters for your queries.
You do not need to configure any of them. They are automatic. But knowing they exist helps you understand why certain queries are fast, why retraction is safe, and why recursive queries do not loop infinitely.
Worst-Case Optimal Joins (WCOJ)
The problem
Classical binary join plans — the kind every SQL query planner uses — are suboptimal for certain triangle and clique patterns. Given three tables R, S, T:
-- Triangle query: find all A-B-C triples where all three pair-wise edges exist.
SELECT r.a, s.b, t.c
FROM R r JOIN S s ON r.b = s.b
JOIN T t ON s.c = t.c
WHERE r.a = t.a; -- closing the triangle
A binary join plan processes R⋈S first (potentially generating many intermediate rows), then joins with T. For dense triangle patterns, the intermediate result can be quadratically larger than the final output. The join is correct, but wasteful.
Worst-case optimal joins (specifically, the Leapfrog Triejoin algorithm) process all three relations simultaneously, interleaving the enumeration so the intermediate result is never larger than the final output. The result is output-sensitive performance: fast on sparse results, fast on dense results, never pathological.
When pg_ripple uses it
WCOJ is used automatically for multi-predicate star patterns and triangle patterns in SPARQL. You trigger it without knowing:
# Triangle pattern: Alice-knows-someone-who-created-something-that-Alice-rated.
SELECT ?mid ?thing WHERE {
ex:alice foaf:knows ?mid .
?mid schema:creator ?thing .
ex:alice ex:rated ?thing .
}
This is a triangle (Alice → knows → mid → creator → thing ← rated ← Alice). pg_ripple's SPARQL-to-SQL translator detects the pattern and emits a WCOJ plan rather than two binary joins.
When to care
You almost never need to think about this. The relevant scenario: if you see a SPARQL query involving three or more VP tables in a cycle pattern performing significantly worse than expected, run EXPLAIN SPARQL and check that the plan shows a leapfrog node. If it shows a nested-loop join instead, the query optimizer may have missed the opportunity — file an issue with the query.
DRed — Decremental Re-evaluation
The problem
Materialised Datalog inference creates derived triples. When you delete a source triple, some derived triples may no longer be valid. Naïve re-derivation would mean rerunning the entire inference from scratch — expensive for large graphs.
DRed (Decremental Re-evaluation with Deletion) is a more efficient algorithm:
- Mark the derived triples that might be invalid (the "over-delete" set).
- Re-derive everything in the over-delete set from scratch, using only surviving source triples.
- Re-insert anything that can still be derived.
The cost is proportional to the affected sub-graph, not the entire graph.
In pg_ripple
DRed kicks in automatically whenever you call sparql_update with a DELETE or DELETE WHERE that removes a triple that is used as a body atom in an active rule set.
# Delete a source triple.
DELETE DATA {
<https://example.org/alice> foaf:knows <https://example.org/bob>
}
If any Datalog rule derives triples from foaf:knows, pg_ripple marks those derived triples for DRed re-evaluation and launches the algorithm. The derived triples that are still provable (via other paths) are kept; the ones that have no other derivation are removed.
What this means for you
- Deletes are safe. You never have to manually reconcile derived triples with base triples after a deletion. The engine handles it.
- Deletes are proportionally expensive. The cost of a deletion is proportional to the number of derived triples that depended on the deleted fact, not on the total triple count. For well-factored rule sets, this is small.
- Avoid manual derived-triple cleanup. Do not write
DELETE WHEREqueries that target_pg_ripple.vp_*tables directly. The DRed bookkeeping uses the OID of the source triple; direct table manipulation bypasses it and can leave the derived set inconsistent.
Tabling
The problem
Recursive Datalog rules can loop. Given:
ancestor(X, Y) :- parent(X, Y) .
ancestor(X, Z) :- ancestor(X, Y), ancestor(Y, Z) .
A naïve evaluator would keep expanding ancestor forever if there is a cycle in the parent graph (a real risk with, for example, owl:sameAs or skos:broader*).
Tabling (also called memoisation or SLG resolution) avoids infinite loops by caching — tabling — intermediate results and suspending evaluation when a recursive call would revisit a goal already on the active stack.
In pg_ripple
pg_ripple's Datalog evaluator uses a bottom-up semi-naïve iteration (SN) as its primary strategy. Tabling is used as a fallback for rule sets that cannot be safely stratified.
Under SN evaluation:
- Start from the base facts.
- Derive every new triple that the rules allow (the "delta" derivation).
- Repeat with the new facts as seed until no new triples are produced (fixpoint).
The fixpoint condition ensures termination. Tabling is used during the fixpoint iteration to detect and break circular derivation chains that would otherwise prevent convergence.
What this means for you
- You can write mutually recursive rules safely. pg_ripple does not require you to manually identify the fixed point.
- Cycles in the data do not cause infinite loops.
owl:sameAschains,skos:broadercycles, orrdfs:subClassOfdiamond hierarchies are all handled. - Well-founded semantics for negation. When a rule uses negation-as-failure (
NOTin the body), pg_ripple uses well-founded semantics (WFS) to assign a third truth value ("unknown") to atoms that participate in cycles through negation. This is the most principled treatment of negation under recursion available in any Datalog system.
How they compose
All three mechanisms operate together on the same query/rule execution:
| Mechanism | Role |
|---|---|
| WCOJ (Leapfrog Triejoin) | Speed up join evaluation for multi-way patterns |
| Tabling | Ensure fixpoint termination for recursive rules |
| DRed | Maintain consistency of derived triples after base-triple deletions |
None of them requires configuration. They are part of the engine's normal operation and are activated automatically based on query shape, rule structure, and update type.
See also
- Reasoning and Inference — the full Datalog reference.
- Lattice Datalog — when your reasoning needs to propagate values, not just facts.
- OWL 2 Profiles — the specific built-in rule sets.
- SPARQL Query Debugger — inspect which join strategy the planner chose.
Further reading
- Blog: Leapfrog Triejoin — how worst-case optimal joins accelerate cyclic patterns
- Blog: Well-Founded Semantics — three-valued logic for cyclic rules
- Blog: Magic Sets for Goal-Directed Inference — demand-driven evaluation
PageRank & Graph Analytics (v0.88.0+)
pg_ripple v0.88.0 adds a Datalog-native PageRank engine that runs entirely inside PostgreSQL
via SPI and the existing Datalog aggregation infrastructure. All scores are persisted in
_pg_ripple.pagerank_scores and are queryable with standard SQL — no external process required.
v0.90.0 adds WCOJ integration, convergence norm GUC, IVM full-recompute threshold, and
Count-Min Sketch approximate top-N. v0.91.0 adds IVM Prometheus gauges and explain_pagerank_json().
Quick Start
-- Load some RDF triples
SELECT pg_ripple.load_ntriples($nt$
<http://example.org/Alice> <http://xmlns.com/foaf/0.1/knows> <http://example.org/Bob> .
<http://example.org/Bob> <http://xmlns.com/foaf/0.1/knows> <http://example.org/Carol> .
<http://example.org/Carol> <http://xmlns.com/foaf/0.1/knows> <http://example.org/Alice> .
$nt$, 'http://example.org/graph');
-- Run PageRank (default: damping=0.85, 100 iterations)
SELECT node_iri, score
FROM pg_ripple.pagerank_run()
ORDER BY score DESC;
SQL Functions
pg_ripple.pagerank_run(...)
Runs iterative PageRank and persists results. Returns one row per node.
| Parameter | Type | Default | Description |
|---|---|---|---|
damping | float8 | 0.85 | Damping factor (teleportation probability = 1 − damping) |
max_iterations | int | 100 | Maximum power-iteration steps |
convergence_delta | float8 | 0.0001 | L1-norm convergence threshold |
direction | text | 'forward' | 'forward', 'reverse', or 'undirected' |
topic | text | '' | Topic label — enables topic-sensitive PageRank |
edge_predicates | text[] | NULL | Restrict to these predicate IRIs (default: all) |
graph_uri | text | NULL | Named graph to evaluate (default: all graphs) |
decay_rate | float8 | 0.0 | Temporal decay exponent (0 = disabled) |
bias | float8 | 0.15 | Personalization vector weight |
All parameters are optional and can be passed as named arguments (API-03, v0.91.0):
-- Named-argument invocation (recommended for clarity)
SELECT node_iri, score
FROM pg_ripple.pagerank_run(
damping => 0.85,
max_iterations => 50,
topic => 'citation'
);
-- Restrict to a specific named graph, reverse direction
SELECT node_iri, score
FROM pg_ripple.pagerank_run(
graph_uri => 'http://example.org/graph1',
direction => 'reverse'
)
ORDER BY score DESC LIMIT 20;
pg_ripple.centrality_run(metric text, ...)
Computes an alternative centrality measure and stores results in
_pg_ripple.centrality_scores.
Supported metric values: 'betweenness', 'closeness', 'degree', 'pagerank'.
SELECT COUNT(*) FROM pg_ripple.centrality_run('betweenness');
SELECT * FROM _pg_ripple.centrality_scores ORDER BY score DESC LIMIT 10;
pg_ripple.explain_pagerank(node_iri text, top_k int DEFAULT 5)
Returns the score explanation tree for a node — which other nodes contributed how much.
SELECT depth, contributor_iri, contribution, path
FROM pg_ripple.explain_pagerank('<http://example.org/Alice>', 5);
pg_ripple.explain_pagerank_json(node_iri text, top_k int DEFAULT 5) (API-05, v0.91.0)
Returns the same explanation tree as explain_pagerank(), but serialised as a JSONB object.
Use this when you need to return the explanation from a PL/pgSQL function or REST handler:
SELECT pg_ripple.explain_pagerank_json('<http://example.org/Alice>');
-- Returns JSONB: {"node_iri": "...", "total_score": 0.023, "contributors": [...]}
-- Useful in REST queries via pg_ripple_http /pagerank/explain/:iri?format=json
pg_ripple.export_pagerank(format text, top_k bigint DEFAULT 10000, topic text DEFAULT '')
Serialises scores in 'csv', 'turtle', 'ntriples', or 'jsonld' format.
SELECT pg_ripple.export_pagerank('turtle', 1000);
pg_ripple.pagerank_queue_stats()
Returns IVM dirty-edge queue metrics useful for monitoring.
SELECT * FROM pg_ripple.pagerank_queue_stats();
-- queued_edges | max_delta | oldest_enqueue | estimated_drain_seconds
pg_ripple.vacuum_pagerank_dirty()
Drains processed entries from _pg_ripple.pagerank_dirty_edges.
pg_ripple.pagerank_find_duplicates(metric text, centrality_threshold float8, fuzzy_threshold float8)
Returns candidate duplicate node pairs detected via centrality + label similarity.
GUC Parameters
| GUC | Default | Description |
|---|---|---|
pg_ripple.pagerank_enabled | off | Master switch |
pg_ripple.pagerank_damping | 0.85 | Damping factor |
pg_ripple.pagerank_max_iterations | 100 | Iteration cap |
pg_ripple.pagerank_convergence_delta | 0.0001 | Convergence threshold |
pg_ripple.pagerank_convergence_norm | 'l1' | Convergence norm (l1, l2, linf) |
pg_ripple.pagerank_full_recompute_threshold | 0.01 | Stale fraction triggering full recompute |
pg_ripple.pagerank_wcoj_threshold | 10 | WCOJ path threshold (millions of edges) |
pg_ripple.pagerank_sketch_width | 2000 | Count-Min Sketch width (columns) |
pg_ripple.pagerank_sketch_depth | 5 | Count-Min Sketch depth (rows/hash functions) |
pg_ripple.pagerank_temp_threshold | 0 | Temp-table threshold bytes (0 = auto) |
pg_ripple.pagerank_incremental | off | Enable IVM (k-hop refresh) |
pg_ripple.pagerank_confidence_weighted | off | Weight edges by confidence scores |
pg_ripple.pagerank_partition | off | Partition evaluation per named graph |
pg_ripple.pagerank_probabilistic | off | Probabilistic Datalog score bounds |
pg_ripple.pagerank_queue_warn_threshold | 100000 | Warn when dirty-edge queue exceeds this |
Convergence Norm (v0.90.0)
pg_ripple uses the L1 norm (sum of absolute differences) to test convergence
between iterations, consistent with NetworkX's default behaviour. The convergence
threshold pg_ripple.pagerank_convergence_delta (default 0.0001) is compared
against the L1 norm of the score delta vector.
Alternative norms can be selected via the pg_ripple.pagerank_convergence_norm GUC:
| Value | Formula | Notes |
|---|---|---|
'l1' (default) | $\sum_i |\Delta s_i|$ | Fastest to compute; matches NetworkX |
'l2' | $\sqrt{\sum_i \Delta s_i^2}$ | Matches igraph default |
'linf' | $\max_i |\Delta s_i|$ | Most conservative; slowest convergence |
-- Match igraph convergence behaviour
SET pg_ripple.pagerank_convergence_norm = 'l2';
SELECT node_iri, score FROM pg_ripple.pagerank_run();
Incremental Refresh Error Bounds (v0.90.0)
The bounded K-hop incremental refresh approximates the full PageRank recomputation. The theoretical error bound after K hops of incremental propagation is:
$$|\Delta \text{score}| \leq \alpha^K \times \max_\text{delta_per_iteration}$$
where $\alpha$ is the damping factor (default 0.85) and $K$ is
pagerank_khop_limit (default 30). At K=30:
$$\alpha^{30} = 0.85^{30} \approx 0.0076$$
The maximum error per dirty node is < 1% of the per-iteration delta — acceptable
for most use cases. For high-precision applications, use pagerank_run() directly.
Tuning Damping for Your Graph (v0.92.0)
The default damping factor pg_ripple.pagerank_damping = 0.85 was calibrated for
citation graphs and the original web (Brin & Page 1998). For other graph types:
- Sparse social graphs (Twitter-style follows): 0.50–0.75 recommended. High damping over-amplifies hub nodes in sparse graphs.
- Citation graphs (papers citing papers): 0.85 (default) is well-suited.
- Knowledge graphs (diverse predicate types): 0.85 is a safe default; consider per-predicate-filtered runs with lower damping for dense predicate subsets.
- Temporal graphs with decay (
decay_rate > 0): lower damping (0.70–0.80) reduces the influence of long-range links that may be heavily time-discounted.
-- Tune for sparse social graph
SET pg_ripple.pagerank_damping = 0.65;
SELECT node_iri, score FROM pg_ripple.pagerank_run();
Automatic Full Recompute
When the fraction of stale = true rows in pagerank_scores for a given topic
exceeds pg_ripple.pagerank_full_recompute_threshold (default 0.01 = 1%), the
next IVM worker cycle automatically triggers a full pagerank_run() for that topic.
-- Lower threshold: trigger full recompute when 0.5% of scores are stale
SET pg_ripple.pagerank_full_recompute_threshold = 0.005;
Count-Min Sketch Parameters (v0.90.0)
The top-K PageRank query path uses a Count-Min Sketch to track approximate frequency distributions without materialising the full score table. Two GUCs control sketch memory:
| GUC | Default | Formula |
|---|---|---|
pg_ripple.pagerank_sketch_width | 2000 | Columns per hash function |
pg_ripple.pagerank_sketch_depth | 5 | Number of hash functions (rows) |
Memory usage: width × depth × 8 bytes per active topic. With defaults:
2000 × 5 × 8 = 80 KB per topic — negligible even for thousands of topics.
Error bound: with probability $1 - e^{-\text{depth}}$ the frequency estimate overestimates by at most $e / \text{width}$ of the total stream mass.
-- Increase accuracy for large graphs at the cost of more memory
SET pg_ripple.pagerank_sketch_width = 10000;
SET pg_ripple.pagerank_sketch_depth = 7;
REST API (pg_ripple_http)
| Endpoint | Method | Description |
|---|---|---|
/pagerank/run | POST | Trigger computation |
/pagerank/results | GET | Top-N scores |
/pagerank/status | GET | Last run metadata |
/pagerank/vacuum-dirty | POST | Drain queue |
/pagerank/export | GET | Export scores |
/pagerank/explain/{node_iri} | GET | Score explanation |
/pagerank/queue-stats | GET | IVM queue metrics |
/centrality/run | POST | Compute centrality |
/centrality/results | GET | Centrality scores |
/pagerank/find-duplicates | POST | Entity deduplication |
Storage
Scores are persisted in _pg_ripple.pagerank_scores:
\d _pg_ripple.pagerank_scores
-- node BIGINT, topic TEXT, score FLOAT8,
-- score_lower FLOAT8, score_upper FLOAT8,
-- computed_at TIMESTAMPTZ, iterations INT, converged BOOL,
-- stale BOOL, stale_since TIMESTAMPTZ
The IVM dirty-edge queue (_pg_ripple.pagerank_dirty_edges) tracks which edges changed
since the last full run, enabling k-hop incremental refresh (PR-TRICKLE-01).
owl:sameAs Deduplication (v0.92.0)
When owl:sameAs canonicalization is active (v0.23.0), pg_ripple merges entity clusters
into a canonical representative before computing PageRank. This deduplication ensures that
edges to equivalent entities are not double-counted, which would otherwise inflate the
PageRank score of heavily sameAs-clustered nodes.
See Reasoning and Inference for canonicalization details.
Portability Note (STD-04, v0.92.0)
pg:pagerank(), pg:centrality(), and pg:topN_approx() are pg_ripple-specific
extension functions. They are not defined by the W3C SPARQL 1.1 specification and
are not portable to other SPARQL endpoints (Apache Jena, Virtuoso, Stardog, etc.).
In federated queries, use the full IRI form and ensure the pg_ripple-specific endpoint
is the SERVICE target. Federated queries that include pg:pagerank() in a SERVICE block
targeting a non-pg_ripple endpoint will fail with SPARQL function not found.
Feature Status
SELECT feature, status, version FROM pg_ripple.feature_status()
WHERE feature LIKE 'pagerank%' OR feature LIKE 'centrality%';
Further reading
- Blog: PageRank in pg_ripple — Datalog-native PageRank with IVM and Prometheus integration
Uncertain Knowledge: Probabilistic Datalog, Fuzzy SPARQL, and Soft SHACL
pg_ripple v0.87.0 introduces the Uncertain Knowledge Engine — a suite of features for reasoning with imprecise, probabilistic, or fuzzy data.
Overview
Real-world knowledge graphs contain data of varying reliability. A fact extracted from a scientific paper has a different confidence level than one scraped from social media. pg_ripple now lets you:
- Annotate Datalog rules with probability weights (
@weight) so that inferred facts carry a confidence score. - Query confidence with SPARQL using the
pg:confidence()extension function. - Traverse graphs with a fuzzy similarity filter using
pg:confPath(). - Match strings fuzzily with
pg:fuzzy_match()andpg:token_set_ratio(). - Score SHACL validation results with per-shape severity weights.
- Export Turtle with RDF* confidence annotations.
Probabilistic Datalog (@weight)
Add a @weight(0.8) annotation to any Datalog rule to declare that derived facts have at most 0.8 confidence. The engine propagates confidence using noisy-OR combination:
parent(X, Y) :- father(X, Y). @weight(1.0)
parent(X, Y) :- mother(X, Y). @weight(1.0)
ancestor(X, Z) :- parent(X, Z). @weight(0.9)
ancestor(X, Z) :- parent(X, Y), ancestor(Y, Z). @weight(0.85)
Enable with:
SET pg_ripple.probabilistic_datalog = on;
Confidence scores are stored in _pg_ripple.confidence:
SELECT statement_id, confidence, model
FROM _pg_ripple.confidence
LIMIT 10;
GUCs for probabilistic evaluation
| GUC | Default | Description |
|---|---|---|
pg_ripple.probabilistic_datalog | off | Enable @weight rule processing |
pg_ripple.prob_datalog_cyclic | off | Allow approximate evaluation on cyclic rule sets |
pg_ripple.prob_datalog_max_iterations | 100 | Maximum iterations for cyclic evaluation |
pg_ripple.prob_datalog_convergence_delta | 0.001 | Early-exit threshold |
pg_ripple.prob_datalog_cyclic_strict | off | Promote non-convergence from WARNING to ERROR |
pg:confidence() — SPARQL confidence lookup
PREFIX pg: <http://pg-ripple.org/functions/>
SELECT ?s ?p ?o ?conf WHERE {
?s ?p ?o .
BIND(pg:confidence(?s, ?p, ?o) AS ?conf)
FILTER(?conf > 0.7)
}
Returns the maximum confidence across all models for the triple (?s, ?p, ?o). Returns 1.0 when no confidence row exists.
Fuzzy SPARQL functions
These require CREATE EXTENSION IF NOT EXISTS pg_trgm;.
pg:fuzzy_match(a, b)
Returns the trigram similarity (similarity()) between two string literals.
PREFIX pg: <http://pg-ripple.org/functions/>
SELECT ?label WHERE {
?entity rdfs:label ?label .
FILTER(pg:fuzzy_match(?label, "Alice Smith") > 0.6)
}
pg:token_set_ratio(a, b)
Returns word-set similarity (word_similarity()) — better for substring matches.
FILTER(pg:token_set_ratio(?label, "Smith") > 0.5)
pg:confPath(predicate, threshold) — confidence property path
SELECT ?x WHERE {
<http://example.org/alice> <pg:conf_path/http://example.org/knows/0.8> ?x
}
Traverses the knows predicate with a minimum confidence threshold of 0.8.
GUC
| GUC | Default | Description |
|---|---|---|
pg_ripple.default_fuzzy_threshold | 0.7 | Default threshold when not explicit |
Soft SHACL scoring
Instead of a pass/fail validation, pg_ripple can compute a weighted data-quality score for a graph:
SELECT pg_ripple.shacl_score('http://example.org/data');
-- Returns a float8 in [0.0, 1.0], where 1.0 = fully compliant
Annotate shapes with sh:severityWeight to control their contribution:
ex:MyShape a sh:NodeShape ;
sh:targetClass ex:Person ;
sh:property [ sh:path ex:name ; sh:minCount 1 ] ;
sh:severityWeight "2.0"^^xsd:decimal .
Extension note (STD-05, v0.92.0):
sh:severityWeightis a pg_ripple-specific extension to the W3C SHACL Core specification. It is not defined by https://www.w3.org/TR/shacl/. A community submission to the W3C SHACL Community Group is being considered; see GitHub discussion for progress. The annotation is stable for the pg_ripple 1.x API line.
Score history is logged to _pg_ripple.shacl_score_log:
SELECT pg_ripple.log_shacl_score('http://example.org/data');
SELECT * FROM _pg_ripple.shacl_score_log ORDER BY measured_at DESC;
Loading triples with confidence
SELECT pg_ripple.load_triples_with_confidence(
'<http://example.org/alice> <http://example.org/knows> <http://example.org/bob> .',
confidence => 0.85,
format => 'ntriples'
);
Exporting with confidence annotations (RDF*)
SET pg_ripple.export_confidence = on;
SELECT pg_ripple.export_turtle_with_confidence('http://example.org/data');
Returns Turtle with << s p o >> pg:confidence "0.85"^^xsd:float . annotations.
PROV-O confidence propagation
Set pg_ripple.prov_confidence = on to enable automatic confidence propagation from pg:sourceTrust predicates — triples derived from low-trust sources inherit lower confidence.
HTTP API (pg_ripple_http)
| Method | Path | Description |
|---|---|---|
POST | /confidence/load | Load triples with confidence |
GET | /confidence/shacl-score?graph=<IRI> | Compute SHACL quality score |
GET | /confidence/shacl-report?graph=<IRI> | Scored violation report |
POST | /confidence/vacuum | Purge orphaned confidence rows |
Garbage collection
Orphaned confidence rows (whose statement_id no longer exists in any VP table) are purged:
- Automatically during each HTAP merge cycle.
- On demand:
SELECT pg_ripple.vacuum_confidence();
Convergence Guarantees for Cyclic Probabilistic Rules (v0.90.0)
When pg_ripple.prob_datalog_cyclic = on, pg_ripple iterates the noisy-OR composition
to fixpoint. The noisy-OR operator is monotone on [0, 1] (adding more evidence can only
increase confidence, never decrease it), which guarantees that the semi-naive iteration
sequence is non-decreasing and bounded above by 1.0. Therefore, fixpoint convergence is
guaranteed for any finite probabilistic Datalog program with noisy-OR semantics.
This result follows directly from Theorem 2 in De Raedt, Kimmig & Toivonen (2007), ProbLog: A Probabilistic Prolog and its Application in Link Discovery.
Extension note (STD-03, v0.91.0): pg_ripple's noisy-OR confidence composition is a pg_ripple-specific extension implementing probabilistic Datalog semantics. It is not defined by the W3C RDF or SPARQL specifications. The mathematical foundation is:
De Raedt, L., Kimmig, A., & Toivonen, H. (2007). ProbLog: A probabilistic Prolog and its application in link discovery. Proceedings of IJCAI 2007, pp. 2468–2473. https://ijcai.org/proceedings/2007/2
Convergence speed depends on cycle depth and confidence values; programs with near-1.0
confidence in cycles may converge slowly. The prob_datalog_max_iterations GUC (default 100)
and prob_datalog_convergence_delta GUC (default 1e-6) control termination.
-- Tune convergence for deep cyclic programs
SET pg_ripple.prob_datalog_max_iterations = 500;
SET pg_ripple.prob_datalog_convergence_delta = 1e-8;
Formal Guarantee
Let $c_i^{(k)}$ denote the confidence of fact $i$ after $k$ iterations. Under noisy-OR semantics:
$$c_i^{(k+1)} = 1 - \prod_{j \in \text{parents}(i)} (1 - w_{ij} \cdot c_j^{(k)})$$
Since noisy-OR is monotone and the sequence ${c_i^{(k)}}$ is non-decreasing and bounded above by 1.0, by the Knaster–Tarski fixed-point theorem the iteration converges to the least fixed point of the probability propagation operator.
Further reading
- Blog: Probabilistic Datalog — noisy-OR, confidence propagation, and soft reasoning in pg_ripple
- Blog: Uncertain Knowledge — practical patterns for probabilistic reasoning
CDC Subscriptions
Status: Available since v0.42.0 (CDC-01)
Requires: pg_ripple_http for Server-Sent Events delivery. PostgreSQL LISTEN/NOTIFY works without it.
SQL: pg_ripple.subscribe_cdc(), pg_ripple.unsubscribe_cdc(), pg_ripple.list_cdc_subscriptions()
HTTP: GET /subscribe/{id} (Server-Sent Events, via pg_ripple_http)
See also: Live SPARQL Subscriptions · Live Views
Overview
Change Data Capture (CDC) subscriptions let your application subscribe to a real-time stream of RDF triple changes — filtered by SPARQL pattern or SHACL shape — without polling the database.
When a matching triple is inserted or deleted, pg_ripple sends a PostgreSQL NOTIFY message on a named channel. Listeners receive a JSON payload describing the change. The pg_ripple_http companion service exposes these subscriptions as Server-Sent Events (SSE) streams for web and streaming applications.
sequenceDiagram
participant App
participant pg_ripple_http
participant PostgreSQL
participant pg_ripple_ext as pg_ripple extension
App->>pg_ripple_http: GET /subscribe/{id}
pg_ripple_http->>PostgreSQL: LISTEN pg_ripple_sub_{id}
Note over App,pg_ripple_http: SSE connection established
loop Triple Changes
App->>PostgreSQL: INSERT triple (via SPARQL or SQL)
pg_ripple_ext->>PostgreSQL: NOTIFY pg_ripple_sub_{id}, payload
PostgreSQL->>pg_ripple_http: async notification
pg_ripple_http->>App: event: row\ndata: {...}
end
Creating a Subscription
-- Subscribe to all triple changes.
SELECT pg_ripple.create_subscription('my_feed');
-- Subscribe with a SPARQL pattern filter.
SELECT pg_ripple.create_subscription(
'person_changes',
filter_sparql := 'SELECT ?s ?p ?o WHERE { ?s a <https://schema.org/Person> ; ?p ?o }'
);
-- Subscribe with a SHACL shape filter.
SELECT pg_ripple.create_subscription(
'shape_violations',
filter_shape := '<https://shapes.example.org/PersonShape>'
);
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
name | TEXT | required | Unique subscription name (alphanumeric + _/-, max 63 chars) |
filter_sparql | TEXT | NULL | Optional SPARQL SELECT pattern; only matching triples are published |
filter_shape | TEXT | NULL | Optional SHACL shape IRI; only shape-violating triples are published |
Returns TRUE if created, FALSE if a subscription with that name already exists.
Listening for Changes
-- Start listening.
LISTEN pg_ripple_cdc_my_feed;
-- Insert a triple.
SELECT pg_ripple.insert_triple(
'<https://ex.org/alice>',
'<https://schema.org/name>',
'"Alice"'
);
-- In your application, receive notifications via pg_notify/asyncpg/etc.
Notification Payload
Each notification carries a JSON payload:
{
"op": "add",
"s": "<https://ex.org/alice>",
"p": "<https://schema.org/name>",
"o": "\"Alice\"",
"g": ""
}
| Field | Value |
|---|---|
op | "add" for INSERT, "remove" for DELETE |
s | Subject — N-Triples formatted IRI or blank node |
p | Predicate — N-Triples formatted IRI |
o | Object — N-Triples formatted literal or IRI |
g | Named graph IRI, or empty string for the default graph |
Listing Subscriptions
SELECT name, filter_sparql IS NOT NULL AS has_filter, created_at
FROM pg_ripple.list_subscriptions()
ORDER BY created_at;
Dropping a Subscription
-- Returns TRUE if removed, FALSE if not found.
SELECT pg_ripple.drop_subscription('my_feed');
WebSocket Access via pg_ripple_http
When the pg_ripple_http companion service is running, subscriptions are accessible as WebSocket endpoints:
ws://<host>:8080/ws/subscriptions/{name}
The service supports content negotiation via the Accept header:
application/json(default) — JSON payloadtext/turtle— Turtle-serialized change notificationapplication/ld+json— JSON-LD change notification
Integration Patterns
GraphRAG Pipeline
import asyncpg
async def watch_entity_changes():
conn = await asyncpg.connect(dsn)
await conn.execute("LISTEN pg_ripple_cdc_entity_changes")
async for notification in conn.listen("pg_ripple_cdc_entity_changes"):
payload = json.loads(notification.payload)
# Re-embed entity on change.
await update_embedding(payload["s"])
Live Dashboard
const ws = new WebSocket("ws://localhost:8080/ws/subscriptions/dashboard_feed");
ws.onmessage = (event) => {
const change = JSON.parse(event.data);
updateDashboard(change.op, change.s, change.p, change.o);
};
Underlying Tables
| Table | Description |
|---|---|
_pg_ripple.subscriptions | Named subscription registry |
_pg_ripple.cdc_subscriptions | Low-level predicate-pattern subscriptions (v0.6.0 legacy API) |
Related Functions
| Function | Description |
|---|---|
pg_ripple.create_subscription(name, filter_sparql, filter_shape) | Create named subscription |
pg_ripple.drop_subscription(name) | Remove named subscription |
pg_ripple.list_subscriptions() | List all named subscriptions |
pg_ripple.subscribe(pattern, channel) | Low-level subscription (v0.6.0 API) |
pg_ripple.unsubscribe(channel) | Remove low-level subscription |
Further reading
- Blog: CDC and Knowledge Graphs — real-time event streaming from your knowledge graph
Live SPARQL Subscriptions
Status: Experimental — v0.73.0 (SUB-01)
API: pg_ripple.subscribe_sparql() / pg_ripple.unsubscribe_sparql()
HTTP: GET /subscribe/:subscription_id (Server-Sent Events)
See also: CDC Subscriptions · Live Views
Overview
Live SPARQL subscriptions allow applications to receive real-time notifications
when the result of a SPARQL SELECT query changes. Whenever a graph write or
delete touches a graph that a registered subscription monitors, pg_ripple calls
pg_notify('pg_ripple_subscription_<id>', ...) with the updated query result as
a JSONB payload.
The pg_ripple_http companion service exposes a GET /subscribe/:id endpoint
that translates PostgreSQL LISTEN notifications into Server-Sent Events (SSE)
that any HTTP client can consume.
Quick start
1. Register a subscription
-- Register a subscription that fires whenever a triple is written to
-- the default graph and the query over it produces a different result.
SELECT pg_ripple.subscribe_sparql(
'my-sub-01',
'SELECT ?s ?label WHERE { ?s <https://schema.org/name> ?label }',
NULL -- NULL = monitor all graphs
);
2. Listen for changes in any PostgreSQL client
LISTEN pg_ripple_subscription_my-sub-01;
-- Each NOTIFY payload is a JSONB string of the updated result set.
3. Listen via HTTP SSE
curl -N http://localhost:7878/subscribe/my-sub-01 \
-H "Authorization: Bearer $TOKEN"
Each Server-Sent Event has event: sparql_result and data: <jsonb> fields.
4. Unregister
SELECT pg_ripple.unsubscribe_sparql('my-sub-01');
SQL API reference
subscribe_sparql(subscription_id, query, graph_iri)
pg_ripple.subscribe_sparql(
subscription_id TEXT,
query TEXT,
graph_iri TEXT DEFAULT NULL
) RETURNS VOID
Registers a subscription. Raises an error if a subscription with the same ID already exists.
subscription_id— unique identifier; used in the channel namepg_ripple_subscription_<id>.query— SPARQL SELECT query to re-evaluate on change.graph_iri— if set, the subscription fires only when this named graph is written or deleted.NULLfires for any graph write.
unsubscribe_sparql(subscription_id)
pg_ripple.unsubscribe_sparql(subscription_id TEXT) RETURNS VOID
Removes the subscription. Silently succeeds if the ID does not exist.
HTTP SSE endpoint
GET /subscribe/:subscription_id
| Header | Value |
|---|---|
Authorization | Bearer <token> (when auth is configured) |
Accept | text/event-stream |
Event format:
event: sparql_result
id: <notification_id>
data: {"bindings": [...]}
event: keepalive
data: {}
A keepalive event is sent every 15 seconds to keep the TCP connection alive
through proxies and load balancers.
Limitations
- Payload size:
pg_notifyhas an 8 KB payload limit. When the updated result set exceeds this limit, pg_ripple sends a{"changed": true}signal instead of the full result. The client must then re-query to obtain the new result. - At-least-once delivery: SSE is a fire-and-forget protocol. If the client disconnects and reconnects, it will receive the next notification but may miss intermediate ones.
- Prototype: This is an experimental implementation. The subscription mechanism is synchronous in the mutation path; very high write rates on a subscribed graph may add latency.
Implementation notes
Subscriptions are stored in _pg_ripple.sparql_subscriptions:
SELECT * FROM _pg_ripple.sparql_subscriptions;
-- subscription_id | query | graph_iri | created_at
The mutation journal (src/storage/mutation_journal.rs) calls
crate::subscriptions::notify_affected_subscriptions() after flushing CWB
hooks. This function queries the subscriptions catalog, re-executes the SPARQL
query for any matching subscription, and calls pg_notify.
See also
- CDC Subscriptions — pg-trickle-based change data capture for CDC relay patterns.
- Live Views — SPARQL CONSTRUCT live views that automatically refresh derived graphs.
JSON Mapping
pg_ripple's JSON mapping feature provides a bidirectional bridge between JSON
payloads and the RDF knowledge graph. Register a named JSON-LD context once with
register_json_mapping(), then use it for both ingest and export.
Registration
SELECT pg_ripple.register_json_mapping(
'contacts',
'{"contact_id": "http://schema.org/identifier",
"full_name": "http://schema.org/name",
"email_addr": "http://schema.org/email"}'::jsonb
);
Parameters:
| Parameter | Description |
|---|---|
name | Unique mapping name |
context | JSON-LD @context object mapping JSON keys to RDF predicate IRIs |
shape_iri | Optional SHACL shape for consistency validation |
default_graph_iri | Default named graph for ingested triples |
timestamp_path | JSONPath to root timestamp field (diff mode) |
timestamp_predicate | RDF predicate for per-triple change timestamps |
iri_template | IRI template with {id} placeholder |
iri_match_pattern | Prefix or regex for late-binding IRI rewrite |
Ingest (JSON → RDF)
SELECT pg_ripple.ingest_json(
'{"contact_id": "c001", "full_name": "Alice Smith"}'::jsonb,
'https://example.com/contacts/c001',
'contacts'
);
Modes: 'append' (default), 'upsert', 'diff'.
Export (RDF → JSON)
SELECT pg_ripple.export_json_node(subject_id, 'contacts');
Relational Writeback (v0.128.0)
v0.128.0 adds the ability to write RDF graph changes back to the originating relational table — completing the full round-trip.
Configuration
Add writeback configuration to the mapping:
UPDATE _pg_ripple.json_mappings
SET writeback_table = 'contacts',
writeback_schema = 'public',
writeback_key_columns = ARRAY['contact_id'],
writeback_conflict_policy = 'replace'
WHERE name = 'contacts';
Conflict Policies
| Policy | Behaviour |
|---|---|
'replace' (default) | ON CONFLICT (key_cols) DO UPDATE SET … |
'skip' | ON CONFLICT DO NOTHING — returns 0 rows |
'error' | Raises PT0551 on conflict |
Direct Writeback
-- Write a single subject back to the relational table.
SELECT pg_ripple.writeback_json_row('contacts', 'https://example.com/contacts/c001');
-- Delete a subject from the relational table.
SELECT pg_ripple.writeback_json_row_delete('contacts', 'https://example.com/contacts/c001');
Trigger-Based Automation
Enable VP delta triggers for automatic async writeback:
SELECT pg_ripple.enable_json_writeback('contacts');
The background merge worker drains the queue in batches (controlled by
pg_ripple.json_writeback_batch_size, default 100).
Monitor queue status:
SELECT * FROM pg_ripple.json_writeback_status();
-- mapping_name | pending | errors | last_error | last_processed_at
HTTP Writeback
The HTTP companion exposes the same writeback path for applications that do not call SQL directly:
curl -X POST http://localhost:7878/json-mapping/contacts/writeback \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"subject_iri":"https://example.com/contacts/c001"}'
Successful synchronous writeback returns:
{"rows_affected": 1}
Queue status for one mapping is available over HTTP as well:
curl http://localhost:7878/json-mapping/contacts/writeback/status \
-H "Authorization: Bearer $TOKEN"
The status response mirrors json_writeback_status() for the selected mapping:
{
"mapping_name": "contacts",
"pending": 0,
"errors": 0,
"last_error": null,
"last_processed_at": null
}
Disable triggers:
SELECT pg_ripple.disable_json_writeback('contacts');
Both enable_json_writeback() and disable_json_writeback() are idempotent.
Error Codes
| Code | Message |
|---|---|
PT0550 | json mapping writeback target not configured — writeback_table is NULL or writeback_key_columns is empty |
PT0551 | json mapping writeback conflict — conflict detected with policy 'error' |
See Also
- JSON-LD Reverse Mapping blog post
- GUC reference: json_writeback_batch_size
- HTTP API Reference
- R2RML for complex ETL
Cypher / LPG → RDF Mapping
If you are coming from Neo4j, Memgraph, or any other property-graph database, you already have a mental model of your data. This page shows how property graph concepts map to RDF, how to load a property graph schema into pg_ripple, and how to query it in SPARQL.
Nothing about your graph needs to change. The mapping is mechanical and reversible.
Two models, one idea
Both Labeled Property Graphs (LPG) and RDF represent knowledge as a graph. The terminology differs:
| Property graph | RDF / pg_ripple |
|---|---|
| Node | Subject (an IRI or blank node) |
| Node label | rdf:type triple |
| Node property key + value | Predicate + object triple |
| Relationship type | Predicate IRI |
| Relationship property | RDF-star annotation << s p o >> key value |
id() internal identifier | IRI (you choose the naming scheme) |
| Named graph | Named graph (same concept) |
| No equivalent | Datatype-annotated literal ("42"^^xsd:integer) |
The only material difference is relationship properties (properties on an edge in LPG). In RDF these become RDF-star quoted triples.
Naming scheme for IRIs
LPG nodes have internal integer IDs. You need to convert them to IRIs. A simple and robust scheme:
https://example.org/node/{label}/{id}
For example, a Neo4j node (:Person {id: 42, name: "Alice"}) becomes:
<https://example.org/node/Person/42> rdf:type <https://example.org/vocab/Person> .
<https://example.org/node/Person/42> <https://example.org/vocab/name> "Alice" .
If your nodes have a domain-meaningful unique key (email, UUID, slug), use that instead of the internal ID — it makes the IRIs stable across re-imports.
Translating a Cypher schema to RDF
Nodes and node properties
Cypher:
CREATE (:Person {name: "Alice", age: 30, active: true})
RDF (Turtle):
@prefix ex: <https://example.org/node/Person/> .
@prefix voc: <https://example.org/vocab/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
ex:alice a voc:Person ;
voc:name "Alice" ;
voc:age 30 ;
voc:active true .
Relationships without properties
Cypher:
MATCH (a:Person {name: "Alice"}), (b:Person {name: "Bob"})
CREATE (a)-[:KNOWS]->(b)
RDF:
ex:alice voc:KNOWS ex:bob .
Relationships with properties (RDF-star)
Cypher:
CREATE (a)-[:KNOWS {since: 2020, strength: 0.9}]->(b)
RDF-star (Turtle-star):
ex:alice voc:KNOWS ex:bob .
<< ex:alice voc:KNOWS ex:bob >>
voc:since 2020 ;
voc:strength "0.9"^^xsd:decimal .
Store this in pg_ripple:
SELECT pg_ripple.load_turtle($TTL$
@prefix ex: <https://example.org/node/Person/> .
@prefix voc: <https://example.org/vocab/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
ex:alice voc:KNOWS ex:bob .
<< ex:alice voc:KNOWS ex:bob >>
voc:since "2020"^^xsd:integer ;
voc:strength "0.9"^^xsd:decimal .
$TTL$);
Translating common Cypher queries to SPARQL
Node by property
MATCH (p:Person {name: "Alice"}) RETURN p.age
PREFIX voc: <https://example.org/vocab/>
SELECT ?age WHERE {
?p a voc:Person ;
voc:name "Alice" ;
voc:age ?age .
}
Traverse a relationship
MATCH (a:Person {name: "Alice"})-[:KNOWS]->(b) RETURN b.name
PREFIX ex: <https://example.org/node/Person/>
PREFIX voc: <https://example.org/vocab/>
SELECT ?name WHERE {
ex:alice voc:KNOWS ?b .
?b voc:name ?name .
}
Multi-hop traversal (variable depth)
MATCH (a:Person {name: "Alice"})-[:KNOWS*1..]->(b) RETURN b.name
PREFIX ex: <https://example.org/node/Person/>
PREFIX voc: <https://example.org/vocab/>
SELECT ?name WHERE {
ex:alice voc:KNOWS+ ?b . # + = one or more hops
?b voc:name ?name .
}
Or voc:KNOWS* for zero-or-more.
Relationship property filter
MATCH (a)-[r:KNOWS]->(b) WHERE r.strength > 0.8 RETURN a.name, b.name
PREFIX voc: <https://example.org/vocab/>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
SELECT ?aName ?bName WHERE {
?a voc:KNOWS ?b .
<< ?a voc:KNOWS ?b >> voc:strength ?str .
FILTER(?str > "0.8"^^xsd:decimal)
?a voc:name ?aName .
?b voc:name ?bName .
}
Aggregation
MATCH (p:Person)-[:KNOWS]->(friend) RETURN p.name, count(friend) AS friends
PREFIX voc: <https://example.org/vocab/>
SELECT ?name (COUNT(?friend) AS ?friends) WHERE {
?p a voc:Person ;
voc:name ?name ;
voc:KNOWS ?friend .
}
GROUP BY ?name
ORDER BY DESC(?friends)
Bulk migration from Neo4j
The recommended path for migrating a Neo4j database:
- Export with
neo4j-admin dumpor the APOCexport.graphmlprocedure to GraphML or CSV. - Convert to Turtle with a small Python script (or use R2RML if the source is a JDBC view).
- Load into pg_ripple with
pg_ripple.load_turtle_file().
A minimal Python translator for the CSV export:
import csv, sys
BASE = "https://example.org/node/"
VOCAB = "https://example.org/vocab/"
# nodes.csv: nodeId, label, propKey1, propKey2, ...
with open("nodes.csv") as f:
for row in csv.DictReader(f):
iri = f"<{BASE}{row['label']}/{row['nodeId']}>"
print(f"{iri} <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <{VOCAB}{row['label']}> .")
for k, v in row.items():
if k not in ("nodeId", "label") and v:
print(f"{iri} <{VOCAB}{k}> {_literal(v)} .")
For large dumps (> 100 M triples), use COPY via load_ntriples_file() rather than load_turtle() — N-Triples is streamed in bulk without parsing overhead.
What you gain over LPG
Once your graph is in pg_ripple, you have access to capabilities that most LPG databases lack:
- SHACL validation — define and enforce a schema on the graph with formal guarantees.
- OWL reasoning — automatically derive
rdf:typeassertions fromowl:equivalentClassaxioms across multiple schemas. - Federated queries — join your local graph with Wikidata, DBpedia, or any other SPARQL endpoint in a single query.
- Vector + graph hybrid — embed entities and run HNSW similarity search combined with SPARQL graph traversal.
- Transactional writes — graph writes, vector index updates, and relational table updates in a single PostgreSQL transaction.
See also
- Storing Knowledge — RDF-star — relationship properties in detail.
- Querying with SPARQL — full SPARQL 1.1 reference.
- Record Linkage — useful if migrating two systems with overlapping entities.
AI, RAG and LLM Integration — An Overview
pg_ripple is one of the few PostgreSQL extensions that brings knowledge graphs, vector search, and large language models together in a single transaction. This page is the front door to that capability. It explains what each AI feature does, when to use which one, and which deep-dive page to read next.
If you only have five minutes, read this page. If you have an hour, follow the links to the chapters below.
Why pg_ripple for AI workloads?
Modern retrieval pipelines fall into one of three traps:
- Pure vector search is great at fuzzy similarity ("things that look like X") but cannot answer questions that follow precise relationships ("which drugs interact with the medication my patient takes?").
- Pure graph queries capture relationships exactly but cannot match a free-text question to the right entity.
- Pipelines that stitch the two together (vector DB + graph DB + glue code) leak data between systems, suffer from sync lag, and cannot be rolled back atomically.
pg_ripple removes the third trap. Vectors live in pgvector, triples live in vertical-partitioning tables, and a single SQL transaction can update both at once. Every AI feature on this page is built on top of that foundation.
The five AI features at a glance
| Feature | Question it answers | Read next |
|---|---|---|
| Hybrid search | "Find entities that look like X and satisfy this graph pattern." | Vector & Hybrid Search |
| RAG pipeline | "Build a context block I can drop into an LLM prompt." | RAG Pipeline |
| Natural language → SPARQL | "Translate this English question into a SPARQL query I can run." | NL → SPARQL |
| Knowledge-graph embeddings (KGE) | "Learn entity vectors from the graph structure itself, not from text." | Knowledge-Graph Embeddings |
| Record linkage / entity resolution | "Find pairs of entities that refer to the same real-world thing and merge them safely." | Record Linkage |
The features compose. A typical production pipeline uses three or four of them together — see the Use Case Cookbook for end-to-end recipes.
Decision tree
Use this tree to pick the right feature for a new project.
Do you need to answer free-text questions over your data?
├─ No → You probably do not need any AI feature. Use plain SPARQL.
└─ Yes → Does the question name a specific entity, or describe one?
├─ Names it → Plain SPARQL is fastest. (e.g. "show me Alice's papers")
└─ Describes it → You need retrieval. Continue.
│
Is the answer a single fact, or a passage of context?
├─ Fact → Use NL → SPARQL.
└─ Context → Use the RAG pipeline (rag_context()).
│
Do your queries need precise relationships
on top of similarity?
├─ No → Hybrid search may suffice.
└─ Yes → Combine SPARQL + similar() in one query.
Do you have entities arriving from multiple sources that may overlap?
└─ Yes → You need record linkage. Start with KGE for candidate generation,
then SHACL for hard rules, then suggest_sameas + apply.
Do your existing embeddings only capture text, ignoring relationships?
└─ Yes → Train knowledge-graph embeddings (TransE / RotatE) and use them
for entity alignment, recommendations, or link prediction.
How the pieces fit together
┌─────────────────────────────────────┐
│ Application │
└──────────────┬──────────────────────┘
│ SQL
┌──────────────┴──────────────────────┐
│ pg_ripple │
│ │
text query → │ rag_context() │ ← LLM prompt
│ ├─ embed question (HTTP) │
│ ├─ HNSW vector recall ─────────┼─→ pgvector
│ ├─ SPARQL graph expansion ──────┼─→ VP tables
│ └─ assemble JSON-LD │
│ │
text query → │ sparql_from_nl() │ ← SPARQL string
│ ├─ build VoID + SHACL context │
│ ├─ LLM /v1/chat/completions │
│ └─ parse + validate (spargebra) │
│ │
batch run → │ embed_entities() ─────────────────┼─→ pgvector
batch run → │ kge_train() ─────────────────┼─→ kge_embeddings
batch run → │ suggest_sameas() ─────────────────┼─→ owl:sameAs triples
└─────────────────────────────────────┘
│
┌──────────────┴──────────────────────┐
│ PostgreSQL transaction boundary │
└─────────────────────────────────────┘
Everything inside the dashed box runs inside a single PostgreSQL transaction. If anything fails, the whole pipeline rolls back — there is no half-updated vector store to clean up.
Prerequisites
| Requirement | Needed by |
|---|---|
CREATE EXTENSION vector; (pgvector) | All features except NL → SPARQL |
pg_ripple.embedding_api_url configured | RAG, embed_entities, hybrid search via text input |
pg_ripple.llm_endpoint configured | NL → SPARQL, the optional second stage of rag_context() |
| API key in environment variable | LLM and embedding endpoints — keys are never stored in the database |
pg_ripple.kge_enabled = on | KGE training and find_alignments() |
All AI features degrade gracefully when their dependencies are missing — they emit a WARNING and return zero rows rather than raising an ERROR. You can ship code that uses these features into a CI environment that does not have an LLM endpoint configured.
Security notes
- API keys are never stored in PostgreSQL. Configure the name of an environment variable (e.g.
pg_ripple.llm_api_key_env = 'OPENAI_API_KEY') and the extension reads the secret at call time. - Outbound HTTP calls require an allowlisted endpoint. Both LLM endpoints (registered via
llm_endpoint/embedding_api_url) and federated SPARQL services (registered viaregister_endpoint()) are checked against an allowlist on every call. This prevents Server-Side Request Forgery (SSRF). - PII in prompts.
rag_context()andsparql_from_nl()send graph excerpts to the configured LLM. Use named-graph row-level security (see Multi-Tenant Graphs) to keep tenant data out of prompts you do not control.
Where to go next
- Want to try it in five minutes? Cookbook: Chatbot grounded in a knowledge graph
- Already have a knowledge graph and want to add RAG? RAG Pipeline
- Merging customer data from multiple systems? Record Linkage
- Building a recommendation engine over a graph? Knowledge-Graph Embeddings
- Need a structured prompt for OpenAI structured outputs? Exporting and Sharing — JSON-LD framing
Vector Embeddings and Hybrid Search
Status: Available since v0.48.0 (VEC-01)
Requires: pgvector extension (required). An OpenAI-compatible embedding API is required for automatic embedding generation; manual float4[] insertion works without it.
SQL: pg_ripple.store_embedding(), pg_ripple.vector_search(), pg_ripple.hybrid_search()
HTTP: POST /sparql (supports pg:similar() inside SPARQL SELECT)
Degraded: Without pgvector, all embedding storage and pg:similar() queries fail at query time.
Vector search and graph queries answer different questions. Vector search is good at "things that look like X"; graph queries are good at "things in this exact relationship to X". Real-world questions usually need both — "prescriptions semantically similar to ibuprofen, taken by patients in the cardiology cohort". pg_ripple does both in one query.
This chapter is the practical reference. It covers:
- Storing vector embeddings alongside RDF entities.
- Building HNSW indexes with
pgvector. - Running pure similarity, pure SPARQL, and hybrid (RRF) search.
- The
pg:similar()SPARQL function — vector search inside a SPARQL pattern. - Federating to external vector stores (Weaviate, Qdrant, Pinecone, remote pgvector).
For higher-level decision-making, start with AI Overview. For an end-to-end RAG pipeline, see RAG Pipeline.
Setup
-- 1. pgvector is required.
CREATE EXTENSION IF NOT EXISTS vector;
-- 2. Point pg_ripple at an OpenAI-compatible embedding API.
ALTER SYSTEM SET pg_ripple.embedding_api_url = 'https://api.openai.com/v1';
ALTER SYSTEM SET pg_ripple.embedding_api_key_env = 'OPENAI_API_KEY';
ALTER SYSTEM SET pg_ripple.embedding_model = 'text-embedding-3-small';
ALTER SYSTEM SET pg_ripple.embedding_dimensions = 1536;
SELECT pg_reload_conf();
API keys are read from the named environment variable at call time and never stored in the database.
Embedding entities
Every entity that has an rdfs:label (or skos:prefLabel, or any of the configured label predicates) can be embedded:
-- Bulk-embed every labelled entity.
SELECT pg_ripple.embed_entities();
-- Or embed only one named graph.
SELECT pg_ripple.embed_entities('https://example.org/products');
-- Manually store a precomputed vector.
SELECT pg_ripple.store_embedding(
iri := '<https://example.org/aspirin>',
vec := '[...1536 floats...]'::vector,
model := 'text-embedding-3-small'
);
Set pg_ripple.use_graph_context = on to enrich each entity's embedding input with its 1-hop graph neighbourhood — this dramatically improves recall on entities whose labels alone are ambiguous ("Apple" the company vs the fruit).
Set pg_ripple.auto_embed = on to enqueue any newly-inserted labelled entity for background embedding. The merge-worker drains the queue.
The three search modes
1. Pure similarity
SELECT entity_iri, similarity
FROM pg_ripple.similar_entities('headache medication', k := 10);
Equivalent to the cosine-distance HNSW lookup you would write by hand against _pg_ripple.embeddings, but with text-to-vector handled for you.
2. Pure SPARQL
SELECT * FROM pg_ripple.sparql($$
SELECT ?drug WHERE {
?drug a <https://example.org/Drug> ;
<https://example.org/treats> <https://example.org/headache> .
}
$$);
3. Hybrid (Reciprocal Rank Fusion)
Hybrid search returns the fused ranking of (a) the SPARQL query's top-k matches and (b) the vector query's top-k matches, using Reciprocal Rank Fusion. This catches both the exact relational matches and the fuzzy semantic neighbours.
SELECT entity_iri, score
FROM pg_ripple.hybrid_search(
sparql := 'SELECT ?d WHERE { ?d a <https://example.org/Drug> }',
text := 'headache medication',
k := 10,
alpha := 0.5 -- 0.0 = pure SPARQL, 1.0 = pure vector
);
alpha is the relative weight given to the vector ranking. Tune by inspection; 0.5 is a sensible default.
pg:similar() — vector search inside SPARQL
The pg:similar() SPARQL function returns the cosine similarity between an entity and a free-text query. It is callable in BIND, FILTER, and ORDER BY.
PREFIX pg: <http://pg-ripple.io/fn/>
SELECT ?drug ?score WHERE {
?drug a <https://example.org/Drug> .
BIND(pg:similar(?drug, "anti-inflammatory") AS ?score)
FILTER(?score > 0.7)
}
ORDER BY DESC(?score)
LIMIT 20
This is the most expressive form — graph constraints and similarity score live in the same query plan, with no client-side post-processing.
Vector federation — Weaviate, Qdrant, Pinecone, remote pgvector
If you already operate a vector store and do not want to migrate, register it as a vector federation endpoint. hybrid_search() will blend results from local pg_ripple and the remote service.
SELECT pg_ripple.register_vector_endpoint(
url := 'https://qdrant.internal:6333',
api_type := 'qdrant'
);
SELECT pg_ripple.register_vector_endpoint('https://weaviate.internal:8080', 'weaviate');
SELECT pg_ripple.register_vector_endpoint('https://my-index.pinecone.io', 'pinecone');
Supported api_type values: pgvector, weaviate, qdrant, pinecone. Endpoints are stored in _pg_ripple.vector_endpoints and register_vector_endpoint() is idempotent.
A federated hybrid_search() call automatically fans out, gathers per-endpoint top-k results, and re-fuses them.
GUCs at a glance
| GUC | Default | Purpose |
|---|---|---|
pg_ripple.pgvector_enabled | on | Master switch. When off, vector functions emit a WARNING and return zero rows. |
pg_ripple.embedding_api_url | empty | OpenAI-compatible endpoint base URL |
pg_ripple.embedding_api_key_env | PG_RIPPLE_EMBEDDING_API_KEY | Env var name for the API key |
pg_ripple.embedding_model | text-embedding-3-small | Default model |
pg_ripple.embedding_dimensions | 1536 | Vector size |
pg_ripple.embedding_batch_size | 100 | API batch size for embed_entities() |
pg_ripple.use_graph_context | off | Include 1-hop neighbours in embedding input |
pg_ripple.auto_embed | off | Background-embed newly-inserted entities |
Tuning HNSW
The HNSW index built by pg_ripple is a standard pgvector index on _pg_ripple.embeddings(vector vector_cosine_ops). Tune it as you would any pgvector index:
| Setting | Default | Effect |
|---|---|---|
m (build) | 16 | Higher = better recall, more memory |
ef_construction (build) | 64 | Higher = better recall, slower build |
hnsw.ef_search (query) | 40 | Higher = better recall, slower queries |
See Vector Index Trade-offs for measured benchmarks across these knobs.
Graceful degradation
Every vector function in pg_ripple checks for pgvector at call time. If pgvector is missing, or pgvector_enabled = off, the function emits one WARNING and returns zero rows. Your application code can call vector functions in environments (CI, dev) that do not have pgvector without crashing.
See also
- AI Overview
- RAG Pipeline
- Knowledge-Graph Embeddings — graph-structural embeddings, complementary to text embeddings.
- Vector Index Trade-offs
- Embedding function reference
Further reading
- Blog: Vector + SPARQL Hybrid Search — combining graph traversal with similarity search
RAG Pipeline — rag_context()
pg_ripple v0.50.0 introduces pg_ripple.rag_context() — a single SQL function that assembles a retrieval-augmented generation (RAG) context string from your knowledge graph, ready for use as an LLM system prompt or user message.
Function Signature
pg_ripple.rag_context(
question TEXT,
k INT DEFAULT 10
) RETURNS TEXT
Parameters
| Parameter | Default | Description |
|---|---|---|
question | (required) | Natural-language question to retrieve context for |
k | 10 | Maximum number of entities to include in context |
How It Works
The function executes a five-step pipeline entirely inside PostgreSQL:
question TEXT
│
▼ Step 1: Embed question
HNSW cosine search on _pg_ripple.embeddings
│
▼ Step 2: Vector recall
Top-k most similar entities
│
▼ Step 3: SPARQL graph expansion
1-hop neighbourhood for each entity (labels, types, properties, neighbors)
│
▼ Step 4: Assemble context
JSON-LD fragments joined into a plain-text context string
│
▼ Step 5 (optional): NL→SPARQL execution
If pg_ripple.llm_endpoint is set, execute sparql_from_nl(question)
and append the SPARQL result set
│
▼
context TEXT
Prerequisites
rag_context() requires:
- pgvector extension installed (
CREATE EXTENSION vector) pg_ripple.pgvector_enabled = on(default:on)- Entities loaded with embeddings via
pg_ripple.embed_entities()or manually into_pg_ripple.embeddings
When pgvector is absent or the embeddings table is empty, the function degrades gracefully and returns an empty string with a WARNING rather than raising an ERROR.
Examples
Basic context retrieval
-- Retrieve context for a question (returns plain text)
SELECT pg_ripple.rag_context(
'What drugs are used to treat headaches?',
k := 5
);
Use the context as an LLM system prompt
-- Assemble context and pass to sparql_from_nl
SELECT pg_ripple.sparql_from_nl(
'What drugs treat headaches? Use the context: ' ||
pg_ripple.rag_context('What treats headaches?', k := 5)
);
End-to-end RAG with automatic SPARQL execution
When pg_ripple.llm_endpoint is configured, rag_context() automatically calls sparql_from_nl() and appends the SPARQL query result:
-- Set the LLM endpoint (once per session or in postgresql.conf)
SET pg_ripple.llm_endpoint = 'https://api.openai.com/v1';
SET pg_ripple.llm_api_key_env = 'OPENAI_API_KEY';
-- rag_context now includes vector context + SPARQL result
SELECT pg_ripple.rag_context('Who are the key authors in the knowledge graph?', k := 10);
Tuning
Adjusting k
Larger k returns more context but increases token usage. Start with k = 5–10 for most use cases.
-- Narrow context: k=3
SELECT pg_ripple.rag_context('What is aspirin?', k := 3);
-- Wide context: k=20
SELECT pg_ripple.rag_context('Give me a broad overview of drug interactions', k := 20);
Embedding freshness
Context quality depends on the embeddings being up to date. Run embed_entities() periodically or after bulk loads:
-- Re-embed all entities in the default graph
SELECT pg_ripple.embed_entities(graph_iri := NULL, model := NULL, batch_size := 100);
GUC settings
| GUC | Default | Effect |
|---|---|---|
pg_ripple.pgvector_enabled | on | Set to off to disable pgvector (returns empty context) |
pg_ripple.llm_endpoint | '' | When set, enables Step 5 (NL→SPARQL) |
pg_ripple.llm_model | 'gpt-4o' | LLM model name for Step 5 |
Output Format
The context string has the following structure for each entity:
Entity: https://example.org/aspirin
Label: aspirin
Context:
{
"label": "aspirin",
"types": ["https://pharma.example/Drug"],
"properties": [
{"predicate": "...", "object": "..."}
],
"neighbors": ["https://pharma.example/Ibuprofen"]
}
---
Entity: https://example.org/ibuprofen
...
When Step 5 executes a SPARQL query, the result is appended:
---
SPARQL Result for: What treats headaches?
[{"?drug": "<https://pharma.example/aspirin>"}]
Graceful Degradation
| Condition | Behaviour |
|---|---|
| pgvector not installed | WARNING + empty string |
pgvector_enabled = off | WARNING + empty string |
| Embeddings table empty | Empty string (no WARNING) |
llm_endpoint not set | Steps 1–4 only; no SPARQL execution |
Natural Language to SPARQL
pg_ripple v0.49.0 adds pg_ripple.sparql_from_nl(), a SQL function that converts a plain-English question into a SPARQL SELECT query using any configured OpenAI-compatible LLM endpoint.
Quick Start
-- Configure an OpenAI-compatible endpoint
SET pg_ripple.llm_endpoint = 'https://api.openai.com/v1';
SET pg_ripple.llm_model = 'gpt-4o';
-- API key is read from the named environment variable, not stored inline:
SET pg_ripple.llm_api_key_env = 'OPENAI_API_KEY'; -- default
-- Generate a SPARQL query from plain English
SELECT pg_ripple.sparql_from_nl('List all people and their email addresses');
SELECT ?person ?email WHERE {
?person a <http://xmlns.com/foaf/0.1/Person> .
?person <http://xmlns.com/foaf/0.1/mbox> ?email .
}
The returned string is a valid, parseable SPARQL 1.1 query that you can pass directly to pg_ripple.sparql().
Configuring the LLM Endpoint
pg_ripple supports any OpenAI-compatible /v1/chat/completions API, including:
| Provider | Example llm_endpoint |
|---|---|
| OpenAI | https://api.openai.com/v1 |
| Azure OpenAI | https://<resource>.openai.azure.com/openai/deployments/<deployment> |
| Ollama (local) | http://localhost:11434/v1 |
| vLLM | http://localhost:8000/v1 |
| Together AI | https://api.together.xyz/v1 |
GUC Parameters
| GUC | Type | Default | Description |
|---|---|---|---|
pg_ripple.llm_endpoint | string | '' (disabled) | Base URL for the OpenAI-compatible API. Set to 'mock' for testing without a real LLM. |
pg_ripple.llm_model | string | gpt-4o | Model identifier passed in the request body. |
pg_ripple.llm_api_key_env | string | PG_RIPPLE_LLM_API_KEY | Name of the environment variable holding the API key. The key is never stored in the database. |
pg_ripple.llm_include_shapes | bool | on | When on, active SHACL shapes are included in the LLM prompt as schema context. |
Setting the API Key Securely
pg_ripple never stores the API key in the database. Instead, it reads the value from a named environment variable at call time:
# In your shell or service environment:
export PG_RIPPLE_LLM_API_KEY="sk-..."
-- Tell pg_ripple which environment variable to read (default is PG_RIPPLE_LLM_API_KEY):
ALTER SYSTEM SET pg_ripple.llm_api_key_env = 'PG_RIPPLE_LLM_API_KEY';
SELECT pg_reload_conf();
How It Works
For each call to sparql_from_nl(question):
- VoID context: pg_ripple builds a compact description of the graph — the predicate count and the most-frequent predicates — as context for the LLM.
- SHACL context (when
llm_include_shapes = on): active SHACL shapes are appended to the prompt. - Few-shot examples: any rows in
_pg_ripple.llm_examplesare included as question/SPARQL pairs. - LLM call: the prompt is sent to
/v1/chat/completionswithtemperature = 0.0. - Extraction: the SPARQL string is extracted from the response and stripped of any markdown fencing.
- Validation:
spargebraparses the query. If parsing fails, PT702 is raised so callers can handle the error.
Adding Few-Shot Examples
Few-shot examples improve accuracy significantly for domain-specific vocabularies:
SELECT pg_ripple.add_llm_example(
'Find all proteins that interact with insulin',
'SELECT ?protein WHERE {
?protein <https://bio.ontology.org/interactsWith>
<https://bio.ontology.org/Insulin> .
}'
);
SELECT pg_ripple.add_llm_example(
'Which drugs target EGFR?',
'SELECT ?drug WHERE {
?drug <https://bio.ontology.org/targets>
<https://bio.ontology.org/EGFR> .
}'
);
Examples are stored in _pg_ripple.llm_examples and automatically included in every subsequent sparql_from_nl() call. Re-calling add_llm_example() with the same question updates the stored example (upsert behaviour).
Testing Without a Real LLM
Set pg_ripple.llm_endpoint = 'mock' to use the built-in test mock. The mock bypasses the HTTP call and returns a simple SELECT ?s ?p ?o WHERE { ?s ?p ?o } LIMIT 10 query, allowing you to test downstream code (result processing, SPARQL execution) without an external LLM dependency.
SET pg_ripple.llm_endpoint = 'mock';
SELECT pg_ripple.sparql_from_nl('anything') LIKE 'SELECT%'; -- t
Error Handling
| Code | Condition | Remedy |
|---|---|---|
| PT700 | llm_endpoint is empty or the HTTP call fails | Set a valid endpoint URL; check network access and API key |
| PT701 | The LLM response did not contain a SPARQL query | Improve the prompt with few-shot examples; switch to a more capable model |
| PT702 | The generated SPARQL could not be parsed | Add a few-shot example for this question pattern; or use a model fine-tuned for SPARQL |
Pipeline Pattern
A common pattern is to generate a query, log it, and execute it in one step:
DO $$
DECLARE
sparql_q TEXT;
result TEXT;
BEGIN
sparql_q := pg_ripple.sparql_from_nl(
'Find all companies founded after 2010 with more than 500 employees'
);
RAISE NOTICE 'Generated SPARQL: %', sparql_q;
-- Execute the generated query
SELECT json_agg(row_to_json(t))::text
INTO result
FROM pg_ripple.sparql(sparql_q) t;
RAISE NOTICE 'Results: %', result;
END;
$$;
Further reading
- Blog: Natural Language to SPARQL — how pg_ripple translates plain-English questions into SPARQL queries
Knowledge-Graph Embeddings (KGE)
A knowledge-graph embedding is a vector representation of an entity learned from the structure of the graph — its relationships — rather than from any text describing the entity. KGEs power three jobs that text embeddings do badly:
- Entity alignment across graphs — match
Applein graph A toApple Inc.in graph B based on shared neighbours, not on the strings. - Link prediction — score plausible edges that are not in the graph today (recommendations, cold-start, schema completion).
- Cluster discovery — find groups of structurally-similar entities even when they have no labels in common.
pg_ripple ships two well-known KGE models, TransE and RotatE, with a unified SQL interface and an HNSW index for fast nearest-neighbour search.
Available since v0.55.0 (
pg_ripple.kge_enabledGUC). Requirespgvector.
How TransE and RotatE differ
| Model | Geometric idea | Best for | Cost |
|---|---|---|---|
| TransE | A relation is a translation in vector space: head + relation ≈ tail | Hierarchies, simple relational patterns | Cheap; trains in minutes on millions of triples |
| RotatE | A relation is a rotation in complex vector space | Symmetric, antisymmetric, inverse, and composition patterns | ~2× the cost of TransE; better quality on dense graphs |
When in doubt, start with TransE. If your graph has lots of inverse or symmetric relations (spouse, siblingOf, coAuthor), switch to RotatE.
Quick start
-- 1. Enable the feature.
SET pg_ripple.kge_enabled = on;
-- 2. Train a model on the entire store.
SELECT pg_ripple.kge_train(
model := 'TransE',
dimensions := 128,
epochs := 100,
learning_rate:= 0.01,
margin := 1.0
);
-- 3. Inspect the trained vectors.
SELECT entity_iri, vector
FROM _pg_ripple.kge_embeddings
LIMIT 5;
-- 4. Use the vectors for entity alignment.
SELECT * FROM pg_ripple.find_alignments(
source_graph := 'https://example.org/g1',
target_graph := 'https://example.org/g2',
threshold := 0.9
);
Choosing hyperparameters
| Parameter | Default | Tuning advice |
|---|---|---|
dimensions | 128 | 50 for small graphs (< 100 K entities), 200–400 for very large or dense graphs |
epochs | 100 | Until validation loss plateaus; check every 25 epochs |
learning_rate | 0.01 | Halve it if loss oscillates; double it if loss decreases too slowly |
margin | 1.0 | TransE only; the margin between positive and negative triple scores |
batch_size | 1024 | Larger batches give smoother gradients but use more memory |
Training writes its loss curve to the PostgreSQL log so you can monitor convergence in real time.
Three things people get wrong
- Embedding too early. KGEs need a connected graph. If you train before loading
owl:sameAsand inverse properties, the model learns isolated islands. Always materialise built-in RDFS / OWL inference (pg_ripple.infer('rdfs'),pg_ripple.infer('owl-rl')) before training. - Comparing across models. A vector trained with TransE 128-dim is meaningless to a model trained with RotatE 256-dim. The
_pg_ripple.kge_embeddingstable tracks(model, dimensions)per row; queries automatically scope to one model. - Forgetting to re-train. KGE quality drifts as the graph grows. Schedule a retrain whenever the entity count grows by ~25 %, or weekly for high-velocity ingestion.
Use case: link prediction
-- Score the plausibility of a candidate triple.
SELECT pg_ripple.kge_score(
head := '<https://example.org/Alice>',
relation := '<https://example.org/worksAt>',
tail := '<https://example.org/MIT>'
);
-- Returns a real-valued score; higher = more plausible.
-- Find the top-10 most plausible employers for Alice.
SELECT tail, score
FROM pg_ripple.kge_predict_tails(
head := '<https://example.org/Alice>',
relation := '<https://example.org/worksAt>',
k := 10
);
This is the foundation for cold-start recommendations and schema-completion workflows.
Use case: cross-graph alignment
find_alignments() is a thin wrapper that performs an HNSW cosine search of every entity in source_graph against every entity in target_graph, returning pairs above a threshold. The output is shaped exactly like suggest_sameas(), so it plugs into the Record Linkage pipeline unchanged.
SELECT s1, s2, similarity
FROM pg_ripple.find_alignments(
source_graph := 'https://wikidata.example/',
target_graph := 'https://internal-kb.example/',
threshold := 0.92
)
ORDER BY similarity DESC;
Storage and indexing
| Object | Purpose |
|---|---|
_pg_ripple.kge_embeddings(entity_id, model, dimensions, vector) | One row per (entity, model). Vector type is pgvector. |
HNSW index on (model, vector vector_cosine_ops) | Sub-millisecond top-k cosine queries |
_pg_ripple.kge_models(name, dimensions, trained_at, loss) | One row per training run, for monitoring |
A 1 M-entity graph with 128-dim TransE embeddings occupies ~512 MB in _pg_ripple.kge_embeddings. Plan disk accordingly.
When not to use KGE
- Your graph is small (< 10 K entities). TransE will overfit; text embeddings are simpler.
- Your entities have no informative relationships. KGE has nothing to learn from.
- You need explainable scores. KGE is a black box; SHACL constraints and
owl:sameAsare the right answer for regulator-facing decisions.
See also
- Record Linkage — uses
find_alignments()for cross-graph entity resolution. - Vector & Hybrid Search — text-embedding cousin of KGE.
- Reasoning & Inference — materialise inference before training to densify the graph.
Record Linkage and Entity Resolution
Status: Available since v0.67.0 (ER-01)
Requires: pgvector extension for embedding-based candidate generation (optional — rule-based linking works without it). pg_ripple_http optional for REST access.
SQL: pg_ripple.find_entity_candidates(), pg_ripple.merge_entities(), pg_ripple.entity_resolution_pipeline()
Degraded: Without pgvector, vector-similarity candidate generation is unavailable; SHACL hard-veto rules and owl:sameAs canonicalization still work.
Other names for this problem: entity resolution, deduplication, master data management (MDM), identity resolution, fuzzy matching. They all mean: find records that refer to the same real-world thing, and merge them safely.
Record linkage is one of the most consequential and difficult problems in data management. A hospital merging two patient databases, a bank consolidating customer records, a retailer unifying online and in-store profiles — all of them must answer the same question: do these two rows describe the same person?
pg_ripple ships a complete record-linkage stack inside PostgreSQL. It combines four techniques that traditionally each lived in a separate tool:
- Knowledge-graph embeddings for fast, fuzzy candidate generation.
- Vector similarity over text embeddings for semantic matching.
- SHACL hard rules to veto unsafe merges (e.g. "never merge two patients with different blood types").
owl:sameAscanonicalization so the rest of your queries see one entity, not two.
Each step is a single SQL function call. Every decision is auditable.
The pipeline
Source A triples Source B triples
│ │
└────────┬─────────────────┘
│
▼
┌─────────────────────────────┐
│ 1. Generate candidate pairs │
│ suggest_sameas() │ ← KGE or text embeddings + HNSW
│ or find_alignments() │
└──────────────┬──────────────┘
│ (s1, s2, similarity) rows
▼
┌─────────────────────────────┐
│ 2. Apply hard rules │
│ SHACL shapes block pairs │ ← e.g. sh:disjoint on bloodType
│ that violate constraints │
└──────────────┬──────────────┘
│ filtered candidate rows
▼
┌─────────────────────────────┐
│ 3. Human review (optional) │
│ surface candidates │ ← UI / approval queue
│ with similarity, source, │
│ and rule trail │
└──────────────┬──────────────┘
│ accepted pairs
▼
┌─────────────────────────────┐
│ 4. Apply owl:sameAs │
│ apply_sameas_candidates │ ← inserts both directions
└──────────────┬──────────────┘
│
▼
┌─────────────────────────────┐
│ 5. Canonicalize on read │
│ sameas_reasoning = on │ ← SPARQL & Datalog see one entity
│ audit_log captures all │
│ UPDATEs │
└─────────────────────────────┘
A worked example: linking customer records across two systems
Suppose you operate two retail brands, each with its own customer database. You want a unified view without losing brand-specific history.
Step 1: Load both sources into named graphs
Using named graphs preserves provenance — you can always tell which record came from where.
-- Source A
SELECT pg_ripple.load_turtle_into_graph(
'https://example.org/source-a',
$TTL$
@prefix ex: <https://example.org/> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
ex:a/c1 foaf:name "Robert Smith" ; foaf:mbox <mailto:rob.smith@example.com> .
ex:a/c2 foaf:name "Jane Doe" ; foaf:mbox <mailto:jdoe@example.com> .
$TTL$);
-- Source B
SELECT pg_ripple.load_turtle_into_graph(
'https://example.org/source-b',
$TTL$
@prefix ex: <https://example.org/> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
ex:b/c1 foaf:name "Bob Smith" ; foaf:mbox <mailto:rob.smith@example.com> .
ex:b/c2 foaf:name "Jane Q. Doe" ; foaf:mbox <mailto:jane.doe@example.com> .
$TTL$);
Step 2: Generate candidate pairs
You have two options for candidate generation. Pick whichever fits your data.
Option A — text embeddings. Best when you have rich textual descriptions (names, addresses, product titles).
-- Embed every customer using their label.
SELECT pg_ripple.embed_entities();
-- Suggest sameAs pairs at a permissive threshold first.
SELECT s1, s2, similarity
FROM pg_ripple.suggest_sameas(threshold := 0.85)
ORDER BY similarity DESC
LIMIT 50;
Option B — knowledge-graph embeddings (KGE). Best when entities have rich relational structure (a customer's purchases, addresses, devices).
SET pg_ripple.kge_enabled = on;
SELECT pg_ripple.kge_train(model := 'TransE', epochs := 100);
SELECT * FROM pg_ripple.find_alignments(
source_graph := 'https://example.org/source-a',
target_graph := 'https://example.org/source-b',
threshold := 0.85
);
You can run both and union the results — text and KGE embeddings catch different mistakes.
Step 3: Block unsafe merges with SHACL
Define hard rules that say "these two records cannot be the same entity". The classic example is contradictory immutable attributes — different birth dates, different blood types, different national IDs.
SELECT pg_ripple.load_shacl($TTL$
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <https://example.org/> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
ex:CustomerSafetyShape a sh:NodeShape ;
sh:targetClass foaf:Person ;
# If two people share owl:sameAs, their birth dates must agree.
sh:property [
sh:path ex:birthDate ;
sh:disjoint ex:contradictoryBirthDate ;
] .
$TTL$);
When pg_ripple.shacl_mode = 'sync', an apply_sameas_candidates() call that would violate this shape is rejected before commit.
Step 4: Apply the surviving candidates
-- Apply at a strict threshold for auto-merge.
SELECT pg_ripple.apply_sameas_candidates(min_similarity := 0.95);
-- The remaining 0.85–0.95 band goes to a review queue.
CREATE TABLE review_queue AS
SELECT s1, s2, similarity
FROM pg_ripple.suggest_sameas(0.85)
WHERE similarity < 0.95;
A human reviewer can then approve or reject pairs from review_queue; approved pairs are applied with pg_ripple.insert_triple(s1, '<http://www.w3.org/2002/07/owl#sameAs>', s2) plus its inverse.
Step 5: Query the unified graph
With pg_ripple.sameas_reasoning = on (default), every SPARQL and Datalog query sees the merged entity transparently. There is no separate "golden record" table to maintain.
-- Returns Robert/Bob Smith's purchases from BOTH source databases as one customer.
SELECT * FROM pg_ripple.sparql($$
SELECT ?purchase WHERE {
<https://example.org/a/c1>
<https://example.org/purchased> ?purchase .
}
$$);
Tuning thresholds: precision vs. recall
| Threshold band | Precision | Recall | Recommended action |
|---|---|---|---|
| ≥ 0.98 | Very high | Low | Auto-apply with no review |
| 0.95 – 0.98 | High | Medium | Auto-apply, sample-audit weekly |
| 0.90 – 0.95 | Medium | High | Review queue for human approval |
| 0.85 – 0.90 | Low | Very high | Surface only for exploratory analysis |
| < 0.85 | Very low | Near-complete | Avoid — too noisy |
The right band depends on the cost of each error type. In healthcare a wrong merge can endanger a patient — push the auto-merge threshold to 0.99 and route the rest to clinicians. In ad-tech a missed merge means a less-personalised ad — a 0.92 auto-merge is fine.
Auditability
Every record-linkage action leaves a trail. Three GUCs enable the audit chain:
| GUC | What it captures |
|---|---|
pg_ripple.audit_log_enabled = on | All SPARQL UPDATEs (role, txid, query text) — see Audit Log |
pg_ripple.prov_enabled = on | A prov:Activity triple per bulk-load — see Temporal & Provenance |
| (RDF-star quoted triples) | Per-fact confidence, source, timestamp — see Storing Knowledge |
Together these three give you the regulator-defensible trail that pure-ML pipelines cannot.
Why this is hard to get right elsewhere
Most record-linkage systems force a choice between neural (high-recall, opaque) and symbolic (auditable, low-recall). pg_ripple lets you compose both. The extended rationale is in plans/neuro-symbolic-record-linkage.md — that document is the strategic background for everything on this page.
Functions reference
| Function | Purpose | Documented in |
|---|---|---|
embed_entities() | Compute text embeddings for all labelled entities | Vector & Hybrid Search |
kge_train() | Train a TransE/RotatE entity embedding | Knowledge-Graph Embeddings |
suggest_sameas(threshold) | Return candidate pairs from text embeddings | This page |
find_alignments(src, tgt, threshold) | Return candidate pairs from KGE | Knowledge-Graph Embeddings |
apply_sameas_candidates(min_similarity) | Insert owl:sameAs for accepted pairs | This page |
load_shacl() | Load hard-rule shapes that veto unsafe merges | Validating Data Quality |
point_in_time(ts) | Replay record-linkage decisions as of a past timestamp | Temporal & Provenance |
Further reading
- Blog: Neuro-Symbolic Entity Resolution — combining embeddings with symbolic rules for deduplication
- Blog: owl:sameAs Entity Resolution — how equivalence canonicalization works
GraphRAG End-to-End
Microsoft GraphRAG is a popular open-source pipeline that turns unstructured documents into a structured knowledge graph of entities, relationships, text units, and community summaries. pg_ripple is a first-class storage and query backend for GraphRAG: you can ingest GraphRAG output directly, enrich it with Datalog rules, validate it with SHACL, and export it back as Parquet for downstream tools — all without leaving PostgreSQL.
This page is the canonical end-to-end guide. It assumes you already have a working GraphRAG output (the standard entities.parquet, relationships.parquet, text_units.parquet files).
What pg_ripple adds to GraphRAG
| Stock GraphRAG | With pg_ripple |
|---|---|
| Output stored in Parquet on disk | Output stored as queryable RDF in PostgreSQL |
| Search via custom Python pipeline | Search via SPARQL, SHACL, vector + RAG |
| No incremental update — rebuild the whole graph | Incremental: load new documents into a named graph and re-infer |
| Quality issues surface only at query time | SHACL catches missing labels, dangling relationships, etc. on insert |
| One process owns the data | Multiple readers, transactional writers, full ACID |
The data model
GraphRAG is mapped to four RDF classes:
| Class | What it represents |
|---|---|
gr:Entity | A named entity (person, organisation, location, event, …) |
gr:Relationship | A directed, weighted edge between two entities |
gr:TextUnit | A chunk of source text mentioning entities |
gr:Community | A detected cluster of related entities |
gr:CommunityReport | An LLM-generated summary of a community |
Key properties:
| Property | Domain | Range |
|---|---|---|
gr:title | gr:Entity | xsd:string |
gr:type | gr:Entity | xsd:string (PERSON, ORG, …) |
gr:description | any | xsd:string |
gr:source | gr:Relationship | gr:Entity |
gr:target | gr:Relationship | gr:Entity |
gr:weight | gr:Relationship | xsd:float |
gr:text | gr:TextUnit | xsd:string |
gr:tokenCount | gr:TextUnit | xsd:integer |
The full ontology, including all SHACL shapes, lives at examples/graphrag_byog.sql.
End-to-end pipeline
GraphRAG Python pipeline
│
▼
entities.parquet │
relationships.parquet │ Step 1: import
text_units.parquet │
│ │
▼ │
pg_ripple.import_graphrag_parquet() ▼
──────────────────────────────────────────
│
▼
Step 2: enrich with Datalog
pg_ripple.load_rules_builtin('graphrag-enrichment')
pg_ripple.infer('graphrag-enrichment')
→ derives gr:coworker, gr:collaborates,
gr:indirectReport, gr:relatedOrg
│
▼
Step 3: validate with SHACL
pg_ripple.load_shacl(...)
pg_ripple.shacl_validate()
→ catches missing labels, dangling references
│
▼
Step 4: query
- SPARQL for relationship walks
- rag_context() for grounded LLM prompts
- hybrid_search() for similarity + structure
│
▼
Step 5 (optional): export back to Parquet
pg_ripple.export_graphrag_entities()
pg_ripple.export_graphrag_relationships()
→ for downstream Microsoft GraphRAG tooling
Step 1 — Import GraphRAG output
-- Register the gr: prefix once.
SELECT pg_ripple.register_prefix('gr', 'https://graphrag.org/ns/');
-- Import the three core files. Each file becomes triples in the named graph.
SELECT pg_ripple.import_graphrag_parquet(
entities_path := '/data/graphrag/entities.parquet',
relationships_path:= '/data/graphrag/relationships.parquet',
text_units_path := '/data/graphrag/text_units.parquet',
target_graph := 'https://example.org/kb-2026-04'
);
-- Returns the count of triples inserted.
For ad-hoc loading, use load_turtle() directly:
SELECT pg_ripple.load_turtle($TTL$
@prefix gr: <https://graphrag.org/ns/> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
<https://example.org/entity/alice>
rdf:type gr:Entity ;
gr:title "Alice" ;
gr:type "PERSON" .
$TTL$);
Step 2 — Enrich with Datalog
The bundled graphrag-enrichment rule set derives four useful relationships from the raw GraphRAG output:
| Derived property | Meaning |
|---|---|
gr:coworker | Two entities both have relationships targeting the same organisation |
gr:collaborates | Two entities co-occur in the same text unit |
gr:indirectReport | Transitive closure of gr:manages |
gr:relatedOrg | Two organisations share an entity bridge |
SELECT pg_ripple.load_rules_builtin('graphrag-enrichment');
SELECT pg_ripple.infer('graphrag-enrichment');
-- Returns the count of derived triples (source = 1).
You can write your own rules — see Reasoning & Inference — but the bundled set is a good starting point.
Step 3 — Validate with SHACL
GraphRAG output is LLM-generated and can have quality issues: entities without titles, relationships pointing at deleted entities, weights outside [0, 1]. Catch them with SHACL:
SELECT pg_ripple.load_shacl($TTL$
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix gr: <https://graphrag.org/ns/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
gr:EntityShape a sh:NodeShape ;
sh:targetClass gr:Entity ;
sh:property [ sh:path gr:title ; sh:minCount 1 ; sh:datatype xsd:string ] ;
sh:property [ sh:path gr:type ; sh:in ( "PERSON" "ORG" "GEO" "EVENT" ) ] .
gr:RelationshipShape a sh:NodeShape ;
sh:targetClass gr:Relationship ;
sh:property [ sh:path gr:source ; sh:minCount 1 ; sh:class gr:Entity ] ;
sh:property [ sh:path gr:target ; sh:minCount 1 ; sh:class gr:Entity ] ;
sh:property [ sh:path gr:weight ; sh:datatype xsd:float ] .
$TTL$);
-- Validate everything currently in the store.
SELECT focus_node, message FROM pg_ripple.shacl_validate();
Set pg_ripple.shacl_mode = 'sync' to reject offending inserts at write time, or 'async' to route them to a dead-letter queue. See Validating Data Quality.
Step 4 — Query the enriched graph
GraphRAG-specific queries pair naturally with rag_context():
-- Find the LLM-ready context for a question.
SELECT pg_ripple.rag_context(
'Who collaborates with Alice on machine learning?',
k := 10
);
For purely structural queries, use SPARQL directly:
-- All co-workers of Alice's co-workers (a 2-hop search).
SELECT * FROM pg_ripple.sparql($$
PREFIX gr: <https://graphrag.org/ns/>
SELECT DISTINCT ?friend WHERE {
<https://example.org/entity/alice> gr:coworker/gr:coworker ?friend .
FILTER(?friend != <https://example.org/entity/alice>)
}
$$);
Step 5 — Round-trip export
If you need to feed an enriched graph back into Microsoft GraphRAG's Python tools:
SELECT pg_ripple.export_graphrag_entities('', '/tmp/entities.parquet');
SELECT pg_ripple.export_graphrag_relationships('', '/tmp/relationships.parquet');
Pass '' for the default graph or a named-graph IRI to scope the export.
Operational tips
- Use a named graph per ingestion run (
https://example.org/kb-2026-04). When a re-ingest runs,clear_graph()drops the old version atomically. - Run inference after loading, not interleaved. Inference is bulk-friendly and parallelisable; per-row inference is not.
- Materialise embeddings after enrichment. Datalog-derived properties improve KGE quality and
rag_context()recall.
See also
- AI Overview
- Reasoning & Inference
- Validating Data Quality
- Cookbook: Chatbot grounded in a knowledge graph
- GraphRAG function reference
- GraphRAG ontology reference
Further reading
- Blog: GraphRAG Knowledge Export — building a Microsoft GraphRAG pipeline with pg_ripple
Vector Federation
pg_ripple v0.28.0 introduces vector federation: registered external vector services (Weaviate, Qdrant, Pinecone, or another pgvector instance) can be queried alongside the local triple store.
Why Vector Federation?
Organizations often have multiple vector databases: a Qdrant cluster holding product embeddings, a Weaviate instance for customer data, and pg_ripple for the knowledge graph. Vector federation lets you blend all three sources in a single pg_ripple.hybrid_search() call without moving data.
Supported Service Types
api_type | Notes |
|---|---|
pgvector | Remote PostgreSQL + pgvector instance |
weaviate | Weaviate v1 GraphQL endpoint (/v1/graphql) |
qdrant | Qdrant v1 REST API (/collections/{name}/points/search) |
pinecone | Pinecone v1 REST API (/query) |
Registering an Endpoint
-- Register a Qdrant instance.
SELECT pg_ripple.register_vector_endpoint(
'https://qdrant.internal:6333',
'qdrant'
);
-- Register a Weaviate instance.
SELECT pg_ripple.register_vector_endpoint(
'https://weaviate.internal:8080',
'weaviate'
);
-- Register a Pinecone index.
SELECT pg_ripple.register_vector_endpoint(
'https://my-index-abc123.pinecone.io',
'pinecone'
);
Registrations are idempotent — calling register_vector_endpoint() twice with the same URL is safe.
Endpoints are stored in _pg_ripple.vector_endpoints:
SELECT url, api_type, enabled, registered_at
FROM _pg_ripple.vector_endpoints;
Timeout Configuration
The federation timeout applies to all outbound HTTP calls to external vector services:
-- Set 10-second timeout.
SET pg_ripple.vector_federation_timeout_ms = 10000;
Security Considerations
- Endpoints are stored in an internal table accessible only to
pg_rippleschema users. - HTTPS is recommended for all external endpoints.
- API keys for external services are not stored by pg_ripple; configure them via environment variables or secrets management in your application layer.
- The
pg_ripple.vector_federation_timeout_msGUC prevents runaway federated queries from blocking the database.
Catalog Table Reference
_pg_ripple.vector_endpoints
| Column | Type | Description |
|---|---|---|
url | TEXT | Endpoint base URL (primary key) |
api_type | TEXT | One of: pgvector, weaviate, qdrant, pinecone |
enabled | BOOLEAN | Whether this endpoint is active |
registered_at | TIMESTAMPTZ | When the endpoint was registered |
GUC Reference
| GUC | Default | Description |
|---|---|---|
pg_ripple.vector_federation_timeout_ms | 5000 | HTTP timeout for federated vector endpoint queries (milliseconds) |
AI Agent Integration (LangChain, LlamaIndex, and Friends)
pg_ripple's AI capabilities — vector search, rag_context(), sparql_from_nl(), graph expansion — are exposed as plain SQL functions. Any framework that can call PostgreSQL can use them. No SDK required.
This page shows how to wire pg_ripple into the two most common Python agent frameworks, plus a framework-agnostic tool-calling pattern.
The mental model
An AI agent loop looks like this:
user question
│
▼
LLM decides: do I need to look something up?
│
├── yes → call a tool (pg_ripple function via SQL) → get context
│ └── loop back to LLM with context
│
└── no → generate final answer
pg_ripple is the tool. The LLM calls it via rag_context(), sparql_from_nl(), or a custom SQL wrapper — whichever your framework exposes.
Prerequisites
-- Configure the embedding endpoint (once per database).
ALTER SYSTEM SET pg_ripple.embedding_api_url = 'https://api.openai.com/v1';
ALTER SYSTEM SET pg_ripple.embedding_api_key_env = 'OPENAI_API_KEY';
ALTER SYSTEM SET pg_ripple.embedding_model = 'text-embedding-3-small';
ALTER SYSTEM SET pg_ripple.llm_endpoint = 'https://api.openai.com/v1';
ALTER SYSTEM SET pg_ripple.llm_api_key_env = 'OPENAI_API_KEY';
ALTER SYSTEM SET pg_ripple.llm_model = 'gpt-4o';
SELECT pg_reload_conf();
-- Embed your knowledge graph (once, then incrementally as new triples arrive).
SELECT pg_ripple.embed_entities();
LangChain
The cleanest integration is a Tool that calls rag_context() and a second Tool that calls sparql_from_nl() for fact-style answers.
from langchain.tools import Tool
from langchain_openai import ChatOpenAI
from langchain.agents import AgentExecutor, create_openai_tools_agent
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
import psycopg
DB_URL = "postgresql://..."
def graph_context(question: str) -> str:
"""Retrieve graph context for a question from the knowledge graph."""
with psycopg.connect(DB_URL) as conn:
cur = conn.cursor()
cur.execute("SELECT pg_ripple.rag_context(%s, 8)", (question,))
return cur.fetchone()[0]
def graph_query(sparql: str) -> str:
"""Execute a SPARQL query against the knowledge graph and return results as JSON."""
with psycopg.connect(DB_URL) as conn:
cur = conn.cursor()
cur.execute("SELECT jsonb_agg(row_to_json(r)) FROM pg_ripple.sparql(%s) r", (sparql,))
result = cur.fetchone()[0]
return str(result) if result else "No results."
def nl_to_sparql_and_run(question: str) -> str:
"""Convert a natural-language question to SPARQL and run it."""
with psycopg.connect(DB_URL) as conn:
cur = conn.cursor()
cur.execute(
"SELECT jsonb_agg(row_to_json(r)) FROM "
"pg_ripple.sparql(pg_ripple.sparql_from_nl(%s)) r",
(question,)
)
result = cur.fetchone()[0]
return str(result) if result else "No results."
tools = [
Tool(name="graph_context", func=graph_context,
description="Retrieve rich graph context for open-ended questions."),
Tool(name="nl_to_sparql_query", func=nl_to_sparql_and_run,
description="Answer precise factual questions using SPARQL auto-generation."),
]
llm = ChatOpenAI(model="gpt-4o", temperature=0)
prompt = ChatPromptTemplate.from_messages([
("system", "You are a helpful assistant with access to a knowledge graph. "
"Use 'graph_context' for broad questions and 'nl_to_sparql_query' for specific facts."),
("human", "{input}"),
MessagesPlaceholder("agent_scratchpad"),
])
agent = create_openai_tools_agent(llm, tools, prompt)
executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
result = executor.invoke({"input": "Which drugs interact with insulin and should be avoided?"})
print(result["output"])
LlamaIndex
LlamaIndex's FunctionTool maps directly onto pg_ripple SQL calls.
from llama_index.core.tools import FunctionTool
from llama_index.core.agent import ReActAgent
from llama_index.llms.openai import OpenAI
import psycopg
DB_URL = "postgresql://..."
def retrieve_graph_context(question: str) -> str:
"""
Search the knowledge graph for entities and relationships relevant to the question.
Returns a structured text block suitable for LLM prompting.
"""
with psycopg.connect(DB_URL) as conn:
cur = conn.cursor()
cur.execute("SELECT pg_ripple.rag_context(%s, 8)", (question,))
return cur.fetchone()[0]
def sparql_fact_lookup(question: str) -> str:
"""
Answer a precise factual question by auto-generating and executing a SPARQL query.
Use this for questions like 'how many', 'list all', 'who is', 'what is'.
"""
with psycopg.connect(DB_URL) as conn:
cur = conn.cursor()
cur.execute(
"SELECT jsonb_agg(row_to_json(r)) FROM "
"pg_ripple.sparql(pg_ripple.sparql_from_nl(%s)) r",
(question,)
)
result = cur.fetchone()[0]
return str(result) if result else "Query returned no results."
tools = [
FunctionTool.from_defaults(fn=retrieve_graph_context),
FunctionTool.from_defaults(fn=sparql_fact_lookup),
]
agent = ReActAgent.from_tools(tools, llm=OpenAI(model="gpt-4o"), verbose=True)
response = agent.chat("What are the known side effects of combining metformin with insulin?")
print(response)
Framework-agnostic: OpenAI tool-calling
If you are not using LangChain or LlamaIndex, use OpenAI's tool-calling API directly. This works with any framework or plain Python.
import json, psycopg, openai
DB_URL = "postgresql://..."
client = openai.OpenAI()
TOOLS = [
{
"type": "function",
"function": {
"name": "rag_context",
"description": "Retrieve relevant context from the knowledge graph for an open-ended question.",
"parameters": {
"type": "object",
"properties": {
"question": {"type": "string"},
"k": {"type": "integer", "default": 8,
"description": "Number of entities to retrieve."}
},
"required": ["question"],
},
},
},
{
"type": "function",
"function": {
"name": "sparql_fact_lookup",
"description": "Answer a precise factual question using auto-generated SPARQL.",
"parameters": {
"type": "object",
"properties": {"question": {"type": "string"}},
"required": ["question"],
},
},
},
]
def dispatch(tool_name: str, args: dict) -> str:
with psycopg.connect(DB_URL) as conn:
cur = conn.cursor()
if tool_name == "rag_context":
cur.execute("SELECT pg_ripple.rag_context(%s, %s)", (args["question"], args.get("k", 8)))
return cur.fetchone()[0]
elif tool_name == "sparql_fact_lookup":
cur.execute(
"SELECT jsonb_agg(row_to_json(r)) FROM "
"pg_ripple.sparql(pg_ripple.sparql_from_nl(%s)) r",
(args["question"],)
)
result = cur.fetchone()[0]
return str(result) if result else "No results."
return "Unknown tool."
def agent_loop(question: str, max_turns: int = 5) -> str:
messages = [{"role": "user", "content": question}]
for _ in range(max_turns):
response = client.chat.completions.create(
model="gpt-4o", messages=messages, tools=TOOLS, tool_choice="auto"
)
msg = response.choices[0].message
messages.append(msg)
if msg.tool_calls:
for call in msg.tool_calls:
result = dispatch(call.function.name, json.loads(call.function.arguments))
messages.append({
"role": "tool", "tool_call_id": call.id, "content": result
})
else:
return msg.content
return "Max turns reached."
print(agent_loop("Which drugs interact with insulin and should be avoided?"))
Tips for production agents
- Cache
rag_context()results per question hash (most questions repeat). A Redis or PostgreSQL cache in front of the tool call cuts LLM costs and latency significantly. - Set
pg_ripple.sparql_query_timeoutto bound runaway auto-generated SPARQL queries. - Use few-shot examples (
pg_ripple.add_llm_example()) for domain-specific vocabularies — reduces the NL→SPARQL error rate dramatically for specialised graphs. - Log tool call results to
_pg_ripple.audit_log(enabled by default whenaudit_log_enabled = on) — every RAG retrieval is then auditable, which matters for regulated industries. - Multi-tenant: apply graph RLS on the PostgreSQL connection used by each tenant's agent session —
rag_context()respects RLS automatically.
What is coming in v1.1.0
- Native LangChain
BaseTooland LlamaIndexQueryEngineToolwrappers published to PyPI aspg-ripple-langchainandpg-ripple-llamaindex. - Streaming tool results via pg_ripple cursor API (useful for large graph context blocks).
- Graph-aware conversation memory: store conversation history as RDF triples so the agent can reason over past interactions.
See also
- AI Overview — which AI feature to use when.
- RAG Pipeline —
rag_context()deep dive. - NL → SPARQL —
sparql_from_nl()and few-shot tuning. - Cookbook: Grounded Chatbot — simpler single-agent recipe.
Use Case Cookbook
The chapters under Feature Deep Dives explain what each pg_ripple feature does. The recipes in this cookbook explain what you can do with the features chained together. Each recipe is a self-contained story: a real-world goal, the step-by-step SQL, and the trade-offs to be aware of.
If you are evaluating pg_ripple, start here — these are the patterns that decide whether the technology fits your problem.
| Recipe | What you build | Features used |
|---|---|---|
| Knowledge graph from a relational catalogue | A queryable RDF graph generated from existing PostgreSQL tables, validated and kept in sync | R2RML, SHACL, named graphs |
| Chatbot grounded in a knowledge graph | An LLM application that answers questions using your graph as authoritative context | RAG pipeline, NL→SPARQL, JSON-LD framing |
| Deduplicate customer records across systems | A safe, auditable record-linkage pipeline that merges customer rows from two or more sources | KGE, suggest_sameas, SHACL hard rules, owl:sameAs |
| Audit trail with PROV-O and temporal queries | A regulator-defensible chain showing what the system told a user, when, and why | PROV-O, audit log, point_in_time, RDF-star |
| CDC → Kafka via JSON-LD outbox | A stream of structured graph-change events ready to push into Kafka, NATS, or any event bus | CDC subscriptions, JSON-LD framing, transactional outbox |
| Probabilistic rules for soft constraints | A scoring rule set that propagates confidence values, not just facts | Lattice Datalog, RDF-star confidence triples |
| SPARQL repair workflow | An iterative loop that uses the LLM to fix queries that failed to parse or returned no results | sparql_from_nl, explain_sparql, error catalog |
| Ontology mapping and alignment | A pipeline that lifts external vocabularies into a local schema using KGE and SHACL | KGE, suggest_sameas, OWL profiles |
Cookbook: Knowledge Graph from a Relational Catalogue
Goal. You have a working PostgreSQL schema for, say, a product catalogue. You want a queryable RDF graph that stays in sync with that schema, validates the output, and lets analysts answer relationship questions in SPARQL.
Why pg_ripple. Both schemas live in the same database. There is no ETL job, no eventual consistency, and a single SHACL contract guards the output.
Time to first result. ~15 minutes.
What you build
relational tables RDF triples SPARQL clients
───────────────── ─────────── ──────────────
product (id, name, …) ──┐ <…/product/42> a Product
category (id, parent_id) ──┼──► <…/product/42> in <…/category/7> ──► pg_ripple.sparql(...)
inventory (sku, qty) ──┘ <…/product/42> sku "WIDGET-1" "all out-of-stock products
<…/category/7> parent <…/category/3> in the toy hierarchy"
…
▲
│
SHACL validation
fires on every load
Step 1 — The relational source
CREATE TABLE category (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
parent_id INT REFERENCES category(id)
);
CREATE TABLE product (
id SERIAL PRIMARY KEY,
sku TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
category_id INT REFERENCES category(id),
price_cents INT
);
CREATE TABLE inventory (
product_id INT REFERENCES product(id),
qty INT NOT NULL DEFAULT 0
);
Populate it with a few rows; the recipe doesn't care about the data.
Step 2 — Define the R2RML mapping
SELECT pg_ripple.r2rml_load($TTL$
@prefix rr: <http://www.w3.org/ns/r2rml#> .
@prefix ex: <https://example.org/cat/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix schema:<https://schema.org/> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .
<#CategoryMap>
rr:logicalTable [ rr:tableName "category" ] ;
rr:subjectMap [ rr:template "https://example.org/cat/{id}" ;
rr:class skos:Concept ] ;
rr:predicateObjectMap [ rr:predicate skos:prefLabel ; rr:objectMap [ rr:column "name" ] ] ;
rr:predicateObjectMap [ rr:predicate skos:broader ;
rr:objectMap [ rr:template "https://example.org/cat/{parent_id}" ;
rr:termType rr:IRI ] ] .
<#ProductMap>
rr:logicalTable [ rr:tableName "product" ] ;
rr:subjectMap [ rr:template "https://example.org/product/{id}" ;
rr:class schema:Product ] ;
rr:predicateObjectMap [ rr:predicate schema:sku ; rr:objectMap [ rr:column "sku" ] ] ;
rr:predicateObjectMap [ rr:predicate schema:name ; rr:objectMap [ rr:column "name" ] ] ;
rr:predicateObjectMap [ rr:predicate schema:category ;
rr:objectMap [ rr:template "https://example.org/cat/{category_id}" ;
rr:termType rr:IRI ] ] ;
rr:predicateObjectMap [ rr:predicate schema:price ;
rr:objectMap [ rr:column "price_cents" ;
rr:datatype xsd:integer ] ] .
<#InventoryMap>
rr:logicalTable [ rr:sqlQuery "SELECT product_id, qty FROM inventory" ] ;
rr:subjectMap [ rr:template "https://example.org/product/{product_id}" ] ;
rr:predicateObjectMap [ rr:predicate <https://example.org/onHand> ;
rr:objectMap [ rr:column "qty" ; rr:datatype xsd:integer ] ] .
$TTL$);
Step 3 — Define a SHACL contract
This is the single most underappreciated trick in this recipe. The shape encodes the intended shape of the output. If the source schema drifts (a column is renamed, a foreign key is dropped), the SHACL run flags it immediately.
SELECT pg_ripple.load_shacl($TTL$
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix schema:<https://schema.org/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix ex: <https://example.org/> .
<https://shapes.example.org/ProductShape> a sh:NodeShape ;
sh:targetClass schema:Product ;
sh:property [ sh:path schema:sku ; sh:minCount 1 ; sh:maxCount 1 ] ;
sh:property [ sh:path schema:name ; sh:minCount 1 ; sh:datatype xsd:string ] ;
sh:property [ sh:path schema:category ; sh:minCount 1 ; sh:nodeKind sh:IRI ] ;
sh:property [ sh:path schema:price ; sh:datatype xsd:integer ; sh:minInclusive 0 ] ;
sh:property [ sh:path ex:onHand ; sh:maxCount 1 ; sh:datatype xsd:integer ] .
$TTL$);
ALTER SYSTEM SET pg_ripple.shacl_mode = 'sync';
SELECT pg_reload_conf();
Step 4 — Re-run incrementally
Schedule the R2RML load to run after every relational ETL pass. Because the dictionary IDs are deterministic, repeated loads are idempotent — only changed rows generate new triples.
-- Cron job, once per minute:
SELECT pg_ripple.r2rml_reload(); -- shorthand: re-runs the most recent r2rml_load()
SELECT * FROM pg_ripple.shacl_validate() LIMIT 10;
If shacl_validate() returns rows, the relational source has drifted and an alert fires.
Step 5 — Query in SPARQL
SELECT * FROM pg_ripple.sparql($$
PREFIX schema: <https://schema.org/>
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
PREFIX ex: <https://example.org/>
# Out-of-stock products in the "Toys" hierarchy.
SELECT ?sku ?name WHERE {
?p a schema:Product ;
schema:sku ?sku ;
schema:name ?name ;
schema:category/skos:broader* <https://example.org/cat/toys> ;
ex:onHand 0 .
}
$$);
The skos:broader* property path walks the category hierarchy with no recursion-depth cap. Try expressing that in pure SQL and you will rediscover why graph queries exist.
Variations
- Multi-source. Add a
rr:graphMapto each map so triples land in per-source named graphs (https://example.org/source/postgres-prod). Then Multi-Tenant Graphs gives you per-source RLS. - Soft delete. Replace
rr:tableNamewith anrr:sqlQuerythat filters outdeleted_at IS NOT NULL. Re-runs will remove the corresponding triples. - Foreign-data wrapper.
rr:tableNameaccepts foreign tables, so you can ingest a remote PostgreSQL or MySQL schema without copying data.
See also
Cookbook: Chatbot Grounded in a Knowledge Graph
Goal. Build an LLM-powered question-answering assistant that uses your knowledge graph as the source of truth, not the LLM's training data. Hallucinations drop dramatically; every answer is traceable to real triples.
Why pg_ripple. A single SQL function call (rag_context()) returns an LLM-ready prompt that fuses vector recall, graph expansion, and (optionally) executed SPARQL. No vector store to keep in sync, no orchestration framework to deploy.
Time to first result. ~10 minutes.
The pipeline
user question LLM response
│ ▲
▼ │
┌─────────────────────────────────────────────────────┴───────┐
│ application code (Python / TS / Go) │
│ ┌─────────────────────────────────────────────────┐ │
│ │ context = pg_ripple.rag_context(question, k=8) │ │
│ │ prompt = SYSTEM_PROMPT + context + question │ │
│ │ answer = openai.chat(prompt) │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
▲
│
LLM API
rag_context() does the four things every RAG pipeline must do, all inside one PostgreSQL transaction:
- Embed the user question (HTTP call to your embedding endpoint).
- HNSW cosine search to retrieve the top-k matching entities.
- SPARQL graph expansion to gather each entity's 1-hop neighbourhood.
- Assemble the context as JSON-LD or plain text.
Step 1 — Configure the LLM and embedding endpoints
ALTER SYSTEM SET pg_ripple.embedding_api_url = 'https://api.openai.com/v1';
ALTER SYSTEM SET pg_ripple.embedding_api_key_env = 'OPENAI_API_KEY';
ALTER SYSTEM SET pg_ripple.embedding_model = 'text-embedding-3-small';
ALTER SYSTEM SET pg_ripple.llm_endpoint = 'https://api.openai.com/v1';
ALTER SYSTEM SET pg_ripple.llm_api_key_env = 'OPENAI_API_KEY';
ALTER SYSTEM SET pg_ripple.llm_model = 'gpt-4o';
SELECT pg_reload_conf();
API keys are read from environment variables at call time and are never stored in the database.
Step 2 — Load and embed your knowledge graph
-- Load whatever you have. Could be a Turtle file, an R2RML mapping, GraphRAG output, …
SELECT pg_ripple.load_turtle_file('/data/medical_kb.ttl');
-- (Recommended) materialise OWL RL inference so 'subClassOf' chains are visible.
SELECT pg_ripple.load_rules_builtin('owl-rl');
SELECT pg_ripple.infer('owl-rl');
-- Embed every labelled entity. Run once after each big load.
SELECT pg_ripple.embed_entities();
For very large graphs, set pg_ripple.use_graph_context = on so each entity's embedding input includes its 1-hop neighbours — recall jumps significantly on entities whose labels alone are ambiguous.
Step 3 — Retrieve context for a question
SELECT pg_ripple.rag_context(
question := 'What drugs treat moderate hypertension?',
k := 8
);
Returns a single TEXT block, ready to drop into a prompt:
You are answering using the following knowledge graph context:
ENTITY: <https://example.org/lisinopril>
type: Drug, ACEInhibitor
rdfs:label: "Lisinopril"
ex:treats: Hypertension, HeartFailure
ex:contraindication: Pregnancy
ENTITY: <https://example.org/amlodipine>
type: Drug, CalciumChannelBlocker
...
Step 4 — Combine with NL→SPARQL for fact-style answers
For questions where the answer is a small, well-defined set ("how many", "list the", "who is"), have the LLM also generate a SPARQL query that extracts the precise answer:
SELECT pg_ripple.sparql_from_nl(
'How many drugs in the knowledge graph treat hypertension?'
);
-- Returns a parsed, validated SPARQL string.
-- Then execute it.
SELECT * FROM pg_ripple.sparql(
pg_ripple.sparql_from_nl(
'How many drugs in the knowledge graph treat hypertension?'
)
);
When pg_ripple.llm_endpoint is set, rag_context() does this automatically: the assembled context includes both the vector-retrieved neighbourhood and the result rows of the auto-generated SPARQL query.
Step 5 — Wire it into your application
import psycopg
import openai
SYSTEM_PROMPT = """
You are a medical information assistant. Answer ONLY using the
knowledge graph context provided. If the answer is not in the
context, say "I do not have that information." Cite IRIs of
entities you reference.
""".strip()
def answer(question: str) -> str:
with psycopg.connect("...") as conn:
cur = conn.cursor()
cur.execute("SELECT pg_ripple.rag_context(%s, 8)", (question,))
context = cur.fetchone()[0]
prompt = f"{SYSTEM_PROMPT}\n\n=== CONTEXT ===\n{context}\n\n=== QUESTION ===\n{question}"
resp = openai.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
temperature=0.0,
)
return resp.choices[0].message.content
Tuning
| Lever | What it does | When to change |
|---|---|---|
k (rag_context) | Number of entities included | More entities = richer context, more tokens |
pg_ripple.use_graph_context | Embed entities with their neighbourhood | Improves recall for ambiguous labels |
pg_ripple.llm_include_shapes | Include SHACL shapes in NL→SPARQL prompt | Improves query accuracy on schemas with many predicates |
| Few-shot examples | Add via pg_ripple.add_llm_example() | Domain-specific vocabularies need 5–10 examples |
Why this beats a separate vector DB
- One transaction. Loading new triples and updating embeddings happens atomically. There is no half-updated vector store after a crash.
- No drift. A separate vector DB has its own schema; over months, the two diverge. Here there is one schema, one source of truth.
- Multi-tenant by default. Apply graph RLS and the same
rag_context()call returns tenant-scoped context with no application-level filter. - Audit-ready. Enable
audit_log_enabledand every RAG-time UPDATE is captured. A separate vector DB cannot do that.
See also
Cookbook: Deduplicate Customer Records Across Systems
Goal. Two source systems each hold a list of customers. Some customers are in both, with subtle differences (Robert Smith vs Bob Smith, JaneDoe vs Jane Q. Doe). You want a unified view in which each real-world customer appears as one entity, while the original records remain auditable.
Why pg_ripple. Combines knowledge-graph embeddings (high recall), SHACL hard rules (safe), and owl:sameAs canonicalization (transparent at query time) — the three pieces a record-linkage pipeline needs, with no external services.
Time to first result. ~20 minutes.
This recipe is the practical flavour of Record Linkage and Entity Resolution. Read that page first for the strategic background.
Step 1 — Load both sources into named graphs
Named graphs preserve the original provenance of every record.
SELECT pg_ripple.load_turtle_into_graph('https://example.org/source/crm', $TTL$
@prefix ex: <https://example.org/> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
ex:crm/c1 foaf:name "Robert Smith" ; foaf:mbox <mailto:rob@x.com> ; ex:dob "1985-03-12"^^<http://www.w3.org/2001/XMLSchema#date> .
ex:crm/c2 foaf:name "Jane Doe" ; foaf:mbox <mailto:jane@x.com>; ex:dob "1990-07-09"^^<http://www.w3.org/2001/XMLSchema#date> .
$TTL$);
SELECT pg_ripple.load_turtle_into_graph('https://example.org/source/erp', $TTL$
@prefix ex: <https://example.org/> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
ex:erp/c1 foaf:name "Bob Smith" ; foaf:mbox <mailto:rob@x.com> ; ex:dob "1985-03-12"^^<http://www.w3.org/2001/XMLSchema#date> .
ex:erp/c2 foaf:name "Jane Q. Doe" ; foaf:mbox <mailto:j.doe@x.com>; ex:dob "1990-07-09"^^<http://www.w3.org/2001/XMLSchema#date> .
ex:erp/c3 foaf:name "Carl Larsen" ; foaf:mbox <mailto:carl@y.com>; ex:dob "1972-11-02"^^<http://www.w3.org/2001/XMLSchema#date> .
$TTL$);
Step 2 — Add structural context
Customers gain matching power when they have related facts (purchases, addresses, interaction history). Load whatever you have. The example below uses purchases:
SELECT pg_ripple.load_turtle_into_graph('https://example.org/source/crm', $TTL$
@prefix ex: <https://example.org/> .
ex:crm/c1 ex:purchased ex:product/widget1, ex:product/widget2 .
ex:crm/c2 ex:purchased ex:product/widget3 .
$TTL$);
SELECT pg_ripple.load_turtle_into_graph('https://example.org/source/erp', $TTL$
@prefix ex: <https://example.org/> .
ex:erp/c1 ex:purchased ex:product/widget1, ex:product/widget4 .
ex:erp/c2 ex:purchased ex:product/widget3, ex:product/widget5 .
$TTL$);
Step 3 — Generate candidate pairs
Run both text and KGE candidate generators, then union the results. They catch different mistakes.
-- Text-embedding candidates.
SELECT pg_ripple.embed_entities();
CREATE TEMP TABLE candidates AS
SELECT s1, s2, similarity, 'text' AS source
FROM pg_ripple.suggest_sameas(threshold := 0.85);
-- KGE candidates.
SET pg_ripple.kge_enabled = on;
SELECT pg_ripple.kge_train(model := 'TransE', epochs := 100);
INSERT INTO candidates
SELECT s1, s2, similarity, 'kge'
FROM pg_ripple.find_alignments(
source_graph := 'https://example.org/source/crm',
target_graph := 'https://example.org/source/erp',
threshold := 0.85
);
SELECT * FROM candidates ORDER BY similarity DESC;
Step 4 — Block unsafe merges with SHACL
The dob (date of birth) is immutable and unique per person. Two records with different DOBs cannot be the same person, no matter how similar their other attributes look.
SELECT pg_ripple.load_shacl($TTL$
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <https://example.org/> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
ex:CustomerSafetyShape a sh:NodeShape ;
sh:targetClass foaf:Person ;
# If owl:sameAs links two persons, their dob must agree.
sh:property [ sh:path ex:dob ; sh:maxCount 1 ] .
$TTL$);
ALTER SYSTEM SET pg_ripple.shacl_mode = 'sync';
SELECT pg_reload_conf();
When apply_sameas_candidates() would create a violation (two dob values for the merged person), the insert is rejected — the unsafe merge cannot happen.
Step 5 — Apply the auto-merge tier
-- High-confidence pairs auto-merge.
SELECT pg_ripple.apply_sameas_candidates(min_similarity := 0.95);
-- Mid-confidence pairs go to a review table.
CREATE TABLE customer_review_queue AS
SELECT DISTINCT ON (least(s1, s2), greatest(s1, s2))
s1, s2, max(similarity) AS similarity
FROM candidates
WHERE similarity BETWEEN 0.85 AND 0.95
GROUP BY s1, s2;
SELECT * FROM customer_review_queue ORDER BY similarity DESC;
A reviewer marks pairs as accepted or rejected in customer_review_queue; an accepted row triggers:
SELECT pg_ripple.insert_triple(s1, '<http://www.w3.org/2002/07/owl#sameAs>', s2),
pg_ripple.insert_triple(s2, '<http://www.w3.org/2002/07/owl#sameAs>', s1)
FROM customer_review_queue WHERE status = 'accepted';
Step 6 — Query the unified graph
pg_ripple.sameas_reasoning = on (the default) means SPARQL queries see merged customers as one entity:
-- Total spend by Robert/Bob — combined across CRM and ERP.
SELECT * FROM pg_ripple.sparql($$
PREFIX ex: <https://example.org/>
SELECT (COUNT(?p) AS ?n) WHERE {
<https://example.org/crm/c1> ex:purchased ?p .
}
$$);
The query targets the CRM identifier, but sameas_reasoning rewrites it to include the ERP identifier transparently.
Auditing the merge
Every merge action is captured by the audit log. Combined with point_in_time, a regulator can replay exactly what the system thought at any past timestamp.
SELECT ts, role, query
FROM _pg_ripple.audit_log
WHERE query ILIKE '%owl:sameAs%'
ORDER BY ts DESC
LIMIT 50;
See also
- Record Linkage — strategic background.
- Knowledge-Graph Embeddings
- Validating Data Quality
PPRL: Cross-Organization Entity Resolution Without Sharing Raw PII
What you'll learn: How to use
bloom_encode()andpg:dice_similarityto find records that refer to the same real-world person across two organizations — without either side ever seeing the other's raw personal data.
Why This Matters
Hospitals, banks, and government agencies often need to know whether their records overlap — the same patient, account holder, or citizen appearing in both systems. Regulations (GDPR, HIPAA, CCPA) typically prohibit sharing raw identifiers like full names, dates of birth, or Social Security Numbers.
Privacy-Preserving Record Linkage (PPRL) solves this using Bloom-filter encoding: sensitive identifiers are converted into fixed-length bit vectors that support similarity comparison without revealing the underlying values.
Reference: Schnell, Bachteler & Reiher (2009) — "Privacy-preserving record linkage using Bloom filters." BMC Medical Informatics and Decision Making 9:41.
Architecture
Organization A Organization B
────────────── ──────────────
Raw PII → bloom_encode() → bloomA Raw PII → bloom_encode() → bloomB
| |
└──── RDF triple store ─────────────────┘
|
SPARQL SERVICE federation
|
FILTER(pg:dice_similarity(?bloomA, ?bloomB) > 0.85)
|
Candidate matches (no raw PII exchanged)
Both organizations use the same shared secret key (established out-of-band via a secure channel — e.g., TLS-protected key agreement) so that Bloom encodings of the same value from both sides are identical.
Step 1: Encode Patient Identifiers
Each organization runs this on their own database. The shared key must match exactly between organizations.
-- Enable the extension
CREATE EXTENSION IF NOT EXISTS pg_ripple;
-- Encode a patient record. The 'shared_secret_key' must be agreed upon
-- out-of-band between the two organizations.
INSERT INTO patient_triples (subject, predicate, object)
VALUES (
'<http://hospitalA.example.org/patient/1001>',
'<http://pprl.example.org/nameBloom>',
pg_ripple.bloom_encode(
'Alice M Smith', -- concatenate relevant fields
'shared_secret_key', -- must match Organization B's key
30, -- hash_count (minimum recommended: 30)
1024 -- length in bits (minimum recommended: 1024)
)
);
-- Store a full patient record as RDF triples
SELECT pg_ripple.insert_triple(
'<http://hospitalA.example.org/patient/1001>',
'<http://pprl.example.org/nameBloom>',
pg_ripple.bloom_encode('Alice M Smith 1985-04-12', 'shared_secret_key')
);
Step 2: Run Cross-Organization Matching
Use SPARQL SERVICE federation to query both organizations simultaneously.
The SPARQL pg:dice_similarity FILTER compares the Bloom filters without
retrieving the underlying PII.
PREFIX pprl: <http://pprl.example.org/>
PREFIX pg: <http://pg-ripple.org/functions/>
SELECT ?patientA ?patientB ?sim WHERE {
-- Hospital A's Bloom-encoded identifiers (local graph)
?patientA pprl:nameBloom ?bloomA .
-- Hospital B's Bloom-encoded identifiers (remote SPARQL endpoint)
SERVICE <http://hospitalB.example.org/sparql> {
?patientB pprl:nameBloom ?bloomB .
}
-- Find candidates where Bloom filters are highly similar
FILTER(pg:dice_similarity(?bloomA, ?bloomB) > 0.85)
-- Compute exact similarity for ranking
BIND(pg:dice_similarity(?bloomA, ?bloomB) AS ?sim)
}
ORDER BY DESC(?sim)
From SQL:
SELECT * FROM pg_ripple.sparql($$
PREFIX pprl: <http://pprl.example.org/>
PREFIX pg: <http://pg-ripple.org/functions/>
SELECT ?patientA ?patientB ?sim WHERE {
?patientA pprl:nameBloom ?bloomA .
SERVICE <http://hospitalB.example.org/sparql> {
?patientB pprl:nameBloom ?bloomB .
}
FILTER(pg:dice_similarity(?bloomA, ?bloomB) > 0.85)
BIND(pg:dice_similarity(?bloomA, ?bloomB) AS ?sim)
}
ORDER BY DESC(?sim)
$$);
Step 3: Aggregate Reporting with Differential Privacy
If you only need aggregate statistics (e.g., "approximately how many shared
patients do the two organizations have?") without exposing individual identities,
use dp_noisy_count():
-- Count shared patients with ε=0.1 differential privacy
SELECT pg_ripple.dp_noisy_count(
'SELECT COUNT(DISTINCT subject) FROM _pg_ripple.vp_rare
WHERE predicate = pg_ripple.encode_iri(''http://pprl.example.org/nameBloom'')',
0.1 -- epsilon: smaller = more privacy, more noise
) AS approx_shared_patients;
For a histogram of match confidence buckets:
CREATE TEMP TABLE match_buckets (bucket TEXT, n BIGINT);
INSERT INTO match_buckets VALUES
('high (>0.9)', 42),
('medium (0.7-0.9)', 17),
('low (0.5-0.7)', 8);
SELECT * FROM pg_ripple.dp_noisy_histogram(
'SELECT bucket, n FROM match_buckets',
'bucket',
'n',
0.5 -- epsilon
);
Step 4: Datalog Rules for Entity Resolution
You can also express the matching as a Datalog rule and use incremental materialization:
SELECT pg_ripple.load_rules($$
-- Generate candidate pairs whose Bloom filters are similar
pprl:candidate(?x, ?y) :-
?x <http://pprl.example.org/nameBloom> ?bx .
?y <http://pprl.example.org/nameBloom> ?by .
?x != ?y .
pg:dice_similarity(?bx, ?by) > 0.85 .
$$);
-- Run inference to materialize all candidate pairs
SELECT pg_ripple.infer();
Security Notes
Recommended Parameters
| Parameter | Minimum Recommended | Default |
|---|---|---|
hash_count | 30 | 30 |
length | 1024 bits | 1024 |
Using fewer than 30 hash functions or fewer than 1024 bits significantly increases the risk of re-identification through graph-based attacks (Schnell et al. 2009, §4). pg_ripple logs a WARNING when parameters fall below the recommended minimums.
Key Management
The shared secret key (key parameter) must be:
- Agreed out-of-band — never transmitted with the Bloom-encoded data
- Kept secret — compromised keys allow the encoded data to be reversed
- Organization-pair-specific — use a different key for each pair of organizations to limit blast radius
What the Bloom Filter Protects Against
- Honest-but-curious adversary: Cannot infer the original value from the bit vector alone (without the key)
- Eavesdropper: Sees only bit vectors; without the key, values cannot be recovered
Known Limitations
- Bloom filters with low
hash_countor smalllengthare vulnerable to graph-based reconstruction attacks even without the key - High-frequency values (e.g., very common names) are more susceptible to inference attacks
- CLK encodings are not quantum-resistant
Patent Status
The CLK (Cryptographic Longterm Key) construction described in Schnell et al. (2009) is published academic work with no known patent encumbrances.
Verifying Your Encoding
-- Self-test: encoding the same value twice should give Dice = 1.0
SELECT pg_ripple.dice_similarity(
pg_ripple.bloom_encode('Alice M Smith', 'testkey', 30, 1024),
pg_ripple.bloom_encode('Alice M Smith', 'testkey', 30, 1024)
) AS should_be_one;
-- Encoding different values should give Dice < 1.0
SELECT pg_ripple.dice_similarity(
pg_ripple.bloom_encode('Alice M Smith', 'testkey', 30, 1024),
pg_ripple.bloom_encode('Bob J Jones', 'testkey', 30, 1024)
) AS should_be_less_than_half;
Further Reading
- Schnell, R., Bachteler, T., & Reiher, J. (2009). Privacy-preserving record linkage using Bloom filters. BMC Medical Informatics and Decision Making, 9, 41. https://doi.org/10.1186/1472-6947-9-41
- Christen, P., Ranbaduge, T., & Schnell, R. (2020). Linking Sensitive Data: Methods and Techniques for Practical Privacy-Preserving Information Sharing. Springer.
- pg_ripple SPARQL federation guide: federation-wikidata.md
- pg_ripple differential privacy functions:
pg_ripple.dp_noisy_count(),pg_ripple.dp_noisy_histogram()
Cookbook: Audit Trail with PROV-O and Temporal Queries
Goal. Build an evidence chain that lets you answer regulator questions like "on 21 March, did your system tell user X that fact Y was true? On what evidence?".
Why pg_ripple. Three composable features — point_in_time, prov_enabled, audit_log_enabled — combine into the kind of audit trail that pure-ML pipelines cannot produce. Plus RDF-star for per-fact provenance.
Time to first result. ~10 minutes.
Step 1 — Turn on every layer
ALTER SYSTEM SET pg_ripple.prov_enabled = on; -- per-load PROV-O
ALTER SYSTEM SET pg_ripple.audit_log_enabled = on; -- per-UPDATE log
SELECT pg_reload_conf();
The third layer — RDF-star quoted triples for per-fact confidence/source — is loaded with the data itself.
Step 2 — Load data with per-fact provenance
SELECT pg_ripple.load_turtle($TTL$
@prefix ex: <https://example.org/> .
@prefix prov: <http://www.w3.org/ns/prov#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
ex:drugA ex:interactsWith ex:drugB .
# Annotate the fact itself.
<< ex:drugA ex:interactsWith ex:drugB >>
ex:source <https://pubmed.example/article/12345> ;
ex:confidence "0.92"^^xsd:decimal ;
ex:assertedAt "2026-03-15T09:00:00Z"^^xsd:dateTime ;
prov:wasAttributedTo ex:loader/medkb-v3 .
$TTL$);
Step 3 — Time passes and data changes
-- Several updates happen over the next month.
SELECT pg_ripple.sparql_update('
INSERT DATA {
<https://example.org/drugA> <https://example.org/manufacturer>
<https://example.org/acme>
}
');
-- A different role makes a correction.
SET ROLE editor_alice;
SELECT pg_ripple.sparql_update('
DELETE DATA { <https://example.org/drugA> <https://example.org/manufacturer>
<https://example.org/acme> }
');
SELECT pg_ripple.sparql_update('
INSERT DATA { <https://example.org/drugA> <https://example.org/manufacturer>
<https://example.org/acmecorp> }
');
RESET ROLE;
Step 4 — Answer the regulator
The question is: "On 21 March, did your system tell users that drug A interacts with drug B? On what evidence?"
-- 1. Replay the graph as of 21 March 12:00.
SELECT pg_ripple.point_in_time('2026-03-21 12:00:00+00');
-- 2. Re-ask the question.
SELECT * FROM pg_ripple.sparql($$
ASK { <https://example.org/drugA>
<https://example.org/interactsWith>
<https://example.org/drugB> }
$$);
-- → true
-- 3. Pull the per-fact evidence (RDF-star).
SELECT * FROM pg_ripple.sparql($$
SELECT ?source ?confidence ?assertedAt WHERE {
<< <https://example.org/drugA>
<https://example.org/interactsWith>
<https://example.org/drugB> >>
<https://example.org/source> ?source ;
<https://example.org/confidence> ?confidence ;
<https://example.org/assertedAt> ?assertedAt .
}
$$);
-- 4. Pull the loader activity (PROV-O).
SELECT * FROM pg_ripple.prov_stats()
WHERE source_file LIKE '%medkb%';
-- 5. Pull every UPDATE since the load (audit log).
SELECT ts, role, operation, query
FROM _pg_ripple.audit_log
WHERE ts >= '2026-03-15'
AND query ILIKE '%drugA%'
ORDER BY ts;
-- 6. Reset point-in-time to "now".
SELECT pg_ripple.point_in_time(NULL);
That sequence is the kind of evidence chain a regulator looks for: truth value at a point in time, evidence per fact, attribution per UPDATE.
Building a one-shot audit report
Wrap the queries above in a SQL function so the compliance team can run a single command:
CREATE FUNCTION audit_report(
fact_subject TEXT,
fact_predicate TEXT,
fact_object TEXT,
as_of TIMESTAMPTZ
)
RETURNS TABLE (kind TEXT, payload JSONB) AS $$
BEGIN
PERFORM pg_ripple.point_in_time(as_of);
RETURN QUERY
SELECT 'truth_value'::TEXT,
jsonb_build_object('asof', as_of, 'value', (
SELECT bindings FROM pg_ripple.sparql(
format('ASK { %s %s %s }', fact_subject, fact_predicate, fact_object)
)
));
RETURN QUERY
SELECT 'rdf_star_evidence'::TEXT, to_jsonb(s)
FROM pg_ripple.sparql(format($q$
SELECT ?p ?o WHERE {
<< %s %s %s >> ?p ?o
}
$q$, fact_subject, fact_predicate, fact_object)) s;
RETURN QUERY
SELECT 'audit_log'::TEXT, to_jsonb(a)
FROM _pg_ripple.audit_log a
WHERE ts <= as_of AND query ILIKE '%' || fact_subject || '%'
ORDER BY ts;
PERFORM pg_ripple.point_in_time(NULL);
END;
$$ LANGUAGE plpgsql;
SELECT * FROM audit_report(
'<https://example.org/drugA>',
'<https://example.org/interactsWith>',
'<https://example.org/drugB>',
'2026-03-21 12:00:00+00'
);
See also
Cookbook: CDC → Kafka via JSON-LD Outbox
Goal. Push a stream of structured graph-change events into Kafka (or NATS, RabbitMQ, AWS SNS — anywhere your event bus lives), without polling the database, and using a JSON-LD payload that downstream consumers can validate against the same SHACL shapes the database uses.
Why pg_ripple. Combines CDC subscriptions (push, no polling), JSON-LD framing (schema-shaped payloads), and the transactional-outbox pattern (no lost events).
Time to first result. ~30 minutes (most of it is wiring the Kafka producer).
Architecture
triple INSERT/DELETE
│
▼
┌────────────────────────┐
│ CDC subscription │ pg_ripple.create_subscription('out_persons',
│ filter: SHACL shape │ filter_shape := '<…/PersonShape>')
└─────────┬──────────────┘
│ NOTIFY ──── JSON ─────┐
▼ │
┌────────────────────────┐ │
│ outbox table │ │ alternatively, a small
│ INSERT trigger writes │ │ Rust/Python LISTENer
│ JSON-LD framed payload │ │
└─────────┬──────────────┘ │
▼ ▼
┌────────────────────────┐ ┌────────────────────────┐
│ Debezium / outbox │ │ asyncpg LISTENer │
│ connector → Kafka │ │ → Kafka producer │
└────────────────────────┘ └────────────────────────┘
Two valid implementations:
- Outbox + Debezium (recommended for at-least-once durability).
- Direct LISTEN (recommended for low-latency, fire-and-forget streams).
The recipe shows both.
Step 1 — Define the change shape
The simplest payload is the entire updated entity in JSON-LD framed shape, so downstream consumers see a self-contained document. Define the frame once:
SELECT pg_ripple.register_jsonld_frame('person_event', $JSON$
{
"@context": {
"name": "http://xmlns.com/foaf/0.1/name",
"email": "http://xmlns.com/foaf/0.1/mbox",
"knows": { "@id": "http://xmlns.com/foaf/0.1/knows", "@type": "@id" }
},
"@type": "http://xmlns.com/foaf/0.1/Person"
}
$JSON$);
Step 2 — Create a CDC subscription
-- Optional: create a SHACL shape so the subscription only fires for valid Persons.
SELECT pg_ripple.load_shacl($TTL$
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
<https://shapes.example.org/PersonShape> a sh:NodeShape ;
sh:targetClass foaf:Person ;
sh:property [ sh:path foaf:name ; sh:minCount 1 ] .
$TTL$);
SELECT pg_ripple.create_subscription(
'persons_out',
filter_sparql := 'SELECT ?s ?p ?o WHERE { ?s a <http://xmlns.com/foaf/0.1/Person> ; ?p ?o }'
);
Step 3a — Direct-LISTEN producer
The lowest-latency path: a tiny LISTENer reads the NOTIFY stream and produces straight to Kafka.
import asyncio, json, asyncpg
from aiokafka import AIOKafkaProducer
async def main():
conn = await asyncpg.connect("postgresql://…")
kafka = AIOKafkaProducer(bootstrap_servers="kafka:9092")
await kafka.start()
queue: asyncio.Queue = asyncio.Queue()
def callback(_conn, _pid, _channel, payload):
queue.put_nowait(payload)
await conn.add_listener("pg_ripple_cdc_persons_out", callback)
while True:
raw = await queue.get()
evt = json.loads(raw)
# Optionally re-frame the changed entity as JSON-LD.
await kafka.send_and_wait("graph.persons", raw.encode())
asyncio.run(main())
The CDC payload already includes op, s, p, o, g — see CDC subscriptions.
For full-entity payloads (rather than per-triple events), call sparql_construct_jsonld() inside the LISTENer to fetch the framed entity at the time of the change.
Step 3b — Outbox + Debezium
For at-least-once durability you want the events in a table, replicated by Debezium. Add an outbox trigger that materialises the JSON-LD payload at write time:
CREATE TABLE outbox (
id BIGSERIAL PRIMARY KEY,
aggregate TEXT NOT NULL,
event_type TEXT NOT NULL,
payload JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE OR REPLACE FUNCTION enqueue_person_event() RETURNS trigger AS $$
DECLARE
framed JSONB;
BEGIN
framed := pg_ripple.sparql_construct_jsonld(
format(
'CONSTRUCT { %s ?p ?o } WHERE { %s ?p ?o }',
NEW.s, NEW.s
),
frame_name := 'person_event'
);
INSERT INTO outbox (aggregate, event_type, payload)
VALUES (NEW.s, 'PersonChanged', framed);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- pg_ripple.cdc_events is the catch-up table written by every subscription.
CREATE TRIGGER outbox_persons
AFTER INSERT ON _pg_ripple.cdc_events
FOR EACH ROW
WHEN (NEW.subscription = 'persons_out')
EXECUTE FUNCTION enqueue_person_event();
Point Debezium at the outbox table; configure it to delete rows after publishing. The pattern is canonical and is exactly what Debezium's "outbox event router" SMT was designed for.
Step 4 — Validate consumers
The same SHACL shape that filters the CDC subscription can be shipped to downstream consumers as the contract. Consumers that read the JSON-LD payload validate it with any standard SHACL library (Python pyshacl, Java topbraid-shacl, etc.). If the contract changes, only one document changes — the <https://shapes.example.org/PersonShape> definition.
Failure modes and how to handle them
| Failure | Direct LISTEN | Outbox + Debezium |
|---|---|---|
| Subscriber crashes | Events lost | Events persisted, replayed on restart |
| NOTIFY queue overflows | Events dropped | Outbox grows; backpressure handled |
| Consumer slow | Producer backpressures | Outbox grows; cleanup lags |
| Schema drift | Consumer parses garbage | Outbox + SHACL catches it before publish |
The outbox path is more code; in return you get the durability guarantees most production event buses expect.
See also
- Live Views and Subscriptions
- Exporting and Sharing — JSON-LD framing
- APIs and Integration —
pg_ripple_http
Cookbook: Probabilistic Rules for Soft Constraints
Goal. You want rules that propagate confidence, not just facts. "If A is similar to B with confidence 0.9, and B is similar to C with confidence 0.85, then A is similar to C with confidence ≥ 0.85 × 0.9." Classical Datalog cannot do this; it deals in true/false. Lattice Datalog can.
Why pg_ripple. Ships built-in lattices (min, max, set, interval) and lets you register custom ones. Inference fixpoints over a lattice instead of a boolean.
Time to first result. ~10 minutes.
The intuition
Standard Datalog: facts are in the relation or not. There is one truth value: derived.
Lattice Datalog: facts have an associated value — a confidence, a cost, a probability, a time interval. The lattice tells the engine how to combine multiple derivations of the same fact. For confidence we use min: a derivation chain is only as confident as its weakest link.
Step 1 — Pick a lattice
For confidence propagation, the built-in min lattice is exactly what we want. (Top of the lattice = 1.0 = certain. Bottom = 0.0 = unknown. Multiple derivations of the same fact take the strongest — i.e. minimum of the weak-link confidences.)
If you prefer max-of-min semantics (the strongest single chain wins), build a max lattice over chains of min. The bundled min lattice is the most common starting point.
Step 2 — Encode confidence with RDF-star
Confidence is a property of a triple, so RDF-star is the natural encoding:
SELECT pg_ripple.load_turtle($TTL$
@prefix ex: <https://example.org/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
ex:alice ex:similarTo ex:bob .
ex:bob ex:similarTo ex:carol .
ex:carol ex:similarTo ex:dan .
<< ex:alice ex:similarTo ex:bob >> ex:confidence "0.90"^^xsd:decimal .
<< ex:bob ex:similarTo ex:carol >> ex:confidence "0.85"^^xsd:decimal .
<< ex:carol ex:similarTo ex:dan >> ex:confidence "0.95"^^xsd:decimal .
$TTL$);
Step 3 — Write a lattice rule
SELECT pg_ripple.load_rules($RULES$
# Transitive similarity: confidence is the min of the chain.
?x ex:transSimilarTo ?y :- ?x ex:similarTo ?y .
?x ex:transSimilarTo ?z :-
?x ex:similarTo ?y ,
?y ex:transSimilarTo ?z .
# Lattice-typed binding: each derived ex:transSimilarTo carries a confidence.
@lattice ex:transSimilarTo confidence min .
$RULES$, 'similarity');
SELECT pg_ripple.infer_lattice('similarity', 'min');
The @lattice directive tells the engine: whenever a ex:transSimilarTo triple is derived, its confidence is the min of the confidences of the body atoms. The engine then iterates to a fixpoint with the lattice as the join operator.
Step 4 — Query
SELECT * FROM pg_ripple.sparql($$
PREFIX ex: <https://example.org/>
SELECT ?z ?conf WHERE {
<https://example.org/alice> ex:transSimilarTo ?z .
<< <https://example.org/alice> ex:transSimilarTo ?z >> ex:confidence ?conf .
}
ORDER BY DESC(?conf)
$$);
?z ?conf
ex:bob 0.90
ex:carol 0.85 (min of 0.90, 0.85)
ex:dan 0.85 (min of 0.90, 0.85, 0.95)
Note that ex:dan keeps the bottleneck of 0.85, not 0.95 × 0.85 × 0.90. That is exactly what min semantics gives you — the weakest link. If you want multiplicative propagation, register a custom lattice (next section).
Step 5 — Custom lattice for multiplicative confidence
-- The PostgreSQL aggregate that combines two confidences multiplicatively.
CREATE OR REPLACE FUNCTION conf_mul(state DOUBLE PRECISION, val DOUBLE PRECISION)
RETURNS DOUBLE PRECISION
LANGUAGE plpgsql IMMUTABLE AS $$ BEGIN RETURN COALESCE(state, 1.0) * val; END; $$;
CREATE AGGREGATE prob_join(DOUBLE PRECISION) (
SFUNC = conf_mul, STYPE = DOUBLE PRECISION, INITCOND = '1.0'
);
SELECT pg_ripple.create_lattice(
name := 'probability',
join_fn := 'prob_join',
bottom := '0.0'
);
SELECT pg_ripple.infer_lattice('similarity', 'probability');
Now ?dan's confidence is 0.90 × 0.85 × 0.95 = 0.726 — chain decay, not weakest-link.
When lattice Datalog is the right tool
- Confidence propagation (this recipe).
- Shortest path (
minlattice over edge weights). - Maximum bandwidth (
maxlattice over edge capacity, thenminalong the chain). - Time-interval reasoning (
intervallattice — "the period during which all of these are true"). - Provenance semirings (custom lattice over witness sets).
When the rule's body has only boolean conjunction and the head needs only true/false, classical Datalog is simpler. Lattice Datalog earns its complexity only when the value associated with a fact matters.
See also
Rule Libraries
Version: v0.104.0
Foundation: plans/expert-system.md §5
A rule library is a versioned, distributable package of Datalog inference rules and SHACL validation shapes. Libraries allow teams to share domain-specific reasoning logic — patient-matching rules, anti-money-laundering patterns, supply-chain constraints — without each team re-implementing the same logic from scratch.
Rule libraries are analogous to npm packages or pip packages, but for knowledge-graph reasoning.
Library Format
A rule library is a single Turtle (.ttl) file with the following structure:
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix dcterms: <http://purl.org/dc/terms/> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix pg: <http://pg-ripple.org/lib/> .
@prefix sh: <http://www.w3.org/ns/shacl#> .
# ── Required: library declaration ────────────────────────────────────────────
<urn:my-org:my-library> a pg:RuleLibrary ;
dcterms:title "my-library" ;
dcterms:description "Human-readable description of what the library does." ;
dcterms:license <https://spdx.org/licenses/MIT.html> ;
owl:versionInfo "1.2.0" ;
# ── Optional: dependencies ───────────────────────────────────────────────
# Declare another library as a dependency (its URL or local path).
pg:dependsOn <https://example.org/shared-library.ttl> ;
# ── Optional: Datalog rules ──────────────────────────────────────────────
pg:rules """
?x <https://schema.org/ancestor> ?z :-
?x <https://schema.org/parent> ?z .
?x <https://schema.org/ancestor> ?z :-
?x <https://schema.org/parent> ?y ,
?y <https://schema.org/ancestor> ?z .
""" .
# ── Optional: SHACL shapes ────────────────────────────────────────────────────
<https://my-org.example/shapes/PersonShape>
a sh:NodeShape ;
sh:targetClass <https://schema.org/Person> ;
sh:property [
sh:path <https://schema.org/name> ;
sh:minCount 1 ;
sh:datatype <http://www.w3.org/2001/XMLSchema#string> ;
] .
Required metadata triples
All four of these triples must appear on the pg:RuleLibrary subject:
| Property | Value |
|---|---|
dcterms:title | Short identifier used as the library name in the catalog (plain string) |
dcterms:description | Human-readable explanation of what the library does |
dcterms:license | SPDX license IRI (see License requirements) |
owl:versionInfo | Version string (e.g. "1.2.0") |
Optional triples
| Property | Value |
|---|---|
pg:rules | xsd:string literal containing one or more Datalog rule bodies |
pg:dependsOn | IRI of a dependency library (URL or local path); can appear multiple times |
| SHACL shapes | Any sh:NodeShape / sh:PropertyShape resources in the same file |
Installing a Library
-- Install from a local file (absolute path):
SELECT pg_ripple.install_rule_library('/opt/pg_ripple/libraries/my-library.ttl');
-- Install from an HTTPS URL:
SELECT pg_ripple.install_rule_library(
'https://libraries.example.org/my-library-1.2.0.ttl'
);
install_rule_library returns the library name on success.
Idempotency: re-installing the same name + version is a no-op. The function returns the name without error or duplication.
Accepting non-permissive licenses
If a library carries a non-permissive license (anything other than MIT, Apache-2.0, or the PostgreSQL License), you must explicitly acknowledge this:
SELECT pg_ripple.install_rule_library(
'/opt/libraries/gpl-library.ttl',
accept_license => true
);
Without accept_license => true, install_rule_library raises PT0455 and
exits without loading the library.
Listing Installed Libraries
SELECT * FROM pg_ripple.list_rule_libraries();
| Column | Type | Description |
|---|---|---|
name | TEXT | Library identifier (from dcterms:title) |
version | TEXT | Version from owl:versionInfo |
installed_at | TEXT | Timestamp of installation |
description | TEXT | Library description |
license_iri | TEXT | SPDX license IRI |
Upgrading a Library
SELECT pg_ripple.upgrade_rule_library('my-library');
upgrade_rule_library re-fetches the library from its original source_url,
replaces all installed rules and shapes, and updates the version in the catalog.
PT0456: If another installed library depends on my-library, the upgrade
is rejected — uninstall the dependent library first.
Uninstalling a Library
SELECT pg_ripple.uninstall_rule_library('my-library');
This removes:
- All Datalog rules for the library's rule set name.
- All SHACL shapes loaded from the library's Turtle file.
- The catalog row in
_pg_ripple.rule_libraries.
PT0456: Rejected when another installed library declares my-library as
a dependency. Uninstall the dependent library first.
REST API
The pg_ripple_http companion service exposes a read-only endpoint:
GET /rule-libraries
Authorization: Bearer <token>
Returns a JSON array of installed library objects:
[
{
"name": "my-library",
"version": "1.2.0",
"installed_at": "2026-05-10 12:00:00",
"description": "Human-readable description.",
"license_iri": "https://spdx.org/licenses/MIT.html"
}
]
Dependency Resolution
Dependencies declared with pg:dependsOn are resolved recursively in
topological order: dependency libraries are installed before the library that
requires them.
Constraints:
- Dependency graphs must be acyclic. Cycles raise PT0453.
- Only single-version dependencies are supported — no semver ranges.
- If a declared dependency URL cannot be fetched, PT0454 is raised.
License Requirements
pg_ripple distinguishes permissive from non-permissive licenses:
| License | SPDX IRI | Auto-accepted |
|---|---|---|
| MIT | https://spdx.org/licenses/MIT.html | ✅ Yes |
| Apache-2.0 | https://spdx.org/licenses/Apache-2.0.html | ✅ Yes |
| PostgreSQL License | https://spdx.org/licenses/PostgreSQL.html | ✅ Yes |
| Any other | — | ❌ Requires accept_license => true |
Operator responsibilities
Before installing a library with accept_license => true:
- Read the license text at the SPDX IRI.
- Consult your legal team if the library will be used in a commercial product or distributed as part of a service.
- Document the accepted license decision in your change-management log.
- Review the library content — pg_ripple does not audit library rules for correctness, safety, or regulatory compliance.
pg_ripple does not provide legal advice. The accept_license flag is a
technical confirmation mechanism, not a legal guarantee.
SSRF Protection
URL-based library sources are validated against the federation SSRF allowlist
before any network request is made. The check follows the same policy as
SERVICE endpoints in SPARQL queries:
default-deny(default): blocks RFC-1918, loopback, link-local addresses.allowlist: only URLs listed inpg_ripple.federation_allowed_endpoints.open: no restrictions (development/testing only).
A blocked URL raises PT0452.
Authoring a Library
1. Write the Turtle file
Follow the Library Format above. Rules must be valid
pg_ripple Datalog syntax — test them with pg_ripple.load_rules() first.
2. Validate locally
# Test rule syntax:
psql -c "SELECT pg_ripple.load_rules(\$\$<rules here>\$\$, 'test')"
# Test install from local path:
psql -c "SELECT pg_ripple.install_rule_library('/path/to/my-library.ttl')"
# Verify it appears in the catalog:
psql -c "SELECT * FROM pg_ripple.list_rule_libraries()"
# Uninstall after testing:
psql -c "SELECT pg_ripple.uninstall_rule_library('my-library')"
3. Publish
Rule libraries can be published:
- As files on an HTTPS server.
- As GitHub releases with raw URLs.
- As OCI artifacts or Helm config maps (for Kubernetes deployments).
There is no central registry — operators install from any URL or local path they trust.
4. Versioning
Use semantic versioning (MAJOR.MINOR.PATCH) in owl:versionInfo. When you
release a new version, update the Turtle file and re-publish. Users upgrade
with upgrade_rule_library().
Error Reference
| Code | When | Message |
|---|---|---|
| PT0452 | URL blocked by SSRF allowlist | install_rule_library: URL '...' is blocked by the SSRF allowlist |
| PT0453 | Circular dependency detected | install_rule_library: dependency cycle detected involving library '...' |
| PT0454 | Dependency URL unreachable | install_rule_library: dependency '...' could not be fetched |
| PT0455 | Non-permissive license, no acceptance | install_rule_library: library '...' uses license '...' — set accept_license => TRUE |
| PT0456 | Dependent library prevents action | upgrade/uninstall_rule_library: library '...' is required by '...' — uninstall the dependent first |
| PT0459 | Name conflicts with a built-in bundle | install_rule_library: name '...' conflicts with a built-in bundle |
Relationship to Built-in Bundles
pg_ripple ships with several built-in bundles (SKOS, OWL RL, RDFS, DCTERMS,
Schema.org, FOAF) accessible via load_datalog_bundle() and
load_shape_bundle(). These are compiled into the extension binary.
External rule libraries (this feature) are the user-extensible counterpart:
they are Turtle files loaded at runtime and stored in the
_pg_ripple.rule_libraries catalog.
The two systems coexist. A rule library may declare a built-in bundle as a
dependency using pg:dependsOn "skos" — install_rule_library will activate
the bundle via load_datalog_bundle() before loading the library's own rules.
Library names that collide with built-in bundle names are rejected with PT0459.
Cookbook: SPARQL Repair Workflow
Goal. Build an iterative loop where an LLM proposes a SPARQL query, pg_ripple tells it what went wrong (parse error, missing prefix, no rows, schema mismatch), and the LLM tries again. Most "no result" cases self-heal in one or two iterations.
Why pg_ripple. sparql_from_nl() and explain_sparql() were designed to compose. Combine them with the error catalog and you get an automated SPARQL pair-programmer.
Time to first result. ~10 minutes.
The loop
user question (NL)
│
▼
sparql_from_nl(question)
│
├── parse fails (PT702) ──► describe error → LLM retry
│
▼
sparql(query)
│
├── exception (PT…) ──► describe error → LLM retry
│
▼
row count == 0 ?
│
├── yes ──► explain_sparql(query, analyze:=true) → LLM retry
│
▼
answer
Maximum iterations: 3. Anything worse than that is usually a data-quality problem and should escalate to a human.
Step 1 — Configure NL → SPARQL
ALTER SYSTEM SET pg_ripple.llm_endpoint = 'https://api.openai.com/v1';
ALTER SYSTEM SET pg_ripple.llm_api_key_env = 'OPENAI_API_KEY';
ALTER SYSTEM SET pg_ripple.llm_model = 'gpt-4o';
ALTER SYSTEM SET pg_ripple.llm_include_shapes = on;
SELECT pg_reload_conf();
llm_include_shapes = on ships your active SHACL shapes with every prompt — the LLM sees the schema and produces queries that respect it.
Step 2 — Add few-shot examples for your vocabulary
SELECT pg_ripple.add_llm_example(
'List all proteins that interact with insulin',
'PREFIX bio: <https://bio.example/>
SELECT ?p WHERE {
?p a bio:Protein ; bio:interactsWith bio:Insulin .
}'
);
Five to ten examples per domain is usually enough.
Step 3 — The repair function
CREATE OR REPLACE FUNCTION sparql_repair(question TEXT, max_attempts INT DEFAULT 3)
RETURNS TABLE (attempt INT, query TEXT, status TEXT, rows JSONB) AS $$
DECLARE
q TEXT;
feedback TEXT := '';
n INT := 0;
err TEXT;
row_count INT;
out_rows JSONB;
BEGIN
LOOP
n := n + 1;
EXIT WHEN n > max_attempts;
-- 1. Generate a query.
BEGIN
q := pg_ripple.sparql_from_nl(question || E'\n' || feedback);
EXCEPTION WHEN OTHERS THEN
feedback := format('Previous attempt failed to generate SPARQL: %s. Try again.', SQLERRM);
CONTINUE;
END;
-- 2. Try to execute it.
BEGIN
SELECT count(*), jsonb_agg(s) INTO row_count, out_rows
FROM pg_ripple.sparql(q) s;
EXCEPTION WHEN OTHERS THEN
err := SQLERRM;
attempt := n; query := q; status := 'parse_or_runtime_error: ' || err;
rows := NULL;
RETURN NEXT;
feedback := format('Previous SPARQL caused error: %s. The query was: %s', err, q);
CONTINUE;
END;
-- 3. If empty, ask the LLM to broaden.
IF row_count = 0 THEN
attempt := n; query := q; status := 'empty_result_set'; rows := NULL;
RETURN NEXT;
feedback := format(
'Previous query returned zero rows: %s. Loosen FILTERs or remove restrictive prefixes.',
q
);
CONTINUE;
END IF;
-- 4.
attempt := n; query := q; status := 'ok'; rows := out_rows;
RETURN NEXT;
RETURN;
END LOOP;
END;
$$ LANGUAGE plpgsql;
Step 4 — Use it
SELECT * FROM sparql_repair('Which proteins interact with the gene encoding insulin?');
Output:
attempt | query | status | rows
--------+-------------------------------------------------+------------------------+-----
1 | SELECT ?p WHERE { ?p bio:interactsWith ... | parse_or_runtime_error | NULL
2 | PREFIX bio: <https://bio.example/> SELECT ... | empty_result_set | NULL
3 | PREFIX bio: <https://bio.example/> SELECT ... | ok | [...]
Three attempts, automated. The user gets the right answer; the audit log captures every attempted query for later analysis.
Telemetry
Wire sparql_repair() into your application telemetry:
- Average attempts per question — > 2 means you need more few-shot examples.
- Most common error code — likely a vocabulary gap.
- Retry depth — > 3 means the LLM is going in circles; escalate to a human.
Hardening
- Set
pg_ripple.sparql_max_algebra_depthto reject pathological generated queries before they execute. - Set
pg_ripple.sparql_query_timeoutso a bad generation cannot wedge the database. - Cache successful (question → query) pairs — the LLM does not need to be called for repeats.
See also
Cookbook: Ontology Mapping and Alignment
Goal. You import data from an external dataset (Wikidata, schema.org, an industry vocabulary) and want it to align with your internal ontology — same concepts, different IRIs. Manual mapping is tedious; KGE-driven candidate generation plus SHACL gates plus owl:sameAs canonicalization makes the job tractable.
Why pg_ripple. Composes the same building blocks as record linkage, but applied to classes and properties instead of individuals.
Time to first result. ~25 minutes.
The challenge
Your internal ontology calls a person intkb:Employee. Wikidata calls them wd:Q5 (human). Schema.org calls them schema:Person. None of these is wrong — they describe overlapping but not identical concepts. You want SPARQL queries to transparently see them as the same class for retrieval, but you also want to retain the source-specific axioms.
Step 1 — Load all three vocabularies
-- Internal ontology.
SELECT pg_ripple.load_turtle_into_graph('https://example.org/ontologies/intkb', $TTL$
@prefix intkb: <https://intkb.example/> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
intkb:Employee rdfs:label "Employee" ;
rdfs:comment "An employee of the organisation" .
intkb:Department rdfs:label "Department" .
intkb:reports rdfs:label "reports to" .
$TTL$);
-- Schema.org subset.
SELECT pg_ripple.load_turtle_file_into_graph(
'https://example.org/ontologies/schemaorg', '/data/schemaorg-people.ttl');
-- Wikidata subset.
SELECT pg_ripple.load_turtle_file_into_graph(
'https://example.org/ontologies/wikidata', '/data/wikidata-people.ttl');
Step 2 — Train KGE across all three
SET pg_ripple.kge_enabled = on;
SELECT pg_ripple.kge_train(model := 'RotatE', dimensions := 200, epochs := 200);
RotatE is preferred for ontology alignment because it captures inverse and symmetric patterns common in owl:inverseOf axioms.
Step 3 — Generate candidate alignments
SELECT * FROM pg_ripple.find_alignments(
source_graph := 'https://example.org/ontologies/intkb',
target_graph := 'https://example.org/ontologies/schemaorg',
threshold := 0.85
)
ORDER BY similarity DESC;
-- Same against Wikidata.
SELECT * FROM pg_ripple.find_alignments(
source_graph := 'https://example.org/ontologies/intkb',
target_graph := 'https://example.org/ontologies/wikidata',
threshold := 0.85
)
ORDER BY similarity DESC;
Step 4 — Gate with structural SHACL
You only want to align classes with classes and properties with properties. A SHACL shape blocks cross-kind alignment errors:
SELECT pg_ripple.load_shacl($TTL$
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
# An owl:equivalentClass link must point Class → Class.
[] a sh:NodeShape ;
sh:targetSubjectsOf owl:equivalentClass ;
sh:property [ sh:path rdf:type ; sh:hasValue owl:Class ] .
# An owl:equivalentProperty link must point Property → Property.
[] a sh:NodeShape ;
sh:targetSubjectsOf owl:equivalentProperty ;
sh:property [ sh:path rdf:type ;
sh:in ( owl:ObjectProperty owl:DatatypeProperty owl:AnnotationProperty ) ] .
$TTL$);
ALTER SYSTEM SET pg_ripple.shacl_mode = 'sync';
SELECT pg_reload_conf();
Step 5 — Apply alignments
For exact equivalences, use owl:equivalentClass / owl:equivalentProperty. For broader relationships, use rdfs:subClassOf / rdfs:subPropertyOf. The OWL 2 RL rule set will then propagate inferences in both directions.
-- High-confidence exact equivalences.
SELECT pg_ripple.insert_triple(
s1, '<http://www.w3.org/2002/07/owl#equivalentClass>', s2
)
FROM pg_ripple.find_alignments(
'https://example.org/ontologies/intkb',
'https://example.org/ontologies/schemaorg', 0.95);
-- Mid-confidence: subclass instead of equivalent.
SELECT pg_ripple.insert_triple(
s1, '<http://www.w3.org/2000/01/rdf-schema#subClassOf>', s2
)
FROM pg_ripple.find_alignments(
'https://example.org/ontologies/intkb',
'https://example.org/ontologies/schemaorg', 0.85)
WHERE similarity < 0.95;
Step 6 — Materialise the inference
SELECT pg_ripple.load_rules_builtin('owl-rl');
SELECT pg_ripple.infer('owl-rl');
After this, a SPARQL query for ?p a schema:Person returns every entity that is intkb:Employee (and vice versa for the equivalent classes) with no application-level glue.
Choosing equivalence vs subclass
| Confidence | Suggested axiom |
|---|---|
| ≥ 0.95 | owl:equivalentClass / owl:equivalentProperty |
| 0.85 – 0.95 | rdfs:subClassOf (one-way generalisation) |
| < 0.85 | Reject; surface to a human ontologist |
A bidirectional owl:equivalentClass is a strong claim — both classes have exactly the same instances. If the source vocabularies disagree on edge cases (e.g. minor employee vs human), the weaker rdfs:subClassOf is safer.
See also
- Knowledge-Graph Embeddings
- OWL 2 Profiles
- Record Linkage — the individual counterpart to ontology alignment.
Cookbook: LLM Workflow — Natural Language to Knowledge Graph Answer
Goal. Build a complete end-to-end workflow where a user asks a question in plain English, an LLM generates a SPARQL query, the query runs against your knowledge graph, and the results are returned to the user as a fluent natural-language answer.
Why pg_ripple. All four steps run inside one SQL call chain — from NL question to SPARQL to result to summarised answer. No separate orchestration layer required.
Time to first result. ~10 minutes.
The four-step workflow
User question (NL)
│
▼
sparql_from_nl() ← LLM generates SPARQL from your schema + few-shot examples
│
▼
sparql() ← execute the SPARQL against the VP tables
│
▼
sparql_construct_turtle() ← optionally fetch the context subgraph as Turtle
│
▼
LLM summarises results into a fluent answer
pg_ripple handles steps 1–3 natively. Step 4 is your LLM call.
Step 1 — Set up the LLM endpoint
ALTER SYSTEM SET pg_ripple.llm_endpoint = 'https://api.openai.com/v1';
ALTER SYSTEM SET pg_ripple.llm_api_key_env = 'OPENAI_API_KEY';
ALTER SYSTEM SET pg_ripple.llm_model = 'gpt-4o';
-- (Optional) embed entities for vector-assisted SPARQL generation.
ALTER SYSTEM SET pg_ripple.embedding_api_url = 'https://api.openai.com/v1';
ALTER SYSTEM SET pg_ripple.embedding_api_key_env = 'OPENAI_API_KEY';
ALTER SYSTEM SET pg_ripple.embedding_model = 'text-embedding-3-small';
SELECT pg_reload_conf();
Step 2 — Load a sample graph
SELECT pg_ripple.load_turtle($TTL$
@prefix ex: <https://example.org/> .
@prefix schema: <https://schema.org/> .
ex:Alice a schema:Person ;
schema:name "Alice Smith" ;
schema:jobTitle "Senior Engineer" ;
schema:knows ex:Bob .
ex:Bob a schema:Person ;
schema:name "Bob Jones" ;
schema:jobTitle "Data Scientist" ;
schema:knows ex:Carol .
ex:Carol a schema:Person ;
schema:name "Carol Kim" ;
schema:jobTitle "VP Engineering" .
$TTL$);
Step 3 — Add few-shot examples (recommended)
Domain-specific examples dramatically improve sparql_from_nl accuracy:
SELECT pg_ripple.add_llm_example(
'Who does Alice know?',
'PREFIX ex: <https://example.org/>
PREFIX schema: <https://schema.org/>
SELECT ?name WHERE {
ex:Alice schema:knows ?p .
?p schema:name ?name .
}'
);
SELECT pg_ripple.add_llm_example(
'What is Bob''s job title?',
'PREFIX ex: <https://example.org/>
PREFIX schema: <https://schema.org/>
SELECT ?title WHERE {
ex:Bob schema:jobTitle ?title .
}'
);
Step 4 — Generate and run a SPARQL query
-- Generate SPARQL from a natural-language question.
SELECT pg_ripple.sparql_from_nl('Who does Bob know?') AS generated_sparql;
Returns:
PREFIX ex: <https://example.org/>
PREFIX schema: <https://schema.org/>
SELECT ?name WHERE {
ex:Bob schema:knows ?person .
?person schema:name ?name .
}
Execute it:
SELECT result->>'name' AS person_name
FROM pg_ripple.sparql(
pg_ripple.sparql_from_nl('Who does Bob know?')
);
Step 5 — Fetch the relevant subgraph as context
For richer answers, pass the whole subgraph to the LLM rather than just the result rows:
SELECT pg_ripple.sparql_construct_turtle($Q$
CONSTRUCT { ?s ?p ?o }
WHERE {
?s ?p ?o .
FILTER(?s IN (<https://example.org/Alice>, <https://example.org/Bob>))
}
$Q$) AS context_turtle;
The Turtle block goes into the LLM prompt alongside the SPARQL result rows and the user question.
Step 6 — Combine with hybrid vector search
For questions where the relevant entity cannot be named precisely ("who works on machine learning"), combine the NL→SPARQL path with a vector similarity pre-filter:
-- Embed all entities first (once per load cycle).
SELECT pg_ripple.embed_entities();
-- Hybrid: find similar entities, then expand via SPARQL.
SELECT * FROM pg_ripple.sparql($$
PREFIX schema: <https://schema.org/>
SELECT ?name ?title WHERE {
?p schema:name ?name ;
schema:jobTitle ?title .
FILTER(?p IN (
SELECT s FROM pg_ripple.similar_entities('machine learning engineer', k := 5)
))
}
$$);
Step 7 — A complete Python application
import psycopg, openai
DB_URL = "postgresql://..."
client = openai.OpenAI()
SYSTEM_PROMPT = """
You are a helpful assistant. You have been given the result of a SPARQL query against
a knowledge graph, as well as the subgraph context as Turtle. Answer the user's
question concisely using only the provided data. If the data does not contain the
answer, say so.
""".strip()
def answer_question(question: str) -> str:
with psycopg.connect(DB_URL) as conn:
cur = conn.cursor()
# Step A: generate SPARQL and run it.
cur.execute(
"SELECT jsonb_agg(row_to_json(r)) FROM pg_ripple.sparql(pg_ripple.sparql_from_nl(%s)) r",
(question,)
)
sparql_rows = cur.fetchone()[0] or []
# Step B: fetch the context subgraph (optional but improves answer quality).
# For simplicity we pull the 1-hop neighbourhood of every ?s in the result.
turtle_context = ""
if sparql_rows:
subjects = [row.get("s") or row.get("p") or "" for row in sparql_rows if row]
if subjects:
subj_filter = ", ".join(f"<{s}>" for s in subjects if s.startswith("http"))
if subj_filter:
cur.execute(
"SELECT pg_ripple.sparql_construct_turtle("
" 'CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o . FILTER(?s IN (" + subj_filter + ")) }'"
")"
)
turtle_context = cur.fetchone()[0] or ""
prompt = (
f"{SYSTEM_PROMPT}\n\n"
f"=== SPARQL RESULT ===\n{sparql_rows}\n\n"
f"=== GRAPH CONTEXT (Turtle) ===\n{turtle_context}\n\n"
f"=== QUESTION ===\n{question}"
)
resp = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
temperature=0.0,
)
return resp.choices[0].message.content
if __name__ == "__main__":
print(answer_question("Who does Bob know and what are their job titles?"))
When to use each approach
| Approach | Best for |
|---|---|
sparql_from_nl() alone | Precise factual questions ("how many", "list all") |
rag_context() alone | Open-ended, multi-hop questions ("tell me about X") |
| Both combined (this recipe) | Production agents that need both precision and breadth |
sparql_from_nl() + Turtle context | Scenarios where the answer needs rich narrative explanation |
See also
- NL → SPARQL —
sparql_from_nldeep dive and tuning guide. - AI Overview — decision tree for all AI features.
- Cookbook: Grounded Chatbot — simpler single-pass variant.
- Cookbook: SPARQL Repair Workflow — what to do when NL→SPARQL fails.
- AI Agent Integration — LangChain / LlamaIndex wiring.
Schema-Aware NL→SPARQL with Vocabulary Bundles (v0.119.0)
When your dataset uses custom ontologies or domain-specific vocabularies,
sparql_from_nl() can automatically include vocabulary metadata in the LLM
system prompt, improving translation accuracy for ontology-rich knowledge graphs.
Enable bundle injection
-- Enable vocabulary bundle metadata in NL→SPARQL prompts (default: on)
SET pg_ripple.nl_sparql_include_bundles = on;
When enabled, sparql_from_nl() queries the triple store for:
skos:prefLabel— preferred labels for vocabulary termsdcterms:title— dataset and resource titlesschema:name— Schema.org type namesfoaf:name— FOAF person/org names
This metadata is prepended to the LLM prompt as grounding context, helping the model generate property URIs that match your actual data.
Disable for performance
For datasets that do not use these vocabulary predicates, disable to skip the extra SPI query:
SET pg_ripple.nl_sparql_include_bundles = off;
Cookbook: Federation with Wikidata and DBpedia
Goal. Combine data from your local knowledge graph with information from public SPARQL endpoints — Wikidata, DBpedia, or any other — in a single query. No ETL, no copying data, no scheduled synchronisation.
Why pg_ripple. SPARQL 1.1 SERVICE federation is built in. A cost-based planner pushes filters to the remote endpoint, a circuit-breaker handles timeouts, and an in-process cache avoids hitting remote endpoints on every query.
Time to first result. ~10 minutes.
Step 1 — Register remote endpoints
Registering an endpoint once lets the planner assign it a cost estimate and lets the health monitor track its availability.
SELECT pg_ripple.register_federation_endpoint(
endpoint := 'https://dbpedia.org/sparql',
label := 'DBpedia'
);
SELECT pg_ripple.register_federation_endpoint(
endpoint := 'https://query.wikidata.org/sparql',
label := 'Wikidata',
-- Optional: pin the TLS certificate to prevent MITM.
pin_fingerprint := NULL -- e.g. 'sha256:AA:BB:CC:...'
);
Without registration, SERVICE still works but gets a generic cost estimate and no health monitoring.
Step 2 — Load your local facts
SELECT pg_ripple.load_turtle($TTL$
@prefix ex: <https://example.org/people/> .
@prefix dbr: <http://dbpedia.org/resource/> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
ex:alice <http://schema.org/name> "Alice Smith" .
ex:alice owl:sameAs dbr:Alice_Smith_(scientist) .
ex:bob <http://schema.org/name> "Bob Jones" .
ex:bob owl:sameAs dbr:Robert_Jones_(chemist) .
$TTL$);
The owl:sameAs links tell the planner that your local entities correspond to DBpedia resources. With pg_ripple.sameas_reasoning = on (the default), the SERVICE query can use either IRI.
Step 3 — A simple federated query
Retrieve birth dates from DBpedia for people you have locally:
SELECT * FROM pg_ripple.sparql($$
PREFIX schema: <http://schema.org/>
PREFIX dbo: <http://dbpedia.org/ontology/>
PREFIX owl: <http://www.w3.org/2002/07/owl#>
SELECT ?name ?birthDate WHERE {
-- Local: who do we know about?
?local schema:name ?name ;
owl:sameAs ?remote .
-- Remote: fetch their birth dates from DBpedia.
SERVICE <https://dbpedia.org/sparql> {
?remote dbo:birthDate ?birthDate .
}
}
$$);
The planner sends ?remote dbo:birthDate ?birthDate to DBpedia with the values of ?remote bound from the local result — this is bound-join federation, the most efficient pattern.
Step 4 — Multi-source enrichment
Enrich a medical graph with both Wikidata (drug indications) and a local proprietary store (clinical trial data):
SELECT * FROM pg_ripple.sparql($$
PREFIX ex: <https://example.org/>
PREFIX wd: <http://www.wikidata.org/entity/>
PREFIX wdt: <http://www.wikidata.org/prop/direct/>
PREFIX owl: <http://www.w3.org/2002/07/owl#>
SELECT ?drugName ?indication ?trialId WHERE {
-- Local store: drugs and their Wikidata cross-links.
?drug ex:name ?drugName ;
owl:sameAs ?wikidataDrug .
-- Wikidata: approved indications.
SERVICE <https://query.wikidata.org/sparql> {
?wikidataDrug wdt:P2175 ?indicationEntity .
?indicationEntity wdt:P1813 ?indication . -- short name
}
-- Local store: clinical trial IDs.
OPTIONAL { ?drug ex:clinicalTrial ?trialId }
}
ORDER BY ?drugName
$$);
Step 5 — Check endpoint health and cache
-- Which endpoints are reachable right now?
SELECT endpoint, label, last_ping_ms, is_healthy
FROM pg_ripple.federation_endpoint_health()
ORDER BY last_ping_ms;
-- How many remote results are cached?
SELECT * FROM pg_ripple.federation_cache_stats();
-- Clear the cache to force a fresh fetch.
SELECT pg_ripple.reset_cache_stats();
Step 6 — Inspect the federation plan
Before running a heavy federated query, check how the planner intends to execute it:
SELECT pg_ripple.explain_sparql($$
PREFIX schema: <http://schema.org/>
PREFIX dbo: <http://dbpedia.org/ontology/>
SELECT ?name ?birthDate WHERE {
?local schema:name ?name .
SERVICE <https://dbpedia.org/sparql> {
?local dbo:birthDate ?birthDate .
}
}
$$, analyze := false);
A healthy federation plan shows a BoundJoin node rather than an IndependentService node. BoundJoin batches local values into a VALUES clause and sends one remote request; IndependentService executes the SERVICE clause for every row — much slower for large local result sets.
If you see IndependentService on a query that should be bound-join, ensure the shared variable (?local in the example) is bound by the local pattern before the SERVICE clause in the query text.
Circuit breaker and timeouts
| GUC | Default | Effect |
|---|---|---|
pg_ripple.federation_timeout_ms | 5000 | Abort a remote call after this many milliseconds |
pg_ripple.federation_retry_count | 2 | Retry this many times before tripping the circuit breaker |
pg_ripple.federation_circuit_open_ms | 60000 | Once a circuit trips, wait this long before retrying |
When a circuit is open, queries that use that SERVICE endpoint return an empty binding for that sub-pattern and continue with local results — they do not fail outright. The pg_ripple.federation_endpoint_health() view shows which circuits are open.
Security note
Only register endpoints you trust and control (or well-known public endpoints). The federation circuit sends query text to the remote endpoint — potentially including literal values from your local store. For sensitive data, use a local SPARQL-over-HTTP cache (or pg_ripple_http) as a proxy rather than federating directly to a public endpoint.
See also
- Federation (SPARQL SERVICE) — full SQL function reference.
- Vector Federation — federating vector search to Weaviate / Qdrant / Pinecone.
- SPARQL Query Debugger
Cookbook: SHACL + Datalog Data Quality Pipeline
Goal. Validate a bibliographic (or any domain) graph against SHACL rules, identify violations, repair or enrich the data with Datalog inference, and confirm quality in a single reproducible pipeline.
Why pg_ripple. SHACL validation and Datalog inference coexist in the same transaction. The repair-then-revalidate loop runs entirely in SQL — no round trips to an external validator.
Time to first result. ~10 minutes.
The pattern
raw data load
│
▼
define SHACL shapes
│
▼
run shacl_validate() → violations found?
│ │
│ yes ▼
│ ──── Datalog inference enriches missing triples
│ │
│ ▼
│ run shacl_validate() again
│ │
▼ no violation ▼
accepted rejected / escalate
Step 1 — Load an imperfect graph
The example uses a bibliographic graph where some books are missing mandatory metadata:
CREATE EXTENSION IF NOT EXISTS pg_ripple;
SELECT pg_ripple.load_turtle($TTL$
@prefix bib: <https://example.org/bib/> .
@prefix schema: <https://schema.org/> .
@prefix dc: <http://purl.org/dc/terms/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
bib:book1 a schema:Book ;
dc:title "Foundations of Databases" ;
dc:creator bib:author1 ;
dc:date "1995"^^xsd:gYear .
-- Intentionally missing: dc:creator and dc:date.
bib:book2 a schema:Book ;
dc:title "SPARQL 1.1 Query Language" .
bib:author1 a schema:Person ;
schema:name "Abiteboul, Hull, Vianu" .
$TTL$);
Step 2 — Define SHACL shapes
A book must have a title, at least one creator, and a publication date.
SELECT pg_ripple.load_shacl($TTL$
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix schema: <https://schema.org/> .
@prefix dc: <http://purl.org/dc/terms/> .
<https://shapes.example.org/BookShape> a sh:NodeShape ;
sh:targetClass schema:Book ;
sh:property [
sh:path dc:title ;
sh:minCount 1 ;
sh:datatype <http://www.w3.org/2001/XMLSchema#string>
] ;
sh:property [
sh:path dc:creator ;
sh:minCount 1 ;
sh:message "A book must have at least one creator."
] ;
sh:property [
sh:path dc:date ;
sh:minCount 1 ;
sh:message "A book must have a publication date."
] .
$TTL$);
Step 3 — Run the first validation pass
SELECT *
FROM pg_ripple.shacl_validate()
WHERE severity = 'Violation'
ORDER BY focus_node, result_path;
Expected output:
focus_node | result_path | message
───────────────────┼─────────────┼────────────────────────────────────
bib:book2 | dc:creator | A book must have at least one creator.
bib:book2 | dc:date | A book must have a publication date.
Two violations. Before escalating to a human reviewer, try to repair them with inference.
Step 4 — Apply Datalog inference
Suppose you have a rule: "if a book's W3C spec URI matches the known SPARQL spec, infer the working group as its creator". This is a domain-specific repair rule.
SELECT pg_ripple.load_rules($RULES$
# If a book has a known W3C spec URI, infer the W3C SPARQL WG as creator.
?book dc:creator ex:W3C_SPARQL_WG :-
?book a schema:Book ,
?book dc:title "SPARQL 1.1 Query Language" .
# Infer publication year from the spec publication record.
?book dc:date "2013"^^xsd:gYear :-
?book a schema:Book ,
?book dc:title "SPARQL 1.1 Query Language" .
$RULES$, 'bib_repair');
SELECT pg_ripple.infer('bib_repair');
For more general inference, materialise OWL RL axioms if your vocabulary uses owl:sameAs or rdfs:subClassOf:
SELECT pg_ripple.load_rules_builtin('owl-rl');
SELECT pg_ripple.infer('owl-rl');
Step 5 — Re-validate after inference
SELECT count(*) AS remaining_violations
FROM pg_ripple.shacl_validate()
WHERE severity = 'Violation';
If the count drops to zero, the data quality pipeline passes. If violations remain, escalate them to a data steward:
-- Export remaining violations as JSON for a ticket system.
SELECT jsonb_agg(to_jsonb(v))
FROM pg_ripple.shacl_validate() v
WHERE severity = 'Violation';
Step 6 — Make the pipeline idempotent
Wrap Steps 3–5 in a function so it can be called after every load:
CREATE OR REPLACE FUNCTION run_quality_gate(
rule_set TEXT DEFAULT 'bib_repair'
) RETURNS TABLE (violations BIGINT, violations_json JSONB) AS $$
BEGIN
-- Re-run inference to pick up any new triples from the last load.
PERFORM pg_ripple.infer(rule_set);
RETURN QUERY
SELECT count(*)::BIGINT,
jsonb_agg(to_jsonb(v))
FROM pg_ripple.shacl_validate() v
WHERE v.severity = 'Violation';
END;
$$ LANGUAGE plpgsql;
SELECT * FROM run_quality_gate();
Production patterns
Hard-fail on load
Set pg_ripple.shacl_mode = 'sync' — any SPARQL UPDATE that creates a violation is rolled back immediately. Use this for schemas where invalid data must never enter the store.
Async queue for review
Set pg_ripple.shacl_mode = 'async' — violations are written to _pg_ripple.shacl_violations without blocking the write. A periodic job checks the queue and routes flagged triples to a review workflow.
Confidence-weighted violations
Combine SHACL with Datalog lattice confidence: only escalate violations where the offending triple has confidence below 0.7 (the data probably arrived by automated inference, not by human entry).
SELECT v.focus_node, v.result_message, conf.confidence
FROM pg_ripple.shacl_validate() v
JOIN LATERAL (
SELECT CAST(o AS FLOAT) AS confidence
FROM pg_ripple.sparql(format(
'SELECT ?conf WHERE { << <%s> %s ?o >> ex:confidence ?conf }',
v.focus_node, v.result_path
))
) conf ON true
WHERE v.severity = 'Violation'
AND conf.confidence < 0.7;
See also
- Validating Data Quality (SHACL) — all shape types and modes.
- Reasoning and Inference (Datalog)
- Cookbook: Audit Trail — combine SHACL with PROV-O for a full evidence chain.
SKOS Thesaurus Management in PostgreSQL
This cookbook chapter shows how to load, query, and validate a SKOS taxonomy inside pg_ripple using the W3C-conformant SKOS entailment rules added in v0.98.0.
What is SKOS?
SKOS (Simple Knowledge Organization System) is the W3C standard vocabulary for
thesauri, taxonomies, classification schemes, and subject heading systems. A SKOS
taxonomy consists of skos:Concept nodes linked by skos:broader/skos:narrower
hierarchies, skos:related associative links, and labelled with skos:prefLabel.
Loading the SKOS Rule Set
-- Load all 28 W3C SKOS entailment rules (S7–S45).
SELECT pg_ripple.load_builtin_rules('skos');
-- Or use the named bundle API for versioned activation:
SELECT pg_ripple.load_datalog_bundle('skos');
Once loaded, pg_ripple automatically infers:
skos:broaderTransitiveandskos:narrowerTransitivetransitive closures- Inverse
skos:narrower/skos:broaderpairs skos:relatedsymmetry- Sub-property hierarchies for mapping predicates (broadMatch, exactMatch, etc.)
- Label sub-properties (
prefLabel→rdfs:label)
Loading a Thesaurus
-- Load Turtle-encoded thesaurus data.
SELECT pg_ripple.load_turtle($$
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .
@prefix ex: <http://example.org/thesaurus/> .
ex:Animals a skos:ConceptScheme ;
skos:hasTopConcept ex:Mammals .
ex:Mammals a skos:Concept ;
skos:prefLabel "Mammals"@en ;
skos:inScheme ex:Animals .
ex:Cats a skos:Concept ;
skos:prefLabel "Cats"@en ;
skos:broader ex:Mammals .
ex:Dogs a skos:Concept ;
skos:prefLabel "Dogs"@en ;
skos:broader ex:Mammals .
ex:Cats skos:related ex:Dogs .
$$);
-- Run inference to materialise transitive closures.
SELECT pg_ripple.infer('skos');
Querying Hierarchies with SPARQL
-- Find all broader transitive ancestors of Cats.
SELECT *
FROM pg_ripple.sparql($$
SELECT ?ancestor WHERE {
<http://example.org/thesaurus/Cats>
<http://www.w3.org/2004/02/skos/core#broaderTransitive> ?ancestor .
}
$$);
-- Find all concepts in the Mammals subtree (narrowerTransitive closure).
SELECT *
FROM pg_ripple.sparql($$
SELECT ?child WHERE {
<http://example.org/thesaurus/Mammals>
<http://www.w3.org/2004/02/skos/core#narrowerTransitive> ?child .
}
$$);
-- Cross-scheme mapping: find exactMatch counterparts.
SELECT *
FROM pg_ripple.sparql($$
SELECT ?concept ?match WHERE {
?concept <http://www.w3.org/2004/02/skos/core#exactMatch> ?match .
}
$$);
SQL Helper Functions
The five SQL helper functions provide convenient access to common thesaurus operations without writing SPARQL:
-- Traverse the broaderTransitive closure.
SELECT * FROM pg_ripple.skos_ancestors(
'http://example.org/thesaurus/Cats'
);
-- Returns: (ancestor_iri, depth)
-- Traverse the narrowerTransitive closure.
SELECT * FROM pg_ripple.skos_descendants(
'http://example.org/thesaurus/Mammals'
);
-- Returns: (descendant_iri, depth)
-- Look up the preferred label in English.
SELECT pg_ripple.skos_label(
'http://example.org/thesaurus/Cats',
'en'
);
-- Returns: "Cats"
-- Find all related concepts.
SELECT * FROM pg_ripple.skos_related(
'http://example.org/thesaurus/Cats'
);
-- Returns: (related_iri, relation) — e.g. Dogs, skos:related
-- Find sibling concepts (sharing a common skos:broader parent).
SELECT * FROM pg_ripple.skos_siblings(
'http://example.org/thesaurus/Cats'
);
-- Returns: (sibling_iri, shared_broader_iri) — e.g. Dogs, Mammals
Enabling SKOS-XL Extended Labels
SKOS-XL allows attaching structured metadata to labels. pg_ripple's SKOS-XL
rule set automatically projects skosxl:Label instances to plain skos:prefLabel
triples:
SELECT pg_ripple.load_builtin_rules('skosxl');
SELECT pg_ripple.infer('skosxl');
After loading, a triple <concept> skosxl:prefLabel <label> with
<label> skosxl:literalForm "text"@en will produce
<concept> skos:prefLabel "text"@en automatically.
Running Integrity Checks
The "skos-integrity" shape bundle validates 10 structural integrity conditions
from the W3C SKOS specification:
-- Activate the integrity shape bundle (automatically loads skos-transitive).
SELECT pg_ripple.load_shape_bundle('skos-integrity');
-- Run validation.
SELECT * FROM pg_ripple.validate_skos();
-- Returns: (violation_id, subject, message)
-- Example violation: SKOS-IC-05: multiple prefLabels with same language tag.
The 10 validators cover:
| ID | W3C Rule | Constraint |
|---|---|---|
| SKOS-IC-01 | S9 | ConceptScheme and Concept are disjoint |
| SKOS-IC-02 | S13 | prefLabel and altLabel must not share literal+lang |
| SKOS-IC-03 | S13 | prefLabel and hiddenLabel must not share literal+lang |
| SKOS-IC-04 | S13 | altLabel and hiddenLabel must not share literal+lang |
| SKOS-IC-05 | S14 | At most one prefLabel per language per concept |
| SKOS-IC-06 | S27 | related and broaderTransitive are disjoint |
| SKOS-IC-07 | S37 | Collection and Concept are disjoint |
| SKOS-IC-08 | S37 | Collection and ConceptScheme are disjoint |
| SKOS-IC-09 | S46 | exactMatch and broadMatch are disjoint |
| SKOS-IC-10 | S46 | exactMatch and relatedMatch are disjoint |
Named Bundle API
Use load_datalog_bundle instead of load_builtin_rules when you need
versioned, machine-checkable bundle tracking:
SELECT pg_ripple.load_datalog_bundle('skos');
SELECT pg_ripple.load_datalog_bundle('skos-transitive');
-- Check what is active.
SELECT * FROM pg_ripple.active_datalog_bundles;
-- Returns: (bundle_name, bundle_version, loaded_at, named_graph)
Performance Notes
skos:broaderTransitiveclosure materialisation viaWITH RECURSIVE … CYCLEtakes ~200 ms for 1,000 concepts and ~8 s for 100,000 concepts on first materialisation.- Subsequent IVM updates (when enabled) take ~10 ms per new
skos:broadertriple. skos_ancestors()runs a liveWITH RECURSIVEquery in ~5 ms for depth ≤ 10, using the materialisedbroaderTransitivetriples as the base.
See Also
- SKOS Reference — W3C Recommendation
- Datalog Reference — built-in RDFS and OWL RL rule support
- SQL Functions Reference — all pg_ripple SQL functions
Common Vocabulary Bundles: DCTERMS, Schema.org, and FOAF
This cookbook chapter shows how to load and use the three most widely-deployed linked-data vocabularies in pg_ripple — Dublin Core Terms (DCTERMS), Schema.org, and FOAF — using the named bundle API introduced in v0.98.0.
Together with SKOS (v0.98.0), these bundles complete the "Big 5" vocabularies that cover the vast majority of real-world knowledge-graph interoperability.
Why These Vocabularies?
| Vocabulary | Namespace prefix | Typical use |
|---|---|---|
| Dublin Core Terms | dcterms: | Metadata — creator, date, subject, rights, format |
| Schema.org | schema: | Structured data — people, orgs, products, places, events |
| FOAF | foaf: | Social-graph data — persons, accounts, social connections |
All three are already used in billions of public RDF documents, Wikidata exports, and enterprise knowledge graphs. Loading their rule sets lets pg_ripple perform property-hierarchy inference, type-subsumption reasoning, and integrity validation over data you already have — without any schema changes.
Loading the Bundles
-- DCTERMS: 11 Datalog rules + 8 integrity validators
SELECT pg_ripple.load_datalog_bundle('dcterms');
SELECT pg_ripple.load_shape_bundle('dcterms-integrity');
-- Schema.org: 15 Datalog rules + 6 integrity validators
SELECT pg_ripple.load_datalog_bundle('schema');
SELECT pg_ripple.load_shape_bundle('schema-integrity');
-- FOAF: 8 Datalog rules + 5 integrity validators
SELECT pg_ripple.load_datalog_bundle('foaf');
SELECT pg_ripple.load_shape_bundle('foaf-integrity');
After loading, confirm activation:
SELECT bundle_name, bundle_version, activated_at
FROM pg_ripple.active_datalog_bundles
ORDER BY activated_at;
DCTERMS — Dublin Core Terms
Inferred Properties
The dcterms bundle adds 11 rules, including:
- DC11 compatibility aliases —
dc11:titleis treated equivalently todcterms:title, and similarly forcreator,subject,description,publisher,date,type,format,identifier,source,language. - Structural inverses —
dcterms:hasPart↔dcterms:isPartOf,dcterms:hasVersion↔dcterms:isVersionOf,dcterms:replaces↔dcterms:isReplacedBy. - DC-SKOS bridge (DC-SKOS-01) — resources whose
dcterms:subjectis a member of a SKOSConceptSchemeautomatically receive askos:Concepttype assertion.
Example
-- Load some metadata triples using the old dc11: namespace.
SELECT pg_ripple.load_turtle($$
@prefix dc11: <http://purl.org/dc/elements/1.1/> .
@prefix dcterms: <http://purl.org/dc/terms/> .
@prefix ex: <https://example.org/> .
ex:Article1 dc11:creator "Alice" ;
dcterms:hasPart ex:Section1 .
$$);
-- Load the dcterms rule set.
SELECT pg_ripple.load_datalog_bundle('dcterms');
-- dc11:creator is now mapped to dcterms:creator.
SELECT pg_ripple.sparql_select($$
SELECT ?article ?creator
WHERE { ?article <http://purl.org/dc/terms/creator> ?creator }
$$);
-- dcterms:isPartOf is inferred from dcterms:hasPart.
SELECT pg_ripple.sparql_select($$
SELECT ?part ?whole
WHERE { ?part <http://purl.org/dc/terms/isPartOf> ?whole }
$$);
Schema.org — Structured Data Vocabulary
Inferred Properties
The schema bundle adds 15 rules:
- Inverse pairs —
schema:subjectOf↔schema:about,schema:hasPart↔schema:isPartOf,schema:workExample↔schema:exampleOfWork,schema:member↔schema:memberOf. - Type hierarchy —
schema:LocalBusiness⊆schema:Organization⊆schema:Thing,schema:Person⊆schema:Thing,schema:Product⊆schema:Thing, and several more. - Cross-vocab bridges —
schema:author→foaf:maker(SCHEMA-FOAF-01),schema:name→dcterms:title(SCHEMA-DC-01),schema:dataset→dcat:Dataset(SCHEMA-DCAT-01).
SQL Helper: schema_type_ancestors
-- Return all Schema.org type ancestors for a given IRI.
SELECT ancestor_type
FROM pg_ripple.schema_type_ancestors('https://schema.org/LocalBusiness');
-- Returns: schema:LocalBusiness, schema:Organization, schema:Thing
Example
-- Load a local business.
SELECT pg_ripple.load_turtle($$
@prefix schema: <https://schema.org/> .
@prefix ex: <https://example.org/> .
ex:AcmeCafe a schema:CafeOrCoffeeShop ;
schema:name "ACME Café" ;
schema:address ex:CafeAddress .
$$);
SELECT pg_ripple.load_datalog_bundle('schema');
-- CafeOrCoffeeShop is a FoodEstablishment, LocalBusiness, Organization, Thing.
SELECT pg_ripple.sparql_select($$
SELECT ?type
WHERE { <https://example.org/AcmeCafe> a ?type }
ORDER BY ?type
$$);
FOAF — Friend-of-a-Friend
Inferred Properties
The foaf bundle adds 8 rules:
foaf:knowssymmetry — if Alice knows Bob, Bob knows Alice.- Type subsumption —
foaf:Person⊆foaf:Agent,foaf:Organization⊆foaf:Agent,foaf:Group⊆foaf:Agent. - Inverse properties —
foaf:account↔foaf:accountFor,foaf:made↔foaf:maker. - DC-FOAF bridge (DC-FOAF-01) —
dcterms:creatortriples generate a correspondingfoaf:makerassertion on the referenced resource.
SQL Helper: foaf_persons
-- Return all foaf:Person IRIs and their foaf:name labels.
SELECT person_iri, name_label
FROM pg_ripple.foaf_persons();
Example
-- Load FOAF data.
SELECT pg_ripple.load_turtle($$
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix ex: <https://example.org/> .
ex:Alice a foaf:Person ;
foaf:name "Alice" ;
foaf:knows ex:Bob .
ex:Bob a foaf:Person ;
foaf:name "Bob" .
$$);
SELECT pg_ripple.load_datalog_bundle('foaf');
-- foaf:knows is symmetric: Bob knows Alice.
SELECT pg_ripple.sparql_select($$
SELECT ?who ?knows
WHERE { ?who <http://xmlns.com/foaf/0.1/knows> ?knows }
$$);
-- foaf:Person ⊆ foaf:Agent: Alice is also a foaf:Agent.
SELECT pg_ripple.sparql_select($$
SELECT ?agent
WHERE { ?agent a <http://xmlns.com/foaf/0.1/Agent> }
$$);
Cross-Vocabulary Inference
When all three bundles (and SKOS) are loaded together, cross-vocabulary bridges activate:
| Rule ID | Effect |
|---|---|
| DC-FOAF-01 | dcterms:creator ?x → foaf:maker ?x |
| SCHEMA-FOAF-01 | schema:author ?x → foaf:maker ?x |
| SCHEMA-DC-01 | schema:name ?x → dcterms:title ?x |
| DC-SKOS-01 | dcterms:subject ?x where ?x is in a ConceptScheme → ?x a skos:Concept |
This means a resource described with schema:author can be found via a
foaf:maker query, and a resource with schema:name can be found via a
dcterms:title filter — without any data transformation.
Integrity Validation
Each vocabulary ships with a corresponding integrity bundle:
-- Validate DCTERMS constraints (title cardinality, date format, etc.)
SELECT pg_ripple.load_shape_bundle('dcterms-integrity');
-- Validate Schema.org constraints (required name, URL format, price range, etc.)
SELECT pg_ripple.load_shape_bundle('schema-integrity');
-- Validate FOAF constraints (homepage IRI, mbox mailto:, name cardinality)
SELECT pg_ripple.load_shape_bundle('foaf-integrity');
Integrity violations are surfaced through the standard SHACL validation report.
Checking Active Bundles
SELECT bundle_name, bundle_version, activated_at
FROM pg_ripple.active_datalog_bundles
ORDER BY bundle_name;
-- bundle_name | bundle_version | activated_at
-- --------------------+----------------+---------------------------
-- dcterms | 0.99.0 | 2026-05-07 10:00:00+00
-- foaf | 0.99.0 | 2026-05-07 10:00:01+00
-- schema | 0.99.0 | 2026-05-07 10:00:02+00
-- skos | 0.98.0 | 2026-05-07 09:59:59+00
See Also
- SKOS Thesaurus Management — the fourth member of the "Big 5" vocabulary bundle suite
- SHACL + Datalog Data Quality Pipeline — how to combine rules and shapes for a full data-quality pipeline
Migrate from Neo4j to pg_ripple
This recipe walks through migrating a Neo4j property graph to pg_ripple's RDF triple store. You will export Neo4j data, convert it to RDF, load it into pg_ripple, and validate the migration.
Prerequisites
- Neo4j 5.x (Community or Enterprise)
- pg_ripple v0.90.0 or later
- Python 3.11+ with
neo4jandrdflibpackages
pip install neo4j rdflib
Step 1: Export the Neo4j Graph
Use the Neo4j Cypher shell or APOC to export nodes and relationships.
Option A: APOC Export to CSV
-- Export nodes
CALL apoc.export.csv.query(
"MATCH (n) RETURN id(n) as id, labels(n) as labels, properties(n) as props",
"/tmp/nodes.csv", {quotes: "always"}
)
-- Export relationships
CALL apoc.export.csv.query(
"MATCH (a)-[r]->(b) RETURN id(a) as from, type(r) as type, properties(r) as props, id(b) as to",
"/tmp/rels.csv", {quotes: "always"}
)
Option B: Python Direct Export
from neo4j import GraphDatabase
driver = GraphDatabase.driver("bolt://localhost:7687", auth=("neo4j", "password"))
def export_nodes(session):
return session.run("MATCH (n) RETURN id(n) as id, labels(n) as labels, properties(n) as props").data()
def export_rels(session):
return session.run(
"MATCH (a)-[r]->(b) RETURN id(a) as from, type(r) as rel, properties(r) as props, id(b) as to"
).data()
with driver.session() as s:
nodes = export_nodes(s)
rels = export_rels(s)
Step 2: Convert to RDF (Turtle)
Map Neo4j property graph concepts to RDF:
| Neo4j | RDF |
|---|---|
Node with label Person | ?node rdf:type ex:Person |
Node property name = "Alice" | ?node ex:name "Alice" |
Relationship KNOWS | ?from ex:knows ?to |
| Node ID | ex:node_{id} IRI |
| Relationship property | RDF-star quoted triple <<ex:from ex:knows ex:to>> ex:since "2021" |
from rdflib import Graph, URIRef, Literal, Namespace, RDF
import json
EX = Namespace("https://example.org/")
g = Graph()
# Bind prefixes
g.bind("ex", EX)
g.bind("rdf", RDF)
# Convert nodes
for node in nodes:
node_uri = EX[f"node_{node['id']}"]
# Type assertions
for label in node["labels"]:
g.add((node_uri, RDF.type, EX[label]))
# Properties
props = json.loads(node["props"]) if isinstance(node["props"], str) else node["props"]
for key, val in props.items():
g.add((node_uri, EX[key], Literal(val)))
# Convert relationships
for rel in rels:
from_uri = EX[f"node_{rel['from']}"]
to_uri = EX[f"node_{rel['to']}"]
pred_uri = EX[rel["rel"].lower()]
g.add((from_uri, pred_uri, to_uri))
# Export to Turtle
turtle_data = g.serialize(format="turtle")
with open("/tmp/neo4j_export.ttl", "w") as f:
f.write(turtle_data)
print(f"Exported {len(g)} triples to neo4j_export.ttl")
Step 3: Load into pg_ripple
-- Create a named graph for the migrated data
SELECT pg_ripple.insert_triple(
'https://example.org/neo4j_import',
'rdf:type',
'pg_ripple:Graph'
);
-- Load the Turtle file
SELECT pg_ripple.load_turtle(
pg_read_file('/tmp/neo4j_export.ttl'),
'https://example.org/neo4j_import'
);
-- Verify the load
SELECT COUNT(*) FROM pg_ripple.sparql(
'SELECT (COUNT(*) AS ?count) WHERE { GRAPH <https://example.org/neo4j_import> { ?s ?p ?o } }'
);
For large exports (millions of triples), use the bulk loader instead:
-- Stream via COPY for maximum throughput
SELECT pg_ripple.bulk_load_turtle_file(
'/tmp/neo4j_export.ttl',
'https://example.org/neo4j_import'
);
Step 4: Define SPARQL Views for Cypher-Style Queries
Map common Neo4j query patterns to SPARQL:
Neo4j: Shortest Path
MATCH p = shortestPath((a:Person {name: "Alice"})-[:KNOWS*]-(b:Person {name: "Bob"}))
RETURN p
-- pg_ripple equivalent (property path)
SELECT ?path WHERE {
<https://example.org/Alice> ex:knows+ <https://example.org/Bob> .
}
Neo4j: Pattern Matching
MATCH (p:Person)-[:WORKS_AT]->(c:Company)<-[:WORKS_AT]-(q:Person)
WHERE p.name = "Alice"
RETURN q.name
SELECT ?coworkerName WHERE {
<https://example.org/Alice> ex:worksAt ?company .
?company rdf:type ex:Company .
?coworker ex:worksAt ?company .
?coworker ex:name ?coworkerName .
FILTER(?coworker != <https://example.org/Alice>)
}
Step 5: Add SHACL Constraints
Recreate Neo4j uniqueness and existence constraints as SHACL shapes:
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <https://example.org/> .
ex:PersonShape a sh:NodeShape ;
sh:targetClass ex:Person ;
sh:property [
sh:path ex:email ;
sh:maxCount 1 ; -- Neo4j uniqueness constraint equivalent
sh:datatype xsd:string
] ;
sh:property [
sh:path ex:name ;
sh:minCount 1 ; -- NOT NULL equivalent
sh:datatype xsd:string
] .
SELECT pg_ripple.load_shacl($$
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <https://example.org/> .
-- paste shapes here
$$);
Step 6: Validate and Reconcile
-- Check constraint violations
SELECT pg_ripple.validate();
-- Find nodes without required properties (replaces Neo4j NULL checks)
SELECT binding->>'?node' AS node
FROM pg_ripple.sparql($$
SELECT ?node WHERE {
?node rdf:type ex:Person .
FILTER NOT EXISTS { ?node ex:name ?n }
}
$$);
-- Verify counts match Neo4j
SELECT binding->>'?count' AS triple_count
FROM pg_ripple.sparql('SELECT (COUNT(*) AS ?count) WHERE { ?s ?p ?o }');
Mapping Reference
| Neo4j Concept | RDF / pg_ripple |
|---|---|
| Node label | rdf:type assertion |
| Node property | predicate-object pair |
| Relationship type | predicate IRI |
| Relationship property | RDF-star quoted triple or reification |
| Node ID | IRI (UUID recommended) |
| Unique constraint | sh:maxCount 1 shape |
| Existence constraint | sh:minCount 1 shape |
| Index on property | VP table automatic indexing |
Cypher MATCH | SPARQL SELECT … WHERE { } |
Cypher CREATE | pg_ripple.insert_triple() |
Cypher MERGE | INSERT … ON CONFLICT DO NOTHING via bulk load |
Cypher shortestPath | ex:pred+ property path |
Cypher CALL db.schema() | pg_ripple.list_predicates() |
Performance Tips
- Batch load with
bulk_load_turtle_file()— 10–100× faster than individual inserts. - Set
pg_ripple.vp_promotion_threshold = 100during migration to create individual VP tables for more predicates, then restore the default after. - Run
VACUUM ANALYZEon_pg_ripple.dictionaryafter a large load. - Use
pg_ripple.run_merge()to flush deltas to the main partition before heavy analytic queries.
Build a RAG Pipeline from Scratch
This recipe walks through building a complete Retrieval-Augmented Generation (RAG) pipeline using pg_ripple's hybrid SPARQL + vector search. By the end you will have a system that answers natural-language questions using your knowledge graph as the context source.
Architecture
flowchart LR
Q[User question] --> E[Embed question\npg_ripple.embed_text]
E --> V[Vector similarity search\n_pg_ripple.embeddings]
V --> F[SPARQL filter\nopt. constraint]
F --> R[Retrieved entities\n+ graph context]
R --> P[Build LLM prompt]
P --> L[LLM API call\ngpt-4o / llama3]
L --> A[Answer + citations]
Prerequisites
- pg_ripple v0.49.0 or later (NL→SPARQL), v0.28.0+ (vector embeddings)
- pgvector extension installed
- OpenAI API key (or any OpenAI-compatible endpoint)
- Python 3.11+ with
psycopg,openai,requests
pip install psycopg openai requests
Step 1: Enable Embeddings
-- Set embedding endpoint (OpenAI-compatible)
ALTER SYSTEM SET pg_ripple.embedding_api_url = 'https://api.openai.com/v1';
ALTER SYSTEM SET pg_ripple.embedding_api_key = 'sk-...';
ALTER SYSTEM SET pg_ripple.embedding_model = 'text-embedding-3-small';
ALTER SYSTEM SET pg_ripple.embedding_dimensions = 1536;
ALTER SYSTEM SET pg_ripple.auto_embed = on;
SELECT pg_reload_conf();
For local Ollama embeddings:
ALTER SYSTEM SET pg_ripple.embedding_api_url = 'http://localhost:11434/v1';
ALTER SYSTEM SET pg_ripple.embedding_model = 'nomic-embed-text';
ALTER SYSTEM SET pg_ripple.embedding_dimensions = 768;
ALTER SYSTEM SET pg_reload_conf();
Step 2: Load Your Knowledge Graph
-- Load domain data
SELECT pg_ripple.load_turtle($$
@prefix ex: <https://pharma.example/> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix schema: <https://schema.org/> .
ex:aspirin a ex:Drug ;
rdfs:label "Aspirin" ;
ex:treats ex:headache, ex:fever, ex:inflammation ;
ex:class ex:NSAID ;
ex:approvedBy ex:FDA .
ex:ibuprofen a ex:Drug ;
rdfs:label "Ibuprofen" ;
ex:treats ex:pain, ex:inflammation ;
ex:class ex:NSAID .
ex:headache rdfs:label "headache" .
ex:fever rdfs:label "fever" .
ex:pain rdfs:label "pain" .
$$, 'https://pharma.example/graph');
Step 3: Embed the Knowledge Graph
Trigger embedding generation for all loaded entities:
-- Manually embed all entities (if auto_embed was off during load)
SELECT pg_ripple.embed_all_entities();
-- Check embedding coverage
SELECT COUNT(*) FROM _pg_ripple.embeddings;
Or with auto_embed = on, embeddings are generated automatically by the background worker as triples are loaded.
Step 4: Query via the HTTP API
The /rag endpoint handles the full pipeline:
# Simple question
curl -X POST http://localhost:7878/rag \
-H "Content-Type: application/json" \
-d '{
"question": "what drugs treat headaches?",
"k": 5
}'
Response:
{
"results": [
{
"entity_iri": "https://pharma.example/aspirin",
"label": "Aspirin",
"context_json": {
"label": "Aspirin",
"types": ["Drug"],
"properties": [
{"predicate": "treats", "object": "headache"},
{"predicate": "class", "object": "NSAID"}
],
"contextText": "Aspirin. Type: Drug, NSAID. Treats: headache, fever, inflammation."
},
"distance": 0.08
}
],
"context": "Aspirin. Type: Drug, NSAID. Treats: headache, fever, inflammation.\n\nIbuprofen. Type: Drug. Treats: pain, inflammation."
}
Step 5: Build a Python RAG Client
import os
import requests
from openai import OpenAI
RAG_ENDPOINT = "http://localhost:7878/rag"
AUTH_TOKEN = os.environ["PG_RIPPLE_HTTP_AUTH_TOKEN"]
OPENAI_KEY = os.environ["OPENAI_API_KEY"]
openai = OpenAI(api_key=OPENAI_KEY)
def retrieve(question: str, sparql_filter: str | None = None, k: int = 5) -> dict:
payload = {"question": question, "k": k}
if sparql_filter:
payload["sparql_filter"] = sparql_filter
r = requests.post(
RAG_ENDPOINT,
json=payload,
headers={"Authorization": f"Bearer {AUTH_TOKEN}"},
timeout=30,
)
r.raise_for_status()
return r.json()
def answer(question: str, sparql_filter: str | None = None) -> str:
retrieval = retrieve(question, sparql_filter)
context = retrieval["context"]
response = openai.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "system",
"content": (
"You are a helpful assistant. Answer the user's question using "
"only the following knowledge graph context. Cite entity IRIs "
"when making claims.\n\n"
f"Context:\n{context}"
),
},
{"role": "user", "content": question},
],
temperature=0.2,
)
return response.choices[0].message.content
# Example usage
print(answer("What NSAIDs are approved by the FDA?"))
print(answer(
"Which drugs treat fever?",
sparql_filter="?entity a <https://pharma.example/Drug>"
))
Step 6: Filter with SPARQL Constraints
Use sparql_filter to restrict retrieval to a class or subgraph:
# Only retrieve drugs approved by the FDA
answer(
"What are the safest pain relievers?",
sparql_filter=(
"?entity a <https://pharma.example/Drug> . "
"?entity <https://pharma.example/approvedBy> <https://pharma.example/FDA>"
)
)
Step 7: Use SHACL for Data Quality
Add SHACL constraints to ensure your knowledge graph contains complete information for RAG:
SELECT pg_ripple.load_shacl($$
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <https://pharma.example/> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
ex:DrugShape a sh:NodeShape ;
sh:targetClass ex:Drug ;
sh:property [
sh:path rdfs:label ;
sh:minCount 1 ;
sh:message "Every drug must have a label for RAG context generation"
] ;
sh:property [
sh:path ex:treats ;
sh:minCount 1 ;
sh:message "Every drug must have at least one indication"
] .
$$);
-- Validate the graph
SELECT pg_ripple.validate();
Step 8: Use Graph Context for Richer Embeddings
For semantically richer embeddings, enable graph-context mode:
ALTER SYSTEM SET pg_ripple.use_graph_context = on;
SELECT pg_reload_conf();
-- Re-embed all entities with graph context
SELECT pg_ripple.embed_all_entities(force_recompute := true);
With use_graph_context = on, each entity is embedded with its full RDF neighborhood serialized as text, producing embeddings that capture graph structure — not just the entity label.
Step 9: NL → SPARQL Generation (Advanced)
For complex queries, use the NL → SPARQL endpoint to generate and execute SPARQL directly:
-- Configure LLM
ALTER SYSTEM SET pg_ripple.llm_endpoint = 'https://api.openai.com/v1';
ALTER SYSTEM SET pg_ripple.llm_model = 'gpt-4o';
ALTER SYSTEM SET pg_ripple.llm_api_key_env = 'OPENAI_API_KEY';
SELECT pg_reload_conf();
-- Generate and run SPARQL from natural language
SELECT pg_ripple.sparql_from_nl(
'Find all NSAIDs approved by the FDA that treat headaches'
);
Production Checklist
-
Set
pg_ripple.embedding_api_keyfrom a secrets manager (notALTER SYSTEM) -
Enable
PG_RIPPLE_HTTP_AUTH_TOKENon all HTTP endpoints -
Configure
pg_ripple.sparql_max_rowsto limit runaway queries -
Monitor
pg_ripple_embedding_queue_depthPrometheus gauge — grow workers if > 10,000 -
Use
pg_ripple.export_max_rowsto prevent large RAG context blowup - Add SHACL shapes to ensure all entities have labels (required for RAG context)
Further Reading
Rule-Library Federation Recipe
Added in v0.120.0
Use this recipe when you already have a versioned rule library and want to share it across pg_ripple instances. For the full walkthrough, including a two-instance setup and monitoring examples, see the Rule Library Federation guide.
When to Use This
Rule-library federation fits teams that maintain shared Datalog or SHACL rule sets, such as RDFS entailment, customer deduplication, or compliance checks, and need the same rules installed consistently across environments.
Minimal Flow
- Install or create the library on the source instance.
- Publish the library with the URL where the HTTP companion serves its stream.
- Record a subscription on the target instance.
- Ask the target HTTP companion to fetch and install the remote stream.
-- Source instance: publish an installed library.
SELECT pg_ripple.publish_rule_library(
'rdfs-entailment',
'https://instance-a.example.com/rule-libraries/rdfs-entailment/stream'
);
-- Target instance: record subscription intent.
SELECT pg_ripple.subscribe_rule_library(
'https://instance-a.example.com/rule-libraries/rdfs-entailment/stream',
'rdfs-entailment'
);
# Target HTTP companion: fetch and install the stream.
curl -X POST https://instance-b.example.com/rule-libraries/rdfs-entailment/subscribe \
-H "Authorization: Bearer $PG_RIPPLE_HTTP_AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{"source_uri":"https://instance-a.example.com/rule-libraries/rdfs-entailment/stream"}'
Notes
publish_rule_library()andsubscribe_rule_library()are catalog operations; they do not start a background daemon.POST /rule-libraries/{name}/subscribeperforms the remote fetch and installs rules into the target instance.- The current HTTP fetcher does not forward a separate Authorization header to the source stream. For protected source streams, use a trusted internal route or fetch from an authenticated client.
See Also
- Rule Library Federation guide
- SQL API: publish_rule_library
- SQL API: subscribe_rule_library
- HTTP API reference
§2.8 APIs and Integration
What and Why
pg_ripple's SQL functions are powerful, but most applications do not talk to PostgreSQL directly. The pg_ripple_http companion service exposes a W3C-compliant SPARQL Protocol endpoint over HTTP, so any SPARQL client, programming language, or tool can query your knowledge graph.
This chapter covers:
- pg_ripple_http: the standalone SPARQL endpoint service.
- Application code examples: Python, JavaScript, and Java.
- SPARQL federation: query remote SPARQL endpoints from within pg_ripple.
- Caching strategies: plan cache, connection pooling, and result caching.
How It Works
pg_ripple_http Architecture
pg_ripple_http is a standalone Rust binary (not a PostgreSQL extension) that:
- Connects to PostgreSQL via a deadpool connection pool.
- Receives SPARQL queries via HTTP GET/POST (W3C SPARQL Protocol).
- Calls
pg_ripple.sparql(),pg_ripple.sparql_construct(), etc. via SQL. - Returns results in standard formats: SPARQL Results JSON/XML, Turtle, N-Triples, JSON-LD.
- Exposes a
/ragendpoint for AI retrieval.
Supported Endpoints
| Method | Path | Content-Type | Description |
|---|---|---|---|
| GET | /sparql?query=... | Accept header | SPARQL query via URL parameter |
| POST | /sparql | application/sparql-query | SPARQL query in request body |
| POST | /sparql | application/x-www-form-urlencoded | SPARQL query as form parameter |
| POST | /sparql | application/sparql-update | SPARQL Update in request body |
| POST | /rag | application/json | RAG retrieval endpoint |
| GET | /health | application/json | Health check |
| GET | /metrics | text/plain | Prometheus metrics |
Response Formats (Content Negotiation)
| Accept header | Format |
|---|---|
application/sparql-results+json | SPARQL Results JSON (default for SELECT/ASK) |
application/sparql-results+xml | SPARQL Results XML |
text/csv | CSV |
text/tab-separated-values | TSV |
text/turtle | Turtle (for CONSTRUCT/DESCRIBE) |
application/n-triples | N-Triples (for CONSTRUCT/DESCRIBE) |
application/ld+json | JSON-LD (for CONSTRUCT/DESCRIBE) |
Worked Examples
Starting pg_ripple_http
# Set environment variables
export PG_RIPPLE_DATABASE_URL="postgresql://user:pass@localhost:5432/mydb"
export PG_RIPPLE_LISTEN="0.0.0.0:8080"
export PG_RIPPLE_AUTH_TOKEN="my-secret-token" # optional
# Start the server
pg_ripple_http
Configuration via environment variables:
| Variable | Default | Description |
|---|---|---|
PG_RIPPLE_DATABASE_URL | postgresql://localhost/postgres | PostgreSQL connection string |
PG_RIPPLE_LISTEN | 127.0.0.1:8080 | Listen address and port |
PG_RIPPLE_AUTH_TOKEN | (none) | Bearer token for authentication |
PG_RIPPLE_POOL_SIZE | 10 | Connection pool size |
PG_RIPPLE_RATE_LIMIT | 100 | Requests per second per IP |
PG_RIPPLE_CORS_ORIGIN | * | CORS allowed origins |
Querying via curl
SPARQL SELECT via GET:
curl -G http://localhost:8080/sparql \
--data-urlencode 'query=PREFIX dct: <http://purl.org/dc/terms/> SELECT ?paper ?title WHERE { ?paper dct:title ?title } LIMIT 10' \
-H "Accept: application/sparql-results+json"
SPARQL SELECT via POST (body):
curl -X POST http://localhost:8080/sparql \
-H "Content-Type: application/sparql-query" \
-H "Accept: application/sparql-results+json" \
-d 'PREFIX dct: <http://purl.org/dc/terms/>
PREFIX bibo: <http://purl.org/ontology/bibo/>
SELECT ?paper ?title
WHERE {
?paper a bibo:AcademicArticle ;
dct:title ?title .
}
ORDER BY ?title
LIMIT 20'
SPARQL CONSTRUCT as Turtle:
curl -X POST http://localhost:8080/sparql \
-H "Content-Type: application/sparql-query" \
-H "Accept: text/turtle" \
-d 'PREFIX dct: <http://purl.org/dc/terms/>
PREFIX ex: <https://example.org/>
CONSTRUCT { ?paper ex:hasTitle ?title }
WHERE { ?paper dct:title ?title }'
SPARQL CONSTRUCT as JSON-LD:
curl -X POST http://localhost:8080/sparql \
-H "Content-Type: application/sparql-query" \
-H "Accept: application/ld+json" \
-d 'PREFIX dct: <http://purl.org/dc/terms/>
PREFIX ex: <https://example.org/>
CONSTRUCT { ?paper ex:hasTitle ?title }
WHERE { ?paper dct:title ?title }'
SPARQL Update:
curl -X POST http://localhost:8080/sparql \
-H "Content-Type: application/sparql-update" \
-d 'PREFIX ex: <https://example.org/>
PREFIX dct: <http://purl.org/dc/terms/>
INSERT DATA {
ex:paper/new1 dct:title "A New Discovery" .
}'
RAG endpoint:
curl -X POST http://localhost:8080/rag \
-H "Content-Type: application/json" \
-d '{
"question": "What papers discuss knowledge graphs?",
"sparql_filter": "?entity a <http://purl.org/ontology/bibo/AcademicArticle> .",
"k": 5,
"output_format": "jsonld"
}'
The RAG response includes a context field with pre-formatted text for LLM prompts:
{
"results": [
{
"entity_iri": "https://example.org/paper/42",
"label": "Knowledge Graphs in Practice",
"context_json": {"@type": ["AcademicArticle"], "...": "..."},
"distance": 0.12
}
],
"context": "Knowledge Graphs in Practice (AcademicArticle): A comprehensive survey..."
}
Authentication (when PG_RIPPLE_AUTH_TOKEN is set):
curl -X POST http://localhost:8080/sparql \
-H "Authorization: Bearer my-secret-token" \
-H "Content-Type: application/sparql-query" \
-d 'SELECT ?s ?p ?o WHERE { ?s ?p ?o } LIMIT 5'
Python with psycopg2
Query pg_ripple directly from Python via SQL:
import json
import psycopg2
conn = psycopg2.connect("dbname=mydb user=postgres")
cur = conn.cursor()
# Execute a SPARQL query
cur.execute("""
SELECT * FROM pg_ripple.sparql(%s)
""", ("""
PREFIX dct: <http://purl.org/dc/terms/>
PREFIX bibo: <http://purl.org/ontology/bibo/>
SELECT ?paper ?title ?author
WHERE {
?paper a bibo:AcademicArticle ;
dct:title ?title ;
dct:creator ?author .
}
ORDER BY ?title
LIMIT 20
""",))
for row in cur.fetchall():
result = json.loads(row[0])
print(f"Paper: {result['paper']}")
print(f"Title: {result['title']}")
print(f"Author: {result['author']}")
print()
# Load Turtle data
cur.execute("""
SELECT pg_ripple.load_turtle(%s)
""", ("""
@prefix ex: <https://example.org/> .
@prefix dct: <http://purl.org/dc/terms/> .
ex:paper/new dct:title "Loaded from Python" .
""",))
conn.commit()
# Export as JSON-LD
cur.execute("SELECT pg_ripple.export_jsonld()")
jsonld = json.loads(cur.fetchone()[0])
print(json.dumps(jsonld, indent=2))
cur.close()
conn.close()
Python with SPARQLWrapper
Query the pg_ripple_http endpoint using the standard SPARQLWrapper library:
from SPARQLWrapper import SPARQLWrapper, JSON, TURTLE
# Point to the pg_ripple_http endpoint
sparql = SPARQLWrapper("http://localhost:8080/sparql")
# SELECT query
sparql.setQuery("""
PREFIX dct: <http://purl.org/dc/terms/>
PREFIX bibo: <http://purl.org/ontology/bibo/>
SELECT ?paper ?title
WHERE {
?paper a bibo:AcademicArticle ;
dct:title ?title .
}
LIMIT 10
""")
sparql.setReturnFormat(JSON)
results = sparql.query().convert()
for binding in results["results"]["bindings"]:
print(f"{binding['paper']['value']}: {binding['title']['value']}")
# CONSTRUCT query as Turtle
sparql.setQuery("""
PREFIX dct: <http://purl.org/dc/terms/>
PREFIX ex: <https://example.org/>
CONSTRUCT { ?paper ex:hasTitle ?title }
WHERE { ?paper dct:title ?title }
""")
sparql.setReturnFormat(TURTLE)
turtle_output = sparql.query().convert()
print(turtle_output.decode("utf-8"))
JavaScript with pg
Query pg_ripple directly from Node.js:
const { Client } = require('pg');
async function main() {
const client = new Client({ connectionString: 'postgresql://localhost/mydb' });
await client.connect();
// SPARQL SELECT
const { rows } = await client.query(
`SELECT * FROM pg_ripple.sparql($1)`,
[`
PREFIX dct: <http://purl.org/dc/terms/>
PREFIX bibo: <http://purl.org/ontology/bibo/>
SELECT ?paper ?title
WHERE {
?paper a bibo:AcademicArticle ;
dct:title ?title .
}
LIMIT 10
`]
);
for (const row of rows) {
const result = row.result;
console.log(`Paper: ${result.paper}, Title: ${result.title}`);
}
// Load Turtle
const loadResult = await client.query(
`SELECT pg_ripple.load_turtle($1)`,
[`
@prefix ex: <https://example.org/> .
@prefix dct: <http://purl.org/dc/terms/> .
ex:paper/fromjs dct:title "Loaded from JavaScript" .
`]
);
console.log(`Loaded: ${loadResult.rows[0].load_turtle} triples`);
// Export JSON-LD
const jsonldResult = await client.query(`SELECT pg_ripple.export_jsonld()`);
console.log(JSON.stringify(jsonldResult.rows[0].export_jsonld, null, 2));
await client.end();
}
main().catch(console.error);
JavaScript with fetch (HTTP endpoint)
async function sparqlQuery(query) {
const response = await fetch('http://localhost:8080/sparql', {
method: 'POST',
headers: {
'Content-Type': 'application/sparql-query',
'Accept': 'application/sparql-results+json',
},
body: query,
});
return response.json();
}
const results = await sparqlQuery(`
PREFIX dct: <http://purl.org/dc/terms/>
SELECT ?paper ?title
WHERE { ?paper dct:title ?title }
LIMIT 10
`);
for (const binding of results.results.bindings) {
console.log(`${binding.paper.value}: ${binding.title.value}`);
}
Java with JDBC
import java.sql.*;
import org.json.JSONObject;
public class PgRippleExample {
public static void main(String[] args) throws Exception {
Connection conn = DriverManager.getConnection(
"jdbc:postgresql://localhost:5432/mydb", "postgres", "password"
);
// SPARQL SELECT
PreparedStatement stmt = conn.prepareStatement(
"SELECT * FROM pg_ripple.sparql(?)"
);
stmt.setString(1,
"PREFIX dct: <http://purl.org/dc/terms/> " +
"PREFIX bibo: <http://purl.org/ontology/bibo/> " +
"SELECT ?paper ?title " +
"WHERE { " +
" ?paper a bibo:AcademicArticle ; " +
" dct:title ?title . " +
"} LIMIT 10"
);
ResultSet rs = stmt.executeQuery();
while (rs.next()) {
String jsonStr = rs.getString("result");
JSONObject result = new JSONObject(jsonStr);
System.out.println("Paper: " + result.getString("paper"));
System.out.println("Title: " + result.getString("title"));
}
rs.close();
stmt.close();
// Load Turtle
PreparedStatement loadStmt = conn.prepareStatement(
"SELECT pg_ripple.load_turtle(?)"
);
loadStmt.setString(1,
"@prefix ex: <https://example.org/> .\n" +
"@prefix dct: <http://purl.org/dc/terms/> .\n" +
"ex:paper/fromjava dct:title \"Loaded from Java\" .\n"
);
ResultSet loadRs = loadStmt.executeQuery();
if (loadRs.next()) {
System.out.println("Loaded: " + loadRs.getLong(1) + " triples");
}
loadRs.close();
loadStmt.close();
conn.close();
}
}
SPARQL Federation
pg_ripple can query remote SPARQL endpoints from within a SPARQL query using the
SERVICE keyword. This lets you join local data with remote datasets like Wikidata
or DBpedia.
Querying a Remote SPARQL Endpoint
SELECT * FROM pg_ripple.sparql('
PREFIX dct: <http://purl.org/dc/terms/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX wd: <http://www.wikidata.org/entity/>
PREFIX wdt: <http://www.wikidata.org/prop/direct/>
SELECT ?paper ?title ?wikidataLabel
WHERE {
?paper dct:title ?title ;
dct:subject ?topic .
SERVICE <https://query.wikidata.org/sparql> {
?topic rdfs:label ?wikidataLabel .
FILTER (LANG(?wikidataLabel) = "en")
}
}
LIMIT 10
');
Vector Federation
Register external vector services for federated similarity search (see Vector Federation for full details):
-- Register a Qdrant endpoint
SELECT pg_ripple.register_vector_endpoint(
'https://qdrant.internal:6333',
'qdrant'
);
-- Register a Weaviate endpoint
SELECT pg_ripple.register_vector_endpoint(
'https://weaviate.internal:8080',
'weaviate'
);
Federation queries add network latency. Set timeouts to prevent slow remote endpoints from blocking local queries:
SET pg_ripple.vector_federation_timeout_ms = 5000;
</div>
</div>
Common Patterns
Pattern: Connection Pooling
For high-traffic applications, use a connection pooler (PgBouncer, pgcat) between your application and PostgreSQL:
App → PgBouncer (port 6432) → PostgreSQL (port 5432)
pg_ripple_http uses its own connection pool internally (configurable via PG_RIPPLE_POOL_SIZE).
Pattern: Result Caching
Cache SPARQL results at the application level for frequently-repeated queries:
import json
import hashlib
import redis
import psycopg2
cache = redis.Redis()
def cached_sparql(query, ttl=300):
key = f"sparql:{hashlib.sha256(query.encode()).hexdigest()}"
cached = cache.get(key)
if cached:
return json.loads(cached)
conn = psycopg2.connect("dbname=mydb")
cur = conn.cursor()
cur.execute("SELECT * FROM pg_ripple.sparql(%s)", (query,))
results = [json.loads(row[0]) for row in cur.fetchall()]
cur.close()
conn.close()
cache.setex(key, ttl, json.dumps(results))
return results
Pattern: SPARQL Views for Pre-Computed Results
For dashboard queries that run frequently, create SPARQL views (requires pg_trickle):
-- Create a pre-computed view of paper counts per institution
SELECT pg_ripple.create_sparql_view(
'papers_by_institution',
'PREFIX dct: <http://purl.org/dc/terms/>
PREFIX schema: <https://schema.org/>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
SELECT ?inst ?instName (COUNT(DISTINCT ?paper) AS ?count)
WHERE {
?paper dct:creator ?author .
?author schema:affiliation ?inst .
?inst foaf:name ?instName .
}
GROUP BY ?inst ?instName',
'30s',
true
);
-- Query the view directly (instant, no SPARQL parsing)
SELECT * FROM pg_ripple.papers_by_institution;
Pattern: Prometheus Monitoring
pg_ripple_http exposes Prometheus metrics at /metrics:
curl http://localhost:8080/metrics
Metrics include:
pg_ripple_http_requests_total— total request count by endpoint and statuspg_ripple_http_request_duration_seconds— request latency histogrampg_ripple_http_active_connections— current active connections
Performance and Trade-offs
Direct SQL vs HTTP Endpoint
| Access method | Latency overhead | Best for |
|---|---|---|
| Direct SQL (psycopg2, JDBC) | None | Server-side applications, ETL |
| pg_ripple_http | ~1-5ms per request | Web applications, REST APIs, federated queries |
Connection Pool Sizing
Rule of thumb: set pool size to 2 * CPU cores for OLTP workloads. For SPARQL-heavy
analytics, 4 * CPU cores may be better:
export PG_RIPPLE_POOL_SIZE=20
Rate Limiting
pg_ripple_http includes built-in rate limiting to prevent abuse:
export PG_RIPPLE_RATE_LIMIT=100 # requests per second per IP
For public-facing endpoints, combine with a reverse proxy (nginx, Caddy) for additional protection.
CORS Configuration
For browser-based applications:
export PG_RIPPLE_CORS_ORIGIN="https://myapp.example.com"
Set to * for development; restrict to specific origins in production.
Gotchas and Debugging
Authentication Errors
If PG_RIPPLE_AUTH_TOKEN is set, all requests must include the Authorization header:
HTTP 401: Missing or invalid authorization token
Fix: include Authorization: Bearer <token> in the request headers.
Connection Refused
If pg_ripple_http cannot connect to PostgreSQL:
Error: connection refused (os error 61)
Fix: check PG_RIPPLE_DATABASE_URL and ensure PostgreSQL is running and accepting connections.
Content-Type Negotiation
If you get unexpected response formats, check the Accept header. pg_ripple_http uses
content negotiation:
# Explicitly request JSON results
curl -H "Accept: application/sparql-results+json" ...
# Explicitly request Turtle for CONSTRUCT
curl -H "Accept: text/turtle" ...
Federation Timeouts
Remote SPARQL endpoints can be slow. If federation queries time out:
-- Increase the timeout
SET pg_ripple.vector_federation_timeout_ms = 30000;
For SPARQL federation (SERVICE keyword), pg_ripple uses PostgreSQL's
statement_timeout for the overall query:
SET statement_timeout = '60s';
Health Check
Use the /health endpoint for load balancer configuration:
curl http://localhost:8080/health
# Returns: {"status": "ok", "pool_size": 10, "pool_available": 8}
Next Steps
- §2.3 Querying with SPARQL — SPARQL query reference for the queries you send via APIs.
- §2.7 AI Retrieval and GraphRAG — RAG endpoint details and LLM integration.
- §2.6 Exporting and Sharing — export formats returned by the HTTP endpoint.
SPARQL Query Debugger — EXPLAIN SPARQL
pg_ripple v0.50.0 extends pg_ripple.explain_sparql() with an interactive query debugger mode (analyze := true) that surfaces the algebra tree, generated SQL, plan-cache status, and per-operator row counts as a structured JSONB document.
Function Signature
pg_ripple.explain_sparql(
query TEXT,
analyze BOOL DEFAULT FALSE
) RETURNS JSONB
Output Schema
| Key | Type | Description |
|---|---|---|
algebra | text | spargebra algebra tree (string representation) |
sql | text | Generated SQL sent to PostgreSQL |
plan | text | PostgreSQL EXPLAIN output (JSON format) |
cache_status | text | "hit", "miss", or "bypass" |
cache_hit | bool | Legacy alias for cache_status = "hit" |
actual_rows | array | Per-operator actual row counts (only when analyze = true) |
topn_applied | bool | Whether TopN push-down optimisation was applied |
encode_calls | int | Dictionary encode calls during translation |
cache_status values
| Value | Meaning |
|---|---|
"hit" | SQL was served from the per-backend plan cache |
"miss" | SQL was freshly compiled; query not yet cached |
"bypass" | Plan caching is disabled (pg_ripple.plan_cache_size = 0) |
actual_rows
Only present when analyze := true. Contains a flat array of actual row counts extracted from PostgreSQL's EXPLAIN ANALYZE JSON output — one integer per plan node, in plan-tree order.
Examples
Basic explain (no execution)
SELECT pg_ripple.explain_sparql(
'SELECT ?name WHERE { ?s <http://schema.org/name> ?name }',
false
) AS explain_out;
Interactive debugger (with row counts)
WITH result AS (
SELECT pg_ripple.explain_sparql(
'SELECT ?name WHERE { ?s <http://schema.org/name> ?name }',
true
) AS j
)
SELECT
j->>'cache_status' AS cache_status,
j->>'sql' AS generated_sql,
j->'actual_rows' AS actual_rows_per_operator
FROM result;
All four query types
-- SELECT
SELECT pg_ripple.explain_sparql('SELECT * WHERE { ?s ?p ?o } LIMIT 5', true);
-- ASK
SELECT pg_ripple.explain_sparql('ASK { <http://example.org/a> ?p ?o }', true);
-- CONSTRUCT
SELECT pg_ripple.explain_sparql(
'CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o } LIMIT 5', true);
-- DESCRIBE (returns algebra + synthetic SQL stub; no error)
SELECT pg_ripple.explain_sparql('DESCRIBE <http://example.org/a>', false);
Interpreting the Algebra Tree
The algebra field is the string representation of the internal spargebra algebra tree. It shows how the SPARQL engine parsed and structured your query before SQL generation. Common nodes:
Project— variable projectionFilter— FILTER expressionJoin/LeftJoin— triple-pattern joins / OPTIONALBGP— basic graph pattern (list of triple patterns)Union— UNION clauseExtend— BIND expression
Tuning with EXPLAIN ANALYZE
Use actual_rows to compare estimated vs actual row counts:
WITH debug AS (
SELECT pg_ripple.explain_sparql(
'SELECT ?s WHERE { ?s <http://schema.org/type> <http://schema.org/Person> }',
true
) AS j
)
SELECT
jsonb_array_length(j->'actual_rows') AS num_plan_nodes,
j->'actual_rows' AS row_counts
FROM debug;
If row estimates are far from actuals, run ANALYZE on the VP tables:
ANALYZE _pg_ripple.vp_rare;
Configuration
| GUC | Default | Effect on explain |
|---|---|---|
pg_ripple.plan_cache_size | 256 | Set to 0 to force cache_status = "bypass" |
pg_ripple.max_path_depth | 10 | Affects property-path SQL depth; changes cache key |
Rule Library Federation
This guide walks through a complete end-to-end example of publishing a rule library on one pg_ripple instance (instance A) and subscribing to it from a second instance (instance B).
Prerequisites
- Two running pg_ripple 0.120.0+ instances with pg_ripple_http
- Both HTTP companions accessible from each other over HTTPS
PG_RIPPLE_HTTP_AUTH_TOKENconfigured for read access to protected stream endpoints. IfPG_RIPPLE_HTTP_DATALOG_WRITE_TOKENis set on the subscribing instance, use it for the subscribe call; otherwise the main auth token is used.- The source stream must be reachable by the subscribing HTTP companion. The
current subscribe handler fetches
source_uridirectly and does not forward a separate source Authorization header, so protect private streams with a trusted internal network or gateway when source auth is required.
Step 1 — Install a rule library on instance A
-- Connect to instance A
\c ripple_a
-- Install the RDFS entailment rule set
SELECT pg_ripple.install_rule_library(
'rdfs-entailment',
'1.0.0',
'RDFS entailment rules: rdfs:subClassOf, rdfs:subPropertyOf, rdfs:domain, rdfs:range'
);
-- Add the core RDFS rules
SELECT pg_ripple.add_rule(
'rdfs-entailment',
'?x rdf:type ?C :- ?x rdf:type ?D, ?D rdfs:subClassOf ?C .'
);
SELECT pg_ripple.add_rule(
'rdfs-entailment',
'?x ?q ?y :- ?x ?p ?y, ?p rdfs:subPropertyOf ?q .'
);
Step 2 — Publish the library on instance A
-- Publish via the SQL function (records in the federation catalog)
SELECT pg_ripple.publish_rule_library(
'rdfs-entailment',
'https://instance-a.example.com/rule-libraries/rdfs-entailment/stream'
);
The HTTP companion immediately begins serving the stream at
GET /rule-libraries/rdfs-entailment/stream.
Step 3 — Subscribe from instance B
-- Connect to instance B
\c ripple_b
-- Record subscription intent. The SQL function validates the URI and catalog
-- state, but the HTTP companion performs the actual fetch/install step.
SELECT pg_ripple.subscribe_rule_library(
'https://instance-a.example.com/rule-libraries/rdfs-entailment/stream',
'rdfs-entailment'
);
-- (void — raises PT046x on error)
Alternatively, use the HTTP API from instance B's companion:
curl -X POST https://instance-b.example.com/rule-libraries/rdfs-entailment/subscribe \
-H "Authorization: Bearer $PG_RIPPLE_HTTP_AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{"source_uri": "https://instance-a.example.com/rule-libraries/rdfs-entailment/stream"}'
Step 4 — Verify shared inference
Load some triples on instance B and verify that RDFS entailment fires using the subscribed rules:
\c ripple_b
-- Load a small ontology fragment
SELECT pg_ripple.load_turtle('
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix ex: <https://example.org/> .
ex:GraduateStudent rdfs:subClassOf ex:Student .
ex:alice a ex:GraduateStudent .
');
-- Enable Datalog inference
SELECT pg_ripple.enable_datalog('rdfs-entailment');
-- Verify entailment: alice should now be inferred as a Student
SELECT pg_ripple.sparql_query('
SELECT ?type WHERE {
<https://example.org/alice> a ?type .
}
');
-- Expected: rows include ex:GraduateStudent (asserted) AND ex:Student (inferred)
Step 5 — Monitor with Prometheus
After step 3 the following metrics will be populated on instance B's HTTP companion:
# Stream duration on instance A (cumulative seconds serving stream requests)
pg_ripple_rule_library_stream_duration_seconds 0.012345
# Subscribe errors on instance B (should be 0 for a successful subscription)
pg_ripple_rule_library_subscribe_errors_total 0
Add an alert rule for failed subscriptions:
- alert: RuleLibrarySubscribeFailed
expr: increase(pg_ripple_rule_library_subscribe_errors_total[5m]) > 0
labels:
severity: warning
annotations:
summary: "Rule library subscribe errors detected"
description: "{{ $value }} subscribe errors in the last 5 minutes."
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
PT0466: SSRF blocked | source_uri resolves to a private IP | Use a public HTTPS endpoint; private addresses are blocked by the SSRF guard |
remote returned HTTP 401 | Source stream requires auth that the subscribing fetcher is not forwarding | Expose the source stream through a trusted internal route, or install the library manually from an authenticated client |
PT0467: catalog write failed | Insufficient DB permissions | Grant USAGE on _pg_ripple schema to the app role |
| Inference not firing | Datalog engine not enabled | Run SELECT pg_ripple.enable_datalog('rdfs-entailment'); |
See also
publish_rule_libraryAPI referencesubscribe_rule_libraryAPI reference- Rule library Prometheus metrics
- Blog: Rule Library Federation
Architecture Overview
pg_ripple is a PostgreSQL 18 extension that implements a high-performance RDF triple store with native SPARQL query execution. This page describes the internal architecture: how data is stored, how queries are executed, and how the subsystems interact.
System Architecture Diagram
┌──────────────────────────────────────────────────────────────────────┐
│ Client Applications │
│ psql / JDBC / SPARQL Protocol (pg_ripple_http) / REST / ODBC │
└────────────────────────────┬─────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ PostgreSQL 18 Backend │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ pg_ripple Extension │ │
│ │ │ │
│ │ ┌──────────────┐ ┌───────────────┐ ┌────────────────────┐ │ │
│ │ │ SPARQL │ │ Datalog │ │ SHACL │ │ │
│ │ │ Engine │ │ Reasoner │ │ Validator │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ parse → │ │ stratify → │ │ shapes → DDL │ │ │
│ │ │ optimize → │ │ compile → │ │ constraints + │ │ │
│ │ │ SQL gen → │ │ semi-naive │ │ async pipeline │ │ │
│ │ │ SPI exec → │ │ fixpoint │ │ │ │ │
│ │ │ decode │ │ │ │ │ │ │
│ │ └──────┬───────┘ └───────┬───────┘ └────────┬───────────┘ │ │
│ │ │ │ │ │ │
│ │ ▼ ▼ ▼ │ │
│ │ ┌─────────────────────────────────────────────────────────┐ │ │
│ │ │ Dictionary Encoder (XXH3-128) │ │ │
│ │ │ IRI / Blank Node / Literal ──→ i64 identifier │ │ │
│ │ │ Shared-Memory LRU Cache (64 shards) │ │ │
│ │ └────────────────────────┬────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────────────────────────────────┐ │ │
│ │ │ VP Storage Engine (HTAP) │ │ │
│ │ │ │ │ │
│ │ │ vp_{id}_delta ──┐ │ │ │
│ │ │ (write inbox) │ │ │ │
│ │ │ ├──→ vp_{id} (read view) │ │ │
│ │ │ vp_{id}_main ──┤ = (main − tombstones) │ │ │
│ │ │ (BRIN archive) │ UNION ALL delta │ │ │
│ │ │ │ │ │ │
│ │ │ vp_{id}_tombstones │ │ │
│ │ │ (pending deletes) │ │ │ │
│ │ │ │ │ │ │
│ │ │ vp_rare ─────────┘ (consolidated rare predicates) │ │ │
│ │ └─────────────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ┌────────────────────────┴────────────────────────────────┐ │ │
│ │ │ Background Merge Worker (BGW) │ │ │
│ │ │ delta + main − tombstones ──→ new main (BRIN) │ │ │
│ │ │ Polling interval: merge_interval_secs (default 60s) │ │ │
│ │ │ Threshold: merge_threshold (default 10,000 rows) │ │ │
│ │ └─────────────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ _pg_ripple schema: dictionary, predicates, vp_*, statements │ │
│ │ pg_ripple schema: public SQL functions (sparql, insert, etc.) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────┘
Dictionary Encoder
The dictionary encoder is the foundation of pg_ripple's storage model. Every RDF term — IRI, blank node, plain literal, typed literal, or language-tagged literal — is mapped to a compact i64 identifier before being stored.
How Encoding Works
- The input term is classified by kind: IRI (0), blank node (1), literal (2), typed literal (3), or language-tagged literal (4).
- The kind discriminant is mixed into the hash input as two little-endian bytes, so the same string encoded as an IRI and as a blank node always produces distinct dictionary rows.
- An XXH3-128 hash is computed over
(kind_le_bytes || term_utf8). - The full 16-byte hash is stored in the
_pg_ripple.dictionarytable with anON CONFLICT (hash) DO NOTHINGupsert. The densei64join key is anIDENTITY-generated column — sequential and independent of the hash. - The resulting
i64is used in all VP table columns.
VP tables never contain raw strings. All joins, comparisons, and index lookups operate on i64 values. This eliminates collation overhead, reduces storage by 5–20x, and makes B-tree index scans uniformly fast regardless of IRI length.
Shared-Memory Cache
The dictionary cache sits in PostgreSQL shared memory (allocated at postmaster start) and is organized as a 64-shard set-associative structure. Each backend reads and writes to the shared cache through atomic operations — no per-backend duplication.
Key parameters:
pg_ripple.dictionary_cache_size— Number of cache entries (default: 65,536). Requires restart.pg_ripple.cache_budget— Memory budget cap in MB (default: 64). Bulk loads throttle at 90% utilization.
The cache hit ratio is reported by pg_ripple.stats() and should stay above 95% in production.
VP (Vertical Partitioning) Tables
pg_ripple uses vertical partitioning: one physical table per unique predicate. This is the storage model used by research systems like SW-Store and column-oriented triple stores.
Table Layout
Each predicate with at least vp_promotion_threshold (default: 1,000) triples gets a dedicated VP table:
-- Columns in every VP table
s BIGINT NOT NULL -- subject dictionary ID
o BIGINT NOT NULL -- object dictionary ID
g BIGINT NOT NULL DEFAULT 0 -- graph ID (0 = default graph)
i BIGINT NOT NULL DEFAULT nextval('statement_id_seq') -- unique SID
source SMALLINT NOT NULL DEFAULT 0 -- 0 = explicit, 1 = inferred
Dual B-tree indexes on (s, o) and (o, s) support both subject-to-object and object-to-subject access patterns.
Rare Predicate Consolidation
Predicates with fewer triples than the promotion threshold are stored in a shared _pg_ripple.vp_rare table with an extra p BIGINT column. This avoids schema bloat for infrequent predicates. When a rare predicate's count crosses the threshold, it is automatically promoted to a dedicated VP table.
Predicate Catalog
The _pg_ripple.predicates table maps each predicate ID to its VP table OID and current triple count:
SELECT id, table_oid, triple_count
FROM _pg_ripple.predicates
ORDER BY triple_count DESC;
The SPARQL-to-SQL translator never concatenates table names into SQL strings. It looks up the OID in _pg_ripple.predicates and uses parameterized queries with format-safe quoting. This prevents SQL injection by design.
HTAP Storage Architecture
Since v0.6.0, pg_ripple uses an HTAP (Hybrid Transactional/Analytical Processing) storage architecture that separates write and read paths for each VP table.
Three-Table Split
For each predicate, the storage layer maintains:
| Table | Purpose | Index Type |
|---|---|---|
vp_{id}_delta | Write inbox — all INSERTs land here | B-tree on (s, o) |
vp_{id}_main | Read-optimized archive | BRIN (block range) |
vp_{id}_tombstones | Pending deletes from main | B-tree on (s, o, g) |
A read view vp_{id} combines them:
(main EXCEPT tombstones) UNION ALL delta
Background Merge Worker
The merge worker is a PostgreSQL background worker (BGWorker) that runs in a polling loop:
- Poll — Wake every
merge_interval_secs(default: 60) or when poked by the write-path latch. - Scan — Check each HTAP predicate's delta row count against
merge_threshold(default: 10,000). - Merge — For qualifying predicates: create a new main table from
(old_main − tombstones) UNION ALL delta, swap atomically viaALTER TABLE ... RENAME, drop the old main aftermerge_retention_seconds. - Maintain — Rebuild subject/object pattern tables, promote rare predicates that crossed the threshold, run
ANALYZEon new main tables (whenauto_analyzeis on), evict expired federation cache entries.
Writers never block on the merge. All INSERTs go directly to the delta table (heap + B-tree). The merge worker operates asynchronously and uses PostgreSQL's MVCC for isolation.
SPARQL Query Execution Flow
When a client calls pg_ripple.sparql('SELECT ...'), the query goes through five stages:
1. Parse
The SPARQL text is parsed by the spargebra crate into an algebraic representation. This handles the full SPARQL 1.1 grammar: SELECT, CONSTRUCT, DESCRIBE, ASK, property paths, subqueries, aggregation, federation (SERVICE), and SPARQL-star.
2. Optimize
The sparopt optimizer rewrites the algebra tree:
- BGP reordering — Triple patterns are sorted by estimated selectivity (smallest VP table first) when
bgp_reorderis on. - Filter pushdown — FILTER constants are encoded to
i64at translation time and pushed into the WHERE clause of the generated SQL. - Self-join elimination — Star patterns (same subject, multiple predicates) are collapsed into multi-way joins instead of redundant subqueries.
- SHACL hints — If
sh:maxCount 1is declared,DISTINCTis omitted; ifsh:minCount 1,LEFT JOINis upgraded toINNER JOIN.
3. Generate SQL
The optimized algebra is compiled into PostgreSQL SQL:
- Each triple pattern becomes a scan of the corresponding VP table (or
vp_rarewith a predicate filter). - Joins between patterns become SQL
JOINclauses withi64equality predicates. - Property paths compile to
WITH RECURSIVE ... CYCLEusing PostgreSQL 18's hash-based cycle detection. SERVICEclauses are compiled into HTTP calls to remote SPARQL endpoints.- Aggregates,
ORDER BY,LIMIT, andOFFSETtranslate directly to their SQL equivalents.
4. SPI Execute
The generated SQL is executed through PostgreSQL's Server Programming Interface (SPI). Results are arrays of i64 dictionary IDs.
The plan cache (plan_cache_size, default: 256) stores compiled SQL for recently-seen SPARQL queries to avoid repeated parse/optimize/generate cycles.
5. Decode
The i64 result columns are decoded back to human-readable RDF terms (IRIs, literals, blank nodes) using the dictionary. The shared-memory cache accelerates this step — a cache hit avoids a dictionary table lookup per value.
The SPARQL engine encodes all bound constants to i64 before generating SQL, and decodes results after execution. VP table queries never contain string comparisons — this is a hard architectural invariant.
Schema Organization
pg_ripple uses two PostgreSQL schemas:
| Schema | Contents | Visibility |
|---|---|---|
pg_ripple | Public SQL functions (sparql(), insert_triple(), stats(), etc.) | User-facing |
_pg_ripple | Dictionary table, predicates catalog, VP tables, statement mappings, internal state | Internal |
The internal schema is managed by the extension. Direct modifications to _pg_ripple tables can corrupt the dictionary or break VP table invariants.
Subsystem Summary
| Subsystem | Source Directory | Purpose |
|---|---|---|
| Dictionary | src/dictionary/ | Term ↔ i64 encoding with XXH3-128 |
| Storage | src/storage/ | VP tables, HTAP partitions, rare predicate consolidation |
| SPARQL | src/sparql/ | Parse → optimize → SQL generation → SPI → decode |
| Datalog | src/datalog/ | Rule parsing, stratification, semi-naive fixpoint, magic sets |
| SHACL | src/shacl/ | Shape validation, DDL constraints, async pipeline |
| Export | src/export/ | Turtle, N-Triples, JSON-LD serialization |
| Worker | src/worker.rs | Background merge worker, embedding queue, SHACL async |
| Stats | src/stats/ | Monitoring, cache metrics, health checks |
| Federation | src/sparql/federation | Remote SERVICE call execution, connection pooling, caching |
| HTTP | pg_ripple_http/ | SPARQL Protocol endpoint (standalone companion service) |
Production Readiness Checklist
Use this checklist before deploying pg_ripple to production. Each item links to the relevant documentation for details.
PostgreSQL Configuration
- PostgreSQL 18 installed — pg_ripple requires PostgreSQL 18.x
-
shared_preload_librariesincludes'pg_ripple'— required for the background merge worker and shared-memory dictionary cache (Configuration) -
pg_ripple.worker_databaseset to your target database — the merge worker connects to this database (Merge Workers) -
Shared memory sized correctly —
pg_ripple.dictionary_cache_sizedetermines shared memory usage; check OS limits (Troubleshooting §6) -
PostgreSQL restarted after
shared_preload_librarieschanges
Security
-
Row-Level Security (RLS) enabled on named graphs if multi-tenant —
pg_ripple.enable_graph_rls()+ role grants (Security, Multi-Tenant Graphs) -
Federation SSRF protection configured —
pg_ripple.federation_allow_private = off(default) prevents SERVICE queries to private IPs (GUC Reference) -
pg_ripple_httpauth token set —PG_RIPPLE_HTTP_AUTH_TOKENenvironment variable for Bearer token authentication (HTTP API Reference) - TLS termination configured — use a reverse proxy (nginx, Caddy) for HTTPS; pg_ripple_http does not handle TLS directly
-
pg_hba.confrestricts connections to the pg_ripple_http service account -
Embedding API keys not stored in
postgresql.conf— useALTER SYSTEMor inject via sessionSET(GUC Reference)
Performance
-
Merge workers tuned —
pg_ripple.merge_workers= 2–4 for workloads with many predicates (Merge Workers) -
Dictionary cache sized to working set — monitor
encode_cache_evictionsviapg_ripple.stats(), target > 90% hit rate (Troubleshooting §7) -
Autovacuum tuned for VP tables — consider
autovacuum_vacuum_scale_factor = 0.01on high-churn delta tables (Performance) -
work_memadequate for SPARQL-generated SQL — 64–256 MB for large queries -
Property path depth bounded —
pg_ripple.max_path_depthprevents runaway recursion (default: 10) (Troubleshooting §3)
Monitoring
-
Prometheus metrics configured —
pg_ripple_httpexposes/metricsendpoint (Monitoring) -
Key metrics monitored:
pg_ripple_triple_count— total stored triplespg_ripple_merge_worker_lag— merge backlogpg_ripple_dictionary_cache_hit_rate— encoding efficiencypg_ripple_sparql_query_duration_seconds— query latency
-
Health check configured —
GET /healthandGET /health/readyfor load balancer probes - Log-based alerting on PT-series error codes — see Error Catalog
Backup and Recovery
-
pg_dumptested — pg_ripple stores all data in standard PostgreSQL tables;pg_dump/pg_restoreworks without special steps (Backup) - WAL archiving enabled for point-in-time recovery
- Backup schedule documented and tested for restore
Upgrade Path
-
Migration scripts verified —
ALTER EXTENSION pg_ripple UPDATEapplies sequential migration scripts (Upgrading) -
Compatibility matrix checked — if using
pg_ripple_http, verify version compatibility (Compatibility) -
Test in staging before production upgrade — run
cargo pgrx regressor the pg_regress suite against the new version
Optional Components
-
pgvector installed if using vector/hybrid search —
CREATE EXTENSION vector(Vector Search) - pg_trickle installed if using live views; pg_tide installed if using the CDC bridge or relay outboxes — (CDC Operations)
- PostGIS installed if using GeoSPARQL — (GeoSPARQL)
Smoke Test
After deployment, verify the extension is working:
-- Check extension version
SELECT pg_ripple.build_info();
-- Verify merge worker is running
SELECT (pg_ripple.stats()->>'merge_worker_pid')::int > 0 AS merge_worker_alive;
-- Verify dictionary cache is active
SELECT pg_ripple.stats()->>'encode_cache_capacity' AS cache_capacity;
-- Load a test triple and query it
SELECT pg_ripple.insert_triple(
'http://example.org/test',
'http://example.org/status',
'"production-ready"'
);
SELECT * FROM pg_ripple.sparql($$
SELECT ?status WHERE {
<http://example.org/test> <http://example.org/status> ?status
}
$$);
Deployment Models
pg_ripple runs as a PostgreSQL 18 extension. It can be deployed in any environment that supports PostgreSQL 18 with extension loading. This page covers the three primary deployment models and provides production-ready configuration examples.
Deployment Options at a Glance
| Model | Best For | Complexity | SPARQL Protocol |
|---|---|---|---|
| Standalone PostgreSQL | Production, existing PG infrastructure | Low | Via pg_ripple_http sidecar |
| Docker / Compose | Evaluation, CI/CD, small deployments | Low | Built-in |
| Managed PostgreSQL | Cloud-native, minimal ops | Medium | Via pg_ripple_http sidecar |
Use Docker Compose for evaluation and development. Use a dedicated PostgreSQL 18 instance for production workloads — this gives full control over shared memory, background workers, and storage configuration.
Model 1: Standalone PostgreSQL
Install pg_ripple into a standard PostgreSQL 18 instance. This is the recommended production deployment.
Prerequisites
- PostgreSQL 18.x installed from packages or source
- Rust toolchain (for building from source) or a pre-built
.so/.dylib pgrx0.18 (if building from source)
Installation
# Build and install from source
cargo pgrx install --pg-config $(which pg_config) --release
# Or if using a specific PG18 binary
cargo pgrx install --pg-config /usr/lib/postgresql/18/bin/pg_config --release
PostgreSQL Configuration
Add to postgresql.conf:
# Required: load pg_ripple at server start for background workers and shared memory
shared_preload_libraries = 'pg_ripple'
# Shared memory for dictionary cache (adjust for your dataset)
pg_ripple.dictionary_cache_size = 65536 # 64K entries (default)
pg_ripple.cache_budget = 64 # MB (default)
# HTAP merge worker
pg_ripple.merge_threshold = 10000
pg_ripple.merge_interval_secs = 60
pg_ripple.worker_database = 'mydb' # database the merge worker connects to
Enable the Extension
CREATE EXTENSION pg_ripple;
-- Verify installation
SELECT pg_ripple.stats();
Add SPARQL Protocol Endpoint
The SPARQL Protocol HTTP endpoint is provided by pg_ripple_http, a standalone companion service:
# Build the HTTP service
cd pg_ripple_http
cargo build --release
# Run it
PG_RIPPLE_HTTP_PG_URL="postgresql://user:pass@localhost/mydb" \
PG_RIPPLE_HTTP_PORT=7878 \
./target/release/pg_ripple_http
You can use pg_ripple entirely through SQL — pg_ripple.sparql(), pg_ripple.insert_triple(), etc. The HTTP service adds W3C SPARQL Protocol compatibility for tools like Yasgui, RDF4J, or federated queries from other endpoints.
Model 2: Docker / Docker Compose
The Docker deployment bundles PostgreSQL 18, pg_ripple, and pg_ripple_http into containers managed by Docker Compose. This is the fastest way to get started.
docker-compose.yml
# Docker Compose for pg_ripple with SPARQL Protocol HTTP endpoint.
#
# Usage:
# docker compose up -d
# curl http://localhost:7878/health
# curl -G http://localhost:7878/sparql \
# --data-urlencode "query=SELECT * WHERE { ?s ?p ?o } LIMIT 10"
services:
postgres:
build: .
ports:
- "5432:5432"
environment:
POSTGRES_PASSWORD: ripple
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
sparql:
build: .
entrypoint: ["/usr/local/bin/pg_ripple_http"]
ports:
- "7878:7878"
environment:
PG_RIPPLE_HTTP_PG_URL: "postgresql://postgres:ripple@postgres/postgres"
PG_RIPPLE_HTTP_PORT: "7878"
PG_RIPPLE_HTTP_POOL_SIZE: "8"
PG_RIPPLE_HTTP_CORS_ORIGINS: "*"
depends_on:
postgres:
condition: service_healthy
volumes:
pgdata:
Starting the Stack
docker compose up -d
# Wait for health check
docker compose ps
# Test SPARQL endpoint
curl http://localhost:7878/health
# Run a query
curl -G http://localhost:7878/sparql \
--data-urlencode "query=SELECT * WHERE { ?s ?p ?o } LIMIT 5"
Loading Data via Docker
# Copy a Turtle file into the container and load it
docker compose cp data.ttl postgres:/tmp/data.ttl
docker compose exec postgres psql -U postgres -c \
"SELECT pg_ripple.load_turtle_file('/tmp/data.ttl');"
# Or load inline
docker compose exec postgres psql -U postgres -c \
"SELECT pg_ripple.load_turtle('@prefix ex: <http://example.org/> .
ex:Alice ex:knows ex:Bob .
ex:Bob ex:age \"30\"^^<http://www.w3.org/2001/XMLSchema#integer> .');"
Production Hardening for Docker
For production Docker deployments, add resource limits and persistent configuration:
services:
postgres:
build: .
ports:
- "5432:5432"
environment:
POSTGRES_PASSWORD: ${PG_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data
- ./postgresql.conf:/etc/postgresql/postgresql.conf
command: postgres -c config_file=/etc/postgresql/postgresql.conf
deploy:
resources:
limits:
memory: 4G
cpus: "2.0"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
sparql:
build: .
entrypoint: ["/usr/local/bin/pg_ripple_http"]
ports:
- "7878:7878"
environment:
PG_RIPPLE_HTTP_PG_URL: "postgresql://postgres:${PG_PASSWORD}@postgres/postgres"
PG_RIPPLE_HTTP_PORT: "7878"
PG_RIPPLE_HTTP_POOL_SIZE: "16"
PG_RIPPLE_HTTP_CORS_ORIGINS: "https://yourdomain.com"
PG_RIPPLE_HTTP_AUTH_TOKEN: ${SPARQL_AUTH_TOKEN}
depends_on:
postgres:
condition: service_healthy
deploy:
resources:
limits:
memory: 512M
cpus: "1.0"
Never use default passwords in production. Set POSTGRES_PASSWORD and PG_RIPPLE_HTTP_AUTH_TOKEN via environment variables or Docker secrets. Restrict PG_RIPPLE_HTTP_CORS_ORIGINS to your actual domain.
Model 3: Managed PostgreSQL Services
pg_ripple can run on managed PostgreSQL services that support custom extensions and PostgreSQL 18. The key requirements are:
- PostgreSQL 18 — pg_ripple uses PG18-specific features (e.g.,
WITH RECURSIVE ... CYCLE). - Custom extension loading — The service must allow installing
.soextensions and adding toshared_preload_libraries. - Shared memory access — Required for the dictionary cache and merge worker.
Supported Managed Services
| Service | Custom Extensions | shared_preload_libraries | Status |
|---|---|---|---|
| AWS RDS for PostgreSQL | Yes (via custom builds) | Yes | Supported with custom AMI |
| Azure Database for PostgreSQL Flexible Server | Yes | Yes | Supported |
| Google Cloud SQL | Limited | Limited | Partial support |
| Self-managed on EC2/GCE/Azure VM | Full control | Full control | Fully supported |
For managed cloud deployments, running PostgreSQL 18 on a cloud VM (EC2, GCE, Azure VM) with the extension installed gives full control and avoids managed service limitations. Use the managed service's block storage for durability and snapshots for backups.
Managed Service Configuration
When running on a managed service:
# Add to the PostgreSQL parameter group / configuration
shared_preload_libraries = 'pg_ripple'
# Shared memory — managed services often cap this; start conservative
pg_ripple.dictionary_cache_size = 32768
pg_ripple.cache_budget = 32
# Merge worker targets the primary database
pg_ripple.worker_database = 'mydb'
pg_ripple_http as a Sidecar
On managed services, run pg_ripple_http as a sidecar container or systemd service:
# Kubernetes sidecar example
PG_RIPPLE_HTTP_PG_URL="postgresql://user:pass@pg-host:5432/mydb" \
PG_RIPPLE_HTTP_PORT=7878 \
PG_RIPPLE_HTTP_POOL_SIZE=16 \
pg_ripple_http
pg_ripple_http Configuration Reference
The HTTP companion service is configured entirely through environment variables:
| Variable | Default | Description |
|---|---|---|
PG_RIPPLE_HTTP_PG_URL | (required) | PostgreSQL connection string |
PG_RIPPLE_HTTP_PORT | 7878 | HTTP listen port |
PG_RIPPLE_HTTP_POOL_SIZE | 8 | Connection pool size |
PG_RIPPLE_HTTP_CORS_ORIGINS | * | Allowed CORS origins |
PG_RIPPLE_HTTP_AUTH_TOKEN | (none) | Bearer token for authentication |
PG_RIPPLE_HTTP_RATE_LIMIT | 0 | Requests per second (0 = unlimited) |
Endpoints
| Path | Method | Description |
|---|---|---|
/sparql | GET, POST | SPARQL Protocol query/update endpoint |
/health | GET | Health check (returns 200 if PG connection is live) |
/metrics | GET | Prometheus-compatible metrics |
Network Architecture
┌─────────────┐
│ Clients │
└──────┬──────┘
│
┌────────────┴────────────┐
│ │
▼ ▼
┌─────────────────┐ ┌──────────────────┐
│ pg_ripple_http │ │ psql / JDBC / │
│ :7878 │ │ application │
│ (SPARQL Proto) │ │ (:5432) │
└────────┬─────────┘ └────────┬─────────┘
│ │
└────────────┬────────────┘
│
▼
┌─────────────────────────┐
│ PostgreSQL 18 │
│ + pg_ripple extension │
│ + merge worker (BGW) │
└─────────────────────────┘
For read-heavy workloads, PostgreSQL streaming replication works out of the box. Read replicas receive all VP table changes through WAL. Point read-only SPARQL queries to replicas via a separate pg_ripple_http instance connected to the replica.
Post-Deployment Verification
After deploying pg_ripple, verify the installation:
-- Check extension version
SELECT extversion FROM pg_extension WHERE extname = 'pg_ripple';
-- Verify stats (confirms shared memory and merge worker)
SELECT pg_ripple.stats();
-- Run a health check
SELECT pg_ripple.canary();
-- Insert and query a test triple
SELECT pg_ripple.insert_triple(
'<http://example.org/test>',
'<http://example.org/status>',
'"deployed"'
);
SELECT * FROM pg_ripple.sparql('
SELECT ?s ?p ?o WHERE { ?s ?p ?o } LIMIT 1
');
stats()returnsmerge_worker_pid > 0canary()showsmerge_worker: "ok"andcatalog_consistent: trueencode_cache_hits / (hits + misses) > 0.90after initial data load- SPARQL queries return results
Docker Deployment
Batteries-Included Image
The ghcr.io/trickle-labs/pg-ripple:<version> image bundles six extensions in a
single container:
| Extension | Purpose |
|---|---|
pg_ripple | RDF triple store with native SPARQL |
PostGIS | Geospatial queries via GeoSPARQL |
pgvector | Vector similarity search for hybrid SPARQL + semantic |
pg_trickle | Incremental materialised SPARQL views |
pg_tide | Relay, outbox, and inbox subsystem for change-data capture |
No additional setup is needed — simply start the container and CREATE EXTENSION.
Quick Start
docker run --rm -p 5432:5432 \
-e POSTGRES_PASSWORD=ripple \
ghcr.io/trickle-labs/pg-ripple:0.127.0
psql -h localhost -U postgres -c "CREATE EXTENSION pg_ripple CASCADE;"
psql -h localhost -U postgres \
-c "SELECT pg_ripple.load_ntriples('<https://example.org/s> <https://example.org/p> <https://example.org/o> .');"
Enable optional extensions:
CREATE EXTENSION postgis; -- GeoSPARQL functions
CREATE EXTENSION vector; -- hybrid vector + SPARQL search
Docker Compose
The repository ships a docker-compose.yml that starts pg_ripple and the
SPARQL HTTP service together:
docker compose up -d
curl http://localhost:7878/health
curl -G http://localhost:7878/sparql \
--data-urlencode "query=SELECT * WHERE { ?s ?p ?o } LIMIT 10"
Pre-Installed Extension Versions
| Extension | Version | Notes |
|---|---|---|
| pg_ripple | 0.127.0 | RDF triple store with native SPARQL |
| PostGIS | 3.5.6 | Geospatial queries via GeoSPARQL |
| pgvector | 0.8.2 | Vector similarity search for hybrid SPARQL + semantic |
| pg_trickle | 0.57.0 | Incremental materialised SPARQL views |
| pg_tide | 0.33.0 | Relay, outbox, and inbox for CDC |
Environment Variables
| Variable | Default | Description |
|---|---|---|
POSTGRES_PASSWORD | (required) | Superuser password |
POSTGRES_DB | postgres | Database to create |
POSTGRES_USER | postgres | Superuser name |
The SPARQL HTTP service (pg_ripple_http) also accepts:
| Variable | Default | Description |
|---|---|---|
PG_RIPPLE_HTTP_PG_URL | postgresql://postgres:…@localhost/postgres | Connection string |
PG_RIPPLE_HTTP_PORT | 7878 | Listening port |
PG_RIPPLE_HTTP_POOL_SIZE | 8 | Connection pool size |
PG_RIPPLE_HTTP_CORS_ORIGINS | * | CORS allowed origins |
Example: GeoSPARQL Query
CREATE EXTENSION postgis;
-- Load a geo triple
SELECT pg_ripple.load_ntriples(
'<https://example.org/Berlin> <http://www.opengis.net/ont/geosparql#asWKT> "POINT(13.405 52.52)"^^<http://www.opengis.net/ont/geosparql#wktLiteral> .'
);
-- Query via SPARQL
SELECT * FROM pg_ripple.sparql(
'SELECT ?city ?wkt WHERE { ?city <http://www.opengis.net/ont/geosparql#asWKT> ?wkt }'
);
Example: Hybrid Vector + SPARQL Search
CREATE EXTENSION vector;
-- Create an embedding for a resource
INSERT INTO _pg_ripple.embeddings (subject_id, embedding)
SELECT id, '[0.1, 0.2, ...]'::vector
FROM _pg_ripple.dictionary
WHERE value = 'https://example.org/Berlin';
-- Hybrid search: semantic similarity + SPARQL filter
SELECT * FROM pg_ripple.hybrid_search(
query_embedding := '[0.1, 0.2, ...]'::vector,
sparql_filter := '?s <http://schema.org/type> <http://schema.org/City>',
k := 10
);
Publishing to GHCR
Every release is automatically published to GitHub Container Registry via the release GitHub Actions workflow. You can also build the batteries-included image locally:
docker build --tag my-pg-ripple:local .
Kubernetes Deployment
pg_ripple ships a Helm chart (charts/pg_ripple/) that deploys the
batteries-included image — PostgreSQL 18 with pg_ripple, PostGIS, and pgvector
pre-installed — on any Kubernetes cluster.
Prerequisites
- Kubernetes ≥ 1.25
- Helm ≥ 3.10
- Persistent volume provisioner (any cloud provider or
local-path-provisioner)
Installation
Add the Helm repository
helm repo add pg-ripple https://trickle-labs.github.io/pg-ripple/charts
helm repo update
Install with defaults
helm install pg-ripple pg-ripple/pg-ripple \
--set postgres.password=mysecretpassword
Install from source
helm install pg-ripple ./charts/pg_ripple \
--set postgres.password=mysecretpassword
Verify deployment
kubectl get pods -l app.kubernetes.io/name=pg-ripple
kubectl exec -it <pod-name> -- psql -U postgres \
-c "SELECT extname, extversion FROM pg_extension WHERE extname = 'pg_ripple';"
Values Reference
| Key | Default | Description |
|---|---|---|
replicaCount | 1 | Number of PostgreSQL Pods |
image.repository | ghcr.io/trickle-labs/pg-ripple | Image repository |
image.tag | 0.54.0 | Image tag |
image.pullPolicy | IfNotPresent | Image pull policy |
postgres.password | ripple | Superuser password (use a Secret in production) |
postgres.database | postgres | Database to create |
persistence.enabled | true | Enable persistent storage |
persistence.size | 10Gi | PVC size |
persistence.storageClass | "" | StorageClass (empty = cluster default) |
service.type | ClusterIP | PostgreSQL service type |
service.port | 5432 | PostgreSQL port |
http.enabled | true | Enable SPARQL HTTP sidecar |
http.service.type | ClusterIP | SPARQL HTTP service type |
http.service.port | 7878 | SPARQL HTTP port |
ripple.federationEndpoints | [] | SPARQL federation endpoints |
ripple.shacl.shapesConfigMap | "" | SHACL shapes ConfigMap |
ripple.llm.apiKeySecret | "" | LLM API key Secret name |
Common Configurations
Expose SPARQL HTTP externally (LoadBalancer)
helm upgrade pg-ripple ./charts/pg_ripple \
--set http.service.type=LoadBalancer \
--set http.service.port=7878
Increase storage
helm upgrade pg-ripple ./charts/pg_ripple \
--set persistence.size=100Gi \
--set persistence.storageClass=premium-ssd
Configure federation endpoints
helm upgrade pg-ripple ./charts/pg_ripple \
--set 'ripple.federationEndpoints[0].name=wikidata' \
--set 'ripple.federationEndpoints[0].url=https://query.wikidata.org/sparql'
Enable Prometheus monitoring
helm upgrade pg-ripple ./charts/pg_ripple \
--set metrics.enabled=true \
--set metrics.serviceMonitor.enabled=true
Health Probes
The chart configures both liveness and readiness probes using pg_isready:
livenessProbe:
exec:
command: ["pg_isready", "-U", "postgres"]
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
exec:
command: ["pg_isready", "-U", "postgres"]
initialDelaySeconds: 5
periodSeconds: 5
Prometheus Integration
When metrics.serviceMonitor.enabled = true, the chart creates a
ServiceMonitor resource for the Prometheus Operator. pg_ripple exposes
query stats via pg_stat_statements and OTEL tracing via the
pg_ripple.tracing_otlp_endpoint GUC.
Configure the OTEL endpoint:
kubectl exec -it <pod-name> -- psql -U postgres \
-c "SET pg_ripple.tracing_otlp_endpoint = 'http://otel-collector:4318';"
Future: Kubernetes Operator
A future release will provide a Go operator built with controller-runtime that
manages pg_ripple clusters as first-class Kubernetes resources — similar to
CloudNativePG but tailored to the RDF workload lifecycle (bulk load, VP merge
scheduling, SHACL validation pipelines).
The operator will provide:
RDFTripleStorecustom resource with declarative schema management- Automated rolling upgrades with zero downtime
- Built-in Prometheus metrics CRDs
- Automated SHACL shape deployment via ConfigMap reference
Pre-Installed Extensions
The batteries-included image pre-installs:
| Extension | Version | Activate with |
|---|---|---|
| pg_ripple | 0.54.0 | CREATE EXTENSION pg_ripple; |
| PostGIS | 3.4.3 | CREATE EXTENSION postgis; |
| pgvector | 0.7.4 | CREATE EXTENSION vector; |
CloudNativePG Deployment
CloudNativePG (CNP) is a Kubernetes operator for managing PostgreSQL clusters. pg_ripple v0.98.0 ships a pre-built extension image for CNP ≥ 1.24, allowing operators to install pg_ripple into a managed cluster with no custom PostgreSQL container image and no custom build step.
Prerequisites
- CloudNativePG operator ≥ 1.24
- Kubernetes ≥ 1.25
- The
Image Volumefeature gate enabled in CNP (enabled by default in CNP ≥ 1.24)
How It Works
CloudNativePG ≥ 1.24 supports extension images:
a minimal OCI image whose only purpose is to supply pre-compiled .so and SQL
files. CNP mounts the image as an init-container volume and copies the files
into the PostgreSQL container at startup.
pg_ripple publishes two images per release:
| Image | Contents |
|---|---|
ghcr.io/trickle-labs/pg-ripple:<version> | Full batteries-included image |
ghcr.io/trickle-labs/pg-ripple:<version>-cnpg | Extension volume for CloudNativePG |
The -cnpg image contains pg_ripple and pgvector compiled for PostgreSQL 18 at
the paths expected by CNP:
/var/lib/postgresql/extension-files/lib/pg_ripple.so
/var/lib/postgresql/extension-files/ext/pg_ripple.control
/var/lib/postgresql/extension-files/ext/pg_ripple--*.sql
/var/lib/postgresql/extension-files/lib/vector.so
/var/lib/postgresql/extension-files/ext/vector.control
Cluster Manifest Walkthrough
The example manifest is at examples/cloudnativepg_cluster.yaml:
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: pg-ripple-cluster
spec:
imageName: ghcr.io/cloudnative-pg/postgresql:18
instances: 3
postgresql:
extensionImages:
- name: pg-ripple-ext
image: ghcr.io/trickle-labs/pg-ripple:0.98.0-cnpg # ← extension volume
parameters:
allow_system_table_mods: "on"
shared_preload_libraries: "pg_ripple"
storage:
size: 20Gi
superuserSecret:
name: pg-ripple-superuser
bootstrap:
initdb:
database: postgres
postInitSQL:
- "CREATE EXTENSION IF NOT EXISTS pg_ripple;"
- "CREATE EXTENSION IF NOT EXISTS vector;"
Key points:
imageNameis the standard CNP base image — not a custom build.extensionImageslists the pg_ripple extension volume. CNP mounts it and copies files before PostgreSQL starts.postInitSQLrunsCREATE EXTENSIONon first cluster startup.
Deploying
# Create the superuser secret first
kubectl create secret generic pg-ripple-superuser \
--from-literal=username=postgres \
--from-literal=password=your-secure-password
# Apply the cluster manifest
kubectl apply -f examples/cloudnativepg_cluster.yaml
# Wait for all instances to be ready
kubectl wait --for=condition=Ready cluster/pg-ripple-cluster --timeout=120s
Post-Deploy Verification
kubectl exec -it pg-ripple-cluster-1 -- psql -U postgres -c \
"SELECT extname, extversion FROM pg_extension WHERE extname IN ('pg_ripple', 'vector');"
Expected output:
extname | extversion
-----------+------------
pg_ripple | 0.98.0
vector | 0.8.2
Load a test triple and run a SPARQL query:
kubectl exec -it pg-ripple-cluster-1 -- psql -U postgres -c \
"SELECT pg_ripple.load_ntriples('<https://example.org/s> <https://example.org/p> <https://example.org/o> .');"
kubectl exec -it pg-ripple-cluster-1 -- psql -U postgres -c \
"SELECT * FROM pg_ripple.sparql('SELECT ?s ?p ?o WHERE { ?s ?p ?o }');"
Upgrade Procedure
Upgrading pg_ripple is a one-line change in the cluster manifest — bump the extension image tag and apply:
# Edit the manifest to change the image tag
sed -i 's/pg_ripple:0.97.0-cnpg/pg_ripple:0.98.0-cnpg/' \
examples/cloudnativepg_cluster.yaml
kubectl apply -f examples/cloudnativepg_cluster.yaml
# Once the rolling restart completes, run the migration
kubectl exec -it pg-ripple-cluster-1 -- psql -U postgres \
-c "ALTER EXTENSION pg_ripple UPDATE TO '0.98.0';"
CNP handles the rolling restart automatically, ensuring zero downtime.
High Availability
CloudNativePG provides built-in HA: the primary is automatically elected from the standby instances if the current primary fails. pg_ripple's shared memory and background workers (merge worker, apply worker) are automatically restarted by PostgreSQL on the new primary.
For RDF logical replication across CNP clusters, see Logical Replication.
High Availability Decision Tree
This page helps you choose the right HA topology for your pg_ripple deployment.
Decision Tree
Do you need sub-second read scalability across multiple nodes?
├─ YES → Use streaming replication (primary + read replicas)
│ + pg_ripple logical replication for RDF-specific apply
└─ NO → Single node with good hardware is likely sufficient
Are you running on Kubernetes?
├─ YES → Use CloudNativePG operator (see cloudnativepg.md)
│ or the pg_ripple Helm chart (see kubernetes.md)
└─ NO → Self-managed PostgreSQL with streaming replication
Do you need the replica to run SPARQL queries against RDF data?
├─ YES → Enable pg_ripple.replication_enabled = on on the replica
│ so the logical apply worker keeps the dictionary + VP tables in sync
└─ NO → Standard PostgreSQL streaming replication is sufficient
(the replica can still serve SELECT queries via PG's built-in machinery)
Supported Topologies
1. Single Node
For workloads up to ~50 M triples and moderate write rates. No HA — use pg_ripple's built-in WAL + periodic backups for durability.
[Client] → [pg_ripple primary]
2. Streaming Replication + RDF Logical Apply
The recommended topology for production HA. PostgreSQL streaming replication keeps the replica byte-for-byte identical. pg_ripple's logical apply worker additionally decodes VP-table changes into N-Triples and re-applies them so the replica's dictionary and VP tables remain queryable independently.
[Writes] → [pg_ripple primary] ──streaming──→ [pg_ripple replica]
──logical──→ [logical_apply_worker]
Requirements:
wal_level = logicalon the primarypg_ripple.replication_enabled = onon the replica- One replication slot per replica
Lag target: < 1 second at 10 k-triple/s insert rate.
3. CloudNativePG
The recommended topology for Kubernetes environments. CNP manages the primary election, failover, and rolling upgrades automatically. Use the extension image volume to avoid maintaining a custom PostgreSQL container.
[Writes] → [CNP primary Pod] ── CNP streaming ──→ [CNP standby Pods ×2]
Requirements: CloudNativePG operator ≥ 1.24. See cloudnativepg.md for setup.
4. Multi-Region Federated Query
For globally distributed data, keep separate pg_ripple instances per region and
use SPARQL SERVICE federation to query across them:
SELECT ?s ?p ?o WHERE {
SERVICE <https://us.example.com/sparql> { ?s ?p ?o }
UNION
SERVICE <https://eu.example.com/sparql> { ?s ?p ?o }
}
Each regional instance is independently HA via topology 2 or 3.
Trade-offs
| Topology | Setup complexity | Failover RTO | Write scale-out | SPARQL on replicas |
|---|---|---|---|---|
| Single node | Low | N/A (manual restore) | No | N/A |
| Streaming + logical apply | Medium | ~30s (manual failover) | No | Yes |
| CloudNativePG | Medium | ~10s (automatic) | No | Yes |
| Multi-region federation | High | Per-region | Yes (writes to local) | Yes |
pg_ripple vs Standard PG Streaming Replication
Standard PostgreSQL streaming replication copies the raw WAL bytes — including internal storage-format details. This is sufficient for read replicas, but the replica's pg_ripple state may not be independently queryable via SPARQL without the logical apply worker (which reconstructs the dictionary and VP tables from decoded N-Triples changes).
Enable pg_ripple.replication_enabled = on on the replica to activate the
logical apply worker and ensure full SPARQL query capability.
Monitoring Replication Lag
-- On the replica
SELECT * FROM pg_ripple.replication_stats();
-- On the primary (standard PG view)
SELECT slot_name, active, lag
FROM pg_replication_slots;
Set up alerting when lag_bytes > 10 MB (roughly 1–2 s at typical write rates).
Logical Replication
pg_ripple v0.54.0 adds RDF logical replication: a primary database streams its RDF triple changes to one or more replica databases in near-real-time using PostgreSQL's built-in logical decoding infrastructure.
Architecture
┌──────────────────────────────────────────────────────────────────┐
│ PRIMARY │
│ ┌──────────────┐ INSERT/DELETE ┌────────────────────────┐ │
│ │ VP delta │ ──────────────→ │ WAL (logical decoding)│ │
│ │ tables │ │ slot: pg_ripple_sub │ │
│ └──────────────┘ └───────────┬────────────┘ │
└─────────────────────────────────────────────────│────────────────┘
│ streaming replication
┌─────────────────────────────────────────────────│────────────────┐
│ REPLICA ↓ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ _pg_ripple.replication_status (pending batches) │ │
│ └─────────────────────────┬────────────────────────────────┘ │
│ ↓ logical_apply_worker │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ pg_ripple.load_ntriples() — applies triples in order │ │
│ └──────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
The logical decoding slot (pg_ripple_sub) captures every INSERT and DELETE
on the _pg_ripple schema's VP delta tables. The changes are decoded into
N-Triples format and written to _pg_ripple.replication_status on the replica.
The logical_apply_worker background process (enabled when
pg_ripple.replication_enabled = on) reads these pending batches and applies
them via load_ntriples().
Setup Walkthrough
1. Primary configuration
In postgresql.conf on the primary:
wal_level = logical
max_replication_slots = 4
max_wal_senders = 4
Create the publication after CREATE EXTENSION pg_ripple:
CREATE PUBLICATION pg_ripple_pub
FOR ALL TABLES IN SCHEMA _pg_ripple;
2. Replica configuration
Add to postgresql.conf on the replica:
pg_ripple.replication_enabled = on
pg_ripple.replication_conflict_strategy = 'last_writer_wins'
Create the extension and subscription on the replica:
CREATE EXTENSION pg_ripple;
CREATE SUBSCRIPTION pg_ripple_sub
CONNECTION 'host=primary-host port=5432 dbname=mydb user=replication password=secret'
PUBLICATION pg_ripple_pub;
3. Verify replication is running
SELECT * FROM pg_ripple.replication_stats();
slot_name | lag_bytes | last_applied_lsn | last_applied_at
----------------+-----------+------------------+---------------------
pg_ripple_sub | 0 | 0/15000A8 | 2026-04-24 12:00:01
A lag_bytes of 0 means the replica is fully caught up.
Lag Monitoring
Query replication lag in bytes at any time:
SELECT slot_name, lag_bytes
FROM pg_ripple.replication_stats()
WHERE lag_bytes > 1000000; -- alert if > 1 MB behind
Integrate with Prometheus via pg_stat_statements or the built-in OTEL tracing
endpoint (pg_ripple.tracing_otlp_endpoint).
Conflict Resolution
The last_writer_wins strategy (the only strategy in v0.54.0) keeps the triple
with the highest Statement ID (SID) when two replicas receive the same (s, p, g)
triple with different objects. This matches eventual-consistency semantics for
typical RDF workloads.
Set the strategy via:
SET pg_ripple.replication_conflict_strategy = 'last_writer_wins';
Failover Procedure
- Stop writes to the primary (or wait for the replica lag to reach 0).
- Promote the replica:
pg_ctl promote -D /var/lib/postgresql/18/main - Update your application connection string to point to the new primary.
- Re-create the replication subscription on any new replicas.
GUC Reference
| GUC | Default | Description |
|---|---|---|
pg_ripple.replication_enabled | off | Enable the logical apply worker |
pg_ripple.replication_conflict_strategy | last_writer_wins | Conflict resolution strategy |
See also the full GUC reference.
Change Data Capture (CDC) with pg_ripple
pg_ripple integrates with PostgreSQL's logical replication infrastructure to provide real-time notifications when triples are inserted, updated, or deleted. This feature is called Change Data Capture (CDC).
Overview
CDC subscriptions allow applications to react immediately when the RDF graph changes — for example, to invalidate caches, trigger downstream processing, or stream updates to external systems.
pg_ripple CDC builds on:
- PostgreSQL logical replication slots (
pg_logical_slot_get_changes) - The
_pg_ripple.cdc_queuetable (populated by triggers on VP delta tables) - The
pg_ripple.cdc_subscribe()/pg_ripple.cdc_poll()API
Configuration
-- Enable CDC (creates the queue table and triggers if not already present)
SELECT pg_ripple.cdc_enable();
-- Subscribe to a specific predicate IRI (NULL = all predicates)
SELECT pg_ripple.cdc_subscribe(
subscriber_id := 'my-app',
predicate_iri := 'http://schema.org/name'
);
Polling for changes
-- Poll up to 100 changes for subscriber 'my-app'
SELECT * FROM pg_ripple.cdc_poll('my-app', max_events := 100);
Returns rows of (event_type TEXT, s TEXT, p TEXT, o TEXT, graph_iri TEXT, event_time TIMESTAMPTZ).
Tuning the CDC queue
| Parameter | Default | Description |
|---|---|---|
pg_ripple.cdc_queue_max_age | '7 days' | Rows older than this are auto-purged |
pg_ripple.cdc_batch_size | 1000 | Maximum events processed per vacuum cycle |
pg_ripple.cdc_slow_subscriber_timeout | '1 hour' | Disconnect subscriber if it hasn't polled in this window |
-- Increase queue retention to 30 days
ALTER SYSTEM SET pg_ripple.cdc_queue_max_age = '30 days';
SELECT pg_reload_conf();
Handling a slow subscriber
If a subscriber is slow to poll, the CDC queue can grow unboundedly. pg_ripple logs a warning when a subscriber exceeds cdc_slow_subscriber_timeout:
WARNING: CDC subscriber 'my-app' has not polled in 2h 15m (threshold: 1h);
consider increasing poll frequency or raising cdc_slow_subscriber_timeout
To force-disconnect a stale subscriber:
SELECT pg_ripple.cdc_unsubscribe('my-app');
Monitoring queue depth
SELECT
subscriber_id,
count(*) AS pending_events,
min(event_time) AS oldest_event,
max(event_time) AS newest_event
FROM _pg_ripple.cdc_queue
GROUP BY subscriber_id
ORDER BY pending_events DESC;
Example: react to new triples via LISTEN/NOTIFY
pg_ripple can optionally send a PostgreSQL NOTIFY on the pg_ripple_cdc channel whenever a CDC event is enqueued:
SELECT pg_ripple.cdc_enable_notify();
-- In your application:
LISTEN pg_ripple_cdc;
-- When NOTIFY arrives, call cdc_poll() to retrieve the actual events.
Limitations
- CDC captures DML on VP delta tables; main-table rows added by the merge worker are not re-captured (they were captured at insert time via the delta).
pg_dumpdoes not include CDC queue contents; restore starts with an empty queue.- Logical replication slots must be managed separately if using
pg_logicaldirectly.
Citus + pg_ripple: End-to-End Integration Guide
Version: v0.59.0 (CITUS-15)
This guide covers deploying pg_ripple with Citus horizontal sharding and optional CDC/IVM compatibility in a multi-worker environment. It assumes you have already read the Citus Integration page.
Overview
The v0.58.0 + v0.59.0 releases complete the Citus sharding story:
| Feature | Version | Description |
|---|---|---|
| VP table distribution | v0.58.0 | enable_citus_sharding() distributes VP delta tables |
| Merge fence advisory lock | v0.58.0 | Prevents split-brain during rebalancing |
| SPARQL shard-pruning | v0.59.0 | Bound-subject queries target one shard (10–100×) |
| Rebalance NOTIFY | v0.59.0 | merge_start/merge_end signals for downstream maintenance hooks |
explain_sparql Citus section | v0.59.0 | Verify pruning with EXPLAIN |
citus_rebalance_progress() | v0.59.0 | Observe live rebalance status |
Prerequisites
- Citus 12+ installed on coordinator and all workers.
- pg_ripple 0.59.0 installed on the coordinator.
- Optional companions as needed: pg_trickle 0.46.0+ for IVM-backed views, and pg_tide 0.33.0+ for relay/outbox CDC transport.
Step 1: Install pg_ripple and Citus on the coordinator
-- On the coordinator node:
CREATE EXTENSION citus;
CREATE EXTENSION pg_ripple;
-- Verify Citus is detected:
SELECT pg_ripple.citus_available(); -- returns true
Step 2: Configure sharding GUCs
ALTER SYSTEM SET pg_ripple.citus_sharding_enabled = on;
-- Enable legacy CDC/IVM co-location compatibility (prevents cross-shard deletes):
ALTER SYSTEM SET pg_ripple.citus_trickle_compat = on;
SELECT pg_reload_conf();
Step 3: Distribute VP tables
After loading your initial data, distribute all VP tables:
SELECT predicate_id, table_name, status
FROM pg_ripple.enable_citus_sharding();
This performs for each VP delta table:
ALTER TABLE … REPLICA IDENTITY FULL(required for logical replication consumers)create_distributed_table(…, 's', colocate_with => 'none')(or'default')pg_notify('pg_ripple.vp_promoted', …)— downstream maintenance hooks can react
Step 4: Verify shard-pruning (v0.59.0)
After loading some data with a known subject IRI, verify shard-pruning works:
SELECT pg_ripple.explain_sparql(
'SELECT ?p ?o WHERE { <http://example.org/Alice> ?p ?o }',
false, -- analyze
true -- citus (new in v0.59.0)
) -> 'citus';
Expected output when pruning succeeds:
{
"available": true,
"pruned_to_shard": 102008,
"worker": "worker1:5432",
"full_fanout_avoided": true,
"estimated_rows_per_shard": 47
}
When full_fanout_avoided is false, check that:
- The subject IRI exists in the dictionary:
SELECT pg_ripple.encode_term('<http://example.org/Alice>', 0). - The VP delta table is distributed:
SELECT logicalrelid FROM pg_dist_partition WHERE logicalrelid::text LIKE '%delta%'.
Step 5: Rebalancing workers
When adding Citus workers, use the pg_ripple-aware rebalancer:
-- Monitor progress (returns empty when no rebalance is running):
SELECT * FROM pg_ripple.citus_rebalance_progress();
-- Trigger a rebalance:
SELECT pg_ripple.citus_rebalance();
citus_rebalance() emits pg_ripple.merge_start before acquiring the fence
lock and pg_ripple.merge_end after releasing it. pg-trickle listens on these
channels and suspends per-worker slot polling until the rebalance completes,
preventing duplicate CDC delivery.
Listen for the signals in a monitoring session:
LISTEN "pg_ripple.merge_start";
LISTEN "pg_ripple.merge_end";
-- (run pg_ripple.citus_rebalance() in another session, then check notifications)
GUC Reference
| GUC | Default | Description |
|---|---|---|
pg_ripple.citus_sharding_enabled | off | Enable Citus shard distribution for VP tables |
pg_ripple.citus_trickle_compat | off | Use colocate_with => 'none' for legacy CDC/IVM compatibility |
pg_ripple.merge_fence_timeout_ms | 0 | Max ms to wait for merge fence (0 = no fence) |
Troubleshooting
PT536: Citus extension is not installed
pg_ripple.enable_citus_sharding() or citus_rebalance() raised PT536.
Fix: CREATE EXTENSION citus; on the coordinator before calling these functions.
full_fanout_avoided: false in explain output
The subject IRI is not in the dictionary (no triples loaded for that subject yet), or the VP table is not distributed.
Fix: Load at least one triple for the subject, then re-run enable_citus_sharding().
pg-trickle stops after rebalance
Check that your pg-trickle version is ≥ 0.34.0, which processes pg_ripple.merge_start
/ merge_end notifications and resumes slot polling automatically.
See also: Citus Integration, High Availability
pg-tide Relay: Hub-and-Spoke Integration
Available since: v0.52.0 (originally pg-trickle relay, migrated to pg-tide in v0.93.0; current bridge semantics refreshed in v0.127.0)
Requires for relay: pg_tide ≥ 0.33.0 (relay process and outbox/inbox); pg_ripple ≥ 0.127.0
Requires for IVM views: pg_trickle ≥ 0.46.0 (IVM only)
Note: pg-trickle v0.46.0 extracted the relay, outbox, and inbox subsystem into the standalone
pg_tideextension (trickle-labs/pg-tide). After v0.46.0, pg_trickle provides IVM only. All relay functionality described here requires pg_tide.
What this integration does
Imagine you have IoT sensor readings arriving via Kafka, customer records coming in from a CRM webhook, and order data flowing through NATS — all using different field names, different identifiers for the same real-world entities, and different data shapes. Downstream teams want a clean, unified, enriched view of this data pushed to their own systems in real time, without any of them having to understand the messy source schemas.
This is the hub-and-spoke pattern. pg-tide acts as the transport network: its relay CLI pulls data in from any source (Kafka, NATS, webhooks) and pushes enriched data out to any sink. pg_ripple acts as the intelligent hub in the middle: it turns the incoming JSON into a knowledge graph, runs inference rules to derive new facts, validates data quality with SHACL, and serializes the enriched results back to JSON or JSON-LD for downstream consumers. The output format is flexible — simple decoded JSON (via raw CDC triggers) or shaped JSON-LD (via framing triggers) — depending on what each downstream consumer needs.
The whole pipeline runs inside a single PostgreSQL database. Both extensions share the same transaction context, so data moves from inbox to triplestore to outbox without ever leaving the database process.
┌────────────────────────────────┐
│ pg-ripple hub │
│ (PostgreSQL + pg_ripple ext) │
INBOUND │ │ OUTBOUND
─────── │ ┌──────────┐ ┌───────────┐ │ ────────
│ │ Datalog │ │ SHACL │ │
┌──────────┐ relay │ │ inference│ │ validation│ │ relay ┌──────────┐
│ Kafka │──reverse──▶│ └────┬─────┘ └─────┬─────┘ │──forward──▶│ NATS │
│ (orders) │ │ │ │ │ │ (events) │
└──────────┘ │ ┌────▼──────────────▼─────┐ │ └──────────┘
│ │ │ │
┌──────────┐ relay │ │ RDF Triple Store │ │ relay ┌──────────┐
│ NATS │──reverse──▶│ │ (VP tables, HTAP) │──│──forward──▶│ Webhook │
│(sensors) │ │ │ │ │ │ (API) │
└──────────┘ │ └────▲──────────────▲─────┘ │ └──────────┘
│ │ │ │
┌──────────┐ relay │ ┌────┴─────┐ ┌────┴─────┐ │ relay ┌──────────┐
│ Webhook │──reverse──▶│ │owl:sameAs│ │ SPARQL │ │──forward──▶│ Kafka │
│ (CRM) │ │ │ linking │ │federation│ │ │(enriched)│
└──────────┘ │ └──────────┘ └──────────┘ │ └──────────┘
│ │
│ pg-tide stream tables │
│ (inbox → transform → outbox) │
└────────────────────────────────┘
The data flow through the hub has five stages:
- Ingest — pg-tide relay reverse mode delivers raw JSON events into inbox tables.
- Transform — a trigger converts the JSON into RDF triples and loads them into the triplestore.
- Enrich — Datalog inference rules derive new facts (alerts, entity links, risk scores).
- Validate — SHACL shapes enforce data quality before anything leaves the hub.
- Distribute — pg-tide relay forward mode pushes enriched, validated JSON events to any number of sinks.
Prerequisites
Relay examples require pg_tide and pg_ripple in the same database. Install
pg_trickle as well only when you also use pg_ripple's IVM-backed live views.
If both companion extensions are installed, create pg_tide before pg_trickle so
pgtrickle.attach_outbox() can call tide.outbox_create():
CREATE EXTENSION pg_tide; -- relay, outbox, inbox (trickle-labs/pg-tide ≥ 0.33.0)
CREATE EXTENSION pg_trickle; -- optional IVM only (trickle-labs/pg-trickle ≥ 0.46.0)
CREATE EXTENSION pg_ripple; -- RDF triple store (≥ 0.127.0)
If pg_tide is not installed, relay-dependent features are unavailable. pg_ripple degrades gracefully: SPARQL views and IVM continue to work (they require pg_trickle, not pg_tide), but outbox/inbox operations raise a descriptive error:
ERROR: pg_tide extension is not installed;
install pg_tide from https://github.com/trickle-labs/pg-tide
then run: CREATE EXTENSION pg_tide
If pg_trickle is not installed, IVM-backed SPARQL, Datalog, CONSTRUCT, DESCRIBE, ASK, and framing views are unavailable:
ERROR: pg_trickle is not installed -- SPARQL views require pg_trickle
Call pg_ripple.relay_available() or pg_ripple.pg_tide_available() before
calling relay functions. Call pg_ripple.pg_trickle_available() before calling
IVM-backed view functions.
A worked example: IoT sensor hub
The following walkthrough builds a complete hub that ingests temperature readings
from IoT sensors on a Kafka topic (iot.sensors), detects anomalies using
inference rules, and publishes enriched alerts back to Kafka on a separate topic
(iot.alerts). The pipeline is intentionally symmetrical: the same broker and
format convention on both ends, with pg_ripple doing the enrichment in the
middle. Each step shows both the SQL to run and what the data looks like at that
point.
Step 1 — Pull sensor events from Kafka
The relay process runs outside PostgreSQL and continuously polls configured
sources. You tell it what to poll and where to write the results using
tide.relay_set_inbox_v2(). Here we subscribe to the iot.sensors Kafka
topic and direct its events into a table called sensor_inbox. Enriched alerts
will flow back out on the iot.alerts topic on the same broker — same system,
both ends:
SELECT tide.relay_set_inbox_v2(jsonb_build_object(
'name', 'sensor-readings',
'inbox', 'sensor_inbox',
'source', 'kafka',
'config', jsonb_build_object(
'brokers', '${env:KAFKA_BROKERS}',
'topic', 'iot.sensors'
)
));
The ${env:KAFKA_BROKERS} syntax tells the relay to expand an environment variable
at runtime. Pipeline configs are stored in the database as JSONB, but sensitive
values like broker addresses can reference environment variables this way —
the actual value stays in the relay process's environment and never needs to be
stored in plaintext in the database.
Each time the relay receives a message from Kafka, it inserts a row into
sensor_inbox. The original Kafka message payload arrives as a JSONB column.
A typical row looks like this:
{
"event_id": "kafka:iot.sensors:0:42",
"event_type": "sensor_reading",
"payload": {
"device": "sensor-7",
"temp": 22.5,
"unit": "°C",
"ts": "2026-04-28T10:00:00Z"
}
}
Step 2 — Convert JSON events to RDF triples
Raw JSON has no standard semantics. Two sensor vendors might both call their
field "temp" but mean very different things. By converting to RDF we attach
well-defined, globally unique meanings to each field — in this case using the
SAREF IoT ontology.
The pg_ripple.json_to_ntriples_and_load() function (v0.52.0+) does this in one
call. Its context parameter works like a JSON-LD @context: it maps the incoming
JSON field names to the full predicate IRIs you want to store. The relay delivers
plain JSON; the IRI mapping is applied at load time inside PostgreSQL, so the
original message is never modified.
A trigger on sensor_inbox fires for every inserted row:
CREATE OR REPLACE FUNCTION transform_sensor_to_rdf()
RETURNS TRIGGER LANGUAGE plpgsql AS $$
BEGIN
PERFORM pg_ripple.json_to_ntriples_and_load(
payload => NEW.payload,
subject_iri => 'https://example.org/observation/' || NEW.event_id,
type_iri => 'https://saref.etsi.org/core/Measurement',
context => '{
"@vocab": "https://saref.etsi.org/core/",
"device": "https://saref.etsi.org/core/measurementMadeBy",
"temp": "https://saref.etsi.org/core/hasValue",
"ts": "https://saref.etsi.org/core/hasTimestamp",
"unit": "https://qudt.org/schema/qudt/unit"
}'::jsonb
);
RETURN NEW;
END;
$$;
CREATE TRIGGER sensor_to_rdf
AFTER INSERT ON sensor_inbox
FOR EACH ROW EXECUTE FUNCTION transform_sensor_to_rdf();
The context object supports two resolution mechanisms:
@vocab— a default IRI prefix applied to every unmapped key. Any field not explicitly listed gets expanded tohttps://saref.etsi.org/core/{field}.- Explicit entries — override specific keys with the exact IRI you want,
regardless of the
@vocabdefault.
Nested JSON objects become blank nodes. Arrays produce one triple per element.
null values are silently skipped. This means you can point the relay at an
arbitrary third-party JSON event and describe the entire mapping in one JSONB
literal — no code changes needed when a vendor renames a field, only a context
update.
After the trigger fires, those five triples exist in the triplestore — one
rdf:type triple (from type_iri) and one for each of the four data fields in
the source JSON. If you queried them back as JSON-LD immediately after load, you
would see:
{
"@context": {
"saref": "https://saref.etsi.org/core/",
"xsd": "http://www.w3.org/2001/XMLSchema#",
"qudt": "https://qudt.org/schema/qudt/"
},
"@id": "https://example.org/observation/kafka:iot.sensors:0:42",
"@type": "saref:Measurement",
"saref:measurementMadeBy": {
"@id": "https://example.org/device/sensor-7"
},
"saref:hasValue": {
"@value": "22.5",
"@type": "xsd:decimal"
},
"saref:hasTimestamp": {
"@value": "2026-04-28T10:00:00Z",
"@type": "xsd:dateTime"
},
"qudt:unit": "°C"
}
This is the data at rest inside the triplestore, expressed as JSON-LD. The
@type and @id fields carry the full semantic meaning from the SAREF ontology,
so any consumer that understands SAREF can correctly interpret the reading.
Step 3 — Add inference rules to detect anomalies
Datalog rules let you express facts that can be derived from the stored triples. Rather than writing triggers for every business rule, you declare the rules once and pg_ripple materialises the inferred triples automatically.
The rules below fire an alert whenever a measurement exceeds 40°C and link devices across sources that share a serial number (entity resolution):
SELECT pg_ripple.load_rules(
rules => $$
% Derive an alert for any observation above the threshold.
% Two triples are inferred per matching observation:
% <obs> ex:tempAlert <device> — links observation to device
% <obs> ex:alertType "high_temperature" — records the alert category
% Storing the alert type as a literal triple keeps the meaning inside
% the triplestore rather than hardcoding it in downstream trigger code.
ex:tempAlert(Obs, Device) :-
saref:measurementMadeBy(Obs, Device),
saref:hasValue(Obs, Val),
Val > 40.0.
ex:alertType(Obs, "high_temperature") :-
saref:hasValue(Obs, Val),
Val > 40.0.
% Link two devices if they share a serial number, even if they
% appear under different identifiers in different source systems.
owl:sameAs(D1, D2) :-
schema:serialNumber(D1, SN),
schema:serialNumber(D2, SN),
D1 \= D2.
$$,
rule_set => 'sensor_enrichment'
);
When a 45°C reading arrives from sensor-7, these rules materialise two new
triples — inferred facts that did not exist in the raw data:
<https://example.org/observation/kafka:iot.sensors:0:99>
<https://example.org/tempAlert>
<https://example.org/device/sensor-7> .
<https://example.org/observation/kafka:iot.sensors:0:99>
<https://example.org/alertType>
"high_temperature" .
Step 4 — Enforce data quality with SHACL
Before enriched data leaves the hub, SHACL shapes act as a quality gate. Any
observation that lacks a measurementMadeBy link or a timestamp will fail
validation and be flagged rather than silently forwarded to downstream consumers:
SELECT pg_ripple.load_shacl($$
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix saref: <https://saref.etsi.org/core/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix ex: <https://example.org/> .
ex:ObservationShape a sh:NodeShape ;
sh:targetClass saref:Measurement ;
sh:property [
sh:path saref:measurementMadeBy ;
sh:minCount 1 ;
sh:maxCount 1 ;
sh:message "Every measurement must reference exactly one device." ;
] ;
sh:property [
sh:path saref:hasTimestamp ;
sh:minCount 1 ;
sh:datatype xsd:dateTime ;
sh:message "Every measurement must have an xsd:dateTime timestamp." ;
] .
$$);
Step 5 — Route enriched events to downstream consumers
Now that observations are stored, enriched, and validated, we set up the outbound pipeline. We create a bridge table to hold outbound events, subscribe to the inferred alert triples, and configure relay forward pipelines to deliver them wherever they are needed.
-- The bridge table holds the outbound alert payloads before the trigger
-- publishes them to a pg_tide outbox.
CREATE TABLE enriched_events (
id BIGSERIAL PRIMARY KEY,
event_type TEXT NOT NULL,
payload JSONB NOT NULL,
created_at TIMESTAMPTZ DEFAULT now()
);
-- Declare which triples the subscription should watch for.
-- Only triples where the predicate is ex:tempAlert will pass through.
SELECT pg_ripple.create_subscription(
name => 'high-temp-alerts',
filter_sparql => 'FILTER(?p = <https://example.org/tempAlert>)'
);
-- Wire the subscription to the outbox table.
-- This installs a trigger on the VP delta table for ex:tempAlert; whenever a
-- matching triple lands, the trigger decodes it and writes a row to
-- enriched_events. That is the only writer to enriched_events in this pipeline.
SELECT pg_ripple.enable_cdc_bridge_trigger(
name => 'high-temp-alerts',
predicate => 'https://example.org/tempAlert',
outbox => 'enriched_events'
);
-- Reformat the raw decoded triple into plain JSON before the row is committed.
-- The frame template is a static JSONB literal: the @context maps short
-- property names to the stored SAREF predicate IRIs, and the property slots
-- ({}) tell pg_ripple which predicates to pull. Only the @id is dynamic —
-- it scopes the generated CONSTRUCT query to this specific observation.
-- The final unwrap + strip step removes JSON-LD structural keywords so the
-- consumer receives the same plain JSON as before, but the output shape is
-- now fully described by the frame rather than hand-assembled field by field.
CREATE OR REPLACE FUNCTION reformat_alert_payload()
RETURNS TRIGGER LANGUAGE plpgsql AS $$
DECLARE
framed JSONB;
item JSONB;
BEGIN
framed := pg_ripple.export_jsonld_framed(
frame => '{
"@context": {
"device": "https://saref.etsi.org/core/measurementMadeBy",
"temp": "https://saref.etsi.org/core/hasValue",
"unit": "https://qudt.org/schema/qudt/unit",
"ts": "https://saref.etsi.org/core/hasTimestamp",
"alert": "https://example.org/alertType"
},
"@type": "https://saref.etsi.org/core/Measurement",
"device": {},
"temp": {},
"unit": {},
"ts": {},
"alert": {}
}'::jsonb || jsonb_build_object('@id', NEW.payload ->> 'subject')
);
-- @context lives at the top level of the framed document and is discarded
-- automatically when we unwrap @graph. @type and @id sit on the node
-- itself; strip them with the jsonb - operator to get plain JSON.
item := (framed -> '@graph' -> 0) - '@type' - '@id';
IF item IS NOT NULL THEN
NEW.payload := item;
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER reformat_alert
BEFORE INSERT ON enriched_events
FOR EACH ROW EXECUTE FUNCTION reformat_alert_payload();
-- Create a pg_tide outbox so the relay can poll it.
-- pg_tide manages all outbox messages in tide.tide_outbox_messages;
-- triggers call tide.outbox_publish() instead of inserting into a bridge table.
SELECT tide.outbox_create(
'enriched-events',
p_retention_hours := 24,
p_inline_threshold := 0
);
-- Install a publish trigger so the relay picks up enriched_events rows.
CREATE OR REPLACE FUNCTION bridge_alert_to_tide_outbox()
RETURNS TRIGGER LANGUAGE plpgsql AS $$
BEGIN
PERFORM tide.outbox_publish(
'enriched-events',
jsonb_build_object(
'subject', pg_ripple.decode_id(NEW.s),
'predicate', pg_ripple.decode_id(TG_ARGV[0]::bigint),
'object', pg_ripple.decode_id(NEW.o),
'graph', pg_ripple.decode_id(NEW.g)
),
'{}'::jsonb -- headers
);
RETURN NEW;
END;
$$;
Now configure the relay to forward those events back to Kafka on the iot.alerts
topic — the same broker the raw sensor readings came from:
-- Publish enriched alerts to iot.alerts on the same Kafka broker
SELECT tide.relay_set_outbox_v2(jsonb_build_object(
'name', 'alerts-to-kafka',
'outbox', 'enriched-events',
'sink_type', 'kafka',
'config', jsonb_build_object(
'brokers', '${env:KAFKA_BROKERS}',
'topic', 'iot.alerts'
)
));
The same outbox can fan out to additional sinks without any changes to the
pg_ripple side — just register extra relay_set_outbox_v2 pipelines:
-- Also push to NATS for real-time dashboard consumers (optional)
SELECT tide.relay_set_outbox_v2(jsonb_build_object(
'name', 'alerts-to-nats',
'outbox', 'enriched-events',
'sink_type', 'nats',
'config', jsonb_build_object(
'url', 'nats://nats:4222',
'subject', 'iot.alerts.{event_type}'
)
));
-- Or to a partner API via webhook (optional)
SELECT tide.relay_set_outbox_v2(jsonb_build_object(
'name', 'alerts-to-partner',
'outbox', 'enriched-events',
'sink_type', 'webhook',
'config', jsonb_build_object(
'url', 'https://partner.example.com/events',
'method', 'POST'
)
));
The reformat_alert_payload trigger produces the same plain JSON as the
previous jsonb_build_object version, but the entire output shape is now
described by the static frame template — including the alert field. The
ex:alertType literal triple was inferred by the Datalog rule in Step 3, so
the frame simply maps the short name "alert" to that predicate and picks it
up like any other property; there is nothing to hardcode in the trigger. The
|| appends only the dynamic @id to scope the generated CONSTRUCT query to
this specific observation. Two steps then strip the JSON-LD structural
keywords: -> '@graph' -> 0 unwraps the node and discards the top-level
@context; - '@type' - '@id' removes the remaining keywords from the node.
A consumer reading from iot.alerts sees a plain JSON document it can process
without any knowledge of RDF or the triplestore:
{
"device": "sensor-7",
"temp": 45.2,
"unit": "°C",
"ts": "2026-04-28T10:00:00Z",
"alert": "high_temperature"
}
Compare this with the original inbound message from Step 1: same field names
(device, temp, unit, ts), one new alert field derived by the inference
rules. The values here come from a different observation (the 45.2°C reading
that triggered the alert), but the shape is identical. The triplestore and
Datalog engine are invisible to both the producer and the consumer — they see
only ordinary JSON.
Choosing a CDC bridge approach
Step 5 used enable_cdc_bridge_trigger (Approach A below) — the simplest
option, with latency under 10 ms. Two other approaches offer different
trade-offs for different data paths in the same hub. All three write to the
same enriched_events outbox table; only the mechanism that detects new
triples and writes outbox rows differs.
Approach A — Trigger bridge (lowest latency, < 10 ms)
The simplest approach: a trigger on VP delta tables fires in the same transaction that inserted the triple. The triple is decoded from its internal integer representation and written directly to the bridge table before the transaction commits. Nothing can slip through — the data is in both places or neither:
CREATE OR REPLACE FUNCTION _pg_ripple.bridge_to_outbox()
RETURNS TRIGGER LANGUAGE plpgsql AS $$
BEGIN
INSERT INTO enriched_events (event_type, payload)
VALUES (
TG_OP,
jsonb_build_object(
'subject', pg_ripple.decode_id(NEW.s),
'predicate', pg_ripple.decode_id(TG_ARGV[0]::bigint),
'object', pg_ripple.decode_id(NEW.o),
'graph', pg_ripple.decode_id(NEW.g)
)
);
RETURN NEW;
END;
$$;
The pg_ripple.enable_cdc_bridge_trigger() call in Step 5 installs exactly
this pattern automatically — you do not need to write the trigger function by
hand. The raw {subject, predicate, object, graph} payload is useful on its
own, but you can also reshape it before it leaves the outbox:
- Plain JSON via framing — add a
BEFORE INSERTtrigger that callsexport_jsonld_framed()with the desired@context, unwraps@graph, and strips@typeand@idwith the jsonb-operator, as Step 5 does withreformat_alert_payload(). This is more declarative than building the payload manually withjsonb_build_object— changing an outbound property name is a one-line@contextedit rather than a code change. - Full JSON-LD — store the framed document directly without stripping (see Outbound framing).
Best for: High-priority alerts, strict transactional guarantees.
Trade-off: decode_id() is called once per row, which adds overhead on high-volume
write paths. For filtering — forwarding only alerts and not every measurement — you
need an extra WHERE condition in the trigger.
Approach B — Background worker bridge (best throughput, 50–500 ms)
The pg_ripple background worker (enabled since v0.52.0) wakes up when it
receives CDC NOTIFY events, collects a batch of them, decodes all the integer
dictionary IDs in a single bulk SPI call, and inserts the decoded rows into the
bridge table in one go. The amortised cost per triple is much lower than the
trigger approach, and the batch size and flush interval are configurable.
This approach adds a small latency window (the batch collection time), but for high-volume enriched-data streams — thousands of triples per second — the throughput improvement is significant. Use this as the default for bulk data paths.
Best for: Bulk enriched data, high-volume streams. Trade-off: Adds configurable milliseconds of latency.
Approach C — Named subscription with SPARQL CONSTRUCT view (most flexible)
Named subscriptions (v0.42.0+) let you attach a SPARQL FILTER expression
that controls exactly which triples are bridged. Only the triples that match
the filter expression will ever touch the outbox, which is useful when the
inference rules produce many intermediate triples that you do not want to
forward:
-- Only bridge the final alert triples, not intermediate inference steps
SELECT pg_ripple.create_subscription(
name => 'alerts',
filter_sparql => 'FILTER(?p = <https://example.org/tempAlert>)'
);
Combine this with export_jsonld_framed() to shape the outbound payload into
exactly the JSON structure the downstream consumer expects. A JSON-LD frame
is a template you write once that describes the desired nesting and field names.
pg_ripple translates it to a SPARQL CONSTRUCT query internally, executes it,
applies the W3C embedding algorithm, and compacts the result with your @context:
SELECT pg_ripple.export_jsonld_framed(
frame => '{
"@context": {
"ex": "https://example.org/",
"schema": "https://schema.org/",
"saref": "https://saref.etsi.org/core/",
"xsd": "http://www.w3.org/2001/XMLSchema#",
"alertLevel": "ex:alertLevel",
"name": "schema:name",
"latestTemp": "ex:latestTemp"
},
"@type": "ex:Alert",
"alertLevel": {},
"name": {},
"latestTemp": {}
}'::jsonb
);
The framed output is a clean, nested JSON-LD document ready to publish through the pg_tide outbox:
{
"@context": {
"ex": "https://example.org/",
"schema": "https://schema.org/",
"xsd": "http://www.w3.org/2001/XMLSchema#",
"alertLevel": "ex:alertLevel",
"name": "schema:name",
"latestTemp": "ex:latestTemp"
},
"@graph": [
{
"@id": "https://example.org/device/sensor-7",
"@type": "ex:Alert",
"alertLevel": "HIGH",
"name": "Boiler Room Sensor 7",
"latestTemp": { "@value": "45.2", "@type": "xsd:decimal" }
}
]
}
Use jsonld_frame_to_sparql(frame => ...) to inspect the generated CONSTRUCT
query before running the full export — this is useful for performance tuning.
Best for: Complex SPARQL-shaped payloads, scheduled reports, ad-hoc shapes. Trade-off: Polling-based unless combined with a LISTEN/NOTIFY wake-up.
Which approach to use
In practice you will use all three for different data paths in the same hub:
| Data path | Mechanism | Typical latency |
|---|---|---|
| High-priority alerts | Approach A — trigger bridge | < 10 ms |
| Bulk enriched data | Approach B — background worker | 50–500 ms |
| Shaped reports / views | Approach C — SPARQL CONSTRUCT | Cron-driven |
Common patterns
Unifying records from multiple sources
A real hub almost always receives data about the same real-world entities from
multiple systems — a customer appears in the CRM as crm:C1 and in the ERP as
erp:A1, but they are the same person. Datalog owl:sameAs rules detect these
overlaps from shared attributes (email address, serial number, phone number) and
create linking triples that allow downstream SPARQL queries to treat both records
as one:
% Link CRM and ERP records that share an email address
owl:sameAs(CrmCust, ErpAcct) :-
crm:emailAddress(CrmCust, E),
erp:contact_email(ErpAcct, E).
Once those owl:sameAs triples are materialised, pg_ripple's OWL RL
canonicalisation ensures that any query for crm:C1 will transparently find
data originally stored under erp:A1 as well.
Speaking a common language to downstream consumers
Each source system uses its own property names. Your CRM calls it
crm:customerName; the ERP calls it erp:accountTitle. Rather than
requiring every downstream consumer to understand every source vocabulary,
Datalog rules project everything onto a single shared ontology (here, Schema.org):
% Both CRM and ERP names map to schema:name
schema:name(X, V) :- crm:customerName(X, V).
schema:name(X, V) :- erp:accountTitle(X, V).
% Both email fields map to schema:email
schema:email(X, V) :- crm:emailAddress(X, V).
schema:email(X, V) :- erp:contact_email(X, V).
Downstream consumers now only need to understand Schema.org. Source schema changes are isolated to a single Datalog rule update in the hub — no downstream changes required.
Rich JSON-LD for event-driven downstream consumers
Here is what a fully enriched and shaped customer record looks like by the time
it reaches a downstream consumer via the relay. Notice how all the messy
source-system vocabulary has been replaced with clean Schema.org terms, and the
document is self-describing thanks to the @context:
{
"@context": {
"schema": "https://schema.org/",
"ex": "https://example.org/",
"xsd": "http://www.w3.org/2001/XMLSchema#"
},
"@id": "https://example.org/customer/C1",
"@type": "schema:Customer",
"schema:name": "Jane Doe",
"schema:email": "jane@example.com",
"ex:riskScore": { "@value": "0.87", "@type": "xsd:decimal" },
"ex:highValueCustomer": true,
"owl:sameAs": [
{ "@id": "https://erp.example.com/accounts/A1" },
{ "@id": "https://support.example.com/tickets/T9" }
],
"_relay_dedup_key": "ripple:4200042"
}
The owl:sameAs array shows the entity resolution result — this one record
links the CRM, ERP, and support-ticket identities together. The ex:riskScore
was derived by a Datalog rule from order history data.
The _relay_dedup_key is a convention for idempotent delivery: its value is
derived from pg_ripple's internal statement ID (the i column in VP tables),
so downstream consumers can detect and discard duplicates even if the relay
restarts and replays part of the outbox. Including it is optional — the worked
example above omits it because the plain-JSON reformat trigger does not add it,
but it is straightforward to include via
'_relay_dedup_key', 'ripple:' || NEW.i in the jsonb_build_object call.
JSON-LD mapping: inbound context and outbound framing
JSON-LD mapping is the mechanism that lets pg_ripple act as a true semantic bridge: arbitrary JSON goes in, canonicalised knowledge graph triples are stored, and shaped JSON-LD comes out. The inbound and outbound sides each have a dedicated function.
Inbound — json_to_ntriples_and_load() with a context map
When the relay delivers a raw JSON event, pg_ripple.json_to_ntriples_and_load()
converts it to RDF triples in one step. The context parameter is a JSONB object
that works like a JSON-LD @context:
Incoming JSON context map stored triples
───────────── ─────────── ──────────────
{ "temp": 45.2, "device": ... } "temp" → saref:hasValue → <obs> saref:hasValue "45.2"^^xsd:decimal
"device"→ saref:madeby → <obs> saref:madeby <device-7>
@vocab → saref: → unmapped keys get saref: prefix
It supports:
@vocab— default IRI prefix for all keys not explicitly listed.- Explicit key-to-IRI mappings — override specific fields with exact predicate IRIs.
- Nested objects — become blank nodes with their own predicates resolved through the same context.
- Arrays — produce one triple per element.
nullvalues — silently skipped.
The context is stored only in the trigger definition, not in the triplestore. When a source vendor renames a field, you update the context JSONB; no triples need to change.
-- Full context example for a heterogeneous event shape
PERFORM pg_ripple.json_to_ntriples_and_load(
payload => NEW.payload,
subject_iri => 'https://example.org/event/' || NEW.event_id,
type_iri => 'https://schema.org/Event',
context => '{
"@vocab": "https://schema.org/",
"ts": "https://schema.org/startDate",
"location": "https://schema.org/location",
"description": "https://schema.org/description",
"external_id": "https://example.org/externalId"
}'::jsonb
);
Outbound — export_jsonld_framed() with a frame template
On the way out, pg_ripple.export_jsonld_framed() (v0.17.0+) shapes the flat
RDF into whatever nested JSON structure the downstream consumer expects. A
frame is a JSON template that describes the desired structure; pg_ripple
handles everything else:
- Translates the frame into a SPARQL CONSTRUCT query.
- Executes the query against the triplestore.
- Applies the W3C JSON-LD 1.1 embedding algorithm to produce nested nodes.
- Compacts IRI strings using the frame's
@context.
stored triples frame template outbound JSON-LD
────────────── ────────────── ───────────────
<device> schema:name "X" "@type": "schema:Device" { "@type": "Device",
<device> ex:temp 45.2 "name": {}, → "name": "X",
<device> ex:alert "HIGH" "temp": {}, "temp": 45.2,
"alert": {} "alert": "HIGH" }
Use this inside the outbox bridge trigger to produce consumer-ready JSON-LD:
-- Write a framed JSON-LD event to the outbox every time an alert triple lands
CREATE OR REPLACE FUNCTION bridge_alert_to_outbox()
RETURNS TRIGGER LANGUAGE plpgsql AS $$
DECLARE
framed JSONB;
BEGIN
framed := pg_ripple.export_jsonld_framed(
frame => '{
"@context": {
"schema": "https://schema.org/",
"ex": "https://example.org/",
"xsd": "http://www.w3.org/2001/XMLSchema#",
"name": "schema:name",
"latestTemp": "ex:latestTemp",
"alertLevel": "ex:alertLevel"
},
"@type": "ex:Alert",
"name": {},
"latestTemp": {},
"alertLevel": {}
}'::jsonb
);
INSERT INTO enriched_events (event_type, payload)
VALUES ('alert', framed);
RETURN NEW;
END;
$$;
The downstream consumer receives a document shaped exactly to the frame — with
short, readable property names from the @context, nested objects where the
frame requests them, and a self-describing @context block so it can be parsed
without any knowledge of pg_ripple's internals.
If the downstream consumer expects plain JSON rather than JSON-LD, strip the structural keywords before inserting into the outbox:
-- @context is at the top level; -> '@graph' -> 0 discards it automatically.
-- @type and @id live on the node; chain - to remove them in one expression.
item := (framed -> '@graph' -> 0) - '@type' - '@id';
INSERT INTO enriched_events (event_type, payload)
VALUES ('alert', item);
The jsonb - operator accepts either a single key (- '@type') or a text
array (- ARRAY['@type','@id']) to remove several keywords at once.
Debugging the frame translation
Before running export_jsonld_framed() in production, use
jsonld_frame_to_sparql() to see the SPARQL CONSTRUCT query that will be
generated. This is useful for verifying that the frame matches your stored
triple shapes and for identifying any missing patterns before they cause
silent empty results:
SELECT pg_ripple.jsonld_frame_to_sparql(
frame => '{
"@context": { "schema": "https://schema.org/" },
"@type": "schema:Device",
"schema:name": {}
}'::jsonb
);
Symmetric round-trip
The two functions form a symmetric pair: json_to_ntriples_and_load() maps
field names from the source JSON vocabulary to RDF predicates on the way in;
export_jsonld_framed() maps those same predicates back to the field names and
nested structure the consumer needs on the way out. You can use different
@context definitions for different consumers — the triplestore is the stable
canonical representation in the middle, and the vocabularies at each edge are
entirely configurable.
Deployment
pg_tide and pg_ripple run in the same PostgreSQL instance. The pg-tide
relay process is separate and stateless. A single relay instance handles both
directions — inbound pipelines (external source → inbox table) and outbound
pipelines (outbox table → external sink) run in the same process. You do not need
separate relay binaries for each direction.
For high availability, run two or three relay instances pointing at the same PostgreSQL database. PostgreSQL advisory locks elect exactly one owner per pipeline — if one instance dies, another acquires its pipelines on the next discovery interval.
The relay only needs one environment variable to start: the database URL.
All pipeline configuration is registered in the database via SQL
(tide.relay_set_outbox_v2() / tide.relay_set_inbox_v2()), and the relay reads it on
startup and hot-reloads it when you make changes — no restart required.
Sensitive values like broker addresses can use ${env:VAR} placeholders inside
the JSONB config. The relay expands them from its own process environment at
runtime, so credentials never need to be stored in the database.
# docker-compose.yml sketch
services:
postgres:
image: ghcr.io/trickle-labs/pg-ripple:latest
# pg_ripple, pg_trickle, pg_tide, PostGIS, and pgvector all pre-installed
relay:
image: ghcr.io/trickle-labs/pg-tide:0.33.0
environment:
PG_TIDE_POSTGRES_URL: postgres://relay:pw@postgres/hub
# All pipeline config (topics, subjects, poll intervals) lives in the DB.
# Broker addresses can use ${env:VAR} refs — set them here as env vars.
# Register pipelines with tide.relay_set_outbox_v2() / tide.relay_set_inbox_v2().
KAFKA_BROKERS: kafka:9092 # expanded by ${env:KAFKA_BROKERS} in pipeline config
NATS_URL: nats://nats:4222 # similarly available via ${env:NATS_URL}
ports:
- "9090:9090" # Prometheus metrics + /health endpoint
pg-ripple-http:
image: pg-ripple-http:latest
environment:
DATABASE_URL: postgres://ripple:pw@postgres/hub
ports:
- "8080:8080"
# SPARQL endpoint for ad-hoc queries from dashboards and tools
kafka:
image: redpandadata/redpanda:latest
nats:
image: nats:latest
command: ["-js"] # JetStream enabled for durable subscriptions
For Kubernetes deployments, the relay's /health endpoint integrates with
readiness probes. See Kubernetes & Helm for a full Helm
chart example.
Things to watch out for
Outbox growing faster than it drains (backpressure)
When a single inbound event triggers many inferred triples — for example, an
owl:sameAs merge that touches hundreds of related facts — the outbox can
accumulate rows faster than the relay drains them. Three controls help:
- Use pg_tide's retention drain (
tide.outbox_create(p_retention_hours => N)) to cap outbox size and drop the oldest rows once a maximum depth is reached. - Use the relay's
/healthendpoint as a Kubernetes readiness probe. When the relay falls behind, it signals not-ready, letting the cluster apply back-pressure to inbound sources. - Use pg_ripple's
sourcecolumn to bridge only explicit triples (source = 0) and suppress inferred triples (source = 1) from a particular outbox, reducing volume without changing the inference rules.
Keeping consumers in sync after rule changes
When you update a Datalog rule or SHACL shape — adding a new derived property, changing a threshold — downstream consumers are receiving a different data shape than before. Manage this with:
- Version the subject template in the relay outbox configuration:
ripple.v2.enriched.{type}rather thanripple.enriched.{type}. - Include a
@contextversion field in outbound JSON-LD payloads so consumers can detect schema changes programmatically. - Use the relay's full-refresh mode to re-snapshot the entire outbox after a rule change, ensuring consumers that missed the transition catch up.
Bidirectional CRM ⇄ ERP Walkthrough (v0.77.0)
Available since: v0.77.0
v0.77.0 adds the generic bidirectional integration primitives that make it possible to build a two-way relay between any two systems through pg_ripple. This walkthrough uses a CRM ⇄ ERP scenario as the concrete example.
Topology
CRM (source of truth ERP (source of truth
for email/phone) for tax ID/status)
│ │
│ pg-tide relay │ pg-tide relay
▼ ▼
<urn:source:crm> <urn:source:erp>
│ │
└──────────┬────────────────┘
▼
pg_ripple hub
(named graphs)
│
conflict policies
(source_priority)
│
resolved projection
│
┌─────────────┴─────────────┐
▼ ▼
CRM outbox ERP outbox
(pg-tide) (pg-tide)
Step 1: Register mappings with default graph IRIs
-- CRM mapping: contacts
SELECT pg_ripple.register_json_mapping(
'crm_contact',
'{"@context": {
"ex:name": "http://schema.org/name",
"ex:email": "http://schema.org/email",
"ex:phone": "http://schema.org/telephone"
}}'::jsonb,
default_graph_iri => '<urn:source:crm>',
timestamp_path => '$.lastModified',
iri_template => 'https://crm.example.com/contacts/{id}'
);
-- ERP mapping: employees
SELECT pg_ripple.register_json_mapping(
'erp_employee',
'{"@context": {
"ex:name": "http://schema.org/name",
"ex:taxId": "http://schema.org/taxID",
"ex:status": "http://schema.org/employmentType"
}}'::jsonb,
default_graph_iri => '<urn:source:erp>',
timestamp_path => '$.updatedAt',
iri_template => 'https://erp.example.com/employees/{id}'
);
Step 2: Declare conflict resolution policies
-- Email: CRM is authoritative
SELECT pg_ripple.register_conflict_policy(
'http://schema.org/email',
'source_priority',
'{"order": ["<urn:source:crm>", "<urn:source:erp>"]}'::jsonb
);
-- Name: last-write wins across both systems
SELECT pg_ripple.register_conflict_policy(
'http://schema.org/name',
'latest_wins'
);
-- Tax ID: ERP is sole source; reject any CRM attempt
SELECT pg_ripple.register_conflict_policy(
'http://schema.org/taxID',
'reject_on_conflict'
);
Step 3: Ingest payloads
-- Ingest a CRM contact update (diff mode: derives per-triple timestamps)
SELECT pg_ripple.ingest_json(
'{"ex:name": "Alice Smith", "ex:email": "alice@crm.example.com"}'::jsonb,
'https://crm.example.com/contacts/42',
'crm_contact',
mode => 'diff'
);
-- Ingest the matching ERP employee (upsert mode: replaces sh:maxCount 1 fields)
SELECT pg_ripple.ingest_json(
'{"ex:name": "Alice Smith", "ex:taxId": "123-45-6789"}'::jsonb,
'https://erp.example.com/employees/E-999',
'erp_employee',
mode => 'upsert'
);
Step 4: Cross-source entity linking (BIDI-REF-01)
After the ERP confirms Alice is the same person as CRM contact 42:
-- Record the linkback: CRM hub IRI ↔ ERP employee IRI
SELECT pg_ripple.record_linkback(
'event-uuid-from-outbox'::uuid,
target_iri => 'https://erp.example.com/employees/E-999'
);
This writes <https://crm.example.com/contacts/42> owl:sameAs <https://erp.example.com/employees/E-999> into the target graph and flushes any buffered events.
Step 5: Loop-safe subscriptions (BIDI-LOOP-01)
When creating subscriptions, use exclude_graphs to prevent echo loops:
-- CRM subscription: exclude CRM's own graph to prevent loop
SELECT pg_ripple.create_subscription(
'crm_contact_sync',
outbox_table => 'pg_ripple_outbox.crm_contact_events'
-- Note: exclude_graphs and propagation_depth are set via the
-- _pg_ripple.subscriptions table after creation:
-- UPDATE _pg_ripple.subscriptions
-- SET exclude_graphs = ARRAY['<urn:source:crm>'],
-- propagation_depth = 1
-- WHERE name = 'crm_contact_sync';
);
Step 6: Observe per-graph metrics (BIDI-OBS-01)
SELECT graph_iri, triple_count, last_write_at, conflicts_total
FROM pg_ripple.graph_stats()
ORDER BY triple_count DESC;
Outbound event wire format (BIDI-WIRE-01)
Every event emitted to an outbox table follows this shape:
{
"version": "1.0",
"event_id": "550e8400-e29b-41d4-a716-446655440000",
"subscription": "crm_contact_sync",
"subject": "https://crm.example.com/contacts/42",
"source_graph": "urn:source:crm",
"after": {
"http://schema.org/name": "Alice Smith",
"http://schema.org/email": "alice@crm.example.com"
},
"base": {
"http://schema.org/name": "Alice"
},
"subject_resolved": true
}
The base object carries the previous values for changed predicates, enabling
compare-and-swap (CAS) safety at the receiving system. See
assert_cas(event, actual) for a built-in CAS helper.
The full JSON Schema is at docs/src/operations/event-schema-v1.json.
Related pages
- CDC Operations
- Citus + pg-trickle Integration
- Cookbook: CDC → Kafka via JSON-LD Outbox
- Reasoning and Inference (Datalog)
- Validating Data Quality (SHACL)
- Exporting and Sharing
- pg_tide Repository
- Compatibility Matrix
pg-trickle Relay Has Moved
The pg-trickle relay no longer exists. Relay, outbox, inbox, consumer-group, and relay-process features now live in pg_tide.
Use the current guide: pg-tide Relay: Hub-and-Spoke.
pg_trickle is still used by pg_ripple for incremental view maintenance (SPARQL views, Datalog views, CONSTRUCT/DESCRIBE/ASK views, and ExtVP), but not for relay transport.
Bidi Operations Runbook
v0.78.0 — BIDIOPS-DOC-01
This runbook covers day-two operations for the bidirectional integration (bidi) subsystem introduced in v0.77.0 and v0.78.0. It assumes the reader has completed the pg-tide relay guide.
Queue draining procedure
When a relay goes down or pg_tide delivery stalls:
-
Check the queue depth for each subscription:
SELECT subscription_name, outbox_depth, outbox_oldest_age, dead_letter_count, pg_tide_paused FROM pg_ripple.bidi_status(); -
Pause delivery if you need a maintenance window:
SELECT tide.relay_disable('<pipeline_name>');bidi_status()will showpg_tide_paused = truewhile paused. New outbox events continue to accumulate. -
Resolve the relay issue, then resume:
SELECT tide.relay_enable('<pipeline_name>'); -
Monitor drain progress until
outbox_depthreturns to 0:SELECT subscription_name, outbox_depth FROM pg_ripple.bidi_status(); -
Inspect dead-letter events if any accumulated during the pause:
SELECT * FROM pg_ripple.list_dead_letters('<subscription_name>');
Token rotation procedure
Per-subscription bearer tokens should be rotated on a schedule (e.g., every 90 days) or immediately after a suspected breach.
-
Register the new token while the old one is still active:
SELECT pg_ripple.register_subscription_token( '<subscription_name>', ARRAY['linkback','divergence','abandon'], 'crm-relay-2026-Q3' );Store the returned raw token securely (it is shown only once).
-
Distribute the new token to the relay(s). Allow a brief overlap period (≥ token cache TTL, 60 seconds).
-
Revoke the old token once
last_used_atconfirms relay traffic has switched:SELECT pg_ripple.revoke_subscription_token(<old_token_hash_bytea>); -
Verify only the new token is active:
SELECT label, scopes, last_used_at, revoked_at FROM pg_ripple.list_subscription_tokens('<subscription_name>');
Redaction policy guidance
- Mark PII and secret-bearing predicates with
"@redact": truein the subscription's JSON-LD frame. - Redacted predicates render as
{"@redacted": true}in the standard outbox. - For compliance pipelines that need cleartext, configure a separate subscription with an unredacted outbox table and grant access only to the elevated relay's credentials (standard PostgreSQL GRANTs).
- The frame is the only source of redaction truth — there is no additional allow-list.
- See Redaction pattern below.
Schema-evolution rollout playbook
When you need to change the subscription frame, IRI template, or exclude list:
-
Frame change (add/remove predicates):
SELECT pg_ripple.alter_subscription( '<name>', frame_change_policy => 'new_events_only' ); -- Then update the subscription's frame column separately.Queued outbox rows drain with the old frame; new events use the updated frame.
-
IRI template or match-pattern change:
SELECT pg_ripple.alter_subscription( '<name>', iri_change_policy => 'new_events_only' );Already-rendered outbox IRIs are not retroactively rewritten.
For broken templates, pause the pg_tide relay pipeline, drop/requeue affected rows if needed, and record the action in_pg_ripple.subscription_schema_changes. -
Exclude-graphs change:
SELECT pg_ripple.alter_subscription( '<name>', exclude_change_policy => 'new_events_only' );All changes are recorded automatically in
_pg_ripple.subscription_schema_changes:SELECT * FROM _pg_ripple.subscription_schema_changes WHERE subscription_name = '<name>' ORDER BY changed_at DESC;
Reconciliation playbook
When a relay reports CAS divergence (actual values differ from base):
-
Enqueue the divergence (the relay calls this automatically):
SELECT pg_ripple.reconciliation_enqueue( '<event_id>'::uuid, '{"ex:phone": {"actual": "+1-555-0100", "base": "+1-555-0200", "after": "+1-555-0300"}}'::jsonb ); -
Pull the next item for review:
SELECT * FROM pg_ripple.reconciliation_next('<subscription_name>'); -
Choose a resolution action:
Action When to use accept_externalExternal system is the authority; ingest its actual values force_internalHub is the authority; re-emit event unconditionally merge_via_owl_sameAsDivergence reveals a duplicate subject; assert owl:sameAsdead_letterCannot resolve now; move to dead-letter for later review SELECT pg_ripple.reconciliation_resolve( <reconciliation_id>, 'accept_external', 'Confirmed with CRM team: external value is correct' ); -
Monitor open items:
SELECT subscription_name, reconciliation_open FROM pg_ripple.bidi_status() WHERE reconciliation_open > 0;
Chaos-test interpretation
The bidi chaos test (tests/stress/bidi_chaos.sh) runs the following smoke scenarios:
- abandon_linkback idempotency — calling twice should not error.
- purge_event_audit zero-row safety — no error when table is empty.
- reconciliation enqueue/resolve cycle — end-to-end round-trip.
- bidi_health valid status — always returns one of:
healthy|degraded|paused|failing. - token registration/revocation — register, then revoke.
If any test fails, the script exits with a non-zero code and identifies the failing case.
Pattern: per-subscription auth
Four-token deployment example (two relays × two scope sets):
-- CRM relay: full bidi scopes.
SELECT pg_ripple.register_subscription_token(
'crm_relay',
ARRAY['linkback','divergence','abandon','outbox_read'],
'crm-relay-prod'
);
-- ERP relay: outbox-read only.
SELECT pg_ripple.register_subscription_token(
'erp_relay',
ARRAY['outbox_read'],
'erp-relay-prod'
);
-- Operator admin token: registered in _pg_ripple.admin_tokens (not shown here).
HTTP endpoints enforce scope checks: POST /subscriptions/crm_relay/events/{id}/linkback requires linkback scope for the crm_relay subscription.
Pattern: redaction with elevated subscription
Standard relay (redacted, normal consumers):
{
"@context": { "ex": "https://example.com/ns#" },
"@type": "ex:Contact",
"ex:name": {},
"ex:phone": { "@redact": true },
"ex:email": {},
"ex:taxId": { "@redact": true }
}
Compliance relay (unredacted, elevated access):
- Register a separate subscription
crm_relay_auditpointing tocrm_relay_outbox_unredacted. - The frame omits
"@redact": truefor all predicates. - Grant
SELECToncrm_relay_outbox_unredactedonly to the compliance relay's database role. - Register a separate token with
outbox_readscope forcrm_relay_audit.
event_audit records action = 'emit_unredacted' for bridge-writer rows in the unredacted outbox when optional emit auditing is enabled (controlled by pg_ripple.audit_log_enabled).
Bidi Production Checklist
v0.78.0 — BIDIOPS-DOC-01
Use this checklist before taking a bidirectional integration to production. Each item links to the relevant runbook section.
1. Extension version
-
SELECT pg_ripple.version()returns0.78.0or later. -
pg_ripple.controldefault_versionmatches the installed version.
2. Schema completeness
Run this query — expect 7 rows (one per required v0.78.0 table):
SELECT tablename FROM pg_tables
WHERE schemaname = '_pg_ripple'
AND tablename IN (
'event_dead_letters','subscription_schema_changes',
'subscription_tokens','admin_tokens','event_audit',
'reconciliation_queue','pending_linkbacks'
)
ORDER BY tablename;
3. Subscriptions
-
Each subscription is registered via
pg_ripple.create_subscription(...). -
overflow_policyis configured (dropordead_letter— neverNULL). -
max_queue_depthis set to a suitable value (recommended: 10,000–100,000). -
dead_letter_afteris set to an appropriate retry count (recommended: 3–10). -
Frame includes
"@redact": truefor all PII predicates.
4. Tokens
-
At least one per-subscription token registered via
pg_ripple.register_subscription_token(...). - All tokens use least-privilege scopes (no extra scopes beyond relay requirements).
- Token rotation schedule is documented (recommended: 90-day rotation).
-
Admin tokens registered in
_pg_ripple.admin_tokens(if HTTP admin API is enabled).
5. Audit log
-
pg_ripple.audit_log_enabledison. -
pg_ripple.audit_retentionis set (default: 90 days; adjust for compliance requirements). -
Confirm
purge_event_audit()runs daily (viapg_cronor equivalent scheduler).
6. Health monitoring
-
pg_ripple.bidi_health()is polled (recommended: every 60 seconds) by the alerting system. -
Alerts set for
status = 'failing'andstatus = 'degraded'for more than 15 minutes. -
pg_ripple.bidi_status()included in dashboard (Grafana / Prometheus export).
7. Dead-letter triage
-
event_dead_lettersreviewed at least weekly. -
Alerts set when
dead_letter_count > 0persists for more than 1 hour. - Runbook for requeue / drop procedures is linked from the on-call guide.
8. Reconciliation
-
reconciliation_openis monitored viabidi_status(). -
On-call procedure documented for each resolution action (
accept_external,force_internal,merge_via_owl_sameAs,dead_letter).
9. Schema evolution
- Any planned frame, IRI template, or exclude-graph changes go through the schema-evolution rollout playbook.
-
subscription_schema_changestable is reviewed after each change.
10. Chaos testing
-
tests/stress/bidi_chaos.shpasses on the production instance before each major release. -
Linkback idempotency is confirmed (
abandon_linkbackre-called ≥ 2× without error).
11. pg_tide relay
- pg_tide version matches the compatibility matrix (see compatibility.md).
-
SELECT pg_ripple.relay_available();returnstruein the relay database. -
Relay acknowledges rows within
outbox_oldest_ageSLA (recommended: ≤ 1 minute under normal load). - Relay TLS certificate expiry is monitored.
12. Backup and restore
-
_pg_ripple.event_dead_letters,event_audit,subscription_tokens,reconciliation_queueare included in the backup target. - Point-in-time recovery drill has verified that these tables are restorable.
Sign-off
| Item | Owner | Date |
|---|---|---|
| Extension version verified | ||
| Schema completeness verified | ||
| Subscriptions configured | ||
| Tokens registered | ||
| Audit log active | ||
| Health monitoring active | ||
| Dead-letter triage procedure documented | ||
| Chaos test passed | ||
| pg_tide relay confirmed | ||
| Backup verified |
Configuration and Tuning
pg_ripple exposes its configuration through PostgreSQL GUC (Grand Unified Configuration) parameters. All parameters use the pg_ripple. prefix and can be set in postgresql.conf, via ALTER SYSTEM, or per-session with SET.
Parameters marked Postmaster require a PostgreSQL restart. Parameters marked SIGHUP can be reloaded with SELECT pg_reload_conf(). All others can be changed per-session with SET.
Storage Parameters
Control how triples are stored in VP tables and the rare-predicate consolidation table.
| Parameter | Type | Default | Range | Context | Description |
|---|---|---|---|---|---|
vp_promotion_threshold | int | 1000 | 10 – 10,000,000 | Userset | Minimum triples before a predicate gets a dedicated VP table. Below this, triples go to vp_rare. |
named_graph_optimized | bool | off | — | Userset | Adds a (g, s, o) index per VP table. Speeds up GRAPH queries but increases write overhead. |
default_graph | text | '' | Any IRI | Userset | IRI used as the default graph when g is not specified on insert. |
dedup_on_merge | bool | off | — | Userset | When on, the merge worker deduplicates (s, o, g) rows, keeping the lowest SID. |
HTAP / Merge Worker Parameters
Control the delta/main split and background merge behavior. These take effect only when pg_ripple is loaded via shared_preload_libraries.
| Parameter | Type | Default | Range | Context | Description |
|---|---|---|---|---|---|
merge_threshold | int | 10000 | 1 – 2,147,483,647 | SIGHUP | Delta row count that triggers a merge for a predicate. Lower = fresher reads, more I/O. |
merge_interval_secs | int | 60 | 1 – 3600 | SIGHUP | Maximum seconds between merge worker poll cycles. |
merge_retention_seconds | int | 60 | 0 – 86,400 | SIGHUP | Seconds to keep the old main table after a merge before dropping it. |
latch_trigger_threshold | int | 10000 | 1 – 2,147,483,647 | SIGHUP | Rows written in a batch before poking the merge worker latch immediately. |
merge_watchdog_timeout | int | 300 | 10 – 86,400 | SIGHUP | Seconds of merge worker inactivity before logging a WARNING. |
worker_database | text | 'postgres' | — | SIGHUP | Database the background merge worker connects to. |
auto_analyze | bool | on | — | SIGHUP | Run ANALYZE on VP main tables after each merge cycle. |
Query Engine Parameters
Tune SPARQL-to-SQL translation and execution.
| Parameter | Type | Default | Range | Context | Description |
|---|---|---|---|---|---|
plan_cache_size | int | 256 | 0 – 65,536 | Userset | Cached SPARQL→SQL translations per backend. 0 disables caching. |
max_path_depth | int | 100 | 0 – 10,000 | Userset | Maximum recursion depth for property path queries (+, *). 0 = unlimited. |
property_path_max_depth | int | 64 | 1 – 100,000 | Userset | Alternative property path depth limit (v0.24.0). |
describe_strategy | text | 'cbd' | 'cbd', 'scbd', 'simple' | Userset | DESCRIBE algorithm: Concise Bounded Description, Symmetric CBD, or simple one-hop. |
bgp_reorder | bool | on | — | Userset | Reorder BGP triple patterns by estimated selectivity before SQL generation. |
parallel_query_min_joins | int | 3 | 1 – 100 | Userset | Minimum VP-table joins before enabling parallel query workers. |
sparql_strict | bool | on | — | Userset | When on, unsupported FILTER functions raise an error; when off, they are silently dropped. |
export_batch_size | int | 10000 | 100 – 1,000,000 | Userset | Triples per cursor batch during streaming export. |
Inference / Datalog Parameters
Control the Datalog reasoning engine, magic sets, and rule caching.
| Parameter | Type | Default | Range | Context | Description |
|---|---|---|---|---|---|
inference_mode | text | 'off' | 'off', 'on_demand', 'materialized' | Userset | Datalog reasoning mode. 'materialized' requires pg_trickle. |
enforce_constraints | text | 'off' | 'off', 'warn', 'error' | Userset | Behavior when Datalog constraint rules detect violations. |
rule_graph_scope | text | 'default' | 'default', 'all' | Userset | Whether unscoped rule atoms operate on the default graph only or all graphs. |
magic_sets | bool | on | — | Userset | Use magic sets for goal-directed inference in infer_goal(). |
datalog_cost_reorder | bool | on | — | Userset | Sort rule body atoms by ascending VP-table cardinality before SQL compilation. |
datalog_antijoin_threshold | int | 1000 | 0 – 10,000,000 | Userset | Minimum VP rows for NOT atoms to use LEFT JOIN anti-join form. |
delta_index_threshold | int | 500 | 0 – 10,000,000 | Userset | Minimum semi-naive delta rows before creating a B-tree index. |
demand_transform | bool | on | — | Userset | Auto-apply demand transformation when multiple goal patterns are specified. |
sameas_reasoning | bool | on | — | Userset | Apply owl:sameAs canonicalization pre-pass during inference. |
rule_plan_cache | bool | on | — | Userset | Cache compiled SQL for each rule set. Invalidated by drop_rules() and load_rules(). |
rule_plan_cache_size | int | 64 | 1 – 4,096 | Userset | Maximum rule sets in the plan cache. |
Well-Founded Semantics / Tabling Parameters
Control WFS evaluation and tabling cache (v0.32.0).
| Parameter | Type | Default | Range | Context | Description |
|---|---|---|---|---|---|
wfs_max_iterations | int | 100 | 1 – 10,000 | Userset | Safety cap on alternating fixpoint rounds per WFS pass. Emits PT520 WARNING if not converged. |
tabling | bool | on | — | Userset | Cache infer_wfs() and SPARQL results in _pg_ripple.tabling_cache. |
tabling_ttl | int | 300 | 0 – 86,400 | Userset | TTL in seconds for tabling cache entries. 0 disables TTL-based expiry. |
SHACL Validation Parameters
| Parameter | Type | Default | Range | Context | Description |
|---|---|---|---|---|---|
shacl_mode | text | 'off' | 'off', 'sync', 'async' | Userset | 'sync' rejects violations inline; 'async' queues for background validation. |
Federation Parameters
Control remote SPARQL endpoint calls via the SERVICE keyword.
| Parameter | Type | Default | Range | Context | Description |
|---|---|---|---|---|---|
federation_timeout | int | 30 | 1 – 3,600 | Userset | Per-SERVICE call wall-clock timeout in seconds. |
federation_max_results | int | 10000 | 1 – 1,000,000 | Userset | Maximum rows accepted from a single remote call. |
federation_on_error | text | 'warning' | 'warning', 'error', 'empty' | Userset | Behavior on SERVICE call failure. |
federation_pool_size | int | 4 | 1 – 32 | Userset | Idle HTTP connections per endpoint host. |
federation_cache_ttl | int | 0 | 0 – 86,400 | Userset | Remote result cache TTL in seconds. 0 disables caching. |
federation_on_partial | text | 'empty' | 'empty', 'use' | Userset | Behavior on mid-stream SERVICE failure. |
federation_adaptive_timeout | bool | off | — | Userset | Derive per-endpoint timeout from P95 latency. |
Shared Memory Parameters (Startup Only)
These must be set in postgresql.conf before PostgreSQL starts. They cannot be changed at runtime.
| Parameter | Type | Default | Range | Context | Description |
|---|---|---|---|---|---|
dictionary_cache_size | int | 4096 | 0 – 1,000,000 | Postmaster | Shared-memory encode cache capacity in entries. |
cache_budget | int | 64 | 0 – 65,536 | Postmaster | Shared-memory budget cap in MB. Bulk loads throttle at 90% utilization. |
Changes to dictionary_cache_size and cache_budget require a full PostgreSQL restart. Plan your cache sizing before deploying to production.
Security Parameters
| Parameter | Type | Default | Range | Context | Description |
|---|---|---|---|---|---|
rls_bypass | bool | off | — | Suset | Superuser override to bypass graph-level Row-Level Security. |
Vector / Embedding Parameters
| Parameter | Type | Default | Range | Context | Description |
|---|---|---|---|---|---|
embedding_model | text | '' | — | Userset | Model name tag stored in _pg_ripple.embeddings. |
embedding_dimensions | int | 1536 | 1 – 16,000 | Userset | Vector dimension count. Must match model output. |
embedding_api_url | text | '' | — | Userset | Base URL for OpenAI-compatible embedding API. |
embedding_api_key | text | '' | — | Suset | API key (superuser-only, masked in pg_settings). |
pgvector_enabled | bool | on | — | Userset | Disable pgvector code paths without uninstalling. |
embedding_index_type | text | 'hnsw' | 'hnsw', 'ivfflat' | Userset | Index type on embeddings table. |
embedding_precision | text | 'single' | 'single', 'half', 'binary' | Userset | Storage precision for embedding vectors. |
auto_embed | bool | off | — | Userset | Auto-embed new entities via background worker. |
embedding_batch_size | int | 100 | 1 – 10,000 | Userset | Entities dequeued per background worker batch. |
Quick-Start Configurations
Small Dataset (< 1M triples)
Suitable for development, prototyping, or small knowledge graphs:
# postgresql.conf
shared_preload_libraries = 'pg_ripple'
# Dictionary cache — small footprint
pg_ripple.dictionary_cache_size = 8192
pg_ripple.cache_budget = 16
# Merge worker — merge early for fresh reads
pg_ripple.merge_threshold = 5000
pg_ripple.merge_interval_secs = 30
# Query engine
pg_ripple.plan_cache_size = 64
pg_ripple.max_path_depth = 50
Medium Dataset (1M – 100M triples)
Production workloads with moderate query complexity:
# postgresql.conf
shared_preload_libraries = 'pg_ripple'
# Dictionary cache — larger cache for better hit rates
pg_ripple.dictionary_cache_size = 131072
pg_ripple.cache_budget = 128
# Merge worker — balance freshness and I/O
pg_ripple.merge_threshold = 50000
pg_ripple.merge_interval_secs = 60
pg_ripple.latch_trigger_threshold = 20000
pg_ripple.auto_analyze = on
# Query engine — larger plan cache for diverse queries
pg_ripple.plan_cache_size = 512
pg_ripple.max_path_depth = 100
pg_ripple.bgp_reorder = on
# Inference (if used)
pg_ripple.inference_mode = 'on_demand'
pg_ripple.magic_sets = on
Large Dataset (> 100M triples)
High-throughput production with heavy query loads:
# postgresql.conf
shared_preload_libraries = 'pg_ripple'
# Dictionary cache — maximize cache coverage
pg_ripple.dictionary_cache_size = 500000
pg_ripple.cache_budget = 512
# Merge worker — batch larger merges, reduce churn
pg_ripple.merge_threshold = 200000
pg_ripple.merge_interval_secs = 120
pg_ripple.latch_trigger_threshold = 100000
pg_ripple.merge_retention_seconds = 120
pg_ripple.auto_analyze = on
# Query engine — large plan cache, parallel queries
pg_ripple.plan_cache_size = 2048
pg_ripple.max_path_depth = 200
pg_ripple.bgp_reorder = on
pg_ripple.parallel_query_min_joins = 2
# Named graph optimization (if heavy GRAPH usage)
pg_ripple.named_graph_optimized = on
# Inference
pg_ripple.inference_mode = 'on_demand'
pg_ripple.magic_sets = on
pg_ripple.rule_plan_cache = on
pg_ripple.rule_plan_cache_size = 256
# Tabling cache for repeated inference patterns
pg_ripple.tabling = on
pg_ripple.tabling_ttl = 600
# Federation (if used)
pg_ripple.federation_timeout = 60
pg_ripple.federation_pool_size = 8
pg_ripple.federation_cache_ttl = 300
Don't forget to tune PostgreSQL itself alongside pg_ripple. Key PostgreSQL parameters for triple store workloads:
shared_buffers= 25% of RAMeffective_cache_size= 75% of RAMwork_mem= 64MB–256MB (for complex joins)maintenance_work_mem= 512MB–1GB (for merge ANALYZE)random_page_cost= 1.1 (if using SSDs)max_parallel_workers_per_gather= 4
Uncertain Knowledge Engine GUCs (v0.87.0)
| GUC | Type | Default | Description |
|---|---|---|---|
pg_ripple.probabilistic_datalog | bool | off | Enable @weight rule annotations |
pg_ripple.prob_datalog_cyclic | bool | off | Allow approximate evaluation on cyclic rule sets |
pg_ripple.prob_datalog_max_iterations | int | 100 | Maximum semi-naive iterations |
pg_ripple.prob_datalog_convergence_delta | float8 | 0.001 | Early-exit convergence threshold |
pg_ripple.prob_datalog_cyclic_strict | bool | off | Promote non-convergence to ERROR (PT0307) |
pg_ripple.default_fuzzy_threshold | float8 | 0.7 | Default fuzzy match threshold |
pg_ripple.prov_confidence | bool | off | Enable PROV-O pg:sourceTrust propagation |
pg_ripple.export_confidence | bool | off | Include RDF-star annotations in Turtle export |
pg_ripple.cwb_confidence_propagation | string | (empty) | CONSTRUCT rule name for CWB trust propagation |
GUC Tuning Guide
This page maps each pg_ripple GUC parameter to workload characteristics.
Use it as a starting point for tuning your deployment.
See Configuration Reference for the full GUC list and default values.
Workload-Class Tuning Matrix
The table below shows recommended GUC settings for five common deployment profiles. Values shown override the default; omit the row to keep the default.
| GUC | OLTP (write-heavy) | SPARQL Analytics | Datalog/Reasoning | Federation | Development |
|---|---|---|---|---|---|
htap_delta_max_rows | 500 000 | 1 000 000 | 200 000 | 200 000 | 10 000 |
merge_interval_secs | 30 | 300 | 60 | 120 | 10 |
auto_analyze | on | on | on | off | on |
plan_cache_size | 256 | 2 048 | 512 | 128 | 64 |
sparql_max_algebra_depth | 64 | 512 | 256 | 128 | 256 |
sparql_max_triple_patterns | 1 024 | 8 192 | 4 096 | 2 048 | 4 096 |
max_path_depth | 32 | 128 | 64 | 32 | 64 |
export_batch_size | 5 000 | 50 000 | 10 000 | 10 000 | 10 000 |
dictionary_cache_size | 131 072 | 262 144 | 65 536 | 65 536 | 16 384 |
datalog_parallel_workers | 1 | 1 | 4 | 1 | 1 |
datalog_parallel_threshold | (n/a) | (n/a) | 10 000 | (n/a) | (n/a) |
tracing_exporter | none | none | none | otlp | stdout |
tracing_otlp_endpoint | (n/a) | (n/a) | (n/a) | http://jaeger:4317 | (n/a) |
Profile Descriptions
OLTP (write-heavy)
Continuous triple ingestion at high rate. Short merge intervals and a smaller delta
threshold ensure that the merge worker keeps up. plan_cache_size is moderate;
reduce sparql_max_triple_patterns to reject accidental full-scan queries.
SPARQL Analytics
Complex SELECT/CONSTRUCT queries on a large, mostly-static dataset. Large plan cache
reduces translation overhead. High sparql_max_triple_patterns allows complex queries.
Infrequent merges reduce background I/O.
Datalog/Reasoning
OWL RL / custom rule materialisation. Enable parallel workers and a high threshold to
parallelise independent strata. Keep merge_interval_secs moderate — the merge worker
competes with inference for I/O.
Federation
Heavy SERVICE {} usage querying remote SPARQL endpoints. Enable OTLP tracing to
observe per-endpoint latency. Reduce local plan_cache_size since remote queries
vary widely in shape.
Development
Local developer machine. Small caches, short merge intervals for fast feedback.
Enable tracing_exporter = 'stdout' for easy debugging without an APM stack.
Security Limits
Always set these in production to protect against runaway or malicious queries:
-- Reject deeply-nested algebra trees (e.g. injected via user input)
SET pg_ripple.sparql_max_algebra_depth = 256;
-- Reject queries with an unreasonable number of triple patterns
SET pg_ripple.sparql_max_triple_patterns = 4096;
-- Cap recursive property path depth
SET pg_ripple.max_path_depth = 64;
See Security Hardening for additional recommendations.
Memory Footprint Estimation
| Component | Memory | Formula |
|---|---|---|
| Dictionary backend cache | ~dictionary_cache_size × 80 B | Per backend connection |
| SPARQL plan cache | ~plan_cache_size × 2 KB | Per backend connection |
| Delta tables (HTAP) | ~htap_delta_max_rows × 24 B | Per VP predicate |
| Merge worker buffers | ~export_batch_size × 40 B | Global (one worker) |
For a deployment with 50 concurrent connections and default GUC values, budget
approximately 400 MB for pg_ripple's own data structures in addition to
PostgreSQL's shared_buffers.
Deprecated GUCs
| Old name | Replacement | Removed in |
|---|---|---|
pg_ripple.property_path_max_depth | pg_ripple.max_path_depth | v1.0.0 |
Set pg_ripple.max_path_depth going forward. The old name is still accepted
but emits a deprecation notice in the server log.
Ready-to-Use Configuration Profiles
Copy-paste these blocks into postgresql.conf (then SELECT pg_reload_conf()).
Small Instance Profile (≤8 cores, ≤32 GB RAM)
Suitable for development, staging, or a small-scale production deployment:
# pg_ripple — small instance profile
pg_ripple.dictionary_cache_size = 65536
pg_ripple.cache_budget_mb = 64
pg_ripple.merge_threshold = 10000
pg_ripple.merge_workers = 1
pg_ripple.merge_interval_secs = 60
pg_ripple.datalog_parallel_workers = 2
pg_ripple.sparql_max_rows = 50000
pg_ripple.export_max_rows = 100000
pg_ripple.plan_cache_capacity = 256
pg_ripple.federation_timeout = 30
pg_ripple.federation_parallel_max = 2
Large Instance Profile (≥32 cores, ≥128 GB RAM)
Suitable for large-scale SPARQL analytics, high-throughput ingestion, or intensive Datalog reasoning:
# pg_ripple — large instance profile
pg_ripple.dictionary_cache_size = 1000000
pg_ripple.cache_budget_mb = 512
pg_ripple.merge_threshold = 50000
pg_ripple.merge_workers = 4
pg_ripple.merge_interval_secs = 120
pg_ripple.datalog_parallel_workers = 8
pg_ripple.sparql_max_rows = 500000
pg_ripple.export_max_rows = 1000000
pg_ripple.plan_cache_capacity = 2048
pg_ripple.federation_timeout = 60
pg_ripple.federation_parallel_max = 8
pg_ripple.pagerank_max_iterations = 200
pg_ripple.topn_pushdown = on
High-Security / Multi-Tenant Profile
Applies strict resource limits for shared or multi-tenant deployments:
# pg_ripple — high-security profile
pg_ripple.sparql_max_rows = 10000
pg_ripple.sparql_overflow_action = 'error'
pg_ripple.sparql_max_algebra_depth = 128
pg_ripple.sparql_max_triple_patterns = 1024
pg_ripple.export_max_rows = 10000
pg_ripple.federation_endpoint_policy = 'allowlist'
pg_ripple.federation_allow_unregistered_service_endpoints = off
pg_ripple.arrow_unsigned_tickets_allowed = off
pg_ripple.strict_sparql_filters = on
pg_ripple.datalog_max_derived = 1000000
pg_ripple.wfs_max_iterations = 50
pg_ripple.rls_bypass = off
Datalog Reasoning Profile
Optimized for intensive OWL RL / Datalog materialization workloads:
# pg_ripple — datalog reasoning profile
pg_ripple.inference_mode = 'materialized'
pg_ripple.magic_sets = on
pg_ripple.datalog_cost_reorder = on
pg_ripple.dred_enabled = on
pg_ripple.datalog_parallel_workers = 8
pg_ripple.datalog_parallel_threshold = 5000
pg_ripple.owl_profile = 'RL'
pg_ripple.tabling = on
pg_ripple.tabling_ttl = 600
pg_ripple.rule_plan_cache = on
pg_ripple.rule_plan_cache_size = 128
pg_ripple.wfs_max_iterations = 200
Monitoring and Observability
pg_ripple provides built-in monitoring through SQL functions, PostgreSQL's standard statistics infrastructure, and Prometheus-compatible metrics via pg_ripple_http. This page explains what to monitor, how to collect the data, and what thresholds indicate a healthy system.
pg_ripple.stats()
The primary monitoring function. Returns a JSONB object with key metrics:
SELECT pg_ripple.stats();
Output Fields
| Field | Type | Description |
|---|---|---|
total_triples | int | Total triple count across all graphs (including delta rows not yet merged) |
dedicated_predicates | int | Number of predicates with their own VP table |
htap_predicates | int | Number of predicates using the HTAP delta/main split |
rare_triples | int | Triples stored in the consolidated vp_rare table |
unmerged_delta_rows | int | Total rows across all delta tables (from shared memory counter). -1 if shared memory is not available |
merge_worker_pid | int | PID of the background merge worker. 0 if not running |
live_statistics_enabled | bool | Whether pg_trickle live statistics are active |
encode_cache_capacity | int | Total entries the shared encode cache can hold |
encode_cache_utilization_pct | int | Percentage of cache slots currently in use |
encode_cache_hits | int | Cumulative cache hit count since server start |
encode_cache_misses | int | Cumulative cache miss count since server start |
encode_cache_evictions | int | Cumulative eviction count |
Example Output
{
"total_triples": 4523891,
"dedicated_predicates": 127,
"htap_predicates": 127,
"rare_triples": 2341,
"unmerged_delta_rows": 8432,
"merge_worker_pid": 12345,
"live_statistics_enabled": false,
"encode_cache_capacity": 65536,
"encode_cache_utilization_pct": 72,
"encode_cache_hits": 18934521,
"encode_cache_misses": 234012,
"encode_cache_evictions": 45123
}
Computing the Cache Hit Rate
SELECT
(s->>'encode_cache_hits')::bigint AS hits,
(s->>'encode_cache_misses')::bigint AS misses,
ROUND(
(s->>'encode_cache_hits')::numeric /
NULLIF((s->>'encode_cache_hits')::numeric + (s->>'encode_cache_misses')::numeric, 0),
4
) AS hit_rate
FROM pg_ripple.stats() s;
A healthy system should maintain a cache hit rate above 95% (0.95). If it drops below 90%, increase pg_ripple.dictionary_cache_size and restart PostgreSQL. Sustained rates below 80% indicate the working set significantly exceeds cache capacity.
pg_ripple.canary()
A health check function that returns a JSONB object with pass/fail indicators:
SELECT pg_ripple.canary();
Output Fields
| Field | Type | Healthy Value | Description |
|---|---|---|---|
merge_worker | text | "ok" | "ok" if merge worker PID is in shared memory; "stalled" otherwise |
cache_hit_rate | float | > 0.95 | Dictionary encode cache hit rate (0.0–1.0) |
catalog_consistent | bool | true | VP table count in pg_class matches promoted predicates |
orphaned_rare_rows | int | 0 | vp_rare rows for predicates that already have dedicated VP tables |
Interpreting Results
SELECT
c->>'merge_worker' AS worker,
(c->>'cache_hit_rate')::float AS hit_rate,
(c->>'catalog_consistent')::bool AS catalog_ok,
(c->>'orphaned_rare_rows')::int AS orphaned
FROM pg_ripple.canary() c;
canary() is designed for load balancer health checks and monitoring systems. Call it periodically and alert when merge_worker = 'stalled', cache_hit_rate < 0.90, or catalog_consistent = false.
SPARQL Query Analysis with sparql_explain()
Analyze SPARQL query performance using the explain functions:
Basic SQL Generation
-- See the generated SQL without executing
SELECT pg_ripple.sparql_explain(
'SELECT ?name WHERE { ?s <http://schema.org/name> ?name }',
false
);
Full EXPLAIN ANALYZE
-- Execute and show timing + row counts
SELECT pg_ripple.sparql_explain(
'SELECT ?name WHERE { ?s <http://schema.org/name> ?name }',
true
);
explain_sparql() with Format Options
The explain_sparql() function (v0.23.0) provides more output formats:
-- Generated SQL only
SELECT pg_ripple.explain_sparql(
'SELECT ?s ?o WHERE { ?s <http://xmlns.com/foaf/0.1/knows> ?o }',
'sql'
);
-- EXPLAIN ANALYZE as text (default)
SELECT pg_ripple.explain_sparql(
'SELECT ?s ?o WHERE { ?s <http://xmlns.com/foaf/0.1/knows> ?o }',
'text'
);
-- EXPLAIN ANALYZE as JSON (for programmatic consumption)
SELECT pg_ripple.explain_sparql(
'SELECT ?s ?o WHERE { ?s <http://xmlns.com/foaf/0.1/knows> ?o }',
'json'
);
-- SPARQL algebra tree (for debugging the optimizer)
SELECT pg_ripple.explain_sparql(
'SELECT ?s ?o WHERE { ?s <http://xmlns.com/foaf/0.1/knows> ?o }',
'sparql_algebra'
);
- Seq Scan on vp_rare — A predicate is not promoted yet. Consider lowering
vp_promotion_thresholdor loading more data. - Nested Loop with high row estimates — BGP reordering may not be optimal. Check
bgp_reorderis on. - Recursive CTE with high loop count — Property path is deep. Check
max_path_depthsetting. - Sort + Unique — A
DISTINCTthat might be avoidable with SHACLsh:maxCount 1hints.
pg_stat_statements Integration
pg_ripple generates standard SQL that is tracked by pg_stat_statements. This gives you deep visibility into the actual SQL performance:
-- Enable pg_stat_statements (if not already)
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
-- Find the slowest SPARQL-generated queries
SELECT
calls,
mean_exec_time::numeric(10,2) AS avg_ms,
total_exec_time::numeric(10,2) AS total_ms,
rows,
LEFT(query, 120) AS query_prefix
FROM pg_stat_statements
WHERE query LIKE '%_pg_ripple.vp_%'
ORDER BY mean_exec_time DESC
LIMIT 20;
Identifying Hot VP Tables
-- Which VP tables are scanned most?
SELECT
regexp_matches(query, '_pg_ripple\.(vp_\d+)', 'g') AS vp_table,
sum(calls) AS total_calls,
sum(total_exec_time)::numeric(10,2) AS total_ms
FROM pg_stat_statements
WHERE query LIKE '%_pg_ripple.vp_%'
GROUP BY 1
ORDER BY total_ms DESC
LIMIT 10;
Prometheus Metrics (pg_ripple_http)
The pg_ripple_http companion service exposes Prometheus-compatible metrics at the /metrics endpoint:
curl http://localhost:7878/metrics
Authentication Model for /metrics (METRICS-AUTH-DOC-01)
The
/metricsendpoint is unauthenticated by design. It does not require anAuthorization: Bearertoken even whenPG_RIPPLE_BEARER_TOKENis configured and all SPARQL/Datalog/admin routes require authentication.
Rationale: Prometheus scrapers typically run inside the same private network as pg_ripple_http and require no-auth scrape targets. Exposing counter and gauge data about query rates and error counts does not reveal graph data or connection credentials.
Operator guidance:
- If you expose
pg_ripple_httpdirectly on a public interface, place/metricsbehind a reverse-proxy access-control rule (e.g.,location /metrics { allow 10.0.0.0/8; deny all; }in nginx). - Alternatively, use a dedicated scrape network interface (e.g.,
--metrics-bind 127.0.0.1:9090) if a future release adds per-endpoint bind configuration. - The
/metrics/extensionendpoint (streaming counters from the PostgreSQL extension) follows the same unauthenticated model.
Security impact: Metrics contain request counts, error counts, and latency totals — no user data or credentials. There is no SSRF or injection risk from the metrics payload.
Available Metrics
| Metric | Type | Description |
|---|---|---|
pg_ripple_http_sparql_queries_total | counter | Total SPARQL queries executed |
pg_ripple_http_datalog_queries_total | counter | Total Datalog API calls |
pg_ripple_http_errors_total | counter | Total query errors |
pg_ripple_http_query_duration_seconds_total | counter | Cumulative query execution time |
pg_ripple_http_pool_size | gauge | Current connection pool size |
Prometheus Scrape Configuration
# prometheus.yml
scrape_configs:
- job_name: 'pg_ripple_http'
scrape_interval: 15s
static_configs:
- targets: ['pg-ripple-http:7878']
metrics_path: /metrics
# No bearer_token needed — /metrics is unauthenticated.
# Restrict access at network level (see operator guidance above).
Derived Metrics for Dashboards
Use PromQL to compute useful rates:
# Queries per second
rate(pg_ripple_http_sparql_queries_total[5m])
# Error rate
rate(pg_ripple_http_errors_total[5m]) / rate(pg_ripple_http_sparql_queries_total[5m])
# Average query latency
rate(pg_ripple_http_query_duration_seconds_total[5m]) / rate(pg_ripple_http_sparql_queries_total[5m])
Monitoring the Merge Worker
The background merge worker is critical for HTAP performance. Monitor it through multiple channels:
Shared Memory Status
-- Is the merge worker running?
SELECT (pg_ripple.stats()->>'merge_worker_pid')::int AS pid;
-- Returns 0 if not running
Delta Table Sizes
-- Check delta accumulation per predicate
SELECT
p.id AS predicate_id,
d.value AS predicate_iri,
p.triple_count,
(SELECT count(*) FROM format('_pg_ripple.vp_%s_delta', p.id)::regclass) AS delta_rows
FROM _pg_ripple.predicates p
JOIN _pg_ripple.dictionary d ON d.id = p.id
WHERE p.htap = true
ORDER BY p.triple_count DESC
LIMIT 10;
Merge Worker Logs
The merge worker logs to PostgreSQL's standard log:
LOG: pg_ripple merge worker: merge cycle complete
LOG: pg_ripple merge worker: processed 3 async validation item(s)
WARNING: pg_ripple merge worker: watchdog timeout (300s)
If you see watchdog timeout warnings in the PostgreSQL log, the merge worker has stalled. Common causes:
- Long-running transactions holding locks on VP tables
worker_databasepointing to the wrong database- Insufficient
max_worker_processesinpostgresql.conf
Health Check Thresholds
Use these thresholds for alerting:
| Metric | Green | Yellow | Red |
|---|---|---|---|
| Cache hit rate | > 95% | 90–95% | < 90% |
| Merge worker PID | > 0 | — | = 0 |
| Delta rows (total) | < 2× merge_threshold | 2–5× | > 5× |
| Catalog consistent | true | — | false |
| Orphaned rare rows | 0 | 1–100 | > 100 |
| Query error rate | < 1% | 1–5% | > 5% |
| Avg query latency | < 100ms | 100–500ms | > 500ms |
Automated Monitoring Query
Run this periodically from your monitoring system:
SELECT
CASE
WHEN (c->>'merge_worker') = 'ok'
AND (c->>'cache_hit_rate')::float > 0.90
AND (c->>'catalog_consistent')::bool
AND (c->>'orphaned_rare_rows')::int = 0
THEN 'healthy'
WHEN (c->>'merge_worker') = 'stalled'
OR (c->>'cache_hit_rate')::float < 0.80
THEN 'critical'
ELSE 'warning'
END AS status,
c->>'merge_worker' AS worker,
c->>'cache_hit_rate' AS hit_rate,
c->>'catalog_consistent' AS catalog,
c->>'orphaned_rare_rows' AS orphaned
FROM pg_ripple.canary() c;
Predicate Inventory
Monitor predicate distribution to catch imbalances:
SELECT
p.id,
d.value AS predicate_iri,
p.triple_count,
p.table_oid IS NOT NULL AS has_vp_table,
CASE WHEN p.htap THEN 'htap' ELSE 'flat' END AS storage_mode
FROM _pg_ripple.predicates p
JOIN _pg_ripple.dictionary d ON d.id = p.id
ORDER BY p.triple_count DESC
LIMIT 20;
If one predicate has 10x more triples than the next, its VP table dominates storage and merge time. Consider partitioning the data by named graph or filtering queries to avoid full scans of that predicate.
Log-Based Monitoring
Configure PostgreSQL logging for SPARQL workload visibility:
# postgresql.conf
log_min_duration_statement = 500 # Log queries slower than 500ms
log_statement = 'none' # Don't log every statement
log_line_prefix = '%t [%p] %d ' # Timestamp, PID, database
SPARQL-generated SQL appears in the PostgreSQL log with VP table references, making it easy to correlate slow log entries with specific SPARQL patterns.
OpenTelemetry Observability Guide
pg_ripple emits OpenTelemetry spans for every SPARQL query, Datalog inference run, SHACL validation, and merge cycle. This page documents the span names, attributes, and example Prometheus/Grafana queries.
Distributed Tracing (v0.61.0+)
Starting with v0.61.0, the pg_ripple_http HTTP service extracts the
W3C traceparent header from
incoming requests and forwards it through the pg_ripple.tracing_traceparent
session GUC into the extension. Every span emitted during that request is
tagged with the originating trace ID, giving an unbroken trace from the load
balancer through the HTTP service into the query engine.
Enabling traceparent propagation
The pg_ripple_http service reads the traceparent header automatically.
No configuration is required. To verify propagation is working, send a
request with a traceparent header and check your tracing backend for the
correlated span.
curl -H "traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" \
-H "Content-Type: application/sparql-query" \
--data "SELECT * WHERE { ?s ?p ?o } LIMIT 10" \
http://localhost:8080/sparql
Span Name Reference
| Span Name | Description | Key Attributes |
|---|---|---|
sparql.select | SPARQL SELECT query execution | query, result_count, duration_ms |
sparql.update | SPARQL UPDATE execution | operation_type, triples_modified |
sparql.ask | SPARQL ASK query | result |
sparql.construct | SPARQL CONSTRUCT query | result_count |
sparql.describe | SPARQL DESCRIBE query | subject_iri |
datalog.infer | Datalog inference run | rule_set, new_triples, iterations |
datalog.retract | DRed retraction run | rule_set, retracted_triples |
shacl.validate | SHACL validation pass | shapes_count, violations_count |
shacl.rule.compile | SHACL-AF rule compilation | rule_count, compiled_count |
htap.merge | HTAP merge cycle | predicate_id, rows_merged, duration_ms |
federation.call | Federated SPARQL endpoint call | endpoint, duration_ms, error |
bulk_load | N-Triples / Turtle bulk load | format, triple_count, duration_ms |
Standard Attributes
All pg_ripple spans include these attributes:
| Attribute | Type | Description |
|---|---|---|
db.system | string | Always "postgresql" |
db.name | string | PostgreSQL database name |
pg_ripple.version | string | Extension version (e.g. "0.61.0") |
pg_ripple.traceparent | string | W3C traceparent forwarded from HTTP layer (if set) |
Example Prometheus Queries
Query latency p95 (5-minute window)
histogram_quantile(0.95,
sum(rate(pg_ripple_span_duration_ms_bucket{span="sparql.select"}[5m])) by (le)
)
Federation error rate by endpoint
sum(rate(pg_ripple_federation_errors_total[5m])) by (endpoint)
/
sum(rate(pg_ripple_federation_calls_total[5m])) by (endpoint)
HTAP merge throughput (rows/sec)
sum(rate(pg_ripple_htap_merge_rows_total[1m]))
SHACL violation rate
sum(rate(pg_ripple_shacl_violations_total[5m])) by (shape)
Grafana Dashboard
A sample Grafana dashboard JSON is available at
docs/fixtures/grafana_pg_ripple_dashboard.json. Import it via
Dashboards → Import → Upload JSON file.
Tracing Backends
pg_ripple's OTel exporter supports any OTLP-compatible backend:
| Backend | Configuration |
|---|---|
| Datadog | Set OTEL_EXPORTER_OTLP_ENDPOINT=https://trace.agent.datadoghq.com |
| Honeycomb | Set OTEL_EXPORTER_OTLP_ENDPOINT=https://api.honeycomb.io and OTEL_EXPORTER_OTLP_HEADERS=x-honeycomb-team=<API_KEY> |
| Grafana Tempo | Set OTEL_EXPORTER_OTLP_ENDPOINT=http://tempo:4317 |
| Jaeger | Set OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4317 |
These environment variables are read by the pg_ripple_http service at startup.
Performance Tuning
pg_ripple performance depends on three interacting subsystems: the query engine, the write path, and the dictionary cache. This page provides diagnostic steps and tuning recipes for each bottleneck area, with realistic numbers from BSBM benchmarks and internal testing.
The Three Bottleneck Areas
┌──────────────────────────────────────────────────────┐
│ Performance │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ Query │ │ Write │ │ Cache │ │
│ │ Engine │ │ Path │ │ Pressure │ │
│ │ │ │ │ │ │ │
│ │ Slow │ │ Merge │ │ Dictionary │ │
│ │ SPARQL │ │ worker │ │ misses → │ │
│ │ queries │ │ lag, │ │ table │ │
│ │ │ │ delta │ │ lookups │ │
│ │ │ │ bloat │ │ │ │
│ └──────────┘ └──────────┘ └──────────────┘ │
└──────────────────────────────────────────────────────┘
Diagnostic Workflow
Before tuning, identify which subsystem is the bottleneck:
-- Step 1: Overall health
SELECT pg_ripple.canary();
-- Step 2: Cache hit rate
SELECT
(s->>'encode_cache_hits')::bigint AS hits,
(s->>'encode_cache_misses')::bigint AS misses,
ROUND(
(s->>'encode_cache_hits')::numeric /
NULLIF((s->>'encode_cache_hits')::numeric + (s->>'encode_cache_misses')::numeric, 0),
4
) AS hit_rate
FROM pg_ripple.stats() s;
-- Step 3: Delta accumulation
SELECT (pg_ripple.stats()->>'unmerged_delta_rows')::int AS delta_rows;
-- Step 4: Slowest queries
SELECT calls, mean_exec_time::numeric(10,2) AS avg_ms, LEFT(query, 100)
FROM pg_stat_statements
WHERE query LIKE '%_pg_ripple.vp_%'
ORDER BY mean_exec_time DESC
LIMIT 10;
| Symptom | Likely Bottleneck | Section |
|---|---|---|
High mean_exec_time on VP queries | Query engine | Query Performance |
delta_rows growing unbounded | Write path / merge | Write Throughput |
| Cache hit rate < 95% | Dictionary cache | Cache Pressure |
merge_worker_pid = 0 | Merge worker not running | Write Throughput |
Query Performance
Typical Performance Numbers
Based on BSBM benchmarks and internal testing with 10M triples on a 4-core/16GB instance:
| Query Pattern | Typical Latency | Notes |
|---|---|---|
| Simple triple pattern (1 BGP) | 0.5–2ms | Single VP table scan with B-tree |
| Star pattern (3–5 joins, same subject) | 2–10ms | Self-join elimination reduces to 1 scan + joins |
| Path query (3 hops) | 5–20ms | WITH RECURSIVE, bounded depth |
| Complex BGP (5–8 patterns) | 10–50ms | Benefits from bgp_reorder |
| Aggregation (COUNT/SUM over 100K rows) | 20–80ms | PostgreSQL native aggregation |
| DESCRIBE (CBD, 50 outgoing arcs) | 5–15ms | Depends on describe_strategy |
| Federation (1 SERVICE call) | 50–500ms | Network-dominated |
Tuning: Slow Single Queries
Step 1: Get the EXPLAIN output
SELECT pg_ripple.explain_sparql(
'SELECT ?name WHERE {
?person <http://schema.org/knows> ?friend .
?friend <http://schema.org/name> ?name
}',
'text'
);
Step 2: Check for common issues
| EXPLAIN Pattern | Problem | Fix |
|---|---|---|
Seq Scan on vp_rare | Predicate below promotion threshold | Lower vp_promotion_threshold or load more data |
Nested Loop with millions of rows | Poor join order | Verify bgp_reorder = on; run ANALYZE on VP tables |
Sort + Unique on large result | Unnecessary DISTINCT | Add SHACL sh:maxCount 1 for functional predicates |
CTE Scan with high loops | Unbounded property path | Lower max_path_depth; add FILTER bounds |
Hash Join with large build side | Join on a high-cardinality predicate | Rewrite query to filter the large predicate first |
Step 3: Enable the plan cache
-- Cache compiled SQL for repeated queries
SET pg_ripple.plan_cache_size = 512;
The plan cache eliminates parse/optimize/generate overhead for repeated SPARQL patterns. With BSBM's mix of 12 query templates, a cache size of 256 achieves ~98% hit rate.
Tuning: Overall Query Throughput
For workloads with many concurrent queries:
# Enable parallel query for complex joins
pg_ripple.parallel_query_min_joins = 2
# PostgreSQL parallel execution
max_parallel_workers_per_gather = 4
max_parallel_workers = 8
# Larger work_mem for complex joins
work_mem = '128MB'
On a 10M triple dataset with 5-pattern BGPs, enabling bgp_reorder reduces median query time from 45ms to 12ms — a 3.7x improvement. Always keep this on unless you have a specific reason to disable it.
Write Throughput
Typical Write Performance
| Operation | Throughput | Notes |
|---|---|---|
insert_triple() (single) | 5,000–15,000 triples/sec | Per-backend, includes dictionary encoding |
load_turtle() (bulk, inline) | 30,000–80,000 triples/sec | Batch dictionary encoding |
load_turtle_file() (bulk, file) | 50,000–120,000 triples/sec | Streaming from disk, larger batches |
sparql_update() INSERT DATA | 10,000–30,000 triples/sec | SPARQL parse overhead |
Tuning: Merge Worker Lag
If unmerged_delta_rows grows continuously, the merge worker cannot keep up with the write rate.
Diagnosis:
-- Check delta accumulation
SELECT (pg_ripple.stats()->>'unmerged_delta_rows')::int AS delta;
-- Run again 60 seconds later — if delta is growing, merges are lagging
Solutions (in order of impact):
-
Lower merge_threshold — Merge smaller batches more frequently:
ALTER SYSTEM SET pg_ripple.merge_threshold = 5000; SELECT pg_reload_conf(); -
Increase merge frequency — Reduce polling interval:
ALTER SYSTEM SET pg_ripple.merge_interval_secs = 15; SELECT pg_reload_conf(); -
Manual compaction — Force an immediate merge:
SELECT pg_ripple.compact(); -
Separate write windows — Batch writes during off-peak hours, then compact.
Tuning: Bulk Load Performance
For large initial data loads:
-- Temporarily disable SHACL validation
SET pg_ripple.shacl_mode = 'off';
-- Use file-based loading for best throughput
SELECT pg_ripple.load_turtle_file('/data/large_dataset.ttl');
-- Re-enable validation
SET pg_ripple.shacl_mode = 'async';
-- Force merge to move data to main tables
SELECT pg_ripple.compact();
During bulk loads, pg_ripple monitors cache utilization against cache_budget. When utilization exceeds 90%, batch sizes are automatically reduced to prevent out-of-memory conditions. If you see slower-than-expected bulk loads, check encode_cache_utilization_pct in stats().
Cache Pressure
Diagnosis
SELECT
(s->>'encode_cache_capacity')::int AS capacity,
(s->>'encode_cache_utilization_pct')::int AS util_pct,
(s->>'encode_cache_hits')::bigint AS hits,
(s->>'encode_cache_misses')::bigint AS misses,
(s->>'encode_cache_evictions')::bigint AS evictions
FROM pg_ripple.stats() s;
| Metric | Healthy | Action Needed |
|---|---|---|
| Hit rate > 95% | Normal operation | None |
| Hit rate 90–95% | Marginal | Consider increasing cache |
| Hit rate < 90% | Cache thrashing | Increase dictionary_cache_size |
| Utilization > 90% | Near-full | Increase cache_budget |
| Evictions > 10% of hits | High churn | Working set exceeds cache |
Sizing the Dictionary Cache
Rule of thumb: the cache should hold at least 80% of your unique terms.
-- Count unique terms
SELECT count(*) AS unique_terms FROM _pg_ripple.dictionary;
| Unique Terms | Recommended dictionary_cache_size | Memory (approx.) |
|---|---|---|
| < 50K | 8,192 | ~2 MB |
| 50K – 500K | 65,536 | ~13 MB |
| 500K – 5M | 262,144 | ~50 MB |
| 5M – 50M | 500,000 | ~100 MB |
| > 50M | 1,000,000 (max) | ~200 MB |
Changing dictionary_cache_size requires a PostgreSQL restart because shared memory is allocated at postmaster start. Plan your cache sizing during initial deployment.
Workload-Specific Recipes
Read-Heavy Analytics
Optimized for complex SPARQL queries with rare writes:
# Large plan cache for diverse query shapes
pg_ripple.plan_cache_size = 2048
# BGP optimization
pg_ripple.bgp_reorder = on
pg_ripple.parallel_query_min_joins = 2
# Large dictionary cache
pg_ripple.dictionary_cache_size = 262144
pg_ripple.cache_budget = 256
# Infrequent merges (writes are rare)
pg_ripple.merge_threshold = 100000
pg_ripple.merge_interval_secs = 300
# PostgreSQL
shared_buffers = '4GB'
effective_cache_size = '12GB'
work_mem = '256MB'
random_page_cost = 1.1
Expected: P95 query latency < 50ms for 5-pattern BGPs on 10M triples.
Write-Heavy Ingestion
Optimized for continuous data ingestion with periodic queries:
# Smaller plan cache (fewer distinct queries)
pg_ripple.plan_cache_size = 64
# Aggressive merging to keep delta small
pg_ripple.merge_threshold = 5000
pg_ripple.merge_interval_secs = 10
pg_ripple.latch_trigger_threshold = 2000
pg_ripple.auto_analyze = on
# Large cache to handle encoding pressure
pg_ripple.dictionary_cache_size = 500000
pg_ripple.cache_budget = 512
# Disable SHACL during ingestion
pg_ripple.shacl_mode = 'off'
# PostgreSQL — optimize for writes
shared_buffers = '2GB'
wal_buffers = '64MB'
checkpoint_completion_target = 0.9
max_wal_size = '4GB'
Expected: Sustained ingestion at 50K+ triples/sec with merge lag < 30 seconds.
Mixed HTAP (Read + Write)
Balanced for concurrent queries and writes:
# Moderate plan cache
pg_ripple.plan_cache_size = 512
# Balanced merge — not too frequent, not too lazy
pg_ripple.merge_threshold = 25000
pg_ripple.merge_interval_secs = 30
pg_ripple.latch_trigger_threshold = 10000
pg_ripple.auto_analyze = on
# Good cache coverage
pg_ripple.dictionary_cache_size = 131072
pg_ripple.cache_budget = 128
# Async SHACL so writes are not blocked
pg_ripple.shacl_mode = 'async'
# BGP optimization for read queries
pg_ripple.bgp_reorder = on
# PostgreSQL
shared_buffers = '4GB'
effective_cache_size = '12GB'
work_mem = '128MB'
max_parallel_workers_per_gather = 2
Expected: Read P95 < 30ms, write throughput > 20K triples/sec, merge lag < 60 seconds.
Benchmarking Your Deployment
Use the built-in compact() function and pg_stat_statements to establish baselines:
-- Reset statistics
SELECT pg_stat_statements_reset();
-- Run your workload (queries, inserts, etc.)
-- Collect results
SELECT
calls,
mean_exec_time::numeric(10,2) AS avg_ms,
stddev_exec_time::numeric(10,2) AS stddev_ms,
min_exec_time::numeric(10,2) AS min_ms,
max_exec_time::numeric(10,2) AS max_ms,
rows,
LEFT(query, 80) AS query_prefix
FROM pg_stat_statements
WHERE query LIKE '%_pg_ripple%'
ORDER BY total_exec_time DESC
LIMIT 20;
Change one parameter at a time, re-run your benchmark, and compare. The most impactful parameters in order are:
dictionary_cache_size(cache hit rate)bgp_reorder(query planning)merge_threshold(read freshness vs. write throughput)plan_cache_size(repeated query overhead)- PostgreSQL
work_mem(complex join performance)
Parallel Merge Worker Pool
Added in v0.42.0
Overview
pg_ripple uses a Vertical Partitioning (VP) architecture where each unique predicate gets its own storage table. The merge worker pool keeps the read-optimised _main partitions in sync with the write-optimised _delta tables.
By default, a single background worker handles all predicates sequentially. For workloads with many distinct predicates — such as rich ontologies with 50+ property types — a pool of parallel workers can significantly improve write throughput.
Configuration
pg_ripple.merge_workers (startup only)
Controls the number of parallel merge worker processes. Must be set in postgresql.conf or before the server starts; it cannot be changed with SET at session level.
# postgresql.conf
shared_preload_libraries = 'pg_ripple'
pg_ripple.merge_workers = 4
- Default:
1(single worker, original behaviour) - Range:
1to16 - Type:
integer,PGC_POSTMASTER(startup-only)
pg_ripple.merge_threshold
Minimum rows in a VP delta table before a merge is triggered. Increasing this reduces merge frequency but increases per-merge cost.
SET pg_ripple.merge_threshold = 50000; -- default: 10000
pg_ripple.merge_interval_secs
Maximum seconds between merge worker polling cycles.
SET pg_ripple.merge_interval_secs = 30; -- default: 60
How It Works
With merge_workers = N, pg_ripple spawns N background worker processes. Each worker owns a disjoint round-robin subset of VP predicates:
- Worker 0 handles predicates where
pred_id % N == 0 - Worker 1 handles predicates where
pred_id % N == 1 - … and so on
Advisory locking prevents races: before merging a predicate, a worker calls pg_try_advisory_lock(pred_id). If another worker already holds the lock, it skips that predicate.
Work-stealing: after processing its assigned predicates, an idle worker checks whether any "foreign" predicate (not in its round-robin slice) has a delta table above the merge threshold and no lock held. If so, it steals that work. This prevents a single overloaded predicate from delaying the merge cycle.
Monitoring
Use pg_ripple.diagnostic_report() to check merge worker activity:
SELECT value FROM pg_ripple.diagnostic_report()
WHERE key LIKE 'merge_%';
Or query the background worker state:
SELECT pid, application_name, state
FROM pg_stat_activity
WHERE application_name LIKE 'pg_ripple merge%';
Choosing the Right Worker Count
| Predicate count | Recommended workers |
|---|---|
| < 20 | 1 (default) |
| 20–100 | 2–4 |
| 100–500 | 4–8 |
| > 500 | 8–16 |
For most workloads, the bottleneck is not the worker count but the merge threshold and interval. Tune those first before scaling workers.
Restart Requirement
Because merge_workers is a PGC_POSTMASTER GUC, changes take effect only after a PostgreSQL restart:
# After updating postgresql.conf:
pg_ctl restart -D $PGDATA
Backup and Disaster Recovery
pg_ripple stores all data in standard PostgreSQL tables within the _pg_ripple schema. This means every PostgreSQL backup tool works out of the box — VP tables, the dictionary, the predicates catalog, SHACL constraints, Datalog rules, and inferred triples are all captured by pg_dump, WAL archiving, and streaming replication.
Unlike triple stores that require a separate RDF dump/reload cycle, pg_ripple data is just PostgreSQL data. Your existing backup infrastructure already covers it.
What Gets Backed Up
| Object | Schema | Captured by pg_dump? | Notes |
|---|---|---|---|
| Dictionary table | _pg_ripple.dictionary | Yes | All IRI, blank node, and literal mappings |
| Predicates catalog | _pg_ripple.predicates | Yes | Predicate → VP table OID mapping |
| VP tables (main + delta + tombstones) | _pg_ripple.vp_{id}_* | Yes | One table set per predicate |
| Rare predicates table | _pg_ripple.vp_rare | Yes | Consolidated low-cardinality predicates |
| SHACL constraints | _pg_ripple.shacl_* | Yes | Shape definitions and validation state |
| Datalog rules | _pg_ripple.rules | Yes | Rule text and compiled plans |
| Inferred triples | VP tables, source = 1 | Yes | Materialized inference results |
| Extension metadata | pg_catalog | Yes | Extension version and control file |
| Shared memory state | In-memory only | No | Dictionary LRU cache, merge worker counters |
Logical Backup with pg_dump
Full Database Dump
# Custom format (recommended — compressed, parallel-restore capable)
pg_dump -Fc -f pg_ripple_backup.dump mydb
# Plain SQL (human-readable, useful for auditing)
pg_dump -Fp -f pg_ripple_backup.sql mydb
Extension-Only Dump
To back up only pg_ripple data without the rest of the database:
pg_dump -Fc \
--schema=_pg_ripple \
--schema=pg_ripple \
-f pg_ripple_only.dump mydb
Always include both _pg_ripple (internal storage) and pg_ripple (public API functions). Restoring one without the other leaves the extension in an inconsistent state.
Parallel Dump for Large Datasets
For databases with millions of triples, use parallel workers:
# Directory format required for parallel dump
pg_dump -Fd -j 4 -f pg_ripple_backup_dir/ mydb
The dictionary table and large VP tables will be dumped in parallel, significantly reducing backup time.
Restoring from Backup
Full Restore to a New Database
# Create the target database
createdb mydb_restored
# Restore (custom format)
pg_restore -d mydb_restored -Fc pg_ripple_backup.dump
# Restore (directory format, parallel)
pg_restore -d mydb_restored -Fd -j 4 pg_ripple_backup_dir/
Restore from Plain SQL
psql -d mydb_restored -f pg_ripple_backup.sql
Post-Restore Verification
After restoring, verify the extension is intact:
-- Check extension version
SELECT extversion FROM pg_extension WHERE extname = 'pg_ripple';
-- Verify triple count
SELECT pg_ripple.stats();
-- Run the health check
SELECT pg_ripple.canary();
-- Spot-check a SPARQL query
SELECT pg_ripple.sparql($$
SELECT (COUNT(*) AS ?n) WHERE { ?s ?p ?o }
$$);
Yes. VP tables are standard PostgreSQL heap tables with B-tree or BRIN indexes. pg_dump captures them exactly like any other table. The HTAP delta/main/tombstone split, indexes, and the merge worker view definitions are all preserved. After restore, the merge worker resumes normal operation once shared_preload_libraries includes pg_ripple.
WAL-Based Continuous Archiving
For point-in-time recovery (PITR), configure WAL archiving:
Enable WAL Archiving
In postgresql.conf:
wal_level = replica
archive_mode = on
archive_command = 'cp %p /backup/wal_archive/%f'
max_wal_senders = 3
Take a Base Backup
pg_basebackup -D /backup/base -Ft -z -P
Point-in-Time Recovery
Create a recovery.signal file and configure the restore target:
# postgresql.conf (or postgresql.auto.conf)
restore_command = 'cp /backup/wal_archive/%f %p'
recovery_target_time = '2026-04-19 14:30:00'
Start PostgreSQL — it will replay WAL up to the specified time.
If you recover to a point mid-merge, the merge worker will detect the incomplete state and re-run the merge on startup. No manual intervention is needed, but the first merge cycle after recovery may take longer than usual.
Streaming Replication
pg_ripple works transparently with PostgreSQL streaming replication:
# On the replica
pg_basebackup -h primary-host -D /var/lib/postgresql/18/main -R -P
The -R flag writes the standby.signal and connection parameters. All VP tables, dictionary data, and HTAP state replicate via WAL.
The background merge worker does not run on read replicas. Replicas receive merged state via WAL replay from the primary. This is correct behavior — replicas should never write.
Backup Strategy Recommendations
Small Datasets (< 1M triples)
| Component | Recommendation |
|---|---|
| Method | pg_dump -Fc nightly |
| Retention | 7 daily + 4 weekly |
| RPO | 24 hours |
| RTO | Minutes |
Medium Datasets (1M – 100M triples)
| Component | Recommendation |
|---|---|
| Method | WAL archiving + daily base backup |
| Retention | 7 daily base + continuous WAL |
| RPO | Seconds (WAL) |
| RTO | Minutes to hours |
Large Datasets (> 100M triples)
| Component | Recommendation |
|---|---|
| Method | WAL archiving + pgBackRest or Barman |
| Retention | Incremental base + continuous WAL |
| RPO | Seconds (WAL) |
| RTO | Proportional to dataset size |
Schedule monthly restore drills. A backup that has never been tested is not a backup. Automate the verification queries shown above as part of the drill.
Disaster Recovery Checklist
- Before disaster: WAL archiving enabled, base backups on schedule, replication lag monitored
- During incident: identify the failure scope (single table, full database, or host loss)
- Recovery steps:
- Host loss → promote replica or restore from base backup + WAL
- Corruption → PITR to last known good time
- Accidental deletion → PITR to just before the DROP/DELETE
- Post-recovery:
- Run
SELECT pg_ripple.canary()to verify health - Check
pg_ripple.stats()for expected triple counts - Verify the merge worker is running (
merge_worker_pid > 0) - Run representative SPARQL queries to confirm data integrity
- Resume WAL archiving and replication
- Run
Common Pitfalls
- Schema ownership: the restoring user must be a superuser or own both
_pg_rippleandpg_rippleschemas - Sequence values:
pg_dumpcaptures sequence state — statement IDs (icolumn) will continue from the correct value after restore - Tablespace placement: if you used custom tablespaces for VP tables, ensure they exist on the target server before restoring
Upgrading Safely
pg_ripple follows PostgreSQL's standard extension upgrade mechanism. Each release ships a migration script that ALTER EXTENSION pg_ripple UPDATE executes automatically, walking the version chain from your current version to the target.
How Extension Upgrades Work
PostgreSQL extensions use a chain of migration scripts to move between versions. pg_ripple provides a script for every consecutive version pair:
pg_ripple--0.1.0--0.2.0.sql
pg_ripple--0.2.0--0.3.0.sql
pg_ripple--0.3.0--0.4.0.sql
...
pg_ripple--0.45.0--0.46.0.sql
When you run ALTER EXTENSION pg_ripple UPDATE, PostgreSQL finds the shortest path from your current version to the latest and executes each script in sequence.
Each migration script uses IF NOT EXISTS, CREATE OR REPLACE, and similar guards. If a migration is partially applied (e.g., due to a crash), re-running it is safe.
Pre-Upgrade Checklist
1. Check Your Current Version
SELECT extversion FROM pg_extension WHERE extname = 'pg_ripple';
2. Back Up the Database
pg_dump -Fc -f pre_upgrade_backup.dump mydb
While migration scripts are tested, a backup lets you restore to the pre-upgrade state if anything goes wrong. This is especially important for major feature releases that add schema changes.
3. Review the Changelog
Read the Release Notes for every version between your current version and the target. Pay attention to:
- Breaking changes: renamed functions, changed return types, removed GUC parameters
- Schema changes: new columns on internal tables, new indexes
- New dependencies: additional
shared_preload_librariesentries
4. Check for Active Connections
SELECT count(*) FROM pg_stat_activity
WHERE datname = current_database()
AND pid != pg_backend_pid();
Disconnect all application connections before upgrading. The upgrade modifies extension catalog entries and may need exclusive locks on internal tables.
5. Verify the New Package Is Installed
The new .so (shared library) and SQL migration files must be present in the PostgreSQL extension directory before running ALTER EXTENSION:
# Check that the target version's migration script exists
ls $(pg_config --sharedir)/extension/pg_ripple--*
# Check that the shared library is updated
ls -la $(pg_config --pkglibdir)/pg_ripple.so
Performing the Upgrade
Step 1: Install the New Package
# From source
cargo pgrx install --pg-config $(pg_config) --release
# Or from a pre-built package
# dpkg -i pg_ripple-0.32.0-pg18.deb
Step 2: Schedule a Maintenance Window
pg_ripple does not yet support zero-downtime upgrades. Schedule the upgrade during a maintenance window. If you have read replicas, route read traffic to a replica during the upgrade window, but note that the replica will also need the new shared library installed before promotion.
Step 3: Restart PostgreSQL (If Required)
Some releases update the shared library or change shared memory layout. Check the release notes — if they mention shared memory changes or new background workers, restart PostgreSQL:
pg_ctl restart -D $PGDATA
Step 4: Run the Migration
-- Upgrade to the latest installed version
ALTER EXTENSION pg_ripple UPDATE;
-- Or upgrade to a specific version
ALTER EXTENSION pg_ripple UPDATE TO '0.46.0';
PostgreSQL will execute each intermediate migration script in order:
NOTICE: updating extension "pg_ripple" from version "0.44.0" to "0.45.0"
NOTICE: updating extension "pg_ripple" from version "0.45.0" to "0.46.0"
Step 5: Verify
-- Confirm the new version
SELECT extversion FROM pg_extension WHERE extname = 'pg_ripple';
-- Run the health check
SELECT pg_ripple.canary();
-- Verify stats
SELECT pg_ripple.stats();
Post-Upgrade Verification
Run these checks after every upgrade:
-- 1. Extension version matches expected
SELECT extversion FROM pg_extension WHERE extname = 'pg_ripple';
-- 2. Health check passes
SELECT pg_ripple.canary();
-- 3. Triple count is unchanged
SELECT pg_ripple.stats();
-- 4. SPARQL queries work
SELECT pg_ripple.sparql($$
SELECT ?s ?p ?o WHERE { ?s ?p ?o } LIMIT 5
$$);
-- 5. Merge worker is running (if shared_preload_libraries is set)
SELECT (pg_ripple.stats()->>'merge_worker_pid')::int > 0 AS merge_worker_ok;
-- 6. Dictionary cache is operational
SELECT
(s->>'encode_cache_hits')::bigint + (s->>'encode_cache_misses')::bigint > 0
AS cache_active
FROM pg_ripple.stats() s;
Add these verification queries to a script that runs immediately after ALTER EXTENSION. If any check fails, the script can alert operators before traffic is routed back to the upgraded instance.
Multi-Version Hop Upgrades
PostgreSQL walks the entire migration chain automatically. Upgrading from v0.5.0 directly to v0.46.0 executes all intermediate scripts:
-- This works — PG finds the path 0.5.0 → 0.5.1 → 0.6.0 → ... → 0.46.0
ALTER EXTENSION pg_ripple UPDATE TO '0.46.0';
Each migration script is typically fast (milliseconds to seconds). However, scripts that add columns or create indexes on large tables may take longer. For very long hops (10+ versions), expect a few minutes on large datasets. Monitor pg_stat_activity for lock waits during the upgrade.
Rollback Strategy
There is no built-in downgrade path. If an upgrade causes problems:
Option A: Restore from Backup
# Drop the upgraded database
dropdb mydb
# Restore the pre-upgrade backup
createdb mydb
pg_restore -d mydb pre_upgrade_backup.dump
This is the safest rollback method — it returns everything to the exact pre-upgrade state.
Option B: Reinstall the Old Version
# Install the old shared library
cargo pgrx install --pg-config $(pg_config) --release # (from old source checkout)
# Restart PostgreSQL
pg_ctl restart -D $PGDATA
Reinstalling the old .so file works only if the migration scripts did not make irreversible schema changes (e.g., dropping a column). Always check the migration script content before relying on this approach.
Upgrading PostgreSQL Itself
When upgrading the PostgreSQL major version (e.g., 17 → 18):
- pg_ripple requires PostgreSQL 18. Earlier versions are not supported.
- Use
pg_upgradeas normal — pg_ripple's tables and extension metadata transfer correctly. - After
pg_upgrade, verify the extension:
psql -d mydb -c "SELECT extversion FROM pg_extension WHERE extname = 'pg_ripple';"
psql -d mydb -c "SELECT pg_ripple.canary();"
Version Compatibility Matrix
| pg_ripple Version | PostgreSQL Version | Notes |
|---|---|---|
| 0.1.0 – 0.46.0 | 18.x | Only supported version |
| Any | < 18 | Not supported |
| Any | 19+ | Not yet tested |
Troubleshooting Upgrades
"no update path from version X to version Y"
The intermediate migration scripts are missing from the extension directory. Reinstall pg_ripple to ensure all SQL files are present:
ls $(pg_config --sharedir)/extension/pg_ripple--*.sql | wc -l
"could not open extension control file"
The pg_ripple.control file is missing. Reinstall the extension.
Migration script fails with a lock timeout
Another session holds a lock on an internal table. Ensure all connections are closed before upgrading, or increase lock_timeout:
SET lock_timeout = '60s';
ALTER EXTENSION pg_ripple UPDATE;
Shared library version mismatch
The .so file version does not match the SQL migration target. Ensure you installed the matching binary before running ALTER EXTENSION:
cargo pgrx install --pg-config $(pg_config) --release
pg_ctl restart -D $PGDATA
ALTER EXTENSION pg_ripple UPDATE;
Schema Version Stamping (v0.37.0+)
Starting with v0.37.0, every ALTER EXTENSION pg_ripple UPDATE stamps a row in _pg_ripple.schema_version. You can verify upgrade completeness:
SELECT version, installed_at, upgraded_from
FROM _pg_ripple.schema_version
ORDER BY installed_at DESC;
Example output after upgrading from 0.36.0 to 0.37.0:
version | installed_at | upgraded_from
-----------+--------------------------------+---------------
0.37.0 | 2026-04-19 10:00:00+00 | 0.36.0
0.36.0 | 2026-02-01 08:00:00+00 | 0.35.0
The diagnostic_report() function also reports the current schema version:
SELECT value FROM pg_ripple.diagnostic_report() WHERE key = 'schema_version';
This is useful in monitoring scripts to confirm a rolling upgrade has completed on all replicas.
Scaling
pg_ripple scales vertically within a single PostgreSQL instance and horizontally for read traffic via streaming replication. This page covers how to allocate resources, tune the merge worker, set up read replicas, and understand current limitations.
pg_ripple runs entirely within PostgreSQL. It inherits PostgreSQL's single-writer architecture: one primary handles all writes, and read replicas serve read-only SPARQL queries. Horizontal sharding across multiple workers is available via the Citus integration (v0.58.0+).
Vertical Scaling
The most impactful scaling lever is giving your single PostgreSQL instance more resources.
Memory
Memory affects three key areas:
| Resource | Controlled By | Impact |
|---|---|---|
| Dictionary LRU cache | pg_ripple.dictionary_cache_size | Reduces disk I/O for IRI/literal lookups. Every SPARQL query touches the dictionary on decode. |
| PostgreSQL shared buffers | shared_buffers | Caches VP table pages. Larger = fewer disk reads for joins. |
| Work memory | work_mem | Memory for sorts, hash joins, and hash aggregates in SPARQL-generated SQL. |
Dictionary Cache Sizing
The dictionary cache is allocated in shared memory at server startup. Each entry consumes approximately 200 bytes.
-- Check current utilization
SELECT
s->>'encode_cache_capacity' AS capacity,
s->>'encode_cache_utilization_pct' AS utilization_pct,
ROUND(
(s->>'encode_cache_hits')::numeric /
NULLIF((s->>'encode_cache_hits')::numeric + (s->>'encode_cache_misses')::numeric, 0),
4
) AS hit_rate
FROM pg_ripple.stats() s;
| Hit Rate | Action |
|---|---|
| > 95% | Healthy — no change needed |
| 90–95% | Consider increasing dictionary_cache_size |
| < 90% | Double dictionary_cache_size and restart |
Set dictionary_cache_size to at least 10% of your total unique IRIs + literals. For a dataset with 5M unique terms, start with 500K entries (~100 MB of shared memory).
PostgreSQL Memory Settings
# postgresql.conf — for a 64 GB server with pg_ripple as the primary workload
shared_buffers = 16GB
effective_cache_size = 48GB
work_mem = 256MB
maintenance_work_mem = 2GB
Complex SPARQL queries with multiple joins, UNIONs, or aggregates can spawn many hash operations. PostgreSQL allocates work_mem per operation per query. Start conservative (64MB–256MB) and increase if you see "temporary file" entries in the logs.
CPU
| Workload | CPU Benefit |
|---|---|
| SPARQL query execution | More cores → more parallel workers for large joins |
| Merge worker | Single-threaded per predicate, but merges run concurrently across predicates |
| Bulk loading | load_turtle / load_ntriples are I/O-bound; CPU helps with dictionary encoding |
| Datalog inference | Semi-naive fixpoint is CPU-intensive; benefits from faster cores |
Set max_parallel_workers_per_gather to allow PostgreSQL to parallelize large VP table scans:
max_parallel_workers_per_gather = 4
max_parallel_workers = 8
parallel_setup_cost = 100
parallel_tuple_cost = 0.001
pg_ripple's parallel_query_min_joins GUC controls when the SPARQL engine enables parallel hints in generated SQL (default: 3 joins).
Storage
| Tier | Recommendation |
|---|---|
| NVMe SSD | Best for all workloads. Random I/O for dictionary lookups and VP table joins. |
| SATA SSD | Acceptable for medium datasets. |
| HDD | Not recommended. Dictionary lookups and VP joins are random-I/O heavy. |
Place pg_wal on a separate NVMe device from the main data directory. pg_ripple's bulk load and merge operations generate significant WAL traffic.
Merge Worker Tuning
The HTAP merge worker is the most important pg_ripple-specific scaling knob. It controls how quickly delta rows (recent writes) are consolidated into the main BRIN-indexed partition.
How the Merge Worker Operates
- The worker polls every
merge_interval_secs(default: 60s) - For each predicate, it checks if
delta row count >= merge_threshold - If yes, it creates a new main table:
(old main − tombstones) UNION ALL delta - It swaps the view to point at the new main, then drops the old main after
merge_retention_seconds - If
auto_analyzeis on, it runsANALYZEon the new main
Tuning for Write-Heavy Workloads
Lower the merge threshold and interval to keep the delta tables small:
pg_ripple.merge_threshold = 5000
pg_ripple.merge_interval_secs = 30
pg_ripple.latch_trigger_threshold = 5000
This gives fresher reads but increases I/O from more frequent merges.
Tuning for Read-Heavy Workloads
Raise the threshold to batch more writes before merging:
pg_ripple.merge_threshold = 50000
pg_ripple.merge_interval_secs = 120
This reduces merge I/O overhead but means queries scan larger delta tables.
Monitoring Merge Activity
-- Is the merge worker running?
SELECT (pg_ripple.stats()->>'merge_worker_pid')::int AS pid;
-- How many unmerged delta rows?
SELECT (pg_ripple.stats()->>'unmerged_delta_rows')::int AS delta_rows;
If unmerged_delta_rows grows continuously while merge_worker_pid is non-zero, the worker may be stuck. Check pg_stat_activity for long-running merge transactions and look for lock contention. The merge_watchdog_timeout GUC (default: 300s) logs a WARNING if the worker is idle too long.
Read Replicas
PostgreSQL streaming replication provides horizontal read scaling for SPARQL queries.
Architecture
┌────────────┐ WAL stream ┌────────────┐
│ Primary │ ──────────────────→ │ Replica 1 │ ← SPARQL reads
│ (writes) │ └────────────┘
│ │ WAL stream ┌────────────┐
│ │ ──────────────────→ │ Replica 2 │ ← SPARQL reads
└────────────┘ └────────────┘
Setting Up a Read Replica
On the primary:
# postgresql.conf
wal_level = replica
max_wal_senders = 5
wal_keep_size = 1GB
Create a replication slot:
SELECT pg_create_physical_replication_slot('replica1');
On the replica:
pg_basebackup -h primary-host -D /var/lib/postgresql/18/main -R -S replica1 -P
Start the replica — it will begin streaming WAL and replaying changes, including all VP table mutations.
Replica Considerations
The background merge worker only runs on the primary. Replicas receive already-merged state through WAL replay. This means replicas always have a consistent view of the data without any additional overhead.
- SPARQL queries work identically on replicas — the query engine reads VP tables the same way
- Dictionary cache is independent per instance — each replica maintains its own LRU cache
- Replication lag: monitor with
pg_stat_replicationon the primary. Under normal load, lag should be sub-second - Hot standby conflicts: long-running SPARQL queries on replicas may conflict with WAL replay. Set
max_standby_streaming_delayappropriately:
# On the replica
max_standby_streaming_delay = 30s
hot_standby_feedback = on
Connection Pooling
For workloads with many concurrent SPARQL clients, use a connection pooler:
pg_ripple uses session-level GUC parameters (e.g., pg_ripple.inference_mode). If you use PgBouncer, configure it in session pooling mode, not transaction mode. Transaction-mode pooling resets GUCs between transactions, which can cause unexpected behavior.
# pgbouncer.ini
[databases]
mydb = host=127.0.0.1 port=5432 dbname=mydb
[pgbouncer]
pool_mode = session
max_client_conn = 200
default_pool_size = 20
Scaling Limits and Honest Boundaries
| Dimension | Current Capability | Limitation |
|---|---|---|
| Triples per instance | Tested to 1B+ | Bound by disk and memory |
| Concurrent SPARQL queries | Hundreds (with pooler) | Bound by max_connections and CPU |
| Write throughput | ~50K–200K triples/sec (bulk load) | Single-writer architecture |
| Read replicas | Unlimited | Standard PG replication |
| Cross-node sharding | Supported (Citus 12+, v0.58.0+) | Subject-hash distribution; see Citus integration |
| Multi-primary writes | Not supported | PostgreSQL limitation |
| Federation | Supported (SERVICE clause) | Remote endpoints add latency |
pg_ripple v0.58.0+ supports distributing VP tables across Citus worker nodes. Triples are hash-sharded by subject ID so star-pattern queries co-locate on a single worker. v0.59.0 adds SPARQL shard-pruning (10–100× speedup for bound-subject queries). See Citus integration for setup instructions.
Capacity Planning
Storage Estimates
| Component | Per Triple (approx.) |
|---|---|
| VP table row (s, o, g, i, source) | ~40 bytes |
| VP indexes (dual B-tree) | ~80 bytes |
| Dictionary entry (per unique term) | ~120 bytes |
| HTAP overhead (delta + tombstone tables) | ~20% of VP size during active writes |
Example: 100M triples with 20M unique terms ≈ 12 GB (VP) + 2.4 GB (dictionary) + overhead ≈ ~20 GB total.
Memory Estimates
| Component | Sizing |
|---|---|
shared_buffers | 25% of RAM |
dictionary_cache_size | 10% of unique terms |
work_mem | 64MB–512MB depending on query complexity |
| OS page cache | Remaining RAM |
Deploy with conservative settings, load your data, and run representative queries. Use pg_ripple.stats() and PostgreSQL's pg_stat_user_tables to identify bottlenecks before adding hardware.
Sequence Exhaustion (statement_id_seq)
(L15-11, v0.97.0)
Every triple stored by pg_ripple receives a globally-unique statement identifier (SID) from
the _pg_ripple.statement_id_seq sequence. The sequence is a standard PostgreSQL BIGINT
sequence, which means it can count from 1 to 9,223,372,036,854,775,807 (approximately 9.2 × 10¹⁸).
How Long Until Exhaustion?
| Insert Rate | Years to Exhaustion |
|---|---|
| 1,000 triples/second | ~292,000 years |
| 1,000,000 triples/second | ~292 years |
| 1,000,000,000 triples/second (theoretical max) | ~0.3 years |
At realistic workloads the sequence will never exhaust. However, if you ever hit this limit, PostgreSQL raises:
ERROR: nextval: reached maximum value of sequence "_pg_ripple.statement_id_seq" (9223372036854775807)
Checking Remaining Capacity
Use pg_ripple.sid_exhaustion_years() to check how many years remain:
SELECT pg_ripple.sid_exhaustion_years();
-- Returns: 291723.6 (years remaining at current insert rate)
The function returns NULL if fewer than 100,000 SIDs have been assigned (the rate estimate
is unreliable at low counts).
You can also query the sequence directly:
SELECT
last_value AS current_sid,
9223372036854775807 - last_value AS sids_remaining,
(9223372036854775807 - last_value)::numeric
/ NULLIF((SELECT count(*) FROM _pg_ripple.vp_rare), 0) AS approx_years_at_current_rate
FROM _pg_ripple.statement_id_seq;
Recovery Procedure (if exhaustion occurs)
Resetting the sequence requires clearing all VP tables. Only do this in a development environment or after a full dump/restore cycle where SID uniqueness is re-established.
- Dump all data using
pg_ripple.export_turtle()orpg_dump. - Truncate all VP tables:
SELECT pg_ripple.truncate_all_triples(); -- development only! - Reset the sequence:
ALTER SEQUENCE _pg_ripple.statement_id_seq RESTART WITH 1; - Reload data from the dump.
In production, the correct long-term solution is to use a CYCLE sequence, which pg_ripple
does not currently support (adding CYCLE would allow SID reuse which could cause
correctness issues with time-travel queries). Track this in a support ticket if you project
you will hit the limit within your deployment horizon.
Read Replicas
pg_ripple_http (v0.120.0+) supports routing read-only SPARQL queries to a PostgreSQL read replica. This page explains the routing semantics, eligible query types, pool exhaustion behaviour, and Prometheus alerting recipes.
Configuration
Set the PG_RIPPLE_REPLICA_DATABASE_URL environment variable to point the HTTP
companion at your read-replica PostgreSQL instance:
PG_RIPPLE_REPLICA_DATABASE_URL="postgres://user:pass@replica-host:5432/rippledb"
When this variable is absent the companion operates in primary-only mode and all
queries go to the primary pool (PG_RIPPLE_DATABASE_URL).
Routing semantics — ?replica=ok
Append ?replica=ok to any SPARQL HTTP endpoint URL to request replica routing:
GET /sparql?query=SELECT+…&replica=ok
The companion evaluates the request against the following decision tree:
- Eligible query type? — Only
SELECT,CONSTRUCT,ASK, andDESCRIBEqueries may be routed to the replica.UPDATE(write) queries always go to the primary regardless of?replica=ok. - Replica pool configured? — If
PG_RIPPLE_REPLICA_DATABASE_URLis unset, the request falls back to the primary silently. - Pool connection available? — If the replica pool is exhausted (all connections in use), the request falls back to the primary. No error is returned to the caller; the response is identical either way.
- Replica healthy? — If the replica returns a connection error, the
companion falls back to the primary and increments
pg_ripple_http_errors_total.
Pool exhaustion fallback
When the replica pool is exhausted, queries automatically fall back to the primary. No client-visible error is raised. This behaviour is intentional: replica routing is a performance optimisation, not a hard requirement. If you need to enforce replica-only execution (e.g., for capacity-separated workloads), implement that policy at the load-balancer or proxy layer.
Prometheus gauges (OBS-M-01)
The HTTP companion exposes two Prometheus gauges for replica pool observability:
| Metric | Type | Description |
|---|---|---|
pg_ripple_http_replica_pool_size{pool="replica"} | gauge | Total connection pool capacity |
pg_ripple_http_replica_pool_available{pool="replica"} | gauge | Currently idle (available) connections |
Both gauges are updated at every /metrics scrape. When no replica pool is
configured both values are 0.
Example Prometheus alert rules
groups:
- name: pg_ripple_replica_pool
rules:
# Alert when the replica pool is completely saturated.
- alert: ReplicaPoolExhausted
expr: |
pg_ripple_http_replica_pool_available{pool="replica"} == 0
and pg_ripple_http_replica_pool_size{pool="replica"} > 0
for: 2m
labels:
severity: warning
annotations:
summary: "pg_ripple read-replica pool exhausted"
description: >
All {{ $value }} replica pool connections are in use. Queries are
falling back to the primary. Consider increasing the pool size or
reducing query concurrency.
# Alert when available connections drop below 20 % of pool capacity.
- alert: ReplicaPoolLow
expr: |
(pg_ripple_http_replica_pool_available{pool="replica"}
/ pg_ripple_http_replica_pool_size{pool="replica"}) < 0.2
for: 5m
labels:
severity: info
annotations:
summary: "pg_ripple replica pool running low"
description: >
Only {{ $value | humanizePercentage }} of replica pool connections
are idle. Pool may become exhausted under load.
See also
Troubleshooting
A runbook of common issues, their causes, and step-by-step resolutions. Each entry follows the pattern: Symptom → Cause → Diagnostic → Fix.
1. SPARQL Query Returns Zero Rows
Symptom: A SPARQL query that should return results returns an empty set.
Cause: The most common cause is querying with unencoded IRIs that don't match the dictionary, or querying the wrong graph.
Diagnostic:
-- Check that triples exist
SELECT pg_ripple.stats();
-- Verify the IRI is in the dictionary
SELECT id FROM _pg_ripple.dictionary WHERE value = 'http://example.org/MyResource';
-- Check the default graph vs named graphs
SELECT pg_ripple.sparql($$
SELECT ?g (COUNT(*) AS ?n) WHERE { GRAPH ?g { ?s ?p ?o } } GROUP BY ?g
$$);
Fix: Ensure the query uses the exact IRI as stored (case-sensitive, no trailing slash differences). If data was loaded into a named graph, use GRAPH or FROM clauses.
2. Merge Worker Not Running
Symptom: pg_ripple.stats() shows merge_worker_pid: 0. Delta rows accumulate.
Cause: pg_ripple is not in shared_preload_libraries, or worker_database points to the wrong database.
Diagnostic:
SHOW shared_preload_libraries;
SHOW pg_ripple.worker_database;
Fix:
# postgresql.conf
shared_preload_libraries = 'pg_ripple'
pg_ripple.worker_database = 'mydb'
Restart PostgreSQL. Verify with:
SELECT (pg_ripple.stats()->>'merge_worker_pid')::int;
3. Slow Queries — Unbounded Property Paths
Symptom: Queries with * or + property paths take minutes or never complete.
Cause: Property path queries compile to WITH RECURSIVE CTEs. On large, highly-connected graphs, recursion explores an enormous search space.
Diagnostic:
SHOW pg_ripple.max_path_depth;
-- Check the generated SQL
SET pg_ripple.plan_cache_size = 0; -- disable cache to see fresh plans
EXPLAIN (ANALYZE, BUFFERS) <generated SQL from logs>;
Fix: Limit recursion depth:
SET pg_ripple.max_path_depth = 10;
Or rewrite the query to use a bounded path ({1,5}) instead of */+.
4. SHACL Validation Not Triggering
Symptom: Data that violates SHACL shapes is inserted without errors.
Cause: SHACL enforcement is asynchronous by default, or the shapes are not loaded.
Diagnostic:
-- Check loaded shapes
SELECT pg_ripple.sparql($$
SELECT ?shape WHERE { ?shape a <http://www.w3.org/ns/shacl#NodeShape> }
$$);
-- Check enforce mode
SHOW pg_ripple.enforce_constraints;
Fix: Set enforcement mode to 'error' for synchronous validation:
SET pg_ripple.enforce_constraints = 'error';
Reload shapes if needed:
SELECT pg_ripple.load_shapes('<shapes-graph-iri>');
5. Datalog Inference Produces No Results
Symptom: pg_ripple.infer() or pg_ripple.infer_goal() returns zero new triples.
Cause: Rules are not loaded, inference mode is 'off', or the rule atoms don't match any data.
Diagnostic:
SHOW pg_ripple.inference_mode;
-- List loaded rule sets
SELECT pg_ripple.list_rule_sets();
-- Test with a simple rule
SELECT pg_ripple.load_rules('test', $$
:Parent(?x, ?z) :- :Parent(?x, ?y), :Parent(?y, ?z).
$$);
SELECT pg_ripple.infer('test');
Fix: Ensure inference_mode is 'on_demand' or 'materialized', rules are loaded, and the predicates in rule atoms match your data's actual IRIs exactly.
6. Shared Memory Errors on Startup
Symptom: PostgreSQL fails to start with could not create shared memory segment or pg_ripple logs insufficient shared memory.
Cause: pg_ripple.dictionary_cache_size is too large for the system's shared memory limits.
Diagnostic:
# Check system shared memory limits
sysctl kern.sysv.shmmax # macOS
sysctl kernel.shmmax # Linux
Fix: Either reduce dictionary_cache_size or increase the OS shared memory limit:
# Linux
sudo sysctl -w kernel.shmmax=17179869184 # 16GB
sudo sysctl -w kernel.shmall=4194304
# macOS
sudo sysctl -w kern.sysv.shmmax=17179869184
7. High Dictionary Cache Eviction Pressure
Symptom: encode_cache_evictions in pg_ripple.stats() is high; cache hit rate drops below 90%.
Cause: The working set of IRIs/literals exceeds the cache capacity.
Diagnostic:
SELECT
s->>'encode_cache_capacity' AS capacity,
s->>'encode_cache_utilization_pct' AS util_pct,
s->>'encode_cache_evictions' AS evictions,
ROUND(
(s->>'encode_cache_hits')::numeric /
NULLIF((s->>'encode_cache_hits')::numeric + (s->>'encode_cache_misses')::numeric, 0),
4
) AS hit_rate
FROM pg_ripple.stats() s;
Fix: Increase dictionary_cache_size in postgresql.conf and restart:
pg_ripple.dictionary_cache_size = 131072 -- double the default
8. Federation Query Timeout
Symptom: Queries with SERVICE clauses hang or return a timeout error.
Cause: The remote SPARQL endpoint is unreachable, slow, or returning an unexpected format.
Diagnostic:
# Test the remote endpoint directly
curl -s -H "Accept: application/sparql-results+json" \
"https://remote.example.org/sparql?query=SELECT+*+WHERE+{?s+?p+?o}+LIMIT+1"
Fix:
- Verify network connectivity to the remote endpoint
- Increase the federation timeout:
SET pg_ripple.federation_timeout = 60; -- seconds
- Check that the remote endpoint supports the required result format (SPARQL JSON Results)
9. pg_ripple_http Not Responding
Symptom: The HTTP SPARQL endpoint returns connection refused or 502 errors.
Cause: The pg_ripple_http companion service is not running, or it cannot connect to PostgreSQL.
Diagnostic:
# Check if the process is running
ps aux | grep pg_ripple_http
# Check the service logs
journalctl -u pg_ripple_http --since "10 minutes ago"
# Test the PostgreSQL connection directly
psql -h localhost -p 5432 -U pg_ripple_http -d mydb -c "SELECT 1"
Fix:
- Start or restart the service
- Verify the connection string in the
pg_ripple_httpconfiguration - Check that
pg_hba.confallows connections from the HTTP service
10. VP Table Bloat
Symptom: Disk usage grows faster than expected; pg_size_pretty(pg_total_relation_size('_pg_ripple.vp_12345')) is much larger than the triple count suggests.
Cause: Frequent deletes and re-inserts without merge cycles, or autovacuum not keeping up.
Diagnostic:
-- Check dead tuples
SELECT relname, n_dead_tup, n_live_tup,
last_autovacuum, last_autoanalyze
FROM pg_stat_user_tables
WHERE schemaname = '_pg_ripple'
ORDER BY n_dead_tup DESC
LIMIT 10;
Fix:
-- Force a vacuum on the bloated table
VACUUM (VERBOSE) _pg_ripple.vp_12345_main;
-- Reclaim space aggressively
VACUUM (FULL) _pg_ripple.vp_12345_main;
Tune autovacuum for VP tables:
ALTER TABLE _pg_ripple.vp_12345_delta
SET (autovacuum_vacuum_scale_factor = 0.01);
11. Bulk Load Slower Than Expected
Symptom: pg_ripple.load_turtle() or pg_ripple.load_ntriples() runs much slower than the documented 50K–200K triples/sec.
Cause: Small batch sizes, synchronous commit overhead, or insufficient work_mem.
Diagnostic:
SHOW synchronous_commit;
SHOW work_mem;
SHOW maintenance_work_mem;
Fix:
-- Disable synchronous commit for bulk loads
SET synchronous_commit = off;
-- Increase work memory
SET work_mem = '256MB';
SET maintenance_work_mem = '2GB';
-- Use the batch loading functions
SELECT pg_ripple.load_turtle_file('/path/to/data.ttl');
Disabling synchronous commit risks losing the last few transactions on a crash. Only use this for bulk loads that can be re-run.
12. RDF-Star Parse Error
Symptom: Loading RDF-star data fails with unexpected token or invalid quoted triple.
Cause: The input file uses RDF-star syntax (<<>>) but the parser is not in RDF-star mode, or the syntax is malformed.
Diagnostic: Check the file around the reported line number for syntax issues. Common problems:
- Nested
<<>>without proper whitespace - Missing datatype on literal objects inside quoted triples
- Using Turtle-star syntax in N-Triples files (or vice versa)
Fix: Verify the file uses the correct format. For Turtle-star:
<<:Alice :knows :Bob>> :since "2024"^^xsd:gYear .
For N-Triples-star, every term must be fully qualified — no prefixes.
13. SHACL Validation Queue Backlog
Symptom: pg_ripple.validation_queue_depth() returns a large number; validation results are delayed.
Cause: High write throughput is generating validations faster than the async validator can process them.
Diagnostic:
SELECT pg_ripple.validation_queue_depth();
SELECT pg_ripple.stats();
Fix:
- Increase the validation worker's processing capacity (if applicable)
- Temporarily switch to synchronous validation during low-traffic periods:
SET pg_ripple.enforce_constraints = 'error';
- Reduce write batch sizes to give the validator time to catch up
14. Plan Cache Thrashing
Symptom: SPARQL query latency is inconsistent. The first execution of a query pattern is slow, but subsequent runs are fast — then it becomes slow again.
Cause: The plan cache (pg_ripple.plan_cache_size) is too small for the number of distinct query patterns. Plans are evicted and recompiled repeatedly.
Diagnostic:
SHOW pg_ripple.plan_cache_size;
-- Estimate distinct query patterns in your workload
-- (application-level logging required)
Fix:
-- Increase the plan cache
SET pg_ripple.plan_cache_size = 1024;
If the number of distinct patterns exceeds any reasonable cache size, consider parameterizing queries to reduce pattern diversity.
15. "relation _pg_ripple.vp_XXXXX does not exist"
Symptom: SPARQL queries fail with a "relation does not exist" error for a specific VP table.
Cause: The predicates catalog references a VP table that was dropped or never created. This can happen after an incomplete migration or manual DDL.
Diagnostic:
-- Check the predicates catalog
SELECT id, table_oid, triple_count
FROM _pg_ripple.predicates
WHERE id = XXXXX;
-- Verify the table exists
SELECT oid FROM pg_class WHERE oid = (
SELECT table_oid FROM _pg_ripple.predicates WHERE id = XXXXX
);
Fix:
-- Rebuild the VP table for the predicate
SELECT pg_ripple.reindex_predicate(XXXXX);
If the data is lost, the predicate entry should be removed:
DELETE FROM _pg_ripple.predicates WHERE id = XXXXX;
Directly modifying _pg_ripple.predicates bypasses integrity checks. Only do this as a last resort after confirming the VP table is genuinely missing.
16. "permission denied for schema _pg_ripple"
Symptom: Non-superuser connections get permission errors when running SPARQL queries.
Cause: The user does not have USAGE on _pg_ripple and pg_ripple schemas.
Fix:
GRANT USAGE ON SCHEMA pg_ripple TO myuser;
GRANT USAGE ON SCHEMA _pg_ripple TO myuser;
GRANT SELECT ON ALL TABLES IN SCHEMA _pg_ripple TO myuser;
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA pg_ripple TO myuser;
General Diagnostic Commands
A quick-reference set of commands for any troubleshooting session:
-- Extension health
SELECT pg_ripple.canary();
SELECT pg_ripple.stats();
-- PostgreSQL activity
SELECT pid, state, query, wait_event_type, wait_event
FROM pg_stat_activity
WHERE datname = current_database();
-- Lock contention
SELECT * FROM pg_locks WHERE NOT granted;
-- Table sizes in _pg_ripple
SELECT relname, pg_size_pretty(pg_total_relation_size(oid))
FROM pg_class
WHERE relnamespace = '_pg_ripple'::regnamespace
ORDER BY pg_total_relation_size(oid) DESC
LIMIT 20;
-- GUC settings
SELECT name, setting, source
FROM pg_settings
WHERE name LIKE 'pg_ripple.%'
ORDER BY name;
Lost Deletes After Merge (v0.37.0+)
Symptom: Triples that were deleted still appear in query results after a background merge cycle completes.
Cause: Before v0.37.0, the merge worker did not hold a per-predicate advisory lock during the delta→main swap. A DELETE that arrived after main_new was built but before the truncate of the tombstones table would have its tombstone deleted in the same truncate, leaving the triple alive in the new main.
Detection:
-- Check system health with diagnostic_report
SELECT key, value FROM pg_ripple.diagnostic_report()
WHERE key IN ('schema_version', 'merge_backlog_rows');
If schema_version is older than 0.37.0, upgrade to get the fix.
Fix:
-
Upgrade to v0.37.0 or later:
ALTER EXTENSION pg_ripple UPDATE TO '0.37.0'; -
Verify the fix is active —
diagnostic_report()reports the correct version:SELECT value FROM pg_ripple.diagnostic_report() WHERE key = 'schema_version'; -- Should return: 0.37.0 -
After upgrade, the merge worker acquires
pg_advisory_xact_lock(pred_id)(exclusive) before the delta→main swap, and the delete path acquirespg_advisory_xact_lock_shared(pred_id)before inserting tombstones. These two lock modes are incompatible, guaranteeing serialization.
Impact: Low — requires an unlucky timing window during a merge cycle. Most deployments will not observe lost deletes in practice, but correctness-critical workloads should upgrade.
Security
pg_ripple provides multiple layers of security: PostgreSQL's native authentication and authorization, named-graph row-level security (RLS), SQL injection prevention through dictionary encoding, and secure configuration of the pg_ripple_http companion service.
Authentication and Authorization
pg_ripple relies entirely on PostgreSQL's built-in authentication (pg_hba.conf) and role-based access control. There is no separate user database.
Minimum Privileges for SPARQL Queries
-- Create a read-only role
CREATE ROLE sparql_reader LOGIN PASSWORD 'strong_password';
GRANT USAGE ON SCHEMA pg_ripple TO sparql_reader;
GRANT USAGE ON SCHEMA _pg_ripple TO sparql_reader;
GRANT SELECT ON ALL TABLES IN SCHEMA _pg_ripple TO sparql_reader;
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA pg_ripple TO sparql_reader;
Minimum Privileges for Data Loading
-- Create a writer role
CREATE ROLE sparql_writer LOGIN PASSWORD 'strong_password';
GRANT USAGE ON SCHEMA pg_ripple TO sparql_writer;
GRANT USAGE ON SCHEMA _pg_ripple TO sparql_writer;
GRANT SELECT, INSERT, DELETE ON ALL TABLES IN SCHEMA _pg_ripple TO sparql_writer;
GRANT USAGE ON ALL SEQUENCES IN SCHEMA _pg_ripple TO sparql_writer;
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA pg_ripple TO sparql_writer;
Use ALTER DEFAULT PRIVILEGES to ensure newly created VP tables (created when new predicates are encountered) inherit the correct grants:
ALTER DEFAULT PRIVILEGES IN SCHEMA _pg_ripple
GRANT SELECT ON TABLES TO sparql_reader;
ALTER DEFAULT PRIVILEGES IN SCHEMA _pg_ripple
GRANT SELECT, INSERT, DELETE ON TABLES TO sparql_writer;
Named-Graph Row-Level Security
pg_ripple supports fine-grained access control at the named-graph level using PostgreSQL's row-level security (RLS) infrastructure. This allows different users to see different subsets of the knowledge graph.
Enabling Graph RLS
-- Enable RLS on all VP tables
SELECT pg_ripple.enable_graph_rls();
This creates RLS policies on every VP table (including vp_rare) that filter rows based on the g (graph) column.
Granting Graph Access
-- Grant a role access to a specific named graph
SELECT pg_ripple.grant_graph('sparql_reader', 'http://example.org/confidential');
-- Grant access to the default graph (g = 0)
SELECT pg_ripple.grant_graph('sparql_reader', '');
-- Grant access to all graphs
SELECT pg_ripple.grant_graph('sparql_reader', '*');
Revoking Graph Access
-- Revoke access to a specific graph
SELECT pg_ripple.revoke_graph('sparql_reader', 'http://example.org/confidential');
How It Works
When graph RLS is enabled:
- Each VP table gets an RLS policy that checks the
gcolumn against the user's allowed graph IDs - The dictionary encodes graph IRIs to
i64identifiers - An internal mapping table (
_pg_ripple.graph_grants) stores(role, graph_id)pairs - PostgreSQL enforces the policy transparently — SPARQL queries automatically filter results
PostgreSQL superusers bypass RLS by default. To enforce graph security even for superusers, the user must explicitly SET row_security = on and not be a table owner. For production, use non-superuser roles for application connections.
Example: Multi-Tenant Knowledge Graph
-- Create tenant roles
CREATE ROLE tenant_a LOGIN PASSWORD 'pw_a';
CREATE ROLE tenant_b LOGIN PASSWORD 'pw_b';
-- Grant base access
GRANT USAGE ON SCHEMA pg_ripple TO tenant_a, tenant_b;
GRANT USAGE ON SCHEMA _pg_ripple TO tenant_a, tenant_b;
GRANT SELECT ON ALL TABLES IN SCHEMA _pg_ripple TO tenant_a, tenant_b;
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA pg_ripple TO tenant_a, tenant_b;
-- Enable graph RLS
SELECT pg_ripple.enable_graph_rls();
-- Tenant A sees only their graph
SELECT pg_ripple.grant_graph('tenant_a', 'http://example.org/tenant-a');
-- Tenant B sees only their graph
SELECT pg_ripple.grant_graph('tenant_b', 'http://example.org/tenant-b');
-- Both see shared reference data
SELECT pg_ripple.grant_graph('tenant_a', 'http://example.org/shared');
SELECT pg_ripple.grant_graph('tenant_b', 'http://example.org/shared');
Now SPARQL queries run by tenant_a will only see triples in tenant-a and shared graphs, with no application-level filtering required.
SQL Injection Prevention
pg_ripple's architecture provides strong defense against SQL injection by design.
Dictionary Encoding as a Security Layer
All SPARQL queries go through a multi-step translation pipeline:
- Parse: SPARQL text is parsed by
spargebrainto an abstract algebra tree - Encode: All bound constants (IRIs, literals) are dictionary-encoded to
i64integers before SQL generation - Generate: SQL is constructed using parameterized queries with integer placeholders
- Execute: SQL runs via
pgrx::SpiClientwith bound parameters
Because VP tables store only BIGINT columns (s, o, g, i, source), there is no surface for string-based SQL injection. Even if a malicious IRI is passed in a SPARQL query, it is hashed to an integer before any SQL is generated.
Table Name Safety
VP table references use OID lookups from _pg_ripple.predicates, not string concatenation:
#![allow(unused)] fn main() { // Internal: table names are never interpolated from user input let table_oid = predicates::get_table_oid(predicate_id)?; // SQL uses the OID directly: FROM pg_class WHERE oid = $1 }
User-Facing Function Safety
Functions that accept text input (like pg_ripple.sparql()) parse the SPARQL text through spargebra, which rejects anything that is not valid SPARQL. No raw SQL is passed through.
File-Path Loaders and Superuser Requirement
Functions that read from the server's filesystem require superuser privileges:
| Function | Requires Superuser | Reason |
|---|---|---|
pg_ripple.load_turtle_file(path) | Yes | Reads arbitrary filesystem paths |
pg_ripple.load_ntriples_file(path) | Yes | Reads arbitrary filesystem paths |
pg_ripple.load_rdfxml_file(path) | Yes | Reads arbitrary filesystem paths |
pg_ripple.load_turtle(text) | No | Parses in-memory text only |
pg_ripple.load_ntriples(text) | No | Parses in-memory text only |
File-path loaders can read any file the PostgreSQL process has access to. Never grant superuser to application roles. Instead, load data as a superuser and grant read access to application roles via schema permissions.
Safe Bulk Load Pattern
-- As superuser: load the data
SELECT pg_ripple.load_turtle_file('/data/import/dataset.ttl');
-- As superuser: grant access to the app role
GRANT SELECT ON ALL TABLES IN SCHEMA _pg_ripple TO app_role;
pg_ripple_http Security
The pg_ripple_http companion service exposes a SPARQL Protocol endpoint over HTTP. Secure it appropriately.
TLS Configuration
Always run pg_ripple_http behind TLS in production:
# pg_ripple_http.toml
[server]
bind = "0.0.0.0:8443"
tls_cert = "/etc/ssl/certs/pg_ripple_http.crt"
tls_key = "/etc/ssl/private/pg_ripple_http.key"
SPARQL queries may contain sensitive data patterns. Without TLS, queries and results are transmitted in plaintext. Always terminate TLS either at the service or at a reverse proxy.
Authentication
Configure pg_ripple_http to authenticate incoming requests:
[auth]
# HTTP Basic authentication backed by PostgreSQL roles
method = "pg_role"
# Or use a static API key
# method = "api_key"
# api_key = "your-secret-key-here"
With pg_role authentication, HTTP Basic credentials are forwarded to PostgreSQL. Graph RLS policies apply to the authenticated role.
Reverse Proxy Setup
For production, place pg_ripple_http behind a reverse proxy:
# nginx configuration
server {
listen 443 ssl;
server_name sparql.example.org;
ssl_certificate /etc/ssl/certs/sparql.crt;
ssl_certificate_key /etc/ssl/private/sparql.key;
location /sparql {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# Rate limiting
limit_req zone=sparql burst=20 nodelay;
}
}
CORS Configuration
If the SPARQL endpoint is accessed from browser applications:
[cors]
allowed_origins = ["https://app.example.org"]
allowed_methods = ["GET", "POST"]
allowed_headers = ["Content-Type", "Authorization"]
max_age = 3600
Do not set allowed_origins = ["*"] in production. This allows any website to send SPARQL queries to your endpoint using the visitor's credentials.
Network Isolation
Production Topology
┌─────────────┐ TLS ┌──────────────────┐ Unix socket ┌─────────────┐
│ Clients │ ────────────→ │ pg_ripple_http │ ──────────────────→ │ PostgreSQL │
│ │ │ (reverse proxy) │ │ (pg_ripple) │
└─────────────┘ └──────────────────┘ └─────────────┘
Recommendations
- PostgreSQL: bind to
localhostor a private network interface only. Never expose port 5432 to the public internet.
# postgresql.conf
listen_addresses = '127.0.0.1'
-
pg_ripple_http: connect to PostgreSQL via Unix socket for lowest latency and no network exposure.
-
Firewall rules: only allow traffic on the HTTPS port (443) from expected client networks.
# iptables example
iptables -A INPUT -p tcp --dport 443 -s 10.0.0.0/8 -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -j DROP
iptables -A INPUT -p tcp --dport 5432 -j DROP
- pg_hba.conf: restrict connections by source IP and authentication method:
# TYPE DATABASE USER ADDRESS METHOD
local all postgres peer
host mydb pg_ripple_http 127.0.0.1/32 scram-sha-256
host mydb sparql_reader 10.0.0.0/8 scram-sha-256
host all all 0.0.0.0/0 reject
Always use scram-sha-256 authentication (the default in PostgreSQL 18). Avoid md5 and never use trust in production.
Security Checklist
| Item | Status |
|---|---|
shared_preload_libraries includes only trusted extensions | ☐ |
| Non-superuser roles used for all application connections | ☐ |
| Graph RLS enabled for multi-tenant deployments | ☐ |
pg_hba.conf restricts connections to known networks | ☐ |
TLS enabled on pg_ripple_http or reverse proxy | ☐ |
| File-path loaders restricted to superuser only (default) | ☐ |
synchronous_commit enabled for production (not off) | ☐ |
Connection pooler uses scram-sha-256 | ☐ |
| CORS origins are not wildcarded | ☐ |
| PostgreSQL logs enabled for audit trail | ☐ |
| Regular security updates for PostgreSQL and pg_ripple | ☐ |
Audit Logging
Enable PostgreSQL's logging to maintain an audit trail:
# postgresql.conf
log_statement = 'all' # or 'ddl' for schema changes only
log_connections = on
log_disconnections = on
log_line_prefix = '%t [%p] %u@%d '
For fine-grained audit logging, consider the pgaudit extension alongside pg_ripple.
pg_ripple logs the generated SQL via PostgreSQL's standard statement logging. To see the original SPARQL text, enable log_statement = 'all' — the SPARQL text appears as the argument to pg_ripple.sparql().
Log-Hook Audit and Secret Suppression (LOG-HOOK-01)
v0.76.0 audit finding: No
RegisterEmitLogHookis installed to suppress secrets from PostgreSQL error messages. A defense-in-depth audit was performed to verify that no error or warning call site leaks raw secrets.
Audit Results
The following audit was performed on all pgrx::error!(), pgrx::warning!(), tracing::error!(), and tracing::warn!() call sites in the pg_ripple extension and pg_ripple_http companion service:
| Component | Secrets handled | Logged in errors? | Notes |
|---|---|---|---|
src/security_api.rs | None (policy names, graph IRIs) | No | Only structural metadata |
src/llm/mod.rs | LLM API key (env var) | No | Key is read from env at call time and not referenced in error paths |
pg_ripple_http auth check | Bearer token (HMAC comparison) | No | check_token uses constant_time_eq; returns 401 without token value |
pg_ripple_http Arrow Flight | HMAC signing secret | No | Error returns generic "HMAC key error: {e}" — e is a key-length error only |
pg_ripple_http/src/common.rs | auth_token, datalog_write_token | No | Tokens stored in AppState but never interpolated into error messages |
pg_ripple_http/src/main.rs | ARROW_FLIGHT_SECRET (env var) | No | Env var read at startup; not referenced in tracing calls |
No RegisterEmitLogHook Required
Based on the audit, no error path in the current codebase logs raw HMAC keys, connection strings, bearer tokens, or other credentials. A RegisterEmitLogHook is therefore not required at this time.
If you add a new error path that touches secrets, follow these guidelines:
- Do not interpolate secret values (
token,key,password) intopgrx::error!()orpgrx::warning!()messages. - Use descriptive error types without values (e.g.,
"HMAC key error: invalid key length"not"bad key: {key_value}"). - If a future release handles user-supplied credentials in a hot path, consider installing a
RegisterEmitLogHookto scrub sensitive patterns from log lines.
Connection Strings
PostgreSQL connection strings (DSN) containing passwords may appear in pg_log via log_connections = on. Use pg_hba.conf with scram-sha-256 and a connection pooler (e.g., PgBouncer) that authenticates separately so application DSNs never include inline passwords.
Supported Authentication Schemes (S13-10, v0.86.0)
pg_ripple_http accepts Bearer token authentication only.
| Scheme | Supported | Notes |
|---|---|---|
| Bearer (RFC 6750) | ✅ Yes | Set PG_RIPPLE_HTTP_AUTH_TOKEN env var; pass Authorization: Bearer <token> |
| Basic (RFC 7617) | ❌ No | Not accepted; Basic sends credentials in cleartext (even over TLS, base64 is trivially reversible) |
| API Key (header) | ❌ No | Not a standard HTTP auth scheme; use Bearer instead |
| mTLS | ⏳ Future | Planned for v1.x |
Recommendation: rotate the Bearer token regularly. Use a 32-byte (256-bit) or larger random secret:
openssl rand -base64 32
Set the token as an environment variable or via Docker secrets — never hardcode it in docker-compose.yml or configuration files.
Metrics Port Isolation (S13-09, v0.86.0)
The GET /metrics endpoint exposes Prometheus-format operational data including query counts,
error rates, cache statistics, and federation endpoint information. This endpoint does not
require authentication — it relies on network-level isolation instead.
Do not expose the metrics port (default: same as the API port 7878) to untrusted networks.
In production deployments:
- Run
pg_ripple_httpin a private network (e.g., Kubernetes pod network, Docker internal network). - Use a reverse proxy (nginx, Envoy, Traefik) to expose only
/sparql,/datalog, and/healthexternally. - Restrict
/metricsto Prometheus scraper IPs via the reverse proxy ACL. - Consider deploying
pg_ripple_httpwith a separate metrics port binding (future feature).
CORS permissive mode warning: when PG_RIPPLE_HTTP_CORS_ORIGINS=* is set, every
cross-origin request increments the pg_ripple_http_cors_permissive_requests_total Prometheus
counter. Monitor this counter to detect unexpected cross-origin traffic. Use a specific origin
allowlist in production (e.g., PG_RIPPLE_HTTP_CORS_ORIGINS=https://app.example.com).
pg_ripple / pg_ripple_http Compatibility Matrix
pg_ripple (the PostgreSQL extension) and pg_ripple_http (the standalone HTTP companion
service) are versioned independently. This page documents which extension versions are compatible
with which HTTP companion versions and what guarantees apply to the combination.
Versioning policy
- pg_ripple follows semantic versioning tied to extension features and PostgreSQL catalog changes.
- pg_ripple_http follows its own version series (currently
0.x.y) since it is a standalone binary with its own release cadence. The HTTP companion version number tracks the minimum pg_ripple extension version it was tested against.
A given pg_ripple_http release is compatible with the extension version range
[tested_with, next_major). The HTTP companion logs a warning at startup if the installed
extension version is outside its known-compatible range.
Compatibility table
| pg_ripple_http version | pg_ripple extension range | Notes |
|---|---|---|
| 0.127.x | ≥ 0.126.0 | pg_tide relay bridge migration: CDC bridge publishes named outbox events via tide.outbox_publish; current pg_tide pins use 0.33.0 and the stable relay_set_*_v2 APIs |
| 0.123.x | ≥ 0.122.0 | A17 observability & docs: replica pool Prometheus gauges (OBS-M-01), rule-library stream latency/error counters (OBS-M-02), bench_workload_result() SQL wrapper (ERG-L-01) |
| 0.122.x | ≥ 0.121.0 | A17 god-module decomposition & test coverage closure (H17-02); WatDiv correctness gating; pg_regress coverage for v0.119.0–v0.120.0 features |
| 0.121.x | ≥ 0.120.0 | A17 security hardening (H17-01 SSRF, SEC-M-03 CGNAT/multicast, BUG-M-01 silent degradation, OBS-L-01 mutation journal); mutation journal for rule-library ops |
| 0.120.x | ≥ 0.119.0 | Feature 11: rule-library federation (publish/subscribe over Arrow Flight); Feature 12: read-replica routing (?replica=ok); compat_check() JSON schema |
| 0.119.x | ≥ 0.118.0 | Feature 6: federation circuit breaker Prometheus gauges; Feature 7: bench_workload() BSBM/WatDiv/PageRank/PPRL profiles; Allen's interval relations |
| 0.118.x | ≥ 0.117.0 | Feature 4: diagnostic snapshot bundle (/admin/diagnostic-snapshot); Feature 5: tenant quota enforcement; multi-tenant knowledge graphs |
| 0.117.x | ≥ 0.116.0 | Feature 3: PPRL Bloom-filter encodes; neuro-symbolic entity resolution; LLM explanation cache; proof-tree generation |
| 0.116.x | ≥ 0.115.0 | A16 remediation arc: ER stage latency (M16-03), owl:sameAs assertion counter, Bayesian propagation latency, conflict detection counter; advisory lifecycle policy (M16-21) |
| 0.115.x | ≥ 0.114.0 | A15 remediation: er_stage_duration_seconds histogram labels (M16-03); PPRL/LLM/proof-tree observability; GNN link-prediction integration |
| 0.114.x | ≥ 0.113.0 | A15 High/Medium: bidi_relay_dropped_total counter (H15-03); merge/stratum/SHACL/CDC Prometheus metrics (M15-19); WatDiv 100-template suite |
| 0.113.x | ≥ 0.112.0 | UNNEST-array bulk load path; Citus object shard pruning; direct COPY bulk load; per-graph RLS; explain_inference() |
| 0.93.x | ≥ 0.92.0 | pg_tide integration: pg_tide_available() SQL function; BIDI relay comments updated to pg_tide API |
| 0.92.x | ≥ 0.88.0 | A14 Low polish: PageRank bounds, damping guide, SERVICE SILENT TLS, SOURCE_DATE_EPOCH, conformance badge CI |
| 0.91.x | ≥ 0.88.0 | OBS-01 PageRank IVM Prometheus gauges; HTTP-03 middleware extraction; HTTP-04 EXPLAIN row estimation; API-05 explain_pagerank_json(); CDC watermark batching GUCs |
| 0.90.x | ≥ 0.88.0 | A14 Medium correctness/performance: PageRank streaming, convergence-norm GUC, advisory lock, module splits |
| 0.89.x | ≥ 0.88.0 | A14 High remediation: check_auth_write on mutating handlers, GUC name audit, rate limit 100 req/s default |
| 0.88.x | ≥ 0.87.0 | Adds pagerank_scores, centrality_scores, pagerank_dirty_edges; requires COMPATIBLE_EXTENSION_MIN = "0.87.0" |
| 0.87.x | ≥ 0.86.0 | Adds _pg_ripple.confidence, shacl_score_log; probabilistic Datalog, fuzzy SPARQL filters, soft SHACL scoring |
| 0.86.x | ≥ 0.85.0 | D13-01 (v0.86.0): SSE streaming, graceful shutdown, new Prometheus metrics (O13-02), Arrow 413 guard (S13-08), CORS counter (S13-03), structured JSON logs (O13-04) |
| 0.85.x | ≥ 0.84.0 | COMPATIBLE_EXTENSION_MIN bumped to 0.84.0 (S13-05); strict compat mode (PG_RIPPLE_HTTP_STRICT_COMPAT); schema.rs/federation.rs splits; encode_batch GUC API |
| 0.84.x | ≥ 0.83.0 | OpenAPI spec served at /openapi; per-query-type metrics (METRICS-LABELS-01); /health/ready deep check (HEALTH-DEEP-01); SPARQL Update in HTTP |
| 0.83.x | ≥ 0.82.0 | CDC outbox bridge triggers, JSON-LD event serializer, vocabulary templates, pg-trickle detection |
| 0.82.x | ≥ 0.80.0 | SBOM (cargo-cyclonedx), structured error bodies (HTTP-ERR-01), Arrow ticket nonce cache (FLIGHT-NONCE-01), per-IP governor |
| 0.81.x | ≥ 0.80.0 | SHACL-SPARQL constraints, COPY rdf FROM, RAG hardening, CDC lifecycle events, OpenAPI spec |
| 0.80.x | ≥ 0.79.0 | SQL-injection parameterization (SQL-INJ-01), /explorer auth gate (EXPLORER-AUTH-01) |
| 0.76.x | ≥ 0.79.0 | COMPAT-MIN-01 (v0.80.0): requires sparql_update_cursor() (v0.76.0) and feature_status() wcoj/shacl_sparql entries (v0.79.0); /explorer now requires auth (EXPLORER-AUTH-01) |
| 0.75.x | ≥ 0.78.0 | Bidirectional integration operations (v0.78.0), SPARQL subscription SSE (v0.73.0) |
| 0.74.x | ≥ 0.77.0 | Bidirectional integration primitives (v0.77.0), SPARQL 1.2 tracking (v0.73.0) |
| 0.73.x | ≥ 0.76.0 | Fuzz hardening (v0.76.0), CONTRIBUTING.md (v0.73.0), Helm chart SHA pin |
| 0.16.x | ≥ 0.70.0 | First version with Body::from_stream Arrow Flight; compatibility check added (v0.71.0 COMPAT-01) |
| 0.15.x | 0.66.0 – 0.69.x | Arrow Flight security (FLIGHT-SEC-02), SPARQL cursor streaming (STREAM-01) |
| 0.14.x | 0.63.0 – 0.65.x | CONSTRUCT writeback rules (v0.63.0), Datalog REST API (v0.39.0) |
| 0.13.x | 0.57.0 – 0.62.x | OWL 2 EL/QL profiles, KGE embeddings, visual graph explorer |
| 0.12.x | 0.51.0 – 0.56.x | Non-root container, HTTP streaming, OTLP tracing |
| 0.11.x | 0.40.0 – 0.50.x | SPARQL cursors, explain/observability, OpenTelemetry |
| 0.10.x | 0.38.0 – 0.39.x | Module restructuring, all 27 Datalog SQL functions |
| 0.9.x | 0.33.0 – 0.37.x | Docs site rebuild, parallel Datalog, HTAP stability |
| ≤ 0.8.x | 0.15.0 – 0.32.x | HTTP endpoint, bulk-load, basic SPARQL |
Startup version check
Starting with pg_ripple_http 0.16.0, the HTTP companion performs a compatibility check at
startup. It queries the installed extension version and compares it against its known-compatible
range. If the extension is older than the minimum supported version:
- Warning is logged: the companion starts but logs a prominent warning.
- The
GET /readyendpoint returns HTTP 503 with{"compatible": false, ...}if the extension is below the hard minimum.
The check can be disabled with PG_RIPPLE_HTTP_SKIP_COMPAT_CHECK=1 for testing scenarios where
an older extension is intentionally paired with a newer companion.
⚠ Production warning (COMPAT-DOC-01 / MF-R):
PG_RIPPLE_HTTP_SKIP_COMPAT_CHECK=1is intended only for testing and development where you deliberately need to run a mismatched pair (e.g., integration tests against an older extension). Do not set this in production environments. Skipping the check allows the HTTP companion to serve requests to an incompatible extension, which can result in silent data corruption, unexpected errors, or security vulnerabilities when new SQL functions are called against an older extension schema. If you need to silence the compatibility warning in production, upgrade the extension or the companion to a compatible version pair instead.
Independent versioning rationale
The HTTP companion is distributed as a pre-built binary. Extension upgrades (ALTER EXTENSION pg_ripple UPDATE) are applied in-database and do not require rebuilding or redeploying the
companion. This means:
- A single
pg_ripple_httpbinary can serve multiple extension versions within its compatible range. - Extension-only changes (new SQL functions, GUCs, performance improvements) do not require a companion update.
- Breaking API changes (new required request fields, removed endpoints) do require a companion update.
Upgrade procedure
- Upgrade the extension first:
ALTER EXTENSION pg_ripple UPDATE TO 'X.Y.Z'; - Restart or redeploy
pg_ripple_httpif a companion upgrade is also required. - Verify compatibility via
GET /ready— returns{"compatible": true}when correctly paired.
See also: Arrow Flight Reference, HTTP API.
pg_tide / pg_trickle Extension Compatibility
pg_ripple integrates with two companion PostgreSQL extensions:
| Extension | Purpose | Required for |
|---|---|---|
| pg_trickle ≥ 0.46.0 | Incremental materialized view maintenance (IVM only) | SPARQL views, Datalog views, CONSTRUCT/DESCRIBE/ASK views, ExtVP |
| pg_tide ≥ 0.33.0 | Relay, outbox, and inbox subsystem | Bidirectional relay (BIDI-OUTBOX-01, BIDI-INBOX-01), hub-and-spoke integration |
Architecture change (pg_ripple v0.93.0 / pg_trickle v0.46.0): pg_trickle v0.46.0 extracted the relay, outbox, and inbox subsystem (~6,150 Rust LOC + ~2,500 SQL LOC) into the new standalone
pg_tideextension (trickle-labs/pg-tide). Starting with v0.93.0, pg_ripple recognises pg_tide as the recommended relay transport layer.
pg_ripple + pg_trickle compatibility
| pg_ripple version | pg_trickle version | IVM status |
|---|---|---|
| ≥ 0.112.0 | ≥ 0.57.0 | ✅ IVM only (relay features in pg_tide) |
| ≥ 0.93.0 | ≥ 0.46.0 | ✅ IVM only (relay features in pg_tide) |
| ≥ 0.93.0 | ≤ 0.45.0 | ⚠ IVM works; relay features require manual migration to pg_tide |
| 0.52.0 – 0.92.0 | any | ✅ Full relay+IVM (pre-extraction) |
pg_ripple + pg_tide compatibility
| pg_ripple version | pg_tide version | Relay status |
|---|---|---|
| ≥ 0.127.0 | ≥ 0.33.0 | ✅ Full relay support with named outbox publishing via tide.outbox_publish() and stable relay_set_*_v2 configuration APIs |
| 0.93.0 – 0.126.x | ≥ 0.4.0 | ✅ Relay API detection and docs; CDC bridge users should upgrade to 0.127.0 for current named-outbox publish semantics |
| ≥ 0.93.0 | 0.1.0 – 0.3.x | ✅ Core relay support (tide.* API, older feature set) |
| ≥ 0.93.0 | not installed | ⚠ Core pg_ripple + IVM work; bidirectional relay unavailable |
| < 0.93.0 | any | pg_tide not yet supported (use pg_trickle ≤ 0.45.0 for relay) |
Recommended stack (pg_ripple ≥ 0.127.0)
CREATE EXTENSION pg_tide; -- relay, outbox, inbox (trickle-labs/pg-tide ≥ 0.33.0)
CREATE EXTENSION pg_trickle; -- IVM (trickle-labs/pg-trickle ≥ 0.46.0)
CREATE EXTENSION pg_ripple; -- RDF triple store (≥ 0.127.0)
Call pg_ripple.pg_tide_available() to verify pg_tide is installed at runtime.
Call pg_ripple.pg_trickle_available() to verify pg_trickle is installed at runtime.
Best Practices
This section covers recommended patterns and operational guidance for production pg_ripple deployments.
- Data Modeling — RDF-star vs reification, named graph partitioning, blank-node scoping, LPG mapping
- Bulk Loading — batch sizing, VP promotion, ANALYZE, blank-node pitfalls
- SPARQL Patterns — star patterns, filter pushdown, property path recipes, resource exhaustion safeguards
Bulk Loading Best Practices
UNNEST-array batch INSERT path (v0.113.0)
Since v0.113.0, pg_ripple.bulk_load_use_copy defaults to on. When enabled,
load_ntriples() and load_turtle() write dictionary-encoded triples via the
UNNEST-array batch INSERT helper (copy_into_vp()), which sends entire batches
as BIGINT[] arrays and inserts them with a single parameterized SQL call.
Benchmark results (10 million triple N-Triples file, PG 18, 16 GB RAM):
| Mode | Throughput | Notes |
|---|---|---|
| Per-row VALUES INSERT (pre-v0.113.0 default) | ~180K triples/s | Many small SQL strings |
| UNNEST-array batch INSERT (v0.113.0 default) | ~1.1M triples/s | ~5–10× faster |
The path is shared with the R2RML and CDC loaders so all ingestion paths benefit.
To revert to the old per-row VALUES INSERT behaviour for debugging:
SET pg_ripple.bulk_load_use_copy = off;
SELECT pg_ripple.load_ntriples($$ <data> $$);
SET pg_ripple.bulk_load_use_copy = on; -- restore default
Batch size
load_ntriples() and load_turtle() process the entire input in a single batch. For very large files (hundreds of millions of triples) split the input into chunks of 1–10 million triples each and load them sequentially. This keeps transaction sizes manageable and allows periodic ANALYZE runs between batches.
# Split a large NT file into 1M-triple chunks
split -l 1000000 large.nt chunk_
for f in chunk_*; do
psql -c "SELECT pg_ripple.load_ntriples_file('/data/$f');"
psql -c "ANALYZE _pg_ripple.vp_rare;"
done
VP promotion threshold
The default threshold is 1000 triples. For workloads dominated by a small number of very common predicates (e.g. rdf:type) consider lowering the threshold to trigger promotion sooner:
SET pg_ripple.vp_promotion_threshold = 100;
After promotion, dedicated VP tables get B-tree indexes on (s, o) and (o, s), which are much faster for predicate-specific lookups than the shared vp_rare table.
ANALYZE after large loads
The PostgreSQL query planner relies on table statistics to choose join strategies. After loading more than ~100K triples, run:
-- Analyze the shared rare table
ANALYZE _pg_ripple.vp_rare;
-- Analyze any newly promoted VP tables
-- (replace XXX with the actual predicate IDs shown in _pg_ripple.predicates)
ANALYZE _pg_ripple.vp_XXX;
Without fresh statistics the planner may choose a sequential scan over an index scan on the VP tables.
Blank-node scoping
Each call to a bulk-load function is an independent blank-node scope. If you load two files that each contain _:b0, they will get different dictionary IDs — as required by the RDF specification.
This means: do not split an N-Triples file that uses blank nodes across multiple load_ntriples_file() calls if the blank nodes are shared across the split point. Either load the complete file in one call, or use globally unique blank node IDs (e.g. UUID-based _:b_{uuid}).
Using COPY for extremely large datasets
For multi-billion-triple loads, consider a two-phase approach:
- Pre-encode terms to
BIGINTIDs usingpg_ripple.encode_term()in a staging script - Use PostgreSQL
COPYto stream data directly into the target VP tables
This bypasses the per-row dictionary lookup overhead in the Rust parse-and-insert path. See the Bulk Load implementation notes in plans/implementation_plan.md for details.
HTAP delta growth during bulk loads (v0.6.0)
With v0.6.0's HTAP storage layout, all inserts land in delta tables first. During a large bulk load the delta tables can grow very large before the merge worker has a chance to compress them into main.
Symptoms of runaway delta growth:
- Queries on bulk-loaded predicates scan large delta tables (slower than expected)
SELECT pg_ripple.stats() -> 'unmerged_delta_rows'shows millions of rowspg_stat_user_tablesshows very highn_live_tupon*_deltatables
Strategies:
Option A: Tune merge aggressiveness
Before starting a large load, lower the merge thresholds so the worker keeps up:
ALTER SYSTEM SET pg_ripple.merge_threshold = 10000;
ALTER SYSTEM SET pg_ripple.latch_trigger_threshold = 5000;
ALTER SYSTEM SET pg_ripple.merge_interval_secs = 5;
SELECT pg_reload_conf();
After the load, restore production values.
Option B: Periodic manual compact
For offline bulk loads where query freshness during the load is not important, call compact() at regular intervals:
for f in chunk_*; do
psql -c "SELECT pg_ripple.load_ntriples_file('/data/$f');"
# Compact every 10 chunks
if (( i % 10 == 0 )); then
psql -c "SELECT pg_ripple.compact();"
fi
((i++))
done
# Final compact when done
psql -c "SELECT pg_ripple.compact();"
Option C: Disable HTAP for the load predicate (advanced)
For predicates that will only ever be bulk-loaded (not streamed), you can keep them in the flat layout by migrating back after the load. This is an advanced use case — contact the maintainers for guidance.
Final cleanup
After a bulk load and compact cycle, run ANALYZE to update planner statistics:
-- Analyze the delta, main, and vp_rare tables
ANALYZE _pg_ripple.vp_rare;
-- Analyze all objects in the _pg_ripple schema
DO $$
DECLARE t text;
BEGIN
FOR t IN SELECT relname FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = '_pg_ripple'
AND c.relkind = 'r'
LOOP
EXECUTE 'ANALYZE _pg_ripple.' || quote_ident(t);
END LOOP;
END $$;
Parallel loads
Multiple concurrent load_ntriples() calls are safe — the dictionary insert uses ON CONFLICT DO NOTHING … RETURNING which is MVCC-safe. However, heavy concurrent writes to vp_rare can cause lock contention. For best throughput, load from a single database connection.
Data Modeling
When to use RDF-star vs reification
Reification (traditional RDF) represents a triple as a resource with four properties (rdf:subject, rdf:predicate, rdf:object, rdf:type rdf:Statement). It requires four extra triples per annotated statement and produces verbose query patterns.
RDF-star uses a quoted triple << s p o >> directly as a subject or object:
# RDF-star (compact)
<< <ex:alice> <ex:knows> <ex:bob> >> <ex:since> "2023-01-01"^^xsd:date .
# Reification (verbose — 4 extra triples)
<ex:stmt1> rdf:type rdf:Statement ;
rdf:subject <ex:alice> ;
rdf:predicate <ex:knows> ;
rdf:object <ex:bob> ;
<ex:since> "2023-01-01"^^xsd:date .
Use RDF-star for edge annotations (provenance, confidence, time ranges). Use reification only for legacy compatibility with stores that do not support RDF-star.
Named graphs for partitioning
Named graphs are a lightweight way to partition data by source, time, or topic without changing the triple structure:
-- Load provenance data into separate graphs
SELECT pg_ripple.load_nquads('
<ex:alice> <ex:knows> <ex:bob> <ex:source1> .
<ex:alice> <ex:knows> <ex:bob> <ex:source2> .
');
-- Query within a specific graph
SELECT * FROM pg_ripple.sparql('
SELECT ?s ?p ?o WHERE {
GRAPH <ex:source1> { ?s ?p ?o }
}
');
Blank nodes
Blank nodes are useful for anonymous intermediate resources — nodes that don't need a globally-unique IRI. Common uses:
- List encoding: each list element is a blank node with
rdf:firstandrdf:restpredicates - Structured values: a measurement with multiple facets (value, unit, uncertainty)
- Intermediate join nodes: n-ary relationships without reification
Pitfall: blank nodes have load-scope identity. _:b0 in two separate load_ntriples() calls gets two different dictionary IDs. If you need stable cross-load blank nodes, use IRI-based identifiers instead.
Subject-position vs object-position quoted triples
pg_ripple supports both:
# Object-position: annotating the statement as an object
<ex:carol> <ex:asserted> << <ex:alice> <ex:knows> <ex:bob> >> .
# Subject-position: the statement has properties
<< <ex:alice> <ex:knows> <ex:bob> >> <ex:since> "2023"^^xsd:gYear .
Both are stored via encode_triple() in the dictionary and can be retrieved with decode_triple() or get_statement().
LPG-style edge properties via RDF-star
RDF-star maps cleanly onto LPG edge properties:
| LPG concept | RDF-star encoding |
|---|---|
Node alice | <ex:alice> |
Edge alice --[KNOWS]--> bob | <ex:alice> <ex:knows> <ex:bob> |
Edge property since = 2023 | << <ex:alice> <ex:knows> <ex:bob> >> <ex:since> "2023"^^xsd:gYear |
Node property name = "Alice" | <ex:alice> <ex:name> "Alice" |
This makes pg_ripple a natural backend for LPG data once the Cypher/GQL query layer is added (v0.13.0).
Interop format guide (v0.9.0)
Choose the right serialization format for the tool or context you are integrating with:
| Tool / Context | Recommended format | pg_ripple function |
|---|---|---|
| Protégé / OWL ontologies | RDF/XML | load_rdfxml() |
| Linked Data Platform (LDP) REST APIs | JSON-LD | export_jsonld() / sparql_construct_jsonld() |
| Command-line pipelines, streaming | N-Triples or N-Quads | export_ntriples() / export_nquads() |
| Human-readable files, Git storage | Turtle | export_turtle() / sparql_construct_turtle() |
| Large graph export (memory-efficient) | Streaming Turtle | export_turtle_stream() |
| SPARQL query results for APIs | JSON-LD CONSTRUCT | sparql_construct_jsonld() |
Protégé → RDF/XML
Protégé saves ontologies in OWL/RDF/XML by default. Load them directly:
-- Read the file into PostgreSQL (superuser only)
SELECT pg_ripple.load_rdfxml(pg_read_file('/data/ontology.owl'));
Linked Data Platform → JSON-LD
REST APIs built on LDP typically serve JSON-LD. Use export_jsonld() to get the current state:
SELECT pg_ripple.export_jsonld('https://myapp.example.org/graph/users');
For SPARQL-driven responses:
SELECT pg_ripple.sparql_construct_jsonld('
CONSTRUCT { ?s ?p ?o }
WHERE { ?s a <https://schema.org/Person> ; ?p ?o }
');
CLI / shell pipelines → N-Triples or N-Quads
For processing with rapper, riot, rdfpipe, or awk/grep on the command line:
psql -c "COPY (SELECT pg_ripple.export_ntriples()) TO STDOUT" > snapshot.nt
For multi-graph exports:
psql -c "COPY (SELECT pg_ripple.export_nquads(NULL)) TO STDOUT" > snapshot.nq
JSON-LD Framing for REST APIs (v0.17.0)
Frame-First API Design
When building a REST API backed by pg_ripple, design your JSON-LD frames before writing application code. The frame defines the exact shape of the API response, and export_jsonld_framed() generates the optimised SPARQL CONSTRUCT query automatically.
-- A frame for a "company with employees" API endpoint.
SELECT pg_ripple.export_jsonld_framed('{
"@context": {"schema": "https://schema.org/"},
"@type": "https://schema.org/Organization",
"https://schema.org/name": {},
"https://schema.org/employee": {
"https://schema.org/name": {},
"https://schema.org/email": {}
}
}'::jsonb);
Only the 3 VP tables (rdf:type, schema:name, schema:employee) are scanned — not all 10,000 tables in a large graph.
Using jsonld_frame_to_sparql for Inspection
Before deploying a frame-driven Before deploying a frame-driven Before deploying a frame-driven Before deploying a frame-driven Before deplps:B/schBefore deploying a frame-driven Before de": Before deploying a frame-theBefore deploying a frame-driven Before deploying a frame-driven Before deploying a frame-driven Before deploying a frame-driven Before deplps:B/schBefore deploying a frame-driven Before de": Before deploying a frame-theBefore deploying a frame-driven. Prefer framing for selective API responses.
- Named graph scoping: Pass a
graphIRI to restrict the CONSTRUCT to a single named graph, further reducing scan cost. - Repeated calls: Repeated call- Repeated calls: Repeated cam the SPARQL plan cache — the SPARQL�- Repeated calls: Repeated calche h- Repeated calls: Repeated call- fr-med
vscreate_f- ming_view`
| Use case | Recommended approach |
|---|---|
| One-off API request | export_jsonld_framed() |
| Live dashboard (high read throughput) | create_framing_view() with DIFFERENTIAL schedule |
| Constraint monitoring (violation detection) | create_framing_view() with IMMEDIATE refresh |
| Large export for data warehouse | create_framing_view() with FULL + long schedule |
SPARQL Patterns
Best practices for writing efficient SPARQL queries against pg_ripple.
Star patterns — let the planner collapse joins
When a single subject has multiple predicates, express them as separate triple patterns in the same WHERE clause. The SQL generator collapses these into a single scan with multiple table joins:
-- Good: star pattern — all predicates share the same subject variable
SELECT ?person ?name ?age WHERE {
?person <ex:worksAt> <ex:acme> .
?person <ex:name> ?name .
OPTIONAL { ?person <ex:age> ?age }
}
-- Avoid: separate subqueries for each predicate (forces extra joins)
SELECT ?person ?name ?age WHERE {
{ SELECT ?person WHERE { ?person <ex:worksAt> <ex:acme> } }
{ SELECT ?person ?name WHERE { ?person <ex:name> ?name } }
}
Filter pushdown
SPARQL FILTER expressions on dictionary-encoded constants are evaluated in the SQL WHERE clause at compile time. Constants are encoded to BIGINT before SQL is emitted — no dictionary lookups happen at query time.
For best performance, apply filters as early as possible (close to the bound variable in the triple pattern) rather than late in a wrapping subquery.
OPTIONAL vs INNER JOIN
Use OPTIONAL when a result row should still appear even if the optional pattern is not matched. This compiles to a LEFT JOIN. Use a plain triple pattern (inner join behavior) when all results must have the variable bound.
-- All persons with worksAt, plus name if available (LEFT JOIN)
SELECT ?person ?name WHERE {
?person <ex:worksAt> ?company .
OPTIONAL { ?person <ex:name> ?name }
}
-- Only persons that have both worksAt and name (INNER JOIN)
SELECT ?person ?name WHERE {
?person <ex:worksAt> ?company .
?person <ex:name> ?name .
}
Plan cache hit rate
Compiled SPARQL→SQL plans are cached per-backend in an LRU cache. Identical query strings (including whitespace) hit the cache. To maximize cache hit rate:
- Parameterize queries by binding constants into
VALUESinline data rather than embedding them as literal strings in the query text - Keep the cache large enough for your query workload via
pg_ripple.plan_cache_size
Check cache efficiency with sparql_explain() — repeated calls for the same query return instantly once the plan is cached.
Property path recipes
Transitive closure (follow all hops)
SELECT ?target WHERE {
<ex:alice> <ex:knows>+ ?target
}
Compiles to a WITH RECURSIVE CTE. Specify a depth limit to avoid runaway queries on dense graphs:
SET pg_ripple.max_path_depth = 10;
Include the start node (zero or more hops)
SELECT ?target WHERE {
<ex:alice> <ex:follows>* ?target
}
-- Returns alice herself plus all reachable nodes
Multi-predicate path (alternative)
SELECT ?contact WHERE {
<ex:alice> (<ex:knows>|<ex:follows>) ?contact
}
Sequence (two-hop join)
SELECT ?friend_of_friend WHERE {
<ex:alice> <ex:knows>/<ex:knows> ?friend_of_friend
}
The / operator compiles to a chained join — spargebra represents the intermediate variable as an anonymous blank node which pg_ripple handles by applying an equi-join constraint.
Inverse path (find who points to a node)
SELECT ?who WHERE {
?who ^<ex:knows> <ex:bob>
}
-- Equivalent to: <ex:bob> is the object, ?who is the subject
Resource exhaustion safeguards
For user-facing applications where input queries cannot be fully trusted:
-- Set a per-session depth cap
SET pg_ripple.max_path_depth = 15;
-- Set a per-query time limit
SET statement_timeout = '5s';
Both settings can be applied in a connection pool after_connect hook or in a row-level security policy. The depth cap is included in the plan cache key and does not cause cross-session pollution.
VALUES for multi-value lookup
VALUES compiles to SQL inline data and is efficient for looking up a known list of resources:
SELECT ?person ?name WHERE {
VALUES ?person { <ex:alice> <ex:bob> <ex:carol> }
?person <ex:name> ?name .
}
This is more cache-friendly than embedding the values as individual FILTER clauses, since the query structure stays constant while only the VALUES rows change.
Debugging with sparql_explain
Always inspect the generated SQL before blaming pg_ripple for slow results:
SELECT pg_ripple.sparql_explain(
'SELECT ?name WHERE { ?p <ex:name> ?name . FILTER(?name = "Alice") }',
false
);
Look for:
vp_raretable scans where you expected a dedicated VP table — the predicate may not have been promoted yet- Missing
WHEREclause conditions — a FILTER may have failed to encode its constant - Extra
UNION ALLbranches in property paths — expected for*and?operators
Using the HTTP endpoint
The pg_ripple_http companion service exposes a standard SPARQL endpoint for use with any SPARQL-compatible tool or library.
Python (SPARQLWrapper)
from SPARQLWrapper import SPARQLWrapper, JSON
sparql = SPARQLWrapper("http://localhost:7878/sparql")
sparql.setQuery("""
SELECT ?name WHERE {
?person <https://example.org/name> ?name
} LIMIT 10
""")
sparql.setReturnFormat(JSON)
results = sparql.query().convert()
for result in results["results"]["bindings"]:
print(result["name"]["value"])
Java (Apache Jena)
import org.apache.jena.query.*;
String endpoint = "http://localhost:7878/sparql";
String queryStr = "SELECT ?s ?p ?o WHERE { ?s ?p ?o } LIMIT 10";
Query query = QueryFactory.create(queryStr);
try (QueryExecution qexec = QueryExecutionFactory.sparqlService(endpoint, query)) {
ResultSet results = qexec.execSelect();
ResultSetFormatter.out(System.out, results, query);
}
curl
# URL-encoded GET
curl -G http://localhost:7878/sparql \
--data-urlencode "query=SELECT ?s WHERE { ?s ?p ?o } LIMIT 5"
# POST with SPARQL body
curl -X POST http://localhost:7878/sparql \
-H "Content-Type: application/sparql-query" \
-d "SELECT ?s WHERE { ?s ?p ?o } LIMIT 5"
# SPARQL Update
curl -X POST http://localhost:7878/sparql \
-H "Content-Type: application/sparql-update" \
-d "INSERT DATA { <ex:s> <ex:p> \"hello\" }"
CONSTRUCT views vs SELECT views (v0.18.0)
Both CONSTRUCT views and SELECT views are pg_trickle stream tables that stay current as triples change. Choose based on what the downstream consumer needs.
| Consideration | SELECT view | CONSTRUCT view |
|---|---|---|
| Output shape | Tabular (columns = SPARQL variables) | Triples (s, p, o, g BIGINT) |
| Best for | Dashboards, APIs, SQL joins | Inference, denormalization, RDF export |
| Template count | 1 row per solution | N rows per solution (N = template size) |
decode = true | Decodes each variable column | Decodes s, p, o to TEXT |
Materialising inference results
Use a CONSTRUCT view to materialize RDFS/OWL entailments without running Datalog. This is faster for simple one-hop patterns:
-- Materialise rdfs:subClassOf inheritance one hop
SELECT pg_ripple.create_construct_view(
'subclass_instances',
'CONSTRUCT { ?i <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> ?super }
WHERE {
?i <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> ?sub .
?sub <http://www.w3.org/2000/01/rdf-schema#subClassOf> ?super
}',
'5s'
);
For multi-hop inference (transitive closure), use a Datalog view with the rdfs built-in rule set instead.
Using ASK views as live constraint monitors
An ASK view maintains a single boolean result that flips as the data changes. Ideal for:
- SHACL-style cardinality checks that are too expensive to run as triggers
- Dashboard "health indicator" lights
- Application-side event triggers (poll the stream table)
-- Alert when any order has been unshipped for more than 24 hours
SELECT pg_ripple.create_ask_view(
'stale_orders',
'ASK { ?order <https://schema.org/orderStatus>
<https://schema.org/OrderProcessing> .
FILTER NOT EXISTS { ?order <https://schema.org/estimatedDelivery> ?d } }',
'30s'
);
-- Application polls this:
SELECT result FROM pg_ripple.ask_view_stale_orders;
When result flips from false to true, the constraint is violated. Use a PostgreSQL NOTIFY/LISTEN or pg_logical replication slot to push the change to application subscribers.
SPARQL Performance
Best practices for accelerating SPARQL queries against pg_ripple using tabling, demand transformation, and rule plan caching.
Tabling / memoisation (v0.32.0)
When pg_ripple.tabling = on (default), the results of infer_wfs() calls are stored in an in-database cache keyed by an XXH3-64 hash of the goal string. Repeated calls with the same rule set return the cached result without re-running the fixpoint algorithm.
-- Check current tabling settings.
SHOW pg_ripple.tabling; -- on
SHOW pg_ripple.tabling_ttl; -- 300 (seconds)
-- Inspect what is cached.
SELECT * FROM pg_ripple.tabling_stats() ORDER BY hits DESC LIMIT 10;
When tabling helps most
- Analytical workloads where the same rule set is evaluated repeatedly (e.g., dashboard refreshes, batch jobs).
- SPARQL queries over inferred triples where
infer_wfs()is called before each query to ensure up-to-date materialization. - Short-lived applications that query the same patterns many times within a session.
TTL configuration
-- Never expire cached entries (best for read-heavy, rarely-changing data).
SET pg_ripple.tabling_ttl = 0;
-- Cache for 10 minutes.
SET pg_ripple.tabling_ttl = 600;
-- Disable tabling entirely (always recompute — useful for testing/debugging).
SET pg_ripple.tabling = off;
Cache invalidation
The tabling cache is automatically invalidated on:
pg_ripple.insert_triple()orpg_ripple.delete_triple()— any data changepg_ripple.load_rules()orpg_ripple.drop_rules()— any rule change
No manual invalidation is needed for normal use.
Demand transformation (v0.31.0)
Rather than materializing the entire rule set closure, infer_demand() derives only the facts required to answer a specific set of goal patterns. For SPARQL queries that target a small predicate set within a large rule base, this can reduce inference work by 50–90%.
-- Only derive 'knows' and 'reachable' — skip unrelated predicates.
SELECT pg_ripple.infer_demand('social_rules',
'[{"p": "<https://ex.org/knows>"},
{"p": "<https://ex.org/reachable>"}]'
);
-- Then query normally.
SELECT * FROM pg_ripple.sparql('
SELECT ?a ?b WHERE { ?a <https://ex.org/reachable> ?b }
');
When pg_ripple.demand_transform = on (default), create_datalog_view() automatically applies demand transformation when multiple goal patterns are specified.
Rule plan caching (v0.30.0)
The rule plan cache (pg_ripple.rule_plan_cache = on, default) stores compiled SQL plans for each rule, avoiding repeated compilation overhead on repeated infer() or infer_wfs() calls within a session.
-- Check plan cache settings.
SHOW pg_ripple.rule_plan_cache; -- on
SHOW pg_ripple.rule_plan_cache_size; -- 64
-- Disable for debugging (forces recompilation each time).
SET pg_ripple.rule_plan_cache = off;
The rule plan cache is keyed on rule text, so changes to rules (via load_rules or drop_rules) automatically invalidate affected plan cache entries.
Interaction with tabling
Both the rule plan cache and the tabling cache are active by default and complement each other:
- Rule plan cache eliminates SQL compilation overhead — each fixpoint iteration reuses the compiled SQL for each rule.
- Tabling cache eliminates entire fixpoint computations — if the goal was computed recently and the data has not changed, the cached result is returned immediately.
For long-running services, setting pg_ripple.tabling_ttl = 0 (no expiry) combined with pg_ripple.rule_plan_cache = on gives the best repeated-query performance.
SPARQL property paths and transitive closure
SPARQL property paths (e.g., +, *, /) are expanded into recursive CTEs at query time. For large graphs, this can be expensive. Consider pre-materialising transitive closure using a Datalog rule instead:
-- Pre-materialise transitive closure with a Datalog rule.
SELECT pg_ripple.load_rules('
?x <https://ex.org/reachable> ?y :- ?x <https://ex.org/edge> ?y .
?x <https://ex.org/reachable> ?z :- ?x <https://ex.org/reachable> ?y ,
?y <https://ex.org/edge> ?z .
', 'reach');
SELECT pg_ripple.infer('reach');
-- Query pre-materialised closure — no recursive CTE at query time.
SELECT * FROM pg_ripple.sparql('
SELECT ?a ?b WHERE { ?a <https://ex.org/reachable> ?b }
');
Combined with tabling, this approach amortizes the cost of the transitive closure computation across multiple queries.
Bounded-depth SPARQL property paths (v0.34.0)
SPARQL property path queries (rdfs:subClassOf*, ex:knows+) rely on WITH RECURSIVE CTEs internally. When the graph has a known bounded hierarchy depth, pre-materializing the closure with a depth bound avoids the recursive path at query time entirely.
-- Materialize a bounded closure (at most 5 hops)
SET pg_ripple.datalog_max_depth = 5;
SELECT pg_ripple.load_rules(
'?x <https://ex.org/reach> ?y :- ?x <https://ex.org/step> ?y . '
'?x <https://ex.org/reach> ?z :- ?x <https://ex.org/reach> ?y , ?y <https://ex.org/step> ?z .',
'bounded_reach'
);
SELECT pg_ripple.infer('bounded_reach');
SET pg_ripple.datalog_max_depth = 0;
-- Query the pre-materialized bounded closure
SELECT * FROM pg_ripple.sparql('
SELECT ?a ?b WHERE { ?a <https://ex.org/reach> ?b }
');
For hierarchies where the maximum depth is known (e.g., from a SHACL sh:maxDepth annotation), this pattern typically reduces property path query latency by 30-60% compared to the unbounded inline recursive CTE.
Worst-case optimal joins for cyclic patterns (v0.36.0)
Standard hash-join and nested-loop algorithms are not worst-case optimal for cyclic SPARQL BGPs — query graphs that contain a cycle, such as triangle queries:
SELECT ?a ?b ?c WHERE {
?a <ex:knows> ?b .
?b <ex:knows> ?c .
?c <ex:knows> ?a .
}
When pg_ripple.wcoj_enabled = on (the default), pg_ripple automatically detects cyclic BGPs and forces the PostgreSQL planner towards sort-merge joins, exploiting the (s, o) B-tree indices on VP tables. This simulates the key locality property of the Leapfrog Triejoin algorithm.
-- Check WCOJ settings.
SHOW pg_ripple.wcoj_enabled; -- on
SHOW pg_ripple.wcoj_min_tables; -- 3 (min VP joins before WCOJ kicks in)
-- Detect whether a BGP is cyclic (useful for query plan inspection).
SELECT pg_ripple.wcoj_is_cyclic('[["a","b"],["b","c"],["c","a"]]'); -- true
SELECT pg_ripple.wcoj_is_cyclic('[["root","a"],["root","b"]]'); -- false
-- Benchmark a triangle query with WCOJ on vs. off.
SELECT pg_ripple.wcoj_triangle_query('https://example.org/knows');
-- Returns: {"triangle_count": N, "wcoj_applied": true, "predicate_iri": "..."}
When WCOJ helps most
- Social graph triangle queries — finding mutual connections or common co-authors.
- Transitive closure patterns — property paths rewritten as join chains.
- Cyclic constraint checking — detecting cycles in directed graphs.
Tuning
-- Raise the threshold if you only want WCOJ for large multi-hop joins.
SET pg_ripple.wcoj_min_tables = 5;
-- Disable WCOJ globally if you suspect it is causing a bad plan.
SET pg_ripple.wcoj_enabled = off;
Performance expectation: On triangle queries over a VP table with 1 M edges, WCOJ reduces query time from > 10 s (hash-join plan) to < 1 s (sort-merge plan with B-tree exploitation).
Materialization freshness after parallel inference (v0.35.0)
When pg_ripple.datalog_parallel_workers > 1, the Datalog engine partitions rules into independent groups and executes them in the optimal order within a single transaction. After infer_with_stats() or infer() returns, SPARQL queries immediately observe all derived facts — there is no staleness window within the same session.
-- After bulk loading, re-materialize derived predicates.
SELECT pg_ripple.load_turtle($$ <Alice> a <Person> . $$);
SET pg_ripple.datalog_parallel_workers = 4;
SELECT pg_ripple.infer_with_stats('owl-rl');
-- SPARQL now sees all derived rdf:type, rdfs:subClassOf, owl:sameAs facts.
SELECT pg_ripple.sparql('SELECT ?x ?type WHERE { ?x a ?type . }');
Tip: Check parallel_groups in the infer_with_stats() output to verify that your rule set benefits from parallelism. A value of 1 means all rules are in a single dependency chain; a value > 1 confirms that concurrent execution is possible.
-- Check parallel group count before tuning workers.
SELECT pg_ripple.infer_with_stats('owl-rl')->>'parallel_groups'; -- e.g., "3"
TopN push-down (v0.46.0)
When a SPARQL SELECT query contains both ORDER BY and LIMIT N (with no OFFSET and no DISTINCT), pg_ripple embeds the LIMIT N clause directly in the generated SQL rather than fetching all matching rows and discarding the excess after dictionary decoding. This eliminates the overhead of decoding rows that will never be returned.
When push-down applies
| Condition | Push-down applied? |
|---|---|
ORDER BY … LIMIT N | Yes |
ORDER BY … LIMIT N OFFSET M (M > 0) | No (OFFSET present) |
SELECT DISTINCT … ORDER BY … LIMIT N | No (DISTINCT in scope) |
LIMIT N without ORDER BY | No (no ordering) |
Verifying push-down with EXPLAIN
Use sparql_explain() to confirm that push-down was applied. Look for "topn_applied": true in the JSON output:
SELECT pg_ripple.sparql_explain(
'SELECT ?s ?score
WHERE { ?s <http://example.org/score> ?score }
ORDER BY DESC(?score)
LIMIT 10',
false
) -> 'topn_applied';
-- Returns: true
The "plan" key in the explain output will show a Limit node directly over the VP scan when push-down is active.
Disabling push-down
Push-down is controlled by pg_ripple.topn_pushdown (default on). Disable it for debugging or if you suspect incorrect results:
SET pg_ripple.topn_pushdown = off;
-- run the query
SET pg_ripple.topn_pushdown = on;
Performance expectation: On a 1 M-triple dataset, a
LIMIT 10query withORDER BYreduces dictionary-decode calls from O(result_set) to O(10). The improvement is largest when the result set is large relative to the LIMIT.
SHACL Patterns
Practical patterns for defining and using SHACL shapes with pg_ripple.
NodeShape vs PropertyShape
SHACL defines two kinds of shapes:
| Kind | When to use |
|---|---|
| NodeShape | Applies to a set of focus nodes (identified by sh:targetClass, sh:targetNode, etc.) |
| PropertyShape | Defines constraints on the values of a specific predicate, nested inside a NodeShape |
In pg_ripple's Turtle parser, a sh:NodeShape carries one or more sh:property [...] blocks, each describing a PropertyShape inline:
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <https://example.org/> .
ex:ProductShape # ← NodeShape
a sh:NodeShape ;
sh:targetClass ex:Product ;
sh:property [ # ← inline PropertyShape
sh:path ex:sku ;
sh:minCount 1 ;
sh:maxCount 1 ;
] .
sh:datatype — Enforcing Value Types
Use sh:datatype to require a specific XSD datatype for literal values:
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <https://example.org/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
ex:SensorShape
a sh:NodeShape ;
sh:targetClass ex:Sensor ;
sh:property [
sh:path ex:temperature ;
sh:datatype xsd:decimal ;
] ;
sh:property [
sh:path ex:label ;
sh:datatype xsd:string ;
] .
Insert the shape, then load data using the correct datatype suffix:
SELECT pg_ripple.load_ntriples('
<https://example.org/s1> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://example.org/Sensor> .
<https://example.org/s1> <https://example.org/temperature> "23.5"^^<http://www.w3.org/2001/XMLSchema#decimal> .
');
SELECT pg_ripple.validate();
-- {"conforms": true, ...}
A plain string for ex:temperature (e.g. "23.5" without ^^xsd:decimal) will produce a sh:datatype violation.
sh:minCount and sh:maxCount
These are the most common SHACL constraints and map naturally to cardinality checks.
| Pattern | Meaning |
|---|---|
sh:minCount 1 | Required field — every focus node must have at least one value |
sh:maxCount 1 | At most one value — useful for functional properties |
sh:minCount 1 ; sh:maxCount 1 | Exactly one value |
ex:PersonShape
a sh:NodeShape ;
sh:targetClass ex:Person ;
sh:property [
sh:path ex:fullName ;
sh:minCount 1 ; -- required
sh:maxCount 1 ; -- unique per person
sh:datatype xsd:string ;
] ;
sh:property [
sh:path ex:phoneNumber ;
sh:maxCount 3 ; -- at most 3 phone numbers
] .
Important:
sh:minCountis only checked bypg_ripple.validate(), not enforced duringinsert_triple()in sync mode. This is because a missing value cannot be detected from a single insert — it requires scanning all focus nodes after the fact.
sh:maxCountis checked in sync mode, since exceeding the maximum can be detected as each new value arrives.
Sync Mode: Latency Trade-offs
pg_ripple.shacl_mode = 'sync' runs SHACL validator plans on every insert_triple() call. This adds latency proportional to:
- The number of active shapes
- The selectivity of the target class (fewer focus nodes = faster)
- The cost of counting existing value nodes for
sh:maxCount
Recommended for: low-throughput, high-integrity workflows (master data, configuration graphs, knowledge bases).
Not recommended for: bulk data ingestion, high-frequency event streams, or when violations should be post-processed rather than rejected at insert time.
-- For bulk loads: keep shacl_mode off, validate after:
SET pg_ripple.shacl_mode = 'off';
SELECT pg_ripple.load_turtle($$ ... $$);
SELECT pg_ripple.validate(); -- check after the fact
Calling validate() On Demand
validate() does a full pass over all focus nodes for every active shape. Use it:
- After a bulk load to detect any violations in the imported data
- As part of a scheduled data quality check
- Before publishing a named graph
-- Validate a specific named graph
SELECT pg_ripple.validate('<https://example.org/my-data>');
-- Validate all graphs
SELECT pg_ripple.validate('*');
-- Extract just the violations as a set
SELECT v
FROM jsonb_array_elements(
pg_ripple.validate() -> 'violations'
) AS v;
sh:in — Controlled Vocabulary
Use sh:in to restrict a property to a specific set of allowed values:
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <https://example.org/> .
ex:OrderShape
a sh:NodeShape ;
sh:targetClass ex:Order ;
sh:property [
sh:path ex:status ;
sh:minCount 1 ;
sh:maxCount 1 ;
sh:in ( ex:pending ex:confirmed ex:shipped ex:delivered ex:cancelled ) ;
] .
sh:pattern — Regex Constraints
Validate string values with a POSIX regular expression:
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <https://example.org/> .
ex:ContactShape
a sh:NodeShape ;
sh:targetClass ex:Contact ;
sh:property [
sh:path ex:email ;
sh:pattern "^[a-zA-Z0-9._%+\\-]+@[a-zA-Z0-9.\\-]+\\.[a-zA-Z]{2,}$" ;
] .
Note: pg_ripple uses PostgreSQL's
~operator for regex matching, which follows POSIX extended regex. Backslashes must be doubled in Turtle string literals.
Managing Multiple Shapes
Load shapes from separate Turtle documents, one call per document:
-- Load Person shapes
SELECT pg_ripple.load_shacl(pg_read_file('/etc/shapes/person-shapes.ttl'));
-- Load Product shapes
SELECT pg_ripple.load_shacl(pg_read_file('/etc/shapes/product-shapes.ttl'));
-- List all active shapes
SELECT shape_iri, active FROM pg_ripple.list_shapes();
-- Deactivate a shape without deleting it (set active=false manually or drop it)
SELECT pg_ripple.drop_shape('https://example.org/OldPersonShape');
Pre-deployment Checklist
Before running in production with SHACL:
- Load all shapes before bulk importing data — this ensures violations are caught from the start.
- For large existing datasets, run
SELECT pg_ripple.validate()after loading shapes to identify pre-existing violations. - Choose
shacl_modebased on throughput requirements:offfor ETL pipelines,syncfor interactive / low-volume inserts. - Index
ex:targetClasspredicates —sh:targetClassshapes perform a full scan ofrdf:typetriples to collect focus nodes. Ensurerdf:typehas a dedicated VP table (it usually does after a few hundred triples).
sh:or / sh:and / sh:not Patterns (v0.8.0)
Validating multiple valid types with sh:or
Use sh:or when a focus node can be one of several valid types:
ex:PartyShape
a sh:NodeShape ;
sh:targetClass ex:Party ;
sh:or (ex:PersonShape ex:OrganizationShape) .
The focus node must satisfy at least one of the listed shapes. Useful for union types — a contract party can be a person or a company, but must satisfy the required fields for at least one.
Requiring compliance with multiple shapes using sh:and
Use sh:and when every focus node must simultaneously satisfy all listed shapes:
ex:AuditedEntityShape
a sh:NodeShape ;
sh:targetClass ex:AuditedEntity ;
sh:and (ex:BaseEntityShape ex:ComplianceShape) .
Excluding banned combinations with sh:not
Use sh:not to prevent a focus node from conforming to a specific shape:
ex:ActiveUserShape
a sh:NodeShape ;
sh:targetClass ex:User ;
sh:not ex:SuspendedUserShape .
If a ex:User also becomes a ex:SuspendedUser, validate() reports a sh:not violation.
Performance note
sh:or, sh:and, and sh:not all involve recursive shape conformance checks. For large datasets with deep shape hierarchies, prefer validate() during off-peak hours over sync mode.
Async Mode for High-Throughput Ingestion (v0.8.0)
When inserting data at high speed, use async mode to avoid blocking inserts on SHACL checks:
-- Enable async validation
SET pg_ripple.shacl_mode = 'async';
-- Bulk load (violations queued, not raised)
SELECT pg_ripple.load_ntriples(pg_read_file('/data/large-dataset.nt'));
-- Reset mode
RESET pg_ripple.shacl_mode;
-- Drain queue after load
SELECT pg_ripple.process_validation_queue(10000);
-- Review any violations
SELECT pg_ripple.dead_letter_queue();
-- Clear after fixing data
SELECT pg_ripple.drain_dead_letter_queue();
Reading the dead-letter queue
Each entry in dead_letter_queue() is a JSON object. Decode IDs to IRIs:
SELECT
pg_ripple.decode_id((item->>'s_id')::bigint) AS subject,
pg_ripple.decode_id((item->>'p_id')::bigint) AS predicate,
pg_ripple.decode_id((item->>'o_id')::bigint) AS object,
item->'violation'->>'message' AS violation
FROM jsonb_array_elements(pg_ripple.dead_letter_queue()) AS item;
sh:qualifiedValueShape Patterns (v0.8.0)
Use qualified value shapes when cardinality constraints should only count value nodes that conform to a specific shape:
ex:EmployerShape
a sh:NodeShape ;
sh:targetClass ex:Employer ;
sh:property [
sh:path ex:officeAddress ;
sh:qualifiedValueShape ex:PrimaryAddressShape ;
sh:qualifiedMinCount 1 ;
sh:qualifiedMaxCount 1 ;
] .
This enforces: an employer must have exactly one office address that is a primary address (as defined by ex:PrimaryAddressShape), while allowing any number of other address types along the same path.
Datalog Optimization
This guide helps you get the most out of the pg_ripple Datalog engine. v0.29.0 introduced several performance features that complement semi-naive evaluation: magic sets, cost-based join reordering, anti-join negation, predicate-filter pushdown, and delta-table indexing.
Choosing between infer() and infer_goal()
| Use case | Recommended function |
|---|---|
| Materialize everything for a query workload | infer_with_stats() |
| One-off question with a specific target | infer_goal() |
| SPARQL VIEW backed by inference | infer_with_stats() once, then query VP tables |
| Large ontology, selective query, cold cache | infer_goal() |
infer_goal() shines when the goal pattern eliminates a large fraction of derivable facts. For example, asking "what types does Alice have?" needs only a tiny slice of an RDFS closure. On a 1M-triple dataset with a 5-level rdfs:subClassOf hierarchy, infer_goal() for a single entity can be 100× faster than full materialization.
Use infer_with_stats() when you want to pre-materialize the full closure and then serve many queries from VP storage without re-running inference.
Reading infer_with_stats() output
SELECT pg_ripple.infer_with_stats('rdfs');
Example output:
{
"derived": 42,
"iterations": 3,
"eliminated_rules": []
}
| Field | What it tells you |
|---|---|
derived | Total new triples inserted; 0 means the fixpoint was already reached |
iterations | Fixpoint depth; equals the longest derivation chain length |
eliminated_rules | Rules removed by subsumption before evaluation; reduces SQL statements per iteration |
High iterations value? The engine needed many passes to saturate the relation. This is normal for deep transitive hierarchies. To investigate, increase logging with SET pg_ripple.inference_mode = 'on_demand' and look at EXPLAIN ANALYZE output for the generated semi-naive SQL (available via list_rules()).
Non-empty eliminated_rules? These rules were provably redundant given the other rules in the set. No action needed; this is a free optimization.
Diagnosing slow fixpoint convergence
Step 1: Check iteration count
SELECT pg_ripple.infer_with_stats('my-rules')->>'iterations';
More than 20 iterations usually means either:
- A deep recursive chain (expected for hierarchy data)
- A rule set with many cross-referencing rules (consider splitting into finer-grained rule sets)
Step 2: Check cardinality of VP tables
SELECT relname, reltuples::bigint AS estimated_rows
FROM pg_class
WHERE relname LIKE 'vp_%'
ORDER BY reltuples DESC;
Large VP tables for the predicates in rule bodies slow down each iteration. Use infer_goal() to limit the scope.
Step 3: Force ANALYZE on VP tables
The cost-based reordering (pg_ripple.datalog_cost_reorder) uses pg_class.reltuples. If these statistics are stale, the reordering may be suboptimal:
-- Refresh statistics on all VP tables
ANALYZE;
Step 4: Check for delta-table index creation
-- Temporarily lower the threshold to index all delta tables
SET pg_ripple.delta_index_threshold = 1;
SELECT pg_ripple.infer_with_stats('my-rules');
If the index helps, you can lower the threshold permanently for your workload.
Tuning GUCs by dataset size
| Dataset size | Recommended settings |
|---|---|
| < 10K triples | Default settings work well |
| 10K–500K triples | delta_index_threshold = 100; datalog_antijoin_threshold = 500 |
| 500K–10M triples | delta_index_threshold = 50; enable magic_sets = true for selective queries |
| > 10M triples | Use infer_goal() for selective queries; magic_sets = true; consider partitioned VP tables |
Magic sets GUC
pg_ripple.magic_sets (default: true) controls whether infer_goal() applies the magic sets transformation or falls back to full bottom-up evaluation.
Set to false for debugging: both paths should return the same matching count; if they don't, there is a bug in the magic sets implementation — please report it.
-- Debug: compare magic sets vs full evaluation
SET pg_ripple.magic_sets = true;
SELECT pg_ripple.infer_goal('rdfs', '?x rdf:type foaf:Person') AS magic_result;
SET pg_ripple.magic_sets = false;
SELECT pg_ripple.infer_goal('rdfs', '?x rdf:type foaf:Person') AS full_result;
-- The "matching" field should be identical in both results
Anti-join negation
For rules with NOT in the body, the compiler uses either:
- NOT EXISTS — preferred for small VP tables (good for the planner's nested-loop elimination)
- LEFT JOIN … IS NULL (anti-join) — preferred for large VP tables (allows hash anti-join and merge anti-join plans)
The threshold is controlled by pg_ripple.datalog_antijoin_threshold (default: 1000 rows).
-- Force anti-join for all negated atoms regardless of table size
SET pg_ripple.datalog_antijoin_threshold = 1;
-- Force NOT EXISTS for all negated atoms
SET pg_ripple.datalog_antijoin_threshold = 0;
In practice the default (1000) matches PostgreSQL's own planner heuristics for when hash anti-join becomes beneficial.
Benchmark: magic sets vs full materialization
The included benchmarks/magic_sets.sql file demonstrates the performance difference between infer() and infer_goal() on an RDFS closure over a large class hierarchy.
To run:
cargo pgrx run pg18
# In psql:
\i benchmarks/magic_sets.sql
Aggregate rules — stratification and performance (v0.30.0)
Aggregation stratification
Aggregate literals (COUNT, SUM, MIN, MAX, AVG) add a stratification constraint: the aggregate rule must be evaluated after all the data it groups over is fully materialized. pg_ripple enforces this automatically via its SCC-based stratifier. If a cycle through aggregation is detected (e.g., a derived predicate P feeds an aggregate that produces another predicate Q which feeds P), the engine emits WARNING PT510 and skips the aggregate rules.
Avoid cycles through aggregation:
-- ✗ BAD: foaf:knows is derived by rule 1, but rule 2 aggregates over foaf:knows.
-- If the aggregate result feeds back into foaf:knows, this is a PT510 violation.
SELECT pg_ripple.load_rules(
'?x <foaf:knows> ?y :- ?x <ex:follows> ?y .
?x <ex:followCount> ?n :- COUNT(?y WHERE ?x <foaf:knows> ?y) = ?n .
?x <ex:follows> ?y :- ?x <ex:followCount> ?n , ?n > 1 .', -- cycle!
'bad_set');
-- ✓ GOOD: Aggregate over base data only; result is a new predicate with no back-edge.
SELECT pg_ripple.load_rules(
'?x <ex:followCount> ?n :- COUNT(?y WHERE ?x <ex:follows> ?y) = ?n .',
'good_set');
Performance tips for aggregate rules
-
Run
infer_agg()instead ofinfer()for rule sets that contain aggregate literals.infer()silently skips aggregate literals;infer_agg()evaluates them. -
Plan cache hit ratio: On a warm cache, the second and subsequent calls to
infer_agg()skip compilation entirely. Check hit rates:SELECT * FROM pg_ripple.rule_plan_cache_stats(); -- rule_set | hits | misses | entries -- my_analytics | 9 | 1 | 1A hit rate < 90% may indicate that
load_rules()is being called unnecessarily (eachload_rules()invalidates the cache for that rule set). -
Use narrow predicates for the aggregate atom:
COUNT(?y WHERE ?x <ex:knows> ?y)scans theex:knowsVP table. Ensure that predicate has a B-tree index on(s, o). -
Batch aggregate rules in a single rule set: Multiple aggregate rules for the same rule set are compiled in a single
infer_agg()call; splitting them into separate rule sets multiplies the number of GROUP BY queries.
Rule plan cache tuning (v0.30.0)
The plan cache avoids re-compiling rule SQL on every infer_agg() call. Two GUCs control it:
| GUC | Default | Effect |
|---|---|---|
pg_ripple.rule_plan_cache | true | Master switch — set false to debug cache-related issues |
pg_ripple.rule_plan_cache_size | 64 | Max rule sets cached; oldest entry evicted on overflow |
Sizing guidelines:
- If your application has fewer than 64 rule sets (typical), the default is fine.
- For > 64 rule sets, increase
rule_plan_cache_sizeto avoid constant eviction:ALTER SYSTEM SET pg_ripple.rule_plan_cache_size = 256; SELECT pg_reload_conf(); - Memory cost is low: each cache entry stores a few SQL strings (~1–5 KB typical).
Cache invalidation:
The cache is automatically invalidated per rule set when:
pg_ripple.load_rules()is called for that rule set (new rules may change compiled SQL)pg_ripple.drop_rules()is called for that rule set
The cache is not shared across backends (it is process-local). Each new backend connection starts with an empty cache, so the first infer_agg() call per backend always incurs a compile step.
Demand transformation vs. magic sets (v0.31.0)
Both demand transformation and magic sets (infer_goal()) are goal-directed inference techniques that derive only the facts needed to answer a query. They differ in scope:
| Technique | Function | Best for |
|---|---|---|
| Magic sets | infer_goal(rule_set, goal) | Single goal predicate, one specific goal pattern |
| Demand transformation | infer_demand(rule_set, demands) | Multiple goal predicates, mutually dependent rules |
When to use infer_demand() instead of infer_goal()
Use infer_demand() when:
-
Multiple derived predicates in one query: a SPARQL query touches several derived predicates that share common base predicates.
infer_demand()computes a joint demand set and derives all needed facts in a single pass. -
Mutually recursive rules: rules for predicate A reference predicate B, which in turn references A. Magic sets handles one entry point; demand transformation propagates binding demands through the full dependency graph.
-
Selective analytics: you only need results for a subset of derived predicates, not the full materialization.
-- Derive only "manager" and "department" predicates, ignoring unrelated HR predicates.
SELECT pg_ripple.infer_demand('hr_rules',
'[{"p": "<https://hr.example.org/manager>"},
{"p": "<https://hr.example.org/department>"}]'
);
Auto-application in create_datalog_view()
When pg_ripple.demand_transform = on (default), create_datalog_view() automatically applies demand transformation when multiple goal patterns are specified. This makes materialized views more selective.
Set pg_ripple.demand_transform = off to fall back to full inference within a view definition.
owl:sameAs with demand transformation
When pg_ripple.sameas_reasoning = on (default), infer_demand() applies the owl:sameAs canonicalization pre-pass before the demand-filtered inference. This ensures correct results even when entity aliases are involved, while still limiting the inference to the minimum required work.
Well-founded semantics & tabling (v0.32.0)
When to use infer_wfs()
Use infer_wfs() instead of infer() when:
- Your rules contain mutual negation (cyclic through
NOT) thatinfer()rejects with a stratification error. - You want a three-valued result: facts that cannot be resolved are labeled unknown rather than causing an error.
- You need to reason over open-world ontologies where absence of a fact is not the same as its negation.
For purely positive or stratifiable programs, infer_wfs() detects stratifiability and delegates to the same semi-naive engine as infer() — there is no performance penalty.
-- Test whether a rule set is stratifiable without committing to full inference.
SELECT (pg_ripple.infer_wfs('my_rules') ->> 'stratifiable')::boolean AS stratifiable;
Tuning the WFS iteration cap
The GUC pg_ripple.wfs_max_iterations (default 100) limits alternating-fixpoint rounds. If WARNING PT520 appears, increase the cap or review rules for non-terminating patterns:
SET pg_ripple.wfs_max_iterations = 500;
SELECT pg_ripple.infer_wfs('large_ontology');
Tabling tuning
The tabling cache (pg_ripple.tabling = on) avoids re-running the fixpoint on repeated identical calls. Key settings:
-- Disable tabling for debugging (always recompute).
SET pg_ripple.tabling = off;
-- Set TTL to 10 minutes (default is 5 minutes).
SET pg_ripple.tabling_ttl = 600;
-- Set TTL to 0 to never expire entries.
SET pg_ripple.tabling_ttl = 0;
-- Inspect cache contents and hit rates.
SELECT * FROM pg_ripple.tabling_stats() ORDER BY hits DESC;
Cache invalidation is automatic on data changes (insert_triple, delete_triple) and rule changes (load_rules, drop_rules). No manual cache management is required.
Bounded-depth inference (v0.34.0)
When your ontology has a known maximum hierarchy depth — for example, a class hierarchy that is at most 5 levels deep — you can set pg_ripple.datalog_max_depth to stop recursion early. This avoids running the final empty fixpoint iteration and can reduce inference time by 20-50% on bounded hierarchies.
-- Property hierarchy with at most 5 levels
SET pg_ripple.datalog_max_depth = 5;
SELECT pg_ripple.infer('my_rules');
-- Reset to unlimited after this transaction
SET pg_ripple.datalog_max_depth = 0;
Use 0 (the default) whenever the maximum depth is unknown. Setting too low a bound silently truncates the closure.
DRed vs. full recompute on delete (v0.34.0)
By default, deleting a base triple triggers the Delete-Rederive (DRed) algorithm: only the triples that could have been derived from the deleted fact are over-deleted, and any triples that have alternative derivation paths are immediately reinserted. This is far cheaper than discarding and recomputing the entire closure.
| Scenario | Recommendation |
|---|---|
| Deletes are rare (<1% of writes) | dred_enabled = true (default) |
| Bulk deletes of thousands of triples | dred_enabled = false then call infer() once |
| Rule set changes frequently | Use add_rule() / remove_rule() for surgical updates |
-- Disable DRed for a bulk delete session
SET pg_ripple.dred_enabled = false;
-- ... bulk deletes ...
SELECT pg_ripple.infer('my_rules'); -- full recompute once
SET pg_ripple.dred_enabled = true;
Parallel stratum evaluation (v0.35.0)
Within a single stratum, rules deriving different predicates with no shared body dependencies are fully independent — their INSERT … SELECT statements touch distinct VP tables. pg_ripple analyses this dependency structure at inference time and partitions rules into parallel groups. The infer_with_stats() function reports this analysis.
Reading the parallel fields in infer_with_stats()
SELECT pg_ripple.infer_with_stats('owl-rl');
Example output:
{
"derived": 1240,
"iterations": 4,
"eliminated_rules": [],
"parallel_groups": 3,
"max_concurrent": 3
}
| Field | What it tells you |
|---|---|
parallel_groups | Number of independent rule groups detected in the rule set |
max_concurrent | Effective concurrent worker count: min(parallel_groups, datalog_parallel_workers) |
A parallel_groups value of 1 means all rules form a single dependency chain — no parallelism is possible. Values > 1 indicate independent groups that can execute concurrently.
Tuning datalog_parallel_workers
| Hardware | Recommended setting |
|---|---|
| Single-core or low-memory instance | datalog_parallel_workers = 1 (serial) |
| 2–4 core server | datalog_parallel_workers = 2 |
| 8+ core server | datalog_parallel_workers = 4 (default) |
| Dedicated inference workload | Set to parallel_groups value from infer_with_stats() |
-- Check how many parallel groups your rule set has before tuning workers.
SELECT pg_ripple.infer_with_stats('my_rules')->>'parallel_groups' AS groups;
-- Then set workers to match (capped at your CPU count - 3 for system workers).
SET pg_ripple.datalog_parallel_workers = 4;
Avoiding overhead on small rule sets
The parallel analysis step adds a small overhead (dependency graph construction). For small rule sets or datasets, this overhead exceeds the parallelism benefit. Use datalog_parallel_threshold to skip analysis when the estimated total row count is below the threshold:
-- Skip parallel analysis for small strata (< 5000 rows).
SET pg_ripple.datalog_parallel_threshold = 5000;
-- Always analyse regardless of stratum size.
SET pg_ripple.datalog_parallel_threshold = 0;
The default threshold of 10,000 rows eliminates overhead for typical development datasets while enabling parallelism for production-scale ontology closures.
SPARQL materialization freshness
Parallel evaluation reduces the wall-clock time from a bulk insert_triple call to a fully materialized state. After calling pg_ripple.infer() or pg_ripple.infer_with_stats(), SPARQL queries that target derived VP tables immediately observe the newly derived facts:
-- Insert new data.
SELECT pg_ripple.load_turtle($$ ... $$);
-- Materialize derived predicates in parallel.
SET pg_ripple.datalog_parallel_workers = 4;
SELECT pg_ripple.infer_with_stats('owl-rl');
-- SPARQL queries now see all derived facts.
SELECT pg_ripple.sparql_query($$
SELECT ?x ?type WHERE { ?x a ?type . }
$$);
The staleness window (the gap between data arrival and query visibility of derived facts) shrinks proportionally to the number of independent rule groups and available workers.
Update Patterns
This page covers best practices for writing data to pg_ripple — when to use INSERT DATA / DELETE DATA vs DELETE/INSERT WHERE, how to manage named graphs, and how to write idempotent update scripts.
Choosing the right write API
| Scenario | Recommended API |
|---|---|
| Loading a large file (> ~1 000 triples) | load_ntriples() / load_turtle() |
| Inserting a single known triple from SQL | insert_triple() |
| Inserting triples from a SPARQL-capable client | sparql_update() with INSERT DATA |
| Removing an exact triple | delete_triple() or DELETE DATA |
| Pattern-based updates (find-and-replace) | DELETE/INSERT WHERE |
| Clearing a named graph | CLEAR GRAPH <g> |
| Loading remote RDF data | LOAD <url> |
INSERT DATA vs bulk load
INSERT DATA and bulk load (load_ntriples) both result in identical on-disk storage, but their performance profiles differ:
sparql_update (INSERT DATA) | load_ntriples | |
|---|---|---|
| Per-triple overhead | Medium (SPL + dictionary lookup per term) | Low (batched dictionary ops) |
| Transaction boundary | One PG transaction per call | One PG transaction per call |
| Typical throughput | ~1 000–5 000 triples/sec | ~50 000–200 000 triples/sec |
| Use case | Small, targeted writes | Bulk ingestion |
For initial data loads, always use load_ntriples or load_turtle. Reserve sparql_update / INSERT DATA for incremental updates.
Pattern-based updates (DELETE/INSERT WHERE)
DELETE/INSERT WHERE is the SPARQL equivalent of SQL UPDATE. It matches triples using a WHERE clause, then deletes and/or inserts triples for each match. The WHERE clause is compiled through the same SPARQL→SQL engine as SELECT queries.
Rename a property value
-- Change all "draft" status values to "published":
SELECT pg_ripple.sparql_update('
DELETE { ?s <https://example.org/status> <https://example.org/draft> }
INSERT { ?s <https://example.org/status> <https://example.org/published> }
WHERE { ?s <https://example.org/status> <https://example.org/draft> }
');
Return value: (# deleted) + (# inserted). For N matching subjects, this returns 2N.
Add a property conditionally
-- For every person lacking an email, insert a placeholder:
SELECT pg_ripple.sparql_update('
INSERT { ?person <https://schema.org/email> "no-reply@example.org" }
WHERE {
?person a <https://schema.org/Person> .
FILTER NOT EXISTS { ?person <https://schema.org/email> ?e }
}
');
Delete by pattern only
You can omit INSERT to delete only:
SELECT pg_ripple.sparql_update('
DELETE { ?s <https://example.org/temp> ?o }
WHERE { ?s <https://example.org/temp> ?o }
');
Or omit DELETE to insert only:
SELECT pg_ripple.sparql_update('
INSERT { ?s <https://example.org/indexed> "true"^^<http://www.w3.org/2001/XMLSchema#boolean> }
WHERE { ?s <https://example.org/name> ?name }
');
Performance note
For each WHERE binding, the DELETE phase and INSERT phase run individually. For large result sets (thousands of bindings), consider batching via the load_* APIs or using a single INSERT DATA with pre-computed data.
Graph lifecycle management
Creating and populating a named graph
Named graphs are created implicitly when the first triple is inserted. CREATE GRAPH is useful for SPARQL compliance or to pre-register a graph IRI in the dictionary before any triples arrive.
-- Explicit creation (optional):
SELECT pg_ripple.sparql_update(
'CREATE GRAPH <https://example.org/mygraph>'
);
-- Implicit creation via INSERT:
SELECT pg_ripple.sparql_update('
INSERT DATA {
GRAPH <https://example.org/mygraph> {
<https://example.org/a> <https://example.org/b> <https://example.org/c>
}
}
');
CLEAR vs DROP
Both operations delete all triples from a graph. The difference is conceptual — DROP "removes" the graph while CLEAR keeps it as an empty container. In pg_ripple, both behave identically on storage (the graph IRI remains in the dictionary either way).
-- Remove all triples, keep the graph:
SELECT pg_ripple.sparql_update(
'CLEAR GRAPH <https://example.org/mygraph>'
);
-- Remove all triples and the graph:
SELECT pg_ripple.sparql_update(
'DROP GRAPH <https://example.org/mygraph>'
);
Clearing multiple graphs at once
-- Clear the default graph only:
SELECT pg_ripple.sparql_update('CLEAR DEFAULT');
-- Clear all named graphs (default graph untouched):
SELECT pg_ripple.sparql_update('CLEAR NAMED');
-- Clear everything (default + all named):
SELECT pg_ripple.sparql_update('CLEAR ALL');
SILENT modifier
Adding SILENT suppresses errors (e.g., if a graph does not exist):
SELECT pg_ripple.sparql_update(
'DROP SILENT GRAPH <https://example.org/nonexistent>'
);
SELECT pg_ripple.sparql_update(
'CLEAR SILENT GRAPH <https://example.org/nonexistent>'
);
Loading remote RDF data (LOAD)
LOAD <url> fetches a remote RDF document via HTTP(S) and inserts all triples.
-- Load into the default graph:
SELECT pg_ripple.sparql_update(
'LOAD <https://www.w3.org/People/Berners-Lee/card.rdf>'
);
-- Load into a named graph:
SELECT pg_ripple.sparql_update(
'LOAD <https://example.org/data.ttl> INTO GRAPH <https://example.org/remote>'
);
-- Ignore HTTP errors:
SELECT pg_ripple.sparql_update(
'LOAD SILENT <https://example.org/maybe-missing.nt>'
);
Format is detected from Content-Type or URL extension:
text/turtle/.ttl→ Turtleapplication/rdf+xml/.rdf/.owl→ RDF/XML- Everything else → N-Triples
For large remote files, prefer the file-load APIs (load_ntriples, load_turtle) after fetching the file separately — LOAD buffers the entire response in memory before parsing.
Idempotent insert patterns
Because VP tables use ON CONFLICT DO NOTHING, inserting an already-existing triple is safe — the SID is returned for the existing row and sparql_update() counts it as 1 affected triple.
To write idempotent SQL migration scripts:
-- Safe to run multiple times
SELECT pg_ripple.sparql_update('
INSERT DATA {
<https://example.org/config> <https://example.org/version>
"2"^^<http://www.w3.org/2001/XMLSchema#integer>
}
');
Atomic replace (delete + insert)
Use DELETE/INSERT WHERE for atomic property replacement — it runs both phases in a single operation:
-- Atomic rename — no window where the property is absent:
SELECT pg_ripple.sparql_update('
DELETE { <https://example.org/alice> <https://schema.org/name> "Alice Smith" }
INSERT { <https://example.org/alice> <https://schema.org/name> "Alice Jones" }
WHERE { <https://example.org/alice> <https://schema.org/name> "Alice Smith" }
');
For cases where the old value is not known in advance:
SELECT pg_ripple.sparql_update('
DELETE { <https://example.org/alice> <https://schema.org/name> ?old }
INSERT { <https://example.org/alice> <https://schema.org/name> "Alice Jones" }
WHERE { <https://example.org/alice> <https://schema.org/name> ?old }
');
Using inline-encoded types for efficient range queries
For numeric or date predicates that will be compared in SPARQL FILTERs, use typed literals with an inline-compatible type:
| Use this type | Instead of |
|---|---|
"42"^^xsd:integer | "42" (plain string) |
"2024-01-01"^^xsd:date | "2024-01-01" (plain string) |
"true"^^xsd:boolean | "true" (plain string) |
With inline-encoded types, FILTER comparisons like FILTER(?age > 30) compile to WHERE o > <inline_id> — a simple integer comparison on the VP table column with no dictionary join.
-- Good: uses inline encoding for age
SELECT pg_ripple.sparql_update('
INSERT DATA {
<https://example.org/alice> <https://example.org/age>
"30"^^<http://www.w3.org/2001/XMLSchema#integer>
}
');
This page covers best practices for writing data to pg_ripple — when to use INSERT DATA / DELETE DATA, when to use the lower-level insert_triple / delete_triple functions, and how to write idempotent update scripts.
Choosing the right write API
| Scenario | Recommended API |
|---|---|
| Loading a large file (> ~1 000 triples) | load_ntriples() / load_turtle() |
| Inserting a single known triple from SQL | insert_triple() |
| Inserting triples from a SPARQL-capable client | sparql_update() with INSERT DATA |
| Removing an exact triple | delete_triple() or DELETE DATA |
| Pattern-based updates (find-and-replace) | DELETE/INSERT WHERE (v0.12.0) |
INSERT DATA vs bulk load
INSERT DATA and bulk load (load_ntriples) both result in identical on-disk storage, but their performance profiles differ:
sparql_update (INSERT DATA) | load_ntriples | |
|---|---|---|
| Per-triple overhead | Medium (SPL + dictionary lookup per term) | Low (batched dictionary ops) |
| Transaction boundary | One PG transaction per call | One PG transaction per call |
| Typical throughput | ~1 000–5 000 triples/sec | ~50 000–200 000 triples/sec |
| Use case | Small, targeted writes | Bulk ingestion |
For initial data loads, always use load_ntriples or load_turtle. Reserve sparql_update / INSERT DATA for incremental updates.
Idempotent insert patterns
Because vp_rare and dedicated VP tables use ON CONFLICT DO NOTHING, inserting an already-existing triple is safe — insert_triple() returns the existing SID and sparql_update() counts it as 1 affected triple regardless.
To write idempotent SQL migration scripts:
-- Safe to run multiple times
SELECT pg_ripple.sparql_update('
INSERT DATA {
<https://example.org/config> <https://example.org/version> "2"^^<http://www.w3.org/2001/XMLSchema#integer>
}
');
To implement a "set if not present" pattern (only insert if the subject doesn't already have the predicate):
-- Insert only if alice does not already have an email
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_ripple.find_triples(
'<https://example.org/alice>',
'<https://schema.org/email>',
NULL
)
) THEN
PERFORM pg_ripple.insert_triple(
'<https://example.org/alice>',
'<https://schema.org/email>',
'"alice@example.org"'
);
END IF;
END $$;
Atomic replace (delete + insert)
To atomically replace the value of a property:
BEGIN;
-- Remove old value(s)
SELECT pg_ripple.sparql_update(
'DELETE DATA { <https://example.org/alice> <https://schema.org/name> "Alice Smith" }'
);
-- Insert new value
SELECT pg_ripple.sparql_update(
'INSERT DATA { <https://example.org/alice> <https://schema.org/name> "Alice Jones" }'
);
COMMIT;
Tip: When replacing a value, wrap the delete and insert in a single
BEGIN / COMMITblock so readers never see the intermediate state where the property is absent.
Using inline-encoded types for efficient range queries
For numeric or date predicates that will be compared in SPARQL FILTERs, use typed literals with an inline-compatible type:
| Use this type | Instead of |
|---|---|
"42"^^xsd:integer | "42" (plain string) |
"2024-01-01"^^xsd:date | "2024-01-01" (plain string) |
"true"^^xsd:boolean | "true" (plain string) |
With inline-encoded types, FILTER comparisons like FILTER(?age > 30) compile to WHERE o > <inline_id> — a simple integer comparison on the VP table column with no dictionary join.
-- Good: uses inline encoding for age
SELECT pg_ripple.sparql_update('
INSERT DATA {
<https://example.org/alice> <https://example.org/age>
"30"^^<http://www.w3.org/2001/XMLSchema#integer>
}
');
-- Less efficient: stored as plain string; FILTER comparisons require dict join
SELECT pg_ripple.sparql_update('
INSERT DATA {
<https://example.org/alice> <https://example.org/age> "30"
}
');
Batch deletes
To delete all triples for a subject in a single SQL call (faster than DELETE DATA per-triple):
-- Delete all triples where alice is the subject
SELECT pg_ripple.delete_triple(s, p, o)
FROM pg_ripple.find_triples('<https://example.org/alice>', NULL, NULL)
AS t(s TEXT, p TEXT, o TEXT, g TEXT, i BIGINT);
For named-graph isolation, filter by graph ID:
SELECT pg_ripple.delete_triple(s, p, o)
FROM pg_ripple.find_triples(NULL, NULL, NULL)
WHERE g = pg_ripple.graph_id('<https://example.org/draft-graph>');
Federation Performance
This page covers practical strategies for getting the best performance from SPARQL SERVICE queries in pg_ripple.
Choosing a cache TTL
pg_ripple.federation_cache_ttl controls how long remote results are reused before the endpoint is re-queried. The right value depends on how quickly the source data changes.
| Data type | Suggested TTL |
|---|---|
| Slowly-changing reference data (Wikidata labels, DBpedia categories) | 3600–86400 seconds (1 hour to 1 day) |
| Daily batch data (published reports, snapshots) | 3600 seconds (1 hour) |
| Near-real-time data (news, stock prices) | 0 (disabled) |
| Highly dynamic data (sensor streams) | 0 (disabled) |
-- Cache Wikidata labels for 1 hour
SET pg_ripple.federation_cache_ttl = 3600;
-- Inspect cache hit rate (requires logging extension)
SELECT url,
COUNT(*) AS rows_cached,
MIN(cached_at) AS oldest_entry,
MAX(expires_at) AS latest_expiry
FROM _pg_ripple.federation_cache
GROUP BY url;
Tip: Set federation_cache_ttl at the session level before running batch federation jobs. Reset it to 0 for interactive queries where freshness matters.
Setting complexity hints
When a single query contacts multiple endpoints, set complexity hints so fast endpoints run first. This reduces total wall-clock time because:
- Fast endpoints resolve early, binding variables that may prune later patterns.
- Failures at slow endpoints are detected sooner.
SELECT pg_ripple.register_endpoint('https://fast-mirror.example.com/sparql', NULL, 'fast');
SELECT pg_ripple.register_endpoint('https://main-kb.example.com/sparql', NULL, 'normal');
SELECT pg_ripple.register_endpoint('https://archive.example.com/sparql', NULL, 'slow');
Or update after registration:
SELECT pg_ripple.set_endpoint_complexity('https://archive.example.com/sparql', 'slow');
View current hints:
SELECT url, complexity, enabled
FROM pg_ripple.list_endpoints()
ORDER BY complexity, url;
Designing queries for variable projection
pg_ripple automatically sends SELECT ?v1 ?v2 … WHERE { … } instead of SELECT * to remote endpoints. For maximum data reduction, write patterns that bind only the variables your outer query needs:
-- Less efficient: inner pattern binds ?s, ?p, ?o, ?label, ?type, ?comment
-- but outer query only needs ?label
SERVICE <https://kb.example.com/sparql> {
?s ?p ?o .
?s <rdfs:label> ?label .
?s <rdf:type> ?type .
?s <rdfs:comment> ?comment .
}
-- More efficient: inner pattern binds only ?label
SERVICE <https://kb.example.com/sparql> {
?s <rdfs:label> ?label .
}
Even if the remote endpoint does not honour projection (returning all columns anyway), the explicit projection reduces the size of the inline VALUES clause injected into the local SQL query.
Monitoring with federation_health
The _pg_ripple.federation_health table records every SERVICE call outcome. Use it to identify slow or flaky endpoints:
-- Latency percentiles per endpoint over the last hour
SELECT url,
PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY latency_ms) AS p50_ms,
PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY latency_ms) AS p95_ms,
PERCENTILE_CONT(0.99) WITHIN GROUP (ORDER BY latency_ms) AS p99_ms,
COUNT(*) AS total_calls,
ROUND(100.0 * AVG(CASE WHEN success THEN 1.0 ELSE 0.0 END), 1) AS success_pct
FROM _pg_ripple.federation_health
WHERE probed_at >= now() - INTERVAL '1 hour'
GROUP BY url
ORDER BY p95_ms DESC;
Use pg_ripple.federation_adaptive_timeout = on to automatically tighten timeouts for fast endpoints and give slow ones more headroom:
SET pg_ripple.federation_adaptive_timeout = on;
-- Effective timeout = max(1s, p95_latency * 3).
-- A 200ms p95 endpoint gets a 0.6s timeout (floored to 1s).
-- A 5000ms p95 endpoint gets a 15s timeout.
Monitoring with federation_cache
-- See which queries are being cached
SELECT url,
query_hash,
pg_size_pretty(octet_length(result_jsonb::text)) AS result_size,
cached_at,
expires_at,
CASE WHEN expires_at > now() THEN 'active' ELSE 'expired' END AS status
FROM _pg_ripple.federation_cache
ORDER BY cached_at DESC;
Expired rows are cleaned up automatically by the merge background worker. To evict immediately:
DELETE FROM _pg_ripple.federation_cache WHERE expires_at <= now();
Sidecar vs in-process tradeoffs
The pg_ripple_http sidecar (pg_ripple_http/) executes federation requests in an async Tokio runtime, enabling true parallel HTTP within a single query. The in-process SPI path (this page) is sequential.
| Approach | Latency | Concurrency | Setup |
|---|---|---|---|
| In-process SPI (default) | +1–5ms per call overhead | Sequential | None |
| pg_ripple_http sidecar | ~0 overhead, async | Parallel | Deploy sidecar binary |
For workloads with 3+ independent SERVICE clauses, the sidecar provides significant speedup. For 1–2 clauses or when the batch detection optimisation applies (same endpoint), the in-process path is sufficient.
Connection pooling tips
The thread-local connection pool (federation_pool_size) reuses TCP and TLS connections across multiple SERVICE calls in the same backend session. Each PostgreSQL backend has its own pool.
-- Increase pool size for sessions that query many endpoints
SET pg_ripple.federation_pool_size = 16;
-- Keep at 1 for single-use batch jobs to reduce memory usage
SET pg_ripple.federation_pool_size = 1;
Note: The pool is created on first use in a session and not recreated when federation_pool_size changes. For the new setting to take effect, start a new session.
Query Planning
pg_ripple translates SPARQL algebra into PostgreSQL SQL before execution. This page describes how the plan is constructed and how to tune it.
Plan cache
Every translated plan is cached per-backend in an LRU cache keyed on an
algebra digest (XXH3-128 of the normalised SPARQL IR, plus the current
values of max_path_depth and bgp_reorder). This means:
- Whitespace variants and prefix-alias variants of the same query share one cache slot.
- Changing
SET pg_ripple.bgp_reorder = offcauses a cache miss and triggers re-translation. - The cache is backend-local and cleared on connection close.
Inspect cache health with:
SELECT * FROM pg_ripple.plan_cache_stats();
-- Returns: (hit_count, miss_count, current_size, capacity)
Reset counters without a reconnect:
SELECT pg_ripple.reset_plan_cache();
BGP reordering
By default (pg_ripple.bgp_reorder = on) the SPARQL optimizer permutes triple
patterns in a BGP to minimise intermediate result sizes based on per-predicate
triple counts. Disable to force left-to-right evaluation order:
SET pg_ripple.bgp_reorder = off;
Predicate catalog
The predicate catalog (storage/catalog.rs) caches predicate → VP table OID
mappings to eliminate one SPI lookup per predicate per query. For a 10-atom
BGP this reduces dictionary-related SPI overhead from 10 to 1.
-- Invalidate after schema changes or shape updates:
SELECT pg_ripple.invalidate_catalog_cache();
The catalog cache is enabled by default. Disable for debugging:
SET pg_ripple.predicate_cache_enabled = off;
SHACL-driven SQL hints
After loading SHACL shapes, the SQL generator reads per-predicate hints from
_pg_ripple.shape_hints to produce more efficient SQL:
| SHACL constraint | SQL optimisation |
|---|---|
sh:maxCount 1 | Omit DISTINCT — at most one binding per subject |
sh:minCount 1 | Use INNER JOIN instead of LEFT JOIN for OPTIONAL |
Load shapes and verify hints were written:
SELECT pg_ripple.load_shacl($$ @prefix sh: <...> . @prefix ex: <...> . ex:MyShape a sh:NodeShape ; sh:targetClass ex:Thing ; sh:property [ sh:path ex:name ; sh:maxCount 1 ] . $$);
SELECT * FROM _pg_ripple.shape_hints LIMIT 10;
EXPLAIN a SPARQL query
Use sparql_explain to see the SQL generated for a query:
SELECT pg_ripple.sparql_explain(
'SELECT * WHERE { ?s <http://schema.org/name> ?name }',
analyze := false
);
Pass analyze := true to include actual runtime statistics (runs the query).
Property path depth
Recursive property paths (*, +) use WITH RECURSIVE … CYCLE with a
configurable depth limit:
SET pg_ripple.max_path_depth = 32; -- default: 64
Note:
property_path_max_depthis a deprecated alias formax_path_depthand will be removed in a future release.
Useful GUCs for query planning
| GUC | Default | Effect |
|---|---|---|
pg_ripple.bgp_reorder | on | Reorder triple patterns by selectivity |
pg_ripple.max_path_depth | 64 | Max recursion depth for * / + paths |
pg_ripple.predicate_cache_enabled | on | Cache predicate → VP table OIDs |
pg_ripple.plan_cache_size | 256 | LRU capacity of the per-backend plan cache |
SQL Function Reference
All 157 SQL functions exposed by pg_ripple, grouped by use case. Every function lives in the pg_ripple schema.
All examples assume SET search_path TO pg_ripple, public;. If you prefer explicit qualification, prefix every call with pg_ripple..
Loading
Functions for inserting and bulk-loading RDF data.
insert_triple
Insert a single triple into the default graph.
pg_ripple.insert_triple(
subject TEXT,
predicate TEXT,
object TEXT
) RETURNS BIGINT
SELECT pg_ripple.insert_triple(
'<https://example.org/alice>',
'<https://example.org/knows>',
'<https://example.org/bob>'
);
load_turtle
Parse a Turtle string and load all triples into the default graph.
pg_ripple.load_turtle(
data TEXT,
strict BOOLEAN DEFAULT false
) RETURNS BIGINT
SELECT pg_ripple.load_turtle('
@prefix ex: <https://example.org/> .
ex:alice ex:name "Alice" ;
ex:knows ex:bob .
');
load_turtle_file
Load Turtle from a server-side file path.
pg_ripple.load_turtle_file(
path TEXT,
strict BOOLEAN DEFAULT false
) RETURNS BIGINT
SELECT pg_ripple.load_turtle_file('/data/ontology.ttl');
load_ntriples
Parse an N-Triples string and load all triples into the default graph.
pg_ripple.load_ntriples(
data TEXT,
strict BOOLEAN DEFAULT false
) RETURNS BIGINT
SELECT pg_ripple.load_ntriples('
<https://example.org/alice> <https://example.org/name> "Alice" .
<https://example.org/alice> <https://example.org/knows> <https://example.org/bob> .
');
load_ntriples_file
Load N-Triples from a server-side file path.
pg_ripple.load_ntriples_file(
path TEXT,
strict BOOLEAN DEFAULT false
) RETURNS BIGINT
SELECT pg_ripple.load_ntriples_file('/data/dump.nt');
load_nquads
Parse an N-Quads string and load triples into their respective named graphs.
pg_ripple.load_nquads(
data TEXT,
strict BOOLEAN DEFAULT false
) RETURNS BIGINT
SELECT pg_ripple.load_nquads('
<https://example.org/alice> <https://example.org/name> "Alice" <https://example.org/g1> .
');
load_nquads_file
Load N-Quads from a server-side file path.
pg_ripple.load_nquads_file(
path TEXT,
strict BOOLEAN DEFAULT false
) RETURNS BIGINT
SELECT pg_ripple.load_nquads_file('/data/dump.nq');
load_trig
Parse a TriG string and load triples into their named graphs.
pg_ripple.load_trig(
data TEXT,
strict BOOLEAN DEFAULT false
) RETURNS BIGINT
SELECT pg_ripple.load_trig('
@prefix ex: <https://example.org/> .
ex:g1 { ex:alice ex:name "Alice" . }
');
load_trig_file
Load TriG from a server-side file path.
pg_ripple.load_trig_file(
path TEXT,
strict BOOLEAN DEFAULT false
) RETURNS BIGINT
SELECT pg_ripple.load_trig_file('/data/dataset.trig');
load_rdfxml
Parse an RDF/XML string and load all triples into the default graph.
pg_ripple.load_rdfxml(
data TEXT,
strict BOOLEAN DEFAULT false
) RETURNS BIGINT
SELECT pg_ripple.load_rdfxml('
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:ex="https://example.org/">
<rdf:Description rdf:about="https://example.org/alice">
<ex:name>Alice</ex:name>
</rdf:Description>
</rdf:RDF>
');
load_rdfxml_file
Load RDF/XML from a server-side file path.
pg_ripple.load_rdfxml_file(
path TEXT,
strict BOOLEAN DEFAULT false
) RETURNS BIGINT
SELECT pg_ripple.load_rdfxml_file('/data/ontology.rdf');
load_ntriples_into_graph
Parse N-Triples and load into a specific named graph.
pg_ripple.load_ntriples_into_graph(
data TEXT,
graph TEXT,
strict BOOLEAN DEFAULT false
) RETURNS BIGINT
SELECT pg_ripple.load_ntriples_into_graph(
'<https://example.org/alice> <https://example.org/name> "Alice" .',
'<https://example.org/people>'
);
load_turtle_into_graph
Parse Turtle and load into a specific named graph.
pg_ripple.load_turtle_into_graph(
data TEXT,
graph TEXT,
strict BOOLEAN DEFAULT false
) RETURNS BIGINT
SELECT pg_ripple.load_turtle_into_graph(
'@prefix ex: <https://example.org/> . ex:alice ex:name "Alice" .',
'<https://example.org/people>'
);
load_rdfxml_into_graph
Parse RDF/XML and load into a specific named graph.
pg_ripple.load_rdfxml_into_graph(
data TEXT,
graph TEXT,
strict BOOLEAN DEFAULT false
) RETURNS BIGINT
SELECT pg_ripple.load_rdfxml_into_graph(
'<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:ex="https://example.org/">
<rdf:Description rdf:about="https://example.org/alice">
<ex:name>Alice</ex:name>
</rdf:Description>
</rdf:RDF>',
'<https://example.org/people>'
);
load_ntriples_file_into_graph
Load N-Triples from a server-side file into a named graph.
pg_ripple.load_ntriples_file_into_graph(
path TEXT,
graph TEXT,
strict BOOLEAN DEFAULT false
) RETURNS BIGINT
SELECT pg_ripple.load_ntriples_file_into_graph(
'/data/people.nt',
'<https://example.org/people>'
);
load_turtle_file_into_graph
Load Turtle from a server-side file into a named graph.
pg_ripple.load_turtle_file_into_graph(
path TEXT,
graph TEXT,
strict BOOLEAN DEFAULT false
) RETURNS BIGINT
SELECT pg_ripple.load_turtle_file_into_graph(
'/data/people.ttl',
'<https://example.org/people>'
);
load_rdfxml_file_into_graph
Load RDF/XML from a server-side file into a named graph.
pg_ripple.load_rdfxml_file_into_graph(
path TEXT,
graph TEXT,
strict BOOLEAN DEFAULT false
) RETURNS BIGINT
SELECT pg_ripple.load_rdfxml_file_into_graph(
'/data/people.rdf',
'<https://example.org/people>'
);
load_owl_ontology
Load an OWL ontology from Turtle, extracting class and property declarations for use by the Datalog reasoner.
pg_ripple.load_owl_ontology(
data TEXT
) RETURNS BIGINT
SELECT pg_ripple.load_owl_ontology('
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix ex: <https://example.org/> .
ex:Person a owl:Class .
ex:knows a owl:ObjectProperty ;
owl:inverseOf ex:knownBy .
');
apply_patch
Apply an RDF patch (additions and deletions) atomically.
pg_ripple.apply_patch(
additions TEXT,
deletions TEXT
) RETURNS BIGINT
SELECT pg_ripple.apply_patch(
'<https://example.org/alice> <https://example.org/age> "31"^^<http://www.w3.org/2001/XMLSchema#integer> .',
'<https://example.org/alice> <https://example.org/age> "30"^^<http://www.w3.org/2001/XMLSchema#integer> .'
);
Querying
Functions for querying triples with SPARQL and text search.
sparql
Execute a SPARQL SELECT query and return results as a set of JSON objects.
pg_ripple.sparql(
query TEXT
) RETURNS SETOF JSON
SELECT * FROM pg_ripple.sparql('
PREFIX ex: <https://example.org/>
SELECT ?name WHERE { ex:alice ex:name ?name }
');
sparql_ask
Execute a SPARQL ASK query and return a boolean result.
pg_ripple.sparql_ask(
query TEXT
) RETURNS BOOLEAN
SELECT pg_ripple.sparql_ask('
PREFIX ex: <https://example.org/>
ASK { ex:alice ex:knows ex:bob }
');
sparql_explain
Return the SQL execution plan for a SPARQL query without executing it.
pg_ripple.sparql_explain(
query TEXT
) RETURNS TEXT
SELECT pg_ripple.sparql_explain('
PREFIX ex: <https://example.org/>
SELECT ?x WHERE { ?x ex:knows ex:bob }
');
explain_sparql
Return a detailed query plan showing SPARQL algebra and generated SQL.
pg_ripple.explain_sparql(
query TEXT
) RETURNS TEXT
SELECT pg_ripple.explain_sparql('
PREFIX ex: <https://example.org/>
SELECT ?x ?y WHERE { ?x ex:knows ?y }
');
sparql_construct
Execute a SPARQL CONSTRUCT query and return triples as JSON.
pg_ripple.sparql_construct(
query TEXT
) RETURNS SETOF JSON
SELECT * FROM pg_ripple.sparql_construct('
PREFIX ex: <https://example.org/>
CONSTRUCT { ?x ex:friendOf ?y }
WHERE { ?x ex:knows ?y }
');
sparql_describe
Execute a SPARQL DESCRIBE query and return all triples about a resource.
pg_ripple.sparql_describe(
query TEXT
) RETURNS SETOF JSON
SELECT * FROM pg_ripple.sparql_describe('
PREFIX ex: <https://example.org/>
DESCRIBE ex:alice
');
sparql_construct_turtle
Execute a SPARQL CONSTRUCT query and return the result as a Turtle string.
pg_ripple.sparql_construct_turtle(
query TEXT
) RETURNS TEXT
SELECT pg_ripple.sparql_construct_turtle('
PREFIX ex: <https://example.org/>
CONSTRUCT { ?x ex:friendOf ?y }
WHERE { ?x ex:knows ?y }
');
sparql_construct_jsonld
Execute a SPARQL CONSTRUCT query and return the result as a JSON-LD string.
pg_ripple.sparql_construct_jsonld(
query TEXT
) RETURNS TEXT
SELECT pg_ripple.sparql_construct_jsonld('
PREFIX ex: <https://example.org/>
CONSTRUCT { ?x ex:friendOf ?y }
WHERE { ?x ex:knows ?y }
');
sparql_describe_turtle
Execute a SPARQL DESCRIBE query and return the result as Turtle.
pg_ripple.sparql_describe_turtle(
query TEXT
) RETURNS TEXT
SELECT pg_ripple.sparql_describe_turtle('
PREFIX ex: <https://example.org/>
DESCRIBE ex:alice
');
sparql_describe_jsonld
Execute a SPARQL DESCRIBE query and return the result as JSON-LD.
pg_ripple.sparql_describe_jsonld(
query TEXT
) RETURNS TEXT
SELECT pg_ripple.sparql_describe_jsonld('
PREFIX ex: <https://example.org/>
DESCRIBE ex:alice
');
sparql_update
Execute a SPARQL Update operation (INSERT DATA, DELETE DATA, etc.).
pg_ripple.sparql_update(
query TEXT
) RETURNS BIGINT
SELECT pg_ripple.sparql_update('
PREFIX ex: <https://example.org/>
INSERT DATA { ex:alice ex:age 30 }
');
find_triples
Find triples matching a pattern in the default graph. Pass NULL for wildcards.
pg_ripple.find_triples(
subject TEXT DEFAULT NULL,
predicate TEXT DEFAULT NULL,
object TEXT DEFAULT NULL
) RETURNS TABLE(subject TEXT, predicate TEXT, object TEXT)
SELECT * FROM pg_ripple.find_triples(
'<https://example.org/alice>', NULL, NULL
);
find_triples_in_graph
Find triples matching a pattern in a specific named graph.
pg_ripple.find_triples_in_graph(
subject TEXT DEFAULT NULL,
predicate TEXT DEFAULT NULL,
object TEXT DEFAULT NULL,
graph TEXT DEFAULT NULL
) RETURNS TABLE(subject TEXT, predicate TEXT, object TEXT, graph TEXT)
SELECT * FROM pg_ripple.find_triples_in_graph(
NULL, NULL, NULL, '<https://example.org/people>'
);
triple_count
Return the total number of triples in the default graph.
pg_ripple.triple_count() RETURNS BIGINT
SELECT pg_ripple.triple_count();
triple_count_in_graph
Return the number of triples in a specific named graph.
pg_ripple.triple_count_in_graph(
graph TEXT
) RETURNS BIGINT
SELECT pg_ripple.triple_count_in_graph('<https://example.org/people>');
fts_index
Build or rebuild the full-text search index over literal values.
pg_ripple.fts_index() RETURNS VOID
SELECT pg_ripple.fts_index();
fts_search
Search for triples containing a term in literal values via full-text search.
pg_ripple.fts_search(
query TEXT,
limit_rows INTEGER DEFAULT 100
) RETURNS TABLE(subject TEXT, predicate TEXT, object TEXT, rank REAL)
SELECT * FROM pg_ripple.fts_search('knowledge graph', 10);
Graphs
Functions for managing named graphs.
create_graph
Create a named graph.
pg_ripple.create_graph(
graph TEXT
) RETURNS VOID
SELECT pg_ripple.create_graph('<https://example.org/people>');
drop_graph
Drop a named graph and all its triples.
pg_ripple.drop_graph(
graph TEXT
) RETURNS VOID
SELECT pg_ripple.drop_graph('<https://example.org/people>');
list_graphs
List all named graphs.
pg_ripple.list_graphs() RETURNS TABLE(graph TEXT, triple_count BIGINT)
SELECT * FROM pg_ripple.list_graphs();
clear_graph
Remove all triples from a graph without dropping it.
pg_ripple.clear_graph(
graph TEXT
) RETURNS BIGINT
SELECT pg_ripple.clear_graph('<https://example.org/people>');
Dictionary
Functions for interacting with the dictionary encoder that maps IRIs, blank nodes, and literals to integer IDs.
Most users never need to call dictionary functions directly. They are useful for debugging, performance tuning, and understanding storage internals.
encode_term
Encode an IRI, literal, or blank node to its integer ID.
pg_ripple.encode_term(
term TEXT
) RETURNS BIGINT
SELECT pg_ripple.encode_term('<https://example.org/alice>');
decode_id
Decode an integer ID back to its string representation.
pg_ripple.decode_id(
id BIGINT
) RETURNS TEXT
SELECT pg_ripple.decode_id(42);
encode_triple
Encode a full triple (subject, predicate, object) to integer IDs.
pg_ripple.encode_triple(
subject TEXT,
predicate TEXT,
object TEXT
) RETURNS TABLE(s BIGINT, p BIGINT, o BIGINT)
SELECT * FROM pg_ripple.encode_triple(
'<https://example.org/alice>',
'<https://example.org/knows>',
'<https://example.org/bob>'
);
decode_triple
Decode a triple from integer IDs back to string form.
pg_ripple.decode_triple(
s BIGINT,
p BIGINT,
o BIGINT
) RETURNS TABLE(subject TEXT, predicate TEXT, object TEXT)
SELECT * FROM pg_ripple.decode_triple(1, 2, 3);
decode_id_full
Decode an integer ID returning the full term with type information.
pg_ripple.decode_id_full(
id BIGINT
) RETURNS JSON
SELECT pg_ripple.decode_id_full(42);
lookup_iri
Look up the integer ID for a specific IRI without inserting.
pg_ripple.lookup_iri(
iri TEXT
) RETURNS BIGINT
SELECT pg_ripple.lookup_iri('<https://example.org/alice>');
dictionary_stats
Return statistics about the dictionary table.
pg_ripple.dictionary_stats() RETURNS JSON
SELECT pg_ripple.dictionary_stats();
prewarm_dictionary_hot
Load the most frequently accessed dictionary entries into the shared cache.
pg_ripple.prewarm_dictionary_hot(
limit_rows INTEGER DEFAULT 10000
) RETURNS INTEGER
SELECT pg_ripple.prewarm_dictionary_hot(50000);
cache_stats
Return cache hit/miss statistics for the dictionary LRU cache.
pg_ripple.cache_stats() RETURNS JSON
SELECT pg_ripple.cache_stats();
Prefixes
Functions for managing namespace prefix abbreviations.
register_prefix
Register a namespace prefix for use in SPARQL queries and output.
pg_ripple.register_prefix(
prefix TEXT,
iri TEXT
) RETURNS VOID
SELECT pg_ripple.register_prefix('ex', 'https://example.org/');
prefixes
List all registered prefixes.
pg_ripple.prefixes() RETURNS TABLE(prefix TEXT, iri TEXT)
SELECT * FROM pg_ripple.prefixes();
Validating
Functions for loading SHACL shapes, validating data, and managing async validation.
load_shacl
Load SHACL shapes from a Turtle string.
pg_ripple.load_shacl(
shapes TEXT
) RETURNS INTEGER
SELECT pg_ripple.load_shacl('
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <https://example.org/> .
ex:PersonShape a sh:NodeShape ;
sh:targetClass ex:Person ;
sh:property [ sh:path ex:name ; sh:minCount 1 ; sh:datatype xsd:string ] .
');
validate
Run SHACL validation and return a validation report.
pg_ripple.validate() RETURNS TABLE(
focus_node TEXT,
shape TEXT,
path TEXT,
severity TEXT,
message TEXT
)
SELECT * FROM pg_ripple.validate();
list_shapes
List all loaded SHACL shapes.
pg_ripple.list_shapes() RETURNS TABLE(shape TEXT, target TEXT, property_count INTEGER)
SELECT * FROM pg_ripple.list_shapes();
drop_shape
Drop a SHACL shape by IRI.
pg_ripple.drop_shape(
shape TEXT
) RETURNS VOID
SELECT pg_ripple.drop_shape('<https://example.org/PersonShape>');
enable_shacl_monitors
Enable trigger-based SHACL validation on all VP tables.
pg_ripple.enable_shacl_monitors() RETURNS VOID
SELECT pg_ripple.enable_shacl_monitors();
enable_shacl_dag_monitors
Enable DAG-aware SHACL monitors using pg_trickle for async validation.
pg_ripple.enable_shacl_dag_monitors() RETURNS VOID
SELECT pg_ripple.enable_shacl_dag_monitors();
disable_shacl_dag_monitors
Disable DAG-aware SHACL monitors.
pg_ripple.disable_shacl_dag_monitors() RETURNS VOID
SELECT pg_ripple.disable_shacl_dag_monitors();
list_shacl_dag_monitors
List all active DAG SHACL monitors.
pg_ripple.list_shacl_dag_monitors() RETURNS TABLE(shape TEXT, predicate TEXT, enabled BOOLEAN)
SELECT * FROM pg_ripple.list_shacl_dag_monitors();
process_validation_queue
Process pending items in the async SHACL validation queue.
pg_ripple.process_validation_queue(
batch_size INTEGER DEFAULT 100
) RETURNS INTEGER
SELECT pg_ripple.process_validation_queue(500);
validation_queue_length
Return the number of items pending in the validation queue.
pg_ripple.validation_queue_length() RETURNS BIGINT
SELECT pg_ripple.validation_queue_length();
dead_letter_count
Return the number of items in the validation dead-letter queue.
pg_ripple.dead_letter_count() RETURNS BIGINT
SELECT pg_ripple.dead_letter_count();
dead_letter_queue
Return the contents of the validation dead-letter queue.
pg_ripple.dead_letter_queue() RETURNS TABLE(
id BIGINT,
triple_id BIGINT,
shape TEXT,
error TEXT,
created_at TIMESTAMPTZ
)
SELECT * FROM pg_ripple.dead_letter_queue();
drain_dead_letter_queue
Remove and return all items from the dead-letter queue.
pg_ripple.drain_dead_letter_queue() RETURNS INTEGER
SELECT pg_ripple.drain_dead_letter_queue();
Reasoning
Functions for Datalog rule management and inference.
pg_ripple ships with RDFS and OWL RL rule sets. Load them with load_rules_builtin('rdfs') or load_rules_builtin('owl-rl').
load_rules
Load a named Datalog rule set from a program string.
pg_ripple.load_rules(
name TEXT,
program TEXT
) RETURNS INTEGER
SELECT pg_ripple.load_rules('transitive-knows', '
knows(X, Z) :- knows(X, Y), knows(Y, Z).
');
load_rules_builtin
Load a built-in rule set (rdfs, owl-rl).
pg_ripple.load_rules_builtin(
name TEXT
) RETURNS INTEGER
SELECT pg_ripple.load_rules_builtin('owl-rl');
list_rules
List all loaded rule sets.
pg_ripple.list_rules() RETURNS TABLE(name TEXT, rule_count INTEGER, enabled BOOLEAN)
SELECT * FROM pg_ripple.list_rules();
drop_rules
Drop a rule set by name.
pg_ripple.drop_rules(
name TEXT
) RETURNS VOID
SELECT pg_ripple.drop_rules('transitive-knows');
enable_rule_set
Enable a rule set for inference.
pg_ripple.enable_rule_set(
name TEXT
) RETURNS VOID
SELECT pg_ripple.enable_rule_set('owl-rl');
disable_rule_set
Disable a rule set (triples already inferred are not removed).
pg_ripple.disable_rule_set(
name TEXT
) RETURNS VOID
SELECT pg_ripple.disable_rule_set('owl-rl');
infer
Run materialization using all enabled rule sets (semi-naive evaluation).
pg_ripple.infer() RETURNS BIGINT
SELECT pg_ripple.infer();
infer_with_stats
Run materialization and return iteration statistics.
pg_ripple.infer_with_stats() RETURNS JSON
SELECT pg_ripple.infer_with_stats();
infer_goal
Run goal-directed inference for a specific query pattern.
pg_ripple.infer_goal(
subject TEXT DEFAULT NULL,
predicate TEXT DEFAULT NULL,
object TEXT DEFAULT NULL
) RETURNS BIGINT
SELECT pg_ripple.infer_goal(
'<https://example.org/alice>',
'<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>',
NULL
);
infer_agg
Run Datalog aggregation rules (min, max, sum, count).
pg_ripple.infer_agg() RETURNS BIGINT
SELECT pg_ripple.infer_agg();
infer_demand
Run demand-driven inference with magic sets optimization.
pg_ripple.infer_demand(
subject TEXT DEFAULT NULL,
predicate TEXT DEFAULT NULL,
object TEXT DEFAULT NULL
) RETURNS BIGINT
SELECT pg_ripple.infer_demand(
'<https://example.org/alice>',
'<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>',
NULL
);
infer_wfs
Run well-founded semantics evaluation for programs with negation.
pg_ripple.infer_wfs() RETURNS BIGINT
SELECT pg_ripple.infer_wfs();
tabling_stats
Return statistics about the tabling memo store.
pg_ripple.tabling_stats() RETURNS JSON
SELECT pg_ripple.tabling_stats();
rule_plan_cache_stats
Return statistics about the Datalog rule plan cache.
pg_ripple.rule_plan_cache_stats() RETURNS JSON
SELECT pg_ripple.rule_plan_cache_stats();
check_constraints
Run Datalog constraint rules and report violations.
pg_ripple.check_constraints() RETURNS TABLE(rule TEXT, subject TEXT, message TEXT)
SELECT * FROM pg_ripple.check_constraints();
Exporting
Functions for serializing triples to various formats.
export_ntriples
Export all triples as an N-Triples string.
pg_ripple.export_ntriples() RETURNS TEXT
SELECT pg_ripple.export_ntriples();
export_nquads
Export all triples (with named graphs) as an N-Quads string.
pg_ripple.export_nquads() RETURNS TEXT
SELECT pg_ripple.export_nquads();
export_turtle
Export all triples as a Turtle string.
pg_ripple.export_turtle() RETURNS TEXT
SELECT pg_ripple.export_turtle();
export_jsonld
Export all triples as a JSON-LD string.
pg_ripple.export_jsonld() RETURNS TEXT
SELECT pg_ripple.export_jsonld();
export_turtle_stream
Export triples as a streaming set of Turtle chunks for large datasets.
pg_ripple.export_turtle_stream(
batch_size INTEGER DEFAULT 1000
) RETURNS SETOF TEXT
SELECT * FROM pg_ripple.export_turtle_stream(5000);
export_jsonld_stream
Export triples as a streaming set of JSON-LD chunks for large datasets.
pg_ripple.export_jsonld_stream(
batch_size INTEGER DEFAULT 1000
) RETURNS SETOF TEXT
SELECT * FROM pg_ripple.export_jsonld_stream(5000);
export_graphrag_entities
Export entities in GraphRAG entity format for Microsoft GraphRAG or compatible tools.
pg_ripple.export_graphrag_entities() RETURNS SETOF JSON
SELECT * FROM pg_ripple.export_graphrag_entities();
export_graphrag_relationships
Export relationships in GraphRAG relationship format.
pg_ripple.export_graphrag_relationships() RETURNS SETOF JSON
SELECT * FROM pg_ripple.export_graphrag_relationships();
export_graphrag_text_units
Export text units in GraphRAG text-unit format.
pg_ripple.export_graphrag_text_units() RETURNS SETOF JSON
SELECT * FROM pg_ripple.export_graphrag_text_units();
JSON-LD Framing
Functions for JSON-LD framing and tree-shaped output.
jsonld_frame_to_sparql
Convert a JSON-LD frame to a SPARQL CONSTRUCT query.
pg_ripple.jsonld_frame_to_sparql(
frame JSON
) RETURNS TEXT
SELECT pg_ripple.jsonld_frame_to_sparql('{
"@type": "https://example.org/Person",
"https://example.org/name": {}
}'::json);
export_jsonld_framed
Export triples shaped by a JSON-LD frame as a JSON-LD string.
pg_ripple.export_jsonld_framed(
frame JSON
) RETURNS TEXT
SELECT pg_ripple.export_jsonld_framed('{
"@type": "https://example.org/Person",
"https://example.org/name": {},
"https://example.org/knows": { "@type": "https://example.org/Person" }
}'::json);
export_jsonld_framed_stream
Export framed JSON-LD as a streaming set of chunks.
pg_ripple.export_jsonld_framed_stream(
frame JSON,
batch_size INTEGER DEFAULT 100
) RETURNS SETOF TEXT
SELECT * FROM pg_ripple.export_jsonld_framed_stream('{
"@type": "https://example.org/Person"
}'::json, 50);
jsonld_frame
Apply a JSON-LD frame to an existing JSON-LD document.
pg_ripple.jsonld_frame(
document JSON,
frame JSON
) RETURNS JSON
SELECT pg_ripple.jsonld_frame(
pg_ripple.export_jsonld()::json,
'{"@type": "https://example.org/Person"}'::json
);
Views
Functions for creating and managing materialized SPARQL, Datalog, CONSTRUCT, DESCRIBE, ASK, and framing views.
Views are backed by PostgreSQL tables or views. Use the corresponding drop_*_view function to remove them. Dropping the extension also removes all views.
create_sparql_view
Create a PostgreSQL view backed by a SPARQL SELECT query.
pg_ripple.create_sparql_view(
name TEXT,
query TEXT
) RETURNS VOID
SELECT pg_ripple.create_sparql_view('people', '
PREFIX ex: <https://example.org/>
SELECT ?name WHERE { ?person a ex:Person ; ex:name ?name }
');
drop_sparql_view
Drop a SPARQL view.
pg_ripple.drop_sparql_view(
name TEXT
) RETURNS VOID
SELECT pg_ripple.drop_sparql_view('people');
list_sparql_views
List all SPARQL views.
pg_ripple.list_sparql_views() RETURNS TABLE(name TEXT, query TEXT)
SELECT * FROM pg_ripple.list_sparql_views();
create_datalog_view
Create a PostgreSQL view backed by a Datalog rule.
pg_ripple.create_datalog_view(
name TEXT,
rule TEXT
) RETURNS VOID
SELECT pg_ripple.create_datalog_view('ancestor',
'ancestor(X, Z) :- parent(X, Y), ancestor(Y, Z).'
);
create_datalog_view_from_rule_set
Create a view from a named rule set's head predicate.
pg_ripple.create_datalog_view_from_rule_set(
view_name TEXT,
rule_set_name TEXT,
head_predicate TEXT
) RETURNS VOID
SELECT pg_ripple.create_datalog_view_from_rule_set(
'inferred_types', 'owl-rl', 'rdf:type'
);
drop_datalog_view
Drop a Datalog view.
pg_ripple.drop_datalog_view(
name TEXT
) RETURNS VOID
SELECT pg_ripple.drop_datalog_view('ancestor');
list_datalog_views
List all Datalog views.
pg_ripple.list_datalog_views() RETURNS TABLE(name TEXT, rule_set TEXT, head TEXT)
SELECT * FROM pg_ripple.list_datalog_views();
create_framing_view
Create a PostgreSQL view backed by a JSON-LD frame.
pg_ripple.create_framing_view(
name TEXT,
frame JSON
) RETURNS VOID
SELECT pg_ripple.create_framing_view('person_frame', '{
"@type": "https://example.org/Person",
"https://example.org/name": {}
}'::json);
drop_framing_view
Drop a framing view.
pg_ripple.drop_framing_view(
name TEXT
) RETURNS VOID
SELECT pg_ripple.drop_framing_view('person_frame');
list_framing_views
List all framing views.
pg_ripple.list_framing_views() RETURNS TABLE(name TEXT, frame JSON)
SELECT * FROM pg_ripple.list_framing_views();
create_construct_view
Create a view backed by a SPARQL CONSTRUCT query.
pg_ripple.create_construct_view(
name TEXT,
query TEXT
) RETURNS VOID
SELECT pg_ripple.create_construct_view('friends', '
PREFIX ex: <https://example.org/>
CONSTRUCT { ?a ex:friendOf ?b }
WHERE { ?a ex:knows ?b }
');
drop_construct_view
Drop a CONSTRUCT view.
pg_ripple.drop_construct_view(
name TEXT
) RETURNS VOID
SELECT pg_ripple.drop_construct_view('friends');
list_construct_views
List all CONSTRUCT views.
pg_ripple.list_construct_views() RETURNS TABLE(name TEXT, query TEXT)
SELECT * FROM pg_ripple.list_construct_views();
create_describe_view
Create a view backed by a SPARQL DESCRIBE query.
pg_ripple.create_describe_view(
name TEXT,
query TEXT
) RETURNS VOID
SELECT pg_ripple.create_describe_view('alice_detail', '
PREFIX ex: <https://example.org/>
DESCRIBE ex:alice
');
drop_describe_view
Drop a DESCRIBE view.
pg_ripple.drop_describe_view(
name TEXT
) RETURNS VOID
SELECT pg_ripple.drop_describe_view('alice_detail');
list_describe_views
List all DESCRIBE views.
pg_ripple.list_describe_views() RETURNS TABLE(name TEXT, query TEXT)
SELECT * FROM pg_ripple.list_describe_views();
create_ask_view
Create a view backed by a SPARQL ASK query.
pg_ripple.create_ask_view(
name TEXT,
query TEXT
) RETURNS VOID
SELECT pg_ripple.create_ask_view('has_alice', '
PREFIX ex: <https://example.org/>
ASK { ex:alice ex:name ?n }
');
drop_ask_view
Drop an ASK view.
pg_ripple.drop_ask_view(
name TEXT
) RETURNS VOID
SELECT pg_ripple.drop_ask_view('has_alice');
list_ask_views
List all ASK views.
pg_ripple.list_ask_views() RETURNS TABLE(name TEXT, query TEXT)
SELECT * FROM pg_ripple.list_ask_views();
create_extvp
Create an Extended VP (ExtVP) index for a predicate pair to accelerate star-pattern joins.
pg_ripple.create_extvp(
predicate1 TEXT,
predicate2 TEXT
) RETURNS VOID
SELECT pg_ripple.create_extvp(
'<https://example.org/name>',
'<https://example.org/age>'
);
drop_extvp
Drop an ExtVP index.
pg_ripple.drop_extvp(
predicate1 TEXT,
predicate2 TEXT
) RETURNS VOID
SELECT pg_ripple.drop_extvp(
'<https://example.org/name>',
'<https://example.org/age>'
);
list_extvp
List all ExtVP indices.
pg_ripple.list_extvp() RETURNS TABLE(predicate1 TEXT, predicate2 TEXT, row_count BIGINT)
SELECT * FROM pg_ripple.list_extvp();
Federation
Functions for managing SPARQL federation endpoints.
register_endpoint
Register a remote SPARQL endpoint for federated queries.
pg_ripple.register_endpoint(
name TEXT,
url TEXT
) RETURNS VOID
SELECT pg_ripple.register_endpoint('wikidata', 'https://query.wikidata.org/sparql');
set_endpoint_complexity
Set the complexity weight for a federated endpoint (used by the query planner).
pg_ripple.set_endpoint_complexity(
name TEXT,
complexity REAL
) RETURNS VOID
SELECT pg_ripple.set_endpoint_complexity('wikidata', 2.5);
remove_endpoint
Remove a registered endpoint.
pg_ripple.remove_endpoint(
name TEXT
) RETURNS VOID
SELECT pg_ripple.remove_endpoint('wikidata');
disable_endpoint
Temporarily disable an endpoint without removing it.
pg_ripple.disable_endpoint(
name TEXT
) RETURNS VOID
SELECT pg_ripple.disable_endpoint('wikidata');
list_endpoints
List all registered federation endpoints.
pg_ripple.list_endpoints() RETURNS TABLE(name TEXT, url TEXT, enabled BOOLEAN, complexity REAL)
SELECT * FROM pg_ripple.list_endpoints();
register_vector_endpoint
Register a vector similarity search endpoint for hybrid SPARQL+vector queries.
pg_ripple.register_vector_endpoint(
name TEXT,
url TEXT,
model TEXT
) RETURNS VOID
SELECT pg_ripple.register_vector_endpoint(
'openai', 'https://api.openai.com/v1/embeddings', 'text-embedding-3-small'
);
Vector / Hybrid Search
Functions for vector embeddings, similarity search, and RAG retrieval.
All vector functions require pgvector to be installed. Set pg_ripple.pgvector_enabled = off to disable without uninstalling.
store_embedding
Store a precomputed embedding vector for an entity.
pg_ripple.store_embedding(
entity TEXT,
model TEXT,
vector VECTOR
) RETURNS VOID
SELECT pg_ripple.store_embedding(
'<https://example.org/alice>',
'text-embedding-3-small',
'[0.1, 0.2, 0.3]'::vector
);
similar_entities
Find entities similar to a given entity by vector distance.
pg_ripple.similar_entities(
entity TEXT,
model TEXT DEFAULT 'text-embedding-3-small',
k INTEGER DEFAULT 10
) RETURNS TABLE(entity TEXT, distance REAL)
SELECT * FROM pg_ripple.similar_entities('<https://example.org/alice>');
embed_entities
Generate and store embeddings for entities matching a SPARQL pattern.
pg_ripple.embed_entities(
query TEXT,
model TEXT DEFAULT 'text-embedding-3-small'
) RETURNS INTEGER
SELECT pg_ripple.embed_entities('
PREFIX ex: <https://example.org/>
SELECT ?entity WHERE { ?entity a ex:Person }
');
refresh_embeddings
Recompute embeddings for entities whose underlying data has changed.
pg_ripple.refresh_embeddings(
model TEXT DEFAULT 'text-embedding-3-small'
) RETURNS INTEGER
SELECT pg_ripple.refresh_embeddings();
list_embedding_models
List all embedding models with stored vectors.
pg_ripple.list_embedding_models() RETURNS TABLE(model TEXT, entity_count BIGINT, dimensions INTEGER)
SELECT * FROM pg_ripple.list_embedding_models();
add_embedding_triples
Materialize similarity relationships as RDF triples.
pg_ripple.add_embedding_triples(
model TEXT DEFAULT 'text-embedding-3-small',
threshold REAL DEFAULT 0.8,
predicate TEXT DEFAULT '<https://example.org/similarTo>'
) RETURNS BIGINT
SELECT pg_ripple.add_embedding_triples('text-embedding-3-small', 0.9);
contextualize_entity
Return a text summary of an entity's neighborhood for use as LLM context.
pg_ripple.contextualize_entity(
entity TEXT,
hops INTEGER DEFAULT 2
) RETURNS TEXT
SELECT pg_ripple.contextualize_entity('<https://example.org/alice>', 3);
hybrid_search
Combine SPARQL graph pattern matching with vector similarity (Reciprocal Rank Fusion).
pg_ripple.hybrid_search(
sparql_query TEXT,
vector_query TEXT,
k INTEGER DEFAULT 10,
alpha REAL DEFAULT 0.5
) RETURNS TABLE(entity TEXT, score REAL, sparql_rank INTEGER, vector_rank INTEGER)
SELECT * FROM pg_ripple.hybrid_search(
'PREFIX ex: <https://example.org/>
SELECT ?person WHERE { ?person a ex:Person ; ex:knows ex:bob }',
'researchers in knowledge graphs',
10,
0.7
);
rag_retrieve
Retrieve context for RAG (Retrieval-Augmented Generation) using graph + vector search.
pg_ripple.rag_retrieve(
query TEXT,
k INTEGER DEFAULT 5,
hops INTEGER DEFAULT 2
) RETURNS TABLE(entity TEXT, context TEXT, score REAL)
SELECT * FROM pg_ripple.rag_retrieve('Who knows about knowledge graphs?', 5, 2);
Admin
Functions for maintenance, statistics, and administrative operations.
compact
Compact the triple store by removing unreferenced VP tables and dictionary entries.
pg_ripple.compact() RETURNS JSON
SELECT pg_ripple.compact();
vacuum
Vacuum all VP tables to reclaim space and update statistics.
pg_ripple.vacuum() RETURNS VOID
SELECT pg_ripple.vacuum();
reindex
Rebuild all B-tree and BRIN indices on VP tables.
pg_ripple.reindex() RETURNS VOID
SELECT pg_ripple.reindex();
vacuum_dictionary
Vacuum the dictionary table, removing entries not referenced by any VP table.
pg_ripple.vacuum_dictionary() RETURNS BIGINT
SELECT pg_ripple.vacuum_dictionary();
htap_migrate_predicate
Migrate a predicate from the flat VP layout to the HTAP delta/main layout.
pg_ripple.htap_migrate_predicate(
predicate TEXT
) RETURNS VOID
SELECT pg_ripple.htap_migrate_predicate('<https://example.org/knows>');
stats
Return overall triple store statistics.
pg_ripple.stats() RETURNS JSON
SELECT pg_ripple.stats();
canary
Health-check function that returns true if the extension is loaded and functional.
pg_ripple.canary() RETURNS BOOLEAN
SELECT pg_ripple.canary();
enable_live_statistics
Enable real-time statistics collection for VP tables.
pg_ripple.enable_live_statistics() RETURNS VOID
SELECT pg_ripple.enable_live_statistics();
promote_rare_predicates
Promote predicates from vp_rare to dedicated VP tables if they exceed the threshold.
pg_ripple.promote_rare_predicates() RETURNS INTEGER
SELECT pg_ripple.promote_rare_predicates();
deduplicate_predicate
Remove duplicate triples from a specific predicate's VP table.
pg_ripple.deduplicate_predicate(
predicate TEXT
) RETURNS BIGINT
SELECT pg_ripple.deduplicate_predicate('<https://example.org/knows>');
deduplicate_all
Remove duplicate triples from all VP tables.
pg_ripple.deduplicate_all() RETURNS BIGINT
SELECT pg_ripple.deduplicate_all();
delete_triple
Delete a specific triple from the default graph.
pg_ripple.delete_triple(
subject TEXT,
predicate TEXT,
object TEXT
) RETURNS BOOLEAN
SELECT pg_ripple.delete_triple(
'<https://example.org/alice>',
'<https://example.org/knows>',
'<https://example.org/bob>'
);
delete_triple_from_graph
Delete a specific triple from a named graph.
pg_ripple.delete_triple_from_graph(
subject TEXT,
predicate TEXT,
object TEXT,
graph TEXT
) RETURNS BOOLEAN
SELECT pg_ripple.delete_triple_from_graph(
'<https://example.org/alice>',
'<https://example.org/knows>',
'<https://example.org/bob>',
'<https://example.org/people>'
);
get_statement
Retrieve a statement by its globally-unique statement ID (SID).
pg_ripple.get_statement(
sid BIGINT
) RETURNS TABLE(subject TEXT, predicate TEXT, object TEXT, graph TEXT)
SELECT * FROM pg_ripple.get_statement(42);
Security
Functions for row-level security, access control, and schema inspection.
enable_graph_rls
Enable row-level security on VP tables, restricting access by named graph.
pg_ripple.enable_graph_rls() RETURNS VOID
SELECT pg_ripple.enable_graph_rls();
grant_graph
Grant a user access to a named graph.
pg_ripple.grant_graph(
username TEXT,
graph TEXT,
permission TEXT DEFAULT 'read'
) RETURNS VOID
SELECT pg_ripple.grant_graph('analyst', '<https://example.org/public>', 'read');
revoke_graph
Revoke a user's access to a named graph.
pg_ripple.revoke_graph(
username TEXT,
graph TEXT
) RETURNS VOID
SELECT pg_ripple.revoke_graph('analyst', '<https://example.org/public>');
list_graph_access
List all graph access grants.
pg_ripple.list_graph_access() RETURNS TABLE(username TEXT, graph TEXT, permission TEXT)
SELECT * FROM pg_ripple.list_graph_access();
enable_schema_summary
Enable background schema summary generation (requires pg_trickle).
pg_ripple.enable_schema_summary() RETURNS VOID
SELECT pg_ripple.enable_schema_summary();
schema_summary
Return a one-shot schema summary of all predicates, types, and counts.
pg_ripple.schema_summary() RETURNS JSON
SELECT pg_ripple.schema_summary();
CDC
Functions for Change Data Capture subscriptions.
subscribe
Subscribe to change events on the triple store. Returns a subscription ID.
pg_ripple.subscribe(
channel TEXT DEFAULT 'pg_ripple_changes',
filter TEXT DEFAULT NULL
) RETURNS TEXT
SELECT pg_ripple.subscribe('my_changes', 'predicate=<https://example.org/knows>');
unsubscribe
Unsubscribe from a change event subscription.
pg_ripple.unsubscribe(
subscription_id TEXT
) RETURNS VOID
SELECT pg_ripple.unsubscribe('sub_abc123');
Index
Functions for querying predicate indices.
subject_predicates
Return all predicates used by a given subject.
pg_ripple.subject_predicates(
subject TEXT
) RETURNS TABLE(predicate TEXT)
SELECT * FROM pg_ripple.subject_predicates('<https://example.org/alice>');
object_predicates
Return all predicates where a given resource appears as object.
pg_ripple.object_predicates(
object TEXT
) RETURNS TABLE(predicate TEXT)
SELECT * FROM pg_ripple.object_predicates('<https://example.org/alice>');
Cache
Functions for query plan cache management.
plan_cache_stats
Return statistics about the SPARQL-to-SQL plan cache.
pg_ripple.plan_cache_stats() RETURNS JSON
SELECT pg_ripple.plan_cache_stats();
plan_cache_reset
Clear the SPARQL-to-SQL plan cache.
pg_ripple.plan_cache_reset() RETURNS VOID
SELECT pg_ripple.plan_cache_reset();
relay_available
Check whether relay integration is enabled and the pg_tide companion extension is installed.
pg_ripple.relay_available() RETURNS BOOLEAN
SELECT pg_ripple.relay_available();
pg_tide_available
Check whether the pg_tide companion extension is installed. Use relay_available() when the legacy pg_ripple.trickle_integration GUC must also be enabled.
pg_ripple.pg_tide_available() RETURNS BOOLEAN
SELECT pg_ripple.pg_tide_available();
pg_trickle_available
Check whether the pg_trickle companion extension is installed for IVM-backed views.
pg_ripple.pg_trickle_available() RETURNS BOOLEAN
SELECT pg_ripple.pg_trickle_available();
trickle_available
Deprecated relay compatibility alias for relay_available().
pg_ripple.trickle_available() RETURNS BOOLEAN
SELECT pg_ripple.trickle_available();
SQL API Reference
Selected pg_ripple SQL functions with full parameter descriptions, return schemas, and copy-pasteable examples. For the complete alphabetical list of all 157 functions see sql-functions.md.
Compatibility & diagnostics
compat_check
Return a JSON object describing the installed extension version and its compatibility with the HTTP companion.
pg_ripple.compat_check() → TEXT
Returns — a JSON string with the following keys:
| Key | Type | Description |
|---|---|---|
extension_version | STRING | Installed pg_ripple extension version, e.g. "0.123.0" |
http_min_version | STRING | Minimum pg_ripple_http version required by this extension, e.g. "0.122.0" |
compatible | BOOL | true when the running HTTP companion satisfies http_min_version |
Example return value:
{
"extension_version": "0.123.0",
"http_min_version": "0.122.0",
"compatible": true
}
Example usage:
SELECT pg_ripple.compat_check();
-- {"extension_version":"0.123.0","http_min_version":"0.122.0","compatible":true}
-- Parse as JSON:
SELECT
(pg_ripple.compat_check()::jsonb) ->> 'extension_version' AS ext_version,
(pg_ripple.compat_check()::jsonb) ->> 'compatible' AS compatible;
Benchmarking
bench_workload
Run a benchmark workload profile against the local triple store and record the
results in _pg_ripple.bench_history.
pg_ripple.bench_workload(
profile TEXT DEFAULT 'bsbm'
) → BIGINT
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
profile | TEXT | 'bsbm' | Benchmark profile to run. One of: 'bsbm', 'watdiv', 'pagerank', 'pprl' |
Returns — the run_id of the newly inserted _pg_ripple.bench_history row.
-- Run BSBM benchmark:
SELECT pg_ripple.bench_workload('bsbm');
-- Run WatDiv benchmark:
SELECT pg_ripple.bench_workload('watdiv');
bench_workload_result
Return the most recent benchmark run for the given profile. Convenience
wrapper over _pg_ripple.bench_history — no raw SQL required.
pg_ripple.bench_workload_result(
profile TEXT DEFAULT 'bsbm'
) → TABLE (
run_id BIGINT,
profile TEXT,
started_at TIMESTAMPTZ,
duration_ms BIGINT,
queries_per_second FLOAT8,
triples_processed BIGINT
)
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
profile | TEXT | 'bsbm' | Profile name to filter; must match a value previously passed to bench_workload() |
Example usage:
-- Run a benchmark and immediately retrieve the result:
SELECT pg_ripple.bench_workload('bsbm');
SELECT * FROM pg_ripple.bench_workload_result('bsbm');
-- Output:
-- run_id | profile | started_at | duration_ms | queries_per_second | triples_processed
-- -------+---------+--------------------------+-------------+--------------------+------------------
-- 1 | bsbm | 2026-05-19 12:00:00+00 | 342 | 175.44 | 50000
Rule library federation
publish_rule_library
Publish an installed rule library so it can be subscribed to from remote pg_ripple instances through the HTTP companion's rule-library stream endpoint.
pg_ripple.publish_rule_library(
name TEXT,
endpoint_uri TEXT
) → VOID
Parameters:
| Parameter | Type | Description |
|---|---|---|
name | TEXT | Name of an installed rule library (1–64 alphanumeric/hyphen/underscore chars) |
endpoint_uri | TEXT | Full HTTP URI at which the rule stream will be served, e.g. 'https://host/rule-libraries/my-lib/stream' |
Errors — raises PT046x error codes for invalid name/URI, missing library,
or catalog write failure.
-- Publish the "rdfs-base" library on this instance:
SELECT pg_ripple.publish_rule_library(
'rdfs-base',
'https://instance-a.example.com/rule-libraries/rdfs-base/stream'
);
subscribe_rule_library
Record the intent to subscribe to a remote rule-library stream. The source URI
must pass the SSRF policy check. The SQL function does not perform outbound
network I/O; fetching and installing the remote rules is performed by the HTTP
companion's POST /rule-libraries/{name}/subscribe endpoint.
pg_ripple.subscribe_rule_library(
source_uri TEXT,
name TEXT
) → VOID
Parameters:
| Parameter | Type | Description |
|---|---|---|
source_uri | TEXT | Full HTTP URI of the remote stream endpoint |
name | TEXT | Local name to use for the subscribed rule library |
Errors — raises PT046x error codes for SSRF-blocked URIs, invalid names,
or catalog write failure.
-- Record subscription intent for a rule library published on instance A:
SELECT pg_ripple.subscribe_rule_library(
'https://instance-a.example.com/rule-libraries/rdfs-base/stream',
'rdfs-base'
);
Allen's interval relations
All seven Allen's interval relations are available as both SQL functions and
SPARQL extension functions (pg:before, pg:meets, etc.).
Each function accepts four TIMESTAMPTZ arguments representing the start and
end of two intervals: (a_start, a_end, b_start, b_end).
| SQL function | SPARQL IRI | Relation |
|---|---|---|
pg_ripple.allen_before | pg:before | A ends strictly before B starts |
pg_ripple.allen_meets | pg:meets | A ends exactly when B starts |
pg_ripple.allen_overlaps | pg:overlaps | A starts before B and they overlap |
pg_ripple.allen_during | pg:during | A is entirely within B |
pg_ripple.allen_finishes | pg:finishes | A ends at the same time as B |
pg_ripple.allen_starts | pg:starts | A starts at the same time as B |
pg_ripple.allen_equals | pg:equals | A and B have identical bounds |
allen_before
pg_ripple.allen_before(
a_start TIMESTAMPTZ,
a_end TIMESTAMPTZ,
b_start TIMESTAMPTZ,
b_end TIMESTAMPTZ
) → BOOLEAN
Returns true when interval A ends strictly before interval B starts
(a_end < b_start).
SELECT pg_ripple.allen_before(
'2026-01-01'::timestamptz, '2026-01-15'::timestamptz,
'2026-02-01'::timestamptz, '2026-02-28'::timestamptz
); -- true
allen_meets
pg_ripple.allen_meets(
a_start TIMESTAMPTZ,
a_end TIMESTAMPTZ,
b_start TIMESTAMPTZ,
b_end TIMESTAMPTZ
) → BOOLEAN
Returns true when interval A ends at exactly the same instant B starts
(a_end = b_start).
allen_overlaps
pg_ripple.allen_overlaps(
a_start TIMESTAMPTZ,
a_end TIMESTAMPTZ,
b_start TIMESTAMPTZ,
b_end TIMESTAMPTZ
) → BOOLEAN
Returns true when A starts before B and the intervals overlap but neither
contains the other (a_start < b_start AND a_end > b_start AND a_end < b_end).
allen_during
pg_ripple.allen_during(
a_start TIMESTAMPTZ,
a_end TIMESTAMPTZ,
b_start TIMESTAMPTZ,
b_end TIMESTAMPTZ
) → BOOLEAN
Returns true when interval A is entirely within interval B
(b_start < a_start AND a_end < b_end).
allen_finishes
pg_ripple.allen_finishes(
a_start TIMESTAMPTZ,
a_end TIMESTAMPTZ,
b_start TIMESTAMPTZ,
b_end TIMESTAMPTZ
) → BOOLEAN
Returns true when A ends at the same time as B but starts later
(a_start > b_start AND a_end = b_end).
allen_starts
pg_ripple.allen_starts(
a_start TIMESTAMPTZ,
a_end TIMESTAMPTZ,
b_start TIMESTAMPTZ,
b_end TIMESTAMPTZ
) → BOOLEAN
Returns true when A starts at the same time as B but ends earlier
(a_start = b_start AND a_end < b_end).
allen_equals
pg_ripple.allen_equals(
a_start TIMESTAMPTZ,
a_end TIMESTAMPTZ,
b_start TIMESTAMPTZ,
b_end TIMESTAMPTZ
) → BOOLEAN
Returns true when both intervals are identical
(a_start = b_start AND a_end = b_end).
SPARQL example using interval relations:
PREFIX pg: <https://pgrdf.io/fn/>
PREFIX ex: <https://example.org/>
SELECT ?event ?label WHERE {
?event ex:startTime ?s ;
ex:endTime ?e ;
ex:label ?label .
FILTER(pg:before(?s, ?e,
"2026-06-01T00:00:00Z"^^xsd:dateTime,
"2026-12-31T23:59:59Z"^^xsd:dateTime))
}
See also
SQL Reference
This section documents all SQL functions and views exported by pg_ripple.
Conventions
Term format — all IRI, blank node, and literal arguments use N-Triples notation:
| Kind | Format | Example |
|---|---|---|
| IRI | <…> | <https://example.org/alice> |
| Blank node | _:… | _:b0 |
| Plain literal | "…" | "Alice" |
| Language-tagged literal | "…"@lang | "Alice"@en |
| Typed literal | "…"^^<type> | "30"^^<http://www.w3.org/2001/XMLSchema#integer> |
NULL wildcards — in find/delete functions, NULL matches any value for that position.
Schema — all public functions are in the pg_ripple schema. Internal tables are in _pg_ripple.
Function index
| Function | Section |
|---|---|
insert_triple | Triple CRUD |
delete_triple | Triple CRUD |
find_triples | Triple CRUD |
triple_count | Triple CRUD |
load_ntriples | Bulk Loading |
load_ntriples_file | Bulk Loading |
load_turtle | Bulk Loading |
load_turtle_file | Bulk Loading |
load_nquads | Bulk Loading |
load_nquads_file | Bulk Loading |
load_trig | Bulk Loading |
load_trig_file | Bulk Loading |
load_rdfxml | Bulk Loading |
load_rdfxml_file | Bulk Loading |
load_ntriples_into_graph | Bulk Loading |
load_turtle_into_graph | Bulk Loading |
load_rdfxml_into_graph | Bulk Loading |
load_ntriples_file_into_graph | Bulk Loading |
load_turtle_file_into_graph | Bulk Loading |
load_rdfxml_file_into_graph | Bulk Loading |
create_graph | Named Graphs |
drop_graph | Named Graphs |
list_graphs | Named Graphs |
find_triples_in_graph | Named Graphs |
triple_count_in_graph | Named Graphs |
delete_triple_from_graph | Named Graphs |
clear_graph | Named Graphs |
sparql | SPARQL Queries |
sparql_ask | SPARQL Queries |
sparql_explain | SPARQL Queries |
sparql_construct | SPARQL Queries |
sparql_describe | SPARQL Queries |
sparql_update | SPARQL Update |
fts_index | Full-Text Search |
fts_search | Full-Text Search |
encode_triple | RDF-star |
decode_triple | RDF-star |
get_statement | RDF-star |
encode_term | Dictionary |
decode_id | Dictionary |
decode_id_full | Dictionary |
lookup_iri | Dictionary |
register_prefix | Prefix Registry |
prefixes | Prefix Registry |
export_ntriples | Serialization & Export |
export_nquads | Serialization & Export |
export_turtle | Serialization & Export |
export_turtle_stream | Serialization & Export |
export_jsonld | Serialization & Export |
export_jsonld_stream | Serialization & Export |
sparql_construct_turtle | Serialization & Export |
sparql_construct_jsonld | Serialization & Export |
sparql_describe_turtle | Serialization & Export |
sparql_describe_jsonld | Serialization & Export |
load_shacl | SHACL Validation |
validate | SHACL Validation |
list_shapes | SHACL Validation |
drop_shape | SHACL Validation |
process_validation_queue | SHACL Validation |
validation_queue_length | SHACL Validation |
dead_letter_count | SHACL Validation |
dead_letter_queue | SHACL Validation |
drain_dead_letter_queue | SHACL Validation |
enable_shacl_monitors | SHACL Validation |
load_rules | Datalog Reasoning |
load_rules_builtin | Datalog Reasoning |
infer | Datalog Reasoning |
check_constraints | Datalog Reasoning |
list_rules | Datalog Reasoning |
drop_rules | Datalog Reasoning |
enable_rule_set | Datalog Reasoning |
disable_rule_set | Datalog Reasoning |
prewarm_dictionary_hot | Datalog Reasoning |
create_sparql_view | Materialized Views |
drop_sparql_view | Materialized Views |
list_sparql_views | Materialized Views |
create_datalog_view | Materialized Views |
create_datalog_view_from_rule_set | Materialized Views |
drop_datalog_view | Materialized Views |
list_datalog_views | Materialized Views |
create_extvp | Materialized Views |
drop_extvp | Materialized Views |
list_extvp | Materialized Views |
pg_trickle_available | Materialized Views |
compact | Administration |
stats | Administration |
subscribe | Administration |
unsubscribe | Administration |
htap_migrate_predicate | Administration |
subject_predicates | Administration |
object_predicates | Administration |
deduplicate_predicate | Administration |
deduplicate_all | Administration |
vacuum | Administration |
reindex | Administration |
vacuum_dictionary | Administration |
dictionary_stats | Administration |
enable_graph_rls | Administration |
grant_graph | Administration |
revoke_graph | Administration |
list_graph_access | Administration |
schema_summary | Administration |
enable_schema_summary | Administration |
plan_cache_stats | Administration |
plan_cache_reset | Administration |
Triple CRUD
insert_triple
pg_ripple.insert_triple(
subject TEXT,
predicate TEXT,
object TEXT,
graph TEXT DEFAULT NULL
) RETURNS BIGINT
Inserts a single triple and returns its globally-unique statement identifier (SID). If graph is NULL the triple is inserted into the default graph (ID 0).
If the predicate has fewer than pg_ripple.vp_promotion_threshold (default: 1000) distinct triples it is stored in the shared _pg_ripple.vp_rare table; otherwise it gets its own VP table _pg_ripple.vp_{predicate_id}.
Example:
SELECT pg_ripple.insert_triple(
'<https://example.org/alice>',
'<https://example.org/knows>',
'<https://example.org/bob>'
);
-- Returns: 1 (SID)
delete_triple
pg_ripple.delete_triple(
subject TEXT,
predicate TEXT,
object TEXT,
graph TEXT DEFAULT NULL
) RETURNS BIGINT
Deletes all triples matching the given (subject, predicate, object, graph) pattern where NULL is a wildcard. Returns the number of triples deleted.
Examples:
-- Delete a specific triple
SELECT pg_ripple.delete_triple(
'<https://example.org/alice>',
'<https://example.org/knows>',
'<https://example.org/bob>'
);
-- Delete all triples with a given subject
SELECT pg_ripple.delete_triple('<https://example.org/alice>', NULL, NULL);
find_triples
pg_ripple.find_triples(
subject TEXT DEFAULT NULL,
predicate TEXT DEFAULT NULL,
object TEXT DEFAULT NULL,
graph TEXT DEFAULT NULL
) RETURNS TABLE(subject TEXT, predicate TEXT, object TEXT, graph_id BIGINT)
Returns all triples matching the pattern. NULL is a wildcard for any position.
Examples:
-- Find by subject
SELECT * FROM pg_ripple.find_triples('<https://example.org/alice>', NULL, NULL);
-- Find by predicate
SELECT * FROM pg_ripple.find_triples(NULL, '<https://example.org/knows>', NULL);
-- Find exact triple
SELECT * FROM pg_ripple.find_triples(
'<https://example.org/alice>',
'<https://example.org/knows>',
'<https://example.org/bob>'
);
triple_count
pg_ripple.triple_count() RETURNS BIGINT
Returns the total number of triples across all graphs and all VP tables (both dedicated and vp_rare).
SELECT pg_ripple.triple_count();
-- Returns 0 for an empty store
Bulk Loading
pg_ripple provides eight bulk-load functions covering four RDF serialization formats in both inline-string and file variants.
load_ntriples
pg_ripple.load_ntriples(data TEXT) RETURNS BIGINT
Parses and loads N-Triples data from a string. Returns the number of triples loaded.
SELECT pg_ripple.load_ntriples('
<https://example.org/alice> <https://example.org/knows> <https://example.org/bob> .
<https://example.org/bob> <https://example.org/name> "Bob" .
');
-- Returns 2
load_ntriples_file
pg_ripple.load_ntriples_file(path TEXT) RETURNS BIGINT
Loads N-Triples data from a server-side file. The path must be readable by the PostgreSQL server process.
SELECT pg_ripple.load_ntriples_file('/data/dataset.nt');
load_turtle / load_turtle_file
pg_ripple.load_turtle(data TEXT) RETURNS BIGINT
pg_ripple.load_turtle_file(path TEXT) RETURNS BIGINT
Parses and loads Turtle data. Turtle supports prefix declarations (@prefix) and concise syntax for related triples.
SELECT pg_ripple.load_turtle('
@prefix ex: <https://example.org/> .
ex:alice ex:knows ex:bob ;
ex:name "Alice" .
');
load_nquads / load_nquads_file
pg_ripple.load_nquads(data TEXT) RETURNS BIGINT
pg_ripple.load_nquads_file(path TEXT) RETURNS BIGINT
Loads N-Quads data. N-Quads extend N-Triples with an optional fourth component (the named graph). Triples without a graph component go into the default graph.
SELECT pg_ripple.load_nquads('
<https://example.org/alice> <https://example.org/knows> <https://example.org/bob> <https://example.org/graph1> .
<https://example.org/bob> <https://example.org/name> "Bob" .
');
load_trig / load_trig_file
pg_ripple.load_trig(data TEXT) RETURNS BIGINT
pg_ripple.load_trig_file(path TEXT) RETURNS BIGINT
Loads TriG data. TriG is the graph-aware extension of Turtle — triples can be grouped inside named GRAPH { } blocks.
SELECT pg_ripple.load_trig('
@prefix ex: <https://example.org/> .
GRAPH ex:graph1 {
ex:alice ex:knows ex:bob .
}
ex:alice ex:name "Alice" .
');
Graph-aware loaders (v0.15.0)
Starting with v0.15.0, each format has a graph-aware variant that loads triples directly into a named graph. File variants are also available (superuser-only).
Inline loaders
pg_ripple.load_ntriples_into_graph(data TEXT, graph_iri TEXT) RETURNS BIGINT
pg_ripple.load_turtle_into_graph(data TEXT, graph_iri TEXT) RETURNS BIGINT
pg_ripple.load_rdfxml_into_graph(data TEXT, graph_iri TEXT) RETURNS BIGINT
SELECT pg_ripple.load_turtle_into_graph('
@prefix ex: <https://example.org/> .
ex:alice ex:knows ex:bob .
', '<https://example.org/graph1>');
-- Returns: 1
File loaders
pg_ripple.load_ntriples_file_into_graph(path TEXT, graph_iri TEXT) RETURNS BIGINT
pg_ripple.load_turtle_file_into_graph(path TEXT, graph_iri TEXT) RETURNS BIGINT
pg_ripple.load_rdfxml_file_into_graph(path TEXT, graph_iri TEXT) RETURNS BIGINT
SELECT pg_ripple.load_ntriples_file_into_graph(
'/data/people.nt',
'<https://example.org/people>'
);
RDF/XML loader (v0.9.0)
pg_ripple.load_rdfxml(data TEXT) RETURNS BIGINT
pg_ripple.load_rdfxml_file(path TEXT) RETURNS BIGINT
Parses conformant RDF/XML — the format produced by Protégé, OWL editors, and many ontology tools.
SELECT pg_ripple.load_rdfxml('
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:ex="https://example.org/">
<rdf:Description rdf:about="https://example.org/alice">
<ex:name>Alice</ex:name>
</rdf:Description>
</rdf:RDF>
');
Blank-node scoping
Each call to a bulk-load function is an independent blank-node scope. _:b0 in one load_ntriples() call and _:b0 in a later call get different dictionary IDs. This matches the SPARQL/RDF blank-node scoping rules and prevents accidental merging of blank nodes across document loads.
VP promotion after bulk loads
After loading a large dataset, some predicates may cross the pg_ripple.vp_promotion_threshold and be automatically promoted from vp_rare to a dedicated VP table. This promotion is triggered automatically.
After any large load, run ANALYZE to help the PostgreSQL planner choose efficient join strategies:
ANALYZE _pg_ripple.vp_rare;
Or analyze all VP tables at once with a server-side function (available in v0.6.0+):
SELECT pg_ripple.vacuum_analyze();
Strict mode (v0.25.0)
All bulk-load functions accept an optional strict BOOLEAN DEFAULT false parameter.
| Mode | Behaviour on malformed triple |
|---|---|
strict = false (default) | Malformed triples emit a WARNING and are skipped; the remaining triples are committed |
strict = true | Any parse error aborts the entire load with an ERROR; the transaction is rolled back |
Examples
-- Lenient mode (default): one bad triple is skipped, others are committed
SELECT pg_ripple.load_ntriples(
'<https://example.org/s> <https://example.org/p> <https://example.org/o> .' || chr(10) ||
'this is malformed' || chr(10)
);
-- WARNING: skipping malformed triple on line 2
-- Result: 1
-- Strict mode: bad triple causes rollback of the entire load
SELECT pg_ripple.load_ntriples(
'<https://example.org/s> <https://example.org/p> <https://example.org/o> .' || chr(10) ||
'this is malformed' || chr(10),
strict => true
);
-- ERROR: malformed triple on line 2: [...]
-- All triples from this call are rolled back
When to use strict mode
- Data pipelines: use
strict = trueto detect upstream data quality issues early. - Interactive exploration: use
strict = false(default) to load partial data and investigate. - Migration: use
strict = trueto ensure all data was loaded, thenstrict = falsefor recovery.
File-path security (v0.25.0)
The load_*_file() variants (load_turtle_file, load_ntriples_file, etc.) resolve symlinks using realpath() and verify the canonical path lies within the PostgreSQL data directory before reading. This prevents symlink-based path traversal attacks:
-- Rejected: outside the data directory
SELECT pg_ripple.load_turtle_file('/etc/passwd');
-- ERROR: permission denied: "/etc/passwd" is outside the database cluster directory
Only files within current_setting('data_directory') can be loaded.
SPARQL Queries
pg_ripple executes SPARQL 1.1 queries by compiling them to SQL that runs natively inside the PostgreSQL engine. The generated SQL is visible via sparql_explain().
sparql
pg_ripple.sparql(query TEXT) RETURNS TABLE(…)
Executes a SPARQL SELECT query and returns results as a relational table. Each projected variable becomes a TEXT column. Values are returned in N-Triples notation for IRIs and blank nodes, and literal notation for literals.
SELECT name, age
FROM pg_ripple.sparql('
SELECT ?name ?age WHERE {
?person <https://example.org/name> ?name .
OPTIONAL { ?person <https://example.org/age> ?age }
}
');
Note: The column names in the returned table match the SPARQL variable names without the
?prefix.
sparql_ask
pg_ripple.sparql_ask(query TEXT) RETURNS BOOLEAN
Executes a SPARQL ASK query and returns true if at least one solution exists.
SELECT pg_ripple.sparql_ask(
'ASK { <https://example.org/alice> <https://example.org/knows> ?x }'
);
sparql_explain
pg_ripple.sparql_explain(query TEXT, verbose BOOLEAN DEFAULT FALSE) RETURNS TEXT
Returns the SQL generated for a SPARQL query without executing it. Useful for debugging and performance analysis.
SELECT pg_ripple.sparql_explain(
'SELECT ?name WHERE { ?p <https://example.org/name> ?name }',
false
);
Supported SPARQL 1.1 features
SELECT
SELECT ?s ?p WHERE { ?s ?p <https://example.org/bob> }
SELECT DISTINCT ?s WHERE { ?s <ex:knows> ?o }
SELECT ?s WHERE { ?s <ex:knows> ?o } ORDER BY ?s LIMIT 10 OFFSET 5
FILTER
SELECT ?name WHERE {
?p <ex:name> ?name .
FILTER(STRLEN(?name) > 3)
}
SELECT ?age WHERE {
?p <ex:age> ?age .
FILTER(?age >= 18 && ?age < 65)
}
OPTIONAL (LeftJoin)
SELECT ?person ?name ?email WHERE {
?person <ex:worksAt> <ex:acme> .
OPTIONAL { ?person <ex:name> ?name }
OPTIONAL { ?person <ex:email> ?email }
}
UNION / MINUS
-- UNION
SELECT ?contact WHERE {
{ ?alice <ex:knows> ?contact } UNION { ?bob <ex:knows> ?contact }
}
-- MINUS (anti-join)
SELECT ?person WHERE {
?person <ex:worksAt> ?company .
MINUS { ?person <ex:worksAt> <ex:acme> }
}
Aggregates and GROUP BY
SELECT ?company (COUNT(?person) AS ?headcount) WHERE {
?person <ex:worksAt> ?company
} GROUP BY ?company HAVING (COUNT(?person) >= 2)
Supported aggregate functions: COUNT, SUM, AVG, MIN, MAX, GROUP_CONCAT.
Subqueries
SELECT ?company ?headcount WHERE {
{
SELECT ?company (COUNT(?p) AS ?headcount) WHERE {
?p <ex:worksAt> ?company
} GROUP BY ?company
}
FILTER(?headcount >= 2)
}
BIND / VALUES
-- BIND
SELECT ?person ?label WHERE {
?person <ex:worksAt> ?company .
BIND(<ex:employee> AS ?label)
}
-- VALUES (inline data)
SELECT ?person ?company WHERE {
VALUES ?person { <ex:alice> <ex:bob> }
?person <ex:worksAt> ?company
}
Property paths
-- OneOrMore (+): transitive closure
SELECT ?target WHERE { <ex:alice> <ex:knows>+ ?target }
-- ZeroOrMore (*): transitive closure including identity
SELECT ?target WHERE { <ex:alice> <ex:follows>* ?target }
-- ZeroOrOne (?): direct or identity
SELECT ?target WHERE { <ex:alice> <ex:follows>? ?target }
-- Sequence (/)
SELECT ?target WHERE { <ex:alice> <ex:knows>/<ex:knows> ?target }
-- Alternative (|)
SELECT ?target WHERE { <ex:alice> (<ex:knows>|<ex:follows>) ?target }
-- Inverse (^)
SELECT ?who WHERE { ?who ^<ex:knows> <ex:bob> }
Property paths compile to PostgreSQL WITH RECURSIVE CTEs with the PG18 CYCLE clause for hash-based cycle detection. See max_path_depth to limit traversal depth.
Named graphs
SELECT ?s ?p ?o WHERE {
GRAPH <https://example.org/graph1> { ?s ?p ?o }
}
ASK
ASK { <ex:alice> <ex:knows> <ex:bob> }
Plan cache
Compiled SPARQL→SQL plans are cached per-backend in an LRU cache (configurable via pg_ripple.plan_cache_size). Repeated identical queries skip recompilation.
The cache key includes the query text and the current value of pg_ripple.max_path_depth. Changing the GUC invalidates cached path query plans.
sparql_construct
pg_ripple.sparql_construct(query TEXT) RETURNS SETOF JSONB
Executes a SPARQL CONSTRUCT query and returns the constructed triples as JSONB objects. Each result row has three keys: "s" (subject), "p" (predicate), and "o" (object), all in N-Triples notation.
Explicit template form
SELECT *
FROM pg_ripple.sparql_construct('
CONSTRUCT { ?b <https://example.org/knownBy> ?a }
WHERE { ?a <https://example.org/knows> ?b }
');
-- Returns: {"s": "<https://...bob>", "p": "<https://...knownBy>", "o": "<https://...alice>"}
CONSTRUCT WHERE (bare form)
The CONSTRUCT WHERE shorthand returns the matched triples directly:
SELECT *
FROM pg_ripple.sparql_construct('
CONSTRUCT WHERE { <https://example.org/alice> <https://example.org/knows> ?o }
');
sparql_describe
pg_ripple.sparql_describe(query TEXT, strategy TEXT DEFAULT current_setting('pg_ripple.describe_strategy'))
RETURNS SETOF JSONB
Executes a SPARQL DESCRIBE query and returns the description of the named resources as JSONB triples {s, p, o}.
-- Describe a single resource (CBD algorithm)
SELECT *
FROM pg_ripple.sparql_describe(
'DESCRIBE <https://example.org/alice>'
);
-- Describe all people (resources identified by a WHERE pattern)
SELECT *
FROM pg_ripple.sparql_describe(
'DESCRIBE ?person WHERE { ?person a <https://example.org/Person> }'
);
describe_strategy GUC
pg_ripple.describe_strategy (default: 'cbd') sets the default expansion algorithm:
| Value | Algorithm | Description |
|---|---|---|
'cbd' | Concise Bounded Description | All outgoing arcs; recursively expands blank node objects |
'scbd' | Symmetric CBD | CBD + all incoming arcs to the named resource |
'simple' | Simple description | Outgoing arcs only; no blank-node recursion |
-- Use SCBD for this session
SET pg_ripple.describe_strategy = 'scbd';
SELECT * FROM pg_ripple.sparql_describe('DESCRIBE <https://example.org/alice>');
You can also pass the strategy as the second argument to sparql_describe:
SELECT * FROM pg_ripple.sparql_describe(
'DESCRIBE <https://example.org/alice>',
'scbd'
);
Note: CONSTRUCT and DESCRIBE return JSONB in v0.5.1. Turtle and JSON-LD serialization output are planned for v0.9.0.
HTTP Protocol Endpoint
pg_ripple includes a companion HTTP service (pg_ripple_http) that implements the W3C SPARQL 1.1 Protocol, allowing standard SPARQL clients to connect without any pg_ripple-specific drivers.
Starting the HTTP service
export PG_RIPPLE_HTTP_PG_URL="postgresql://user:pass@localhost/mydb"
export PG_RIPPLE_HTTP_PORT=7878
pg_ripple_http
Configuration
| Environment variable | Default | Description |
|---|---|---|
PG_RIPPLE_HTTP_PG_URL | postgresql://localhost/postgres | PostgreSQL connection string |
PG_RIPPLE_HTTP_PORT | 7878 | HTTP listen port |
PG_RIPPLE_HTTP_POOL_SIZE | 16 | Connection pool size |
PG_RIPPLE_HTTP_AUTH_TOKEN | (none) | Bearer/Basic auth token |
PG_RIPPLE_HTTP_CORS_ORIGINS | * | Comma-separated CORS origins |
PG_RIPPLE_HTTP_RATE_LIMIT | 0 | Rate limit (0 = unlimited) |
SPARQL 1.1 Protocol conformance
The endpoint at /sparql supports all standard request forms:
GET /sparql?query=...(URL-encoded query)POST /sparqlwithContent-Type: application/sparql-queryPOST /sparqlwithContent-Type: application/sparql-updatePOST /sparqlwithContent-Type: application/x-www-form-urlencoded(query=...orupdate=...)
Accept header formats
| Accept header | Used for | MIME type |
|---|---|---|
application/sparql-results+json | SELECT, ASK (default) | JSON Results |
application/sparql-results+xml | SELECT, ASK | XML Results |
text/csv | SELECT | CSV |
text/tab-separated-values | SELECT | TSV |
text/turtle | CONSTRUCT, DESCRIBE (default) | Turtle |
application/n-triples | CONSTRUCT, DESCRIBE | N-Triples |
application/ld+json | CONSTRUCT, DESCRIBE | JSON-LD |
Examples
# SELECT query
curl -G http://localhost:7878/sparql \
--data-urlencode "query=SELECT ?s ?p ?o WHERE { ?s ?p ?o } LIMIT 10"
# ASK query with JSON results
curl -G http://localhost:7878/sparql \
--data-urlencode "query=ASK { <http://example.org/alice> ?p ?o }"
# CONSTRUCT query with Turtle output
curl -H "Accept: text/turtle" -G http://localhost:7878/sparql \
--data-urlencode "query=CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o } LIMIT 10"
# SPARQL Update via POST
curl -X POST http://localhost:7878/sparql \
-H "Content-Type: application/sparql-update" \
-d "INSERT DATA { <http://example.org/s> <http://example.org/p> \"value\" }"
# Health check
curl http://localhost:7878/health
# Prometheus metrics
curl http://localhost:7878/metrics
Docker
Use Docker Compose to run PostgreSQL with pg_ripple and the HTTP endpoint together:
docker compose up -d
curl http://localhost:7878/health
Connecting SPARQL tools
The /sparql endpoint is compatible with standard SPARQL tools:
- YASGUI: Set endpoint URL to
http://localhost:7878/sparql - Python SPARQLWrapper:
sparql = SPARQLWrapper("http://localhost:7878/sparql") - Apache Jena:
QueryExecutionFactory.sparqlService("http://localhost:7878/sparql", query) - Protege: Add SPARQL tab, set endpoint to
http://localhost:7878/sparql
SPARQL Update
pg_ripple supports the full SPARQL 1.1 Update specification via the sparql_update() function. All update operations — INSERT DATA, DELETE DATA, DELETE/INSERT WHERE, LOAD, CLEAR, DROP, and CREATE — are available as of v0.12.0.
sparql_update
pg_ripple.sparql_update(query TEXT) RETURNS BIGINT
Executes a SPARQL Update statement and returns the number of triples affected (inserted + deleted).
INSERT DATA
Adds one or more ground triples (no variables) to the store:
-- Insert a single triple
SELECT pg_ripple.sparql_update(
'INSERT DATA { <https://example.org/alice> <https://example.org/age> "30"^^<http://www.w3.org/2001/XMLSchema#integer> }'
);
-- Insert multiple triples in one statement
SELECT pg_ripple.sparql_update('
INSERT DATA {
<https://example.org/alice> <https://example.org/knows> <https://example.org/bob> .
<https://example.org/bob> <https://example.org/name> "Bob"
}
');
All subjects, predicates, and objects are dictionary-encoded before insertion. Typed literals that qualify for inline encoding (xsd:integer, xsd:boolean, xsd:date, xsd:dateTime) are stored as bit-packed IDs rather than dictionary rows.
DELETE DATA
Removes exact-match triples from the store:
SELECT pg_ripple.sparql_update(
'DELETE DATA { <https://example.org/alice> <https://example.org/knows> <https://example.org/bob> }'
);
Returns 0 if the triple does not exist (no error).
DELETE/INSERT WHERE
Pattern-based updates find matching triples via a WHERE clause and delete and/or insert triples for each match. This is the SPARQL equivalent of SQL UPDATE.
-- Replace all "draft" status values with "published":
SELECT pg_ripple.sparql_update('
DELETE { ?s <https://example.org/status> <https://example.org/draft> }
INSERT { ?s <https://example.org/status> <https://example.org/published> }
WHERE { ?s <https://example.org/status> <https://example.org/draft> }
');
-- Add an email placeholder for every person who lacks one:
SELECT pg_ripple.sparql_update('
INSERT { ?person <https://schema.org/email> "no-reply@example.org" }
WHERE {
?person a <https://schema.org/Person> .
FILTER NOT EXISTS { ?person <https://schema.org/email> ?e }
}
');
The WHERE clause is compiled through the existing SPARQL→SQL engine. All bound variables in the WHERE clause are available in the DELETE and INSERT templates. The DELETE phase runs before the INSERT phase for each bound row. The entire operation is transactional.
Return value: (number of triples deleted) + (number of triples inserted).
Named graphs
All forms support named graphs:
SELECT pg_ripple.sparql_update('
INSERT DATA {
GRAPH <https://example.org/graph1> {
<https://example.org/alice> <https://example.org/memberOf> <https://example.org/org1>
}
}
');
-- Pattern-based update in a named graph:
SELECT pg_ripple.sparql_update('
DELETE { GRAPH <https://example.org/graph1> { ?s ?p ?o } }
WHERE { GRAPH <https://example.org/graph1> { ?s ?p ?o } }
');
The default graph has ID 0. Named graphs are created implicitly when the first triple is inserted.
Graph management
LOAD
Fetches an RDF document from a URL via HTTP(S) and inserts all triples into the store.
-- Load into the default graph:
SELECT pg_ripple.sparql_update(
'LOAD <https://www.w3.org/People/Berners-Lee/card.rdf>'
);
-- Load into a named graph:
SELECT pg_ripple.sparql_update(
'LOAD <https://example.org/data.ttl> INTO GRAPH <https://example.org/mygraph>'
);
-- Ignore errors (e.g. network failures):
SELECT pg_ripple.sparql_update(
'LOAD SILENT <https://example.org/data.nt>'
);
Format detection: Turtle if the Content-Type contains turtle or the URL ends in .ttl; RDF/XML if rdf+xml or .rdf/.owl; otherwise N-Triples. Named graphs inside TriG are not split out — the destination graph overrides.
CLEAR
Deletes all triples from the target graph(s) without removing the graph name from the dictionary.
-- Clear a specific named graph:
SELECT pg_ripple.sparql_update(
'CLEAR GRAPH <https://example.org/mygraph>'
);
-- Clear the default graph:
SELECT pg_ripple.sparql_update('CLEAR DEFAULT');
-- Clear all named graphs (default graph untouched):
SELECT pg_ripple.sparql_update('CLEAR NAMED');
-- Clear everything:
SELECT pg_ripple.sparql_update('CLEAR ALL');
-- Ignore if the graph does not exist:
SELECT pg_ripple.sparql_update(
'CLEAR SILENT GRAPH <https://example.org/nonexistent>'
);
DROP
Like CLEAR, but also deregisters the graph. In pg_ripple, the graph IRI remains in the dictionary (deregistering is a no-op beyond clearing triples), so DROP and CLEAR are functionally equivalent.
SELECT pg_ripple.sparql_update(
'DROP GRAPH <https://example.org/mygraph>'
);
SELECT pg_ripple.sparql_update('DROP ALL');
SELECT pg_ripple.sparql_update(
'DROP SILENT GRAPH <https://example.org/nonexistent>'
);
CREATE
Registers a named graph in the dictionary. Since pg_ripple creates graphs implicitly on first insert, CREATE GRAPH is rarely needed but is supported for SPARQL compliance.
SELECT pg_ripple.sparql_update(
'CREATE GRAPH <https://example.org/newgraph>'
);
-- No-op if the graph already exists:
SELECT pg_ripple.sparql_update(
'CREATE SILENT GRAPH <https://example.org/newgraph>'
);
Return value
sparql_update() returns the total count of triples affected:
| Statement | Return value |
|---|---|
INSERT DATA { t1 . t2 . t3 } | 3 if all were new |
DELETE DATA { t1 } | 1 if found, 0 if not found |
DELETE { … } INSERT { … } WHERE { … } | deletes + inserts |
CLEAR GRAPH <g> | triples removed |
DROP GRAPH <g> | triples removed |
CREATE GRAPH <g> | 0 (no triples touched) |
LOAD <url> | triples inserted |
Compared to insert_triple / delete_triple
insert_triple | sparql_update | |
|---|---|---|
| Input format | N-Triples strings (subject, predicate, object) | SPARQL text |
| Multiple triples | One call per triple | Multiple triples per statement |
| Pattern-based | No | Yes (DELETE/INSERT WHERE) |
| Graph management | No | Yes (CLEAR/DROP/CREATE/LOAD) |
| Use case | Programmatic insertion from SQL | SPARQL-based tools and standards-compliant clients |
sparql_update
pg_ripple.sparql_update(query TEXT) RETURNS BIGINT
Executes a SPARQL Update statement and returns the number of triples affected.
INSERT DATA
Adds one or more ground triples (no variables) to the store:
-- Insert a single triple
SELECT pg_ripple.sparql_update(
'INSERT DATA { <https://example.org/alice> <https://example.org/age> "30"^^<http://www.w3.org/2001/XMLSchema#integer> }'
);
-- Insert multiple triples in one statement
SELECT pg_ripple.sparql_update('
INSERT DATA {
<https://example.org/alice> <https://example.org/knows> <https://example.org/bob> .
<https://example.org/bob> <https://example.org/name> "Bob"
}
');
All subjects, predicates, and objects are dictionary-encoded before insertion. Typed literals that qualify for inline encoding (xsd:integer, xsd:boolean, xsd:date, xsd:dateTime) are stored as bit-packed IDs rather than dictionary rows.
DELETE DATA
Removes exact-match triples from the store:
SELECT pg_ripple.sparql_update(
'DELETE DATA { <https://example.org/alice> <https://example.org/knows> <https://example.org/bob> }'
);
Returns 0 if the triple does not exist (no error). DELETE DATA is an atomic set operation — all triples in the statement are deleted or none are (if any are missing, the count reflects only the triples actually removed).
Named graphs
Both forms support named graphs:
SELECT pg_ripple.sparql_update('
INSERT DATA {
GRAPH <https://example.org/graph1> {
<https://example.org/alice> <https://example.org/memberOf> <https://example.org/org1>
}
}
');
The default graph has ID 0. Named graphs are created implicitly when the first triple is inserted.
Return value
sparql_update() returns the count of triples inserted or deleted:
| Statement | Return value |
|---|---|
INSERT DATA { t1 . t2 . t3 } | 3 if all were new; fewer if some already existed |
DELETE DATA { t1 } | 1 if found, 0 if not found |
Compared to insert_triple / delete_triple
insert_triple | sparql_update | |
|---|---|---|
| Input format | N-Triples strings (subject, predicate, object) | SPARQL text |
| Missing arguments | NULL used for absent components | Not applicable — all parts required for INSERT/DELETE DATA |
| Multiple triples | One call per triple | Multiple triples per statement |
| Use case | Programmatic insertion from SQL or application code | SPARQL-based tools and standards-compliant clients |
Unsupported forms (planned for later releases)
The following SPARQL Update forms are not yet supported in v0.5.1:
DELETE/INSERT WHERE { … }— pattern-based updates (v0.12.0)LOAD <url>(v0.12.0)CLEAR GRAPH <g>(v0.12.0)DROP GRAPH <g>(v0.12.0)CREATE GRAPH <g>(v0.12.0)- Update sequences (
; UPDATE1 ; UPDATE2) (v0.12.0)
Graph management operations (v0.38.0)
ADD, COPY, and MOVE are supported from v0.38.0. These operations are
desugared by the spargebra parser into equivalent INSERT DATA / DELETE DATA
sequences before execution.
-- ADD: copy all triples from SOURCE into DEST (SOURCE is preserved)
SELECT sparql_update('ADD GRAPH <http://example.org/source> TO GRAPH <http://example.org/dest>');
-- COPY: replace DEST with the contents of SOURCE (SOURCE is preserved)
SELECT sparql_update('COPY GRAPH <http://example.org/source> TO GRAPH <http://example.org/dest>');
-- MOVE: rename SOURCE to DEST (SOURCE is emptied)
SELECT sparql_update('MOVE GRAPH <http://example.org/source> TO GRAPH <http://example.org/dest>');
All three operations use DEFAULT in place of a named graph IRI to refer to
the default graph.
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:localform (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.
| Name | Rules | Description |
|---|---|---|
'rdfs' | 13 | Full RDFS entailment (rdfs2–rdfs12, subclass, domain, range) |
'owl-rl' | ~20 | Core 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:
- For every rule in the current stratum, re-evaluate the rule body against
ΔR(the delta of its input relations). - Insert any new rows into
ΔR_newwithON CONFLICT DO NOTHING. - After all rules are processed:
ΔR ← ΔR_new, then continue ifΔRis 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 Rule | Axiom | Status |
|---|---|---|
| rdfs2 | domain inference | ✅ |
| rdfs3 | range inference | ✅ |
| rdfs4a/4b | Resource membership | ✅ |
| rdfs5 | subPropertyOf transitivity | ✅ |
| rdfs7 / prp-spo1 | subPropertyOf propagation | ✅ |
| rdfs9 / cax-sco | subClassOf type propagation | ✅ |
| rdfs11 | subClassOf transitivity | ✅ |
| cls-avf | allValuesFrom chaining | ✅ |
| prp-ifp | InverseFunctionalProperty | ✅ |
| prp-sym | SymmetricProperty | ✅ |
| prp-trp | TransitiveProperty | ✅ |
| prp-inv1/2 | inverseOf | ✅ |
| prp-fp | FunctionalProperty | ✅ |
| cax-eqc1 | equivalentClass | ✅ |
| prp-eqp1 | equivalentProperty | ✅ |
| prp-chm | propertyChainAxiom (2-link) | ✅ |
| cls-hv1 | hasValue restriction | ✅ |
| cls-int1 | intersectionOf membership | ✅ |
| eq-sym | sameAs symmetry | ✅ |
| eq-trans | sameAs transitivity | ✅ |
| eq-rep-c | sameAs class propagation | ✅ |
| owl:onProperty + allValuesFrom | cls-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:
| Field | Type | Description |
|---|---|---|
derived | bigint | Total triples inserted by the inference |
iterations | integer | Number of fixpoint iterations |
matching | bigint | Triples 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 = 1from the relevant VP tables, or usevacuum_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:
FUNCis one ofCOUNT,SUM,MIN,MAX,AVG?aggVaris the variable to aggregate over (must appear in the atom's subject or object position)subject pred objectis the atom pattern (each can be a variable or IRI constant)?resultVarmust 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 key | Type | Description |
|---|---|---|
derived | bigint | Total triples derived (non-aggregate + aggregate) |
aggregate_derived | bigint | Triples derived from aggregate rules |
iterations | int | Semi-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.
| Column | Type | Description |
|---|---|---|
rule_set | text | Name of the rule set |
hits | bigint | Number of times the cached SQL was used |
misses | bigint | Number of cache misses (SQL was compiled from scratch) |
entries | int | Total 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):
| Key | Type | Description |
|---|---|---|
derived | number | Total triples derived |
iterations | number | Fixpoint iteration count |
demand_predicates | array | Predicate 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
| GUC | Default | Description |
|---|---|---|
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_sets | true | Master switch for goal-directed magic sets inference (v0.29.0+) |
pg_ripple.datalog_cost_reorder | true | Sort Datalog body atoms by VP-table cardinality at compile time (v0.29.0+) |
pg_ripple.datalog_antijoin_threshold | 1000 | Minimum VP-table row count for using LEFT JOIN … IS NULL anti-join form for negation (v0.29.0+) |
pg_ripple.delta_index_threshold | 500 | Minimum delta-table row count before creating a B-tree index on (s, o) (v0.29.0+) |
pg_ripple.rule_plan_cache | true | Master switch for the Datalog rule plan cache (v0.30.0+) |
pg_ripple.rule_plan_cache_size | 64 | Maximum number of rule sets kept in the plan cache; oldest entries evicted on overflow (v0.30.0+) |
pg_ripple.sameas_reasoning | true | Enable owl:sameAs canonicalization pre-pass before each inference run (v0.31.0+) |
pg_ripple.demand_transform | true | Auto-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
| Table | Description |
|---|---|
_pg_ripple.rules | Stores each parsed rule with its set name, stratum, head, body, and compiled SQL |
_pg_ripple.rule_sets | Tracks named rule sets with their active/inactive flag |
_pg_ripple.dictionary_hot | UNLOGGED 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
| Key | Type | Description |
|---|---|---|
certain | integer | Facts concluded with certainty (true) |
unknown | integer | Facts that are neither true nor false |
derived | integer | Total derived facts (certain + unknown) |
iterations | integer | Number of alternating fixpoint rounds |
stratifiable | boolean | Whether 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
| Column | Type | Description |
|---|---|---|
goal_hash | BIGINT | XXH3-64 hash of the cached goal key |
hits | BIGINT | Number of cache hits for this entry |
computed_ms | FLOAT8 | Wall-clock time (ms) of the original computation |
cached_at | TEXT | ISO-8601 timestamp of when the entry was written |
Tabling GUCs
| GUC | Type | Default | Description |
|---|---|---|---|
pg_ripple.tabling | bool | true | Enable/disable the tabling cache |
pg_ripple.tabling_ttl | integer | 300 | Cache TTL in seconds; 0 = never expire |
pg_ripple.wfs_max_iterations | integer | 100 | Safety 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
| GUC | Type | Default | Description |
|---|---|---|---|
pg_ripple.datalog_max_depth | integer | 0 | Maximum 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.
| Parameter | Description |
|---|---|
pred_id | Encoded predicate ID (from the dictionary) |
s | Encoded subject ID |
o | Encoded object ID |
g | Encoded graph ID |
DRed GUCs
| GUC | Type | Default | Description |
|---|---|---|---|
pg_ripple.dred_enabled | bool | true | Enable DRed incremental retraction; false falls back to full recompute |
pg_ripple.dred_batch_size | integer | 1000 | Maximum 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.
| GUC | Type | Default | Description |
|---|---|---|---|
pg_ripple.datalog_parallel_workers | integer | 4 | Maximum parallel worker count for stratum evaluation; 1 = serial |
pg_ripple.datalog_parallel_threshold | integer | 10000 | Minimum 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):
| Field | Type | Description |
|---|---|---|
parallel_groups | integer | Number of independent rule groups detected in the rule set |
max_concurrent | integer | Effective 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
| Name | Join | Bottom | Typical use |
|---|---|---|---|
min | MIN | +∞ (i64::MAX) | Trust propagation, shortest paths |
max | MAX | −∞ (i64::MIN) | Longest paths, reachability |
set | UNION | {} (empty set) | Set-valued annotations |
interval | interval hull | empty interval | Temporal/numeric ranges |
New GUC parameters (v0.36.0)
| GUC | Default | Description |
|---|---|---|
pg_ripple.lattice_max_iterations | 1000 | Maximum fixpoint iterations before PT540 warning |
Functions
pg_ripple.create_lattice(name, join_fn, bottom) → boolean
Register a user-defined lattice.
name— unique lattice identifierjoin_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 writteniterations— fixpoint iterations performedlattice— lattice type usedrule_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.
SHACL Validation
pg_ripple v0.7.0 adds SHACL Core — the W3C standard for expressing data quality rules over RDF graphs. Rules are loaded from Turtle, stored in the database, and can be enforced inline at insert time or evaluated on demand.
Quick Start
-- 1. Load shapes from Turtle
SELECT pg_ripple.load_shacl($SHACL$
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <https://example.org/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
ex:PersonShape
a sh:NodeShape ;
sh:targetClass ex:Person ;
sh:property [
sh:path ex:name ;
sh:minCount 1 ;
sh:datatype xsd:string ;
] ;
sh:property [
sh:path ex:email ;
sh:maxCount 1 ;
] .
$SHACL$);
-- 2. Validate the default graph
SELECT pg_ripple.validate();
-- 3. Enable inline rejection of violations
SET pg_ripple.shacl_mode = 'sync';
Functions
load_shacl(data TEXT) → INTEGER
Parse data (Turtle-formatted SHACL shapes) and store every shape in _pg_ripple.shacl_shapes. Returns the count of shapes loaded. Raises an error on Turtle syntax failures so no partial state is committed.
Supported shape types:
sh:NodeShape— targets a class or specific nodessh:PropertyShape— constraints on a predicate path
Supported constraints (v0.7.0 Core):
| Constraint | Description |
|---|---|
sh:minCount | Minimum number of value nodes per focus node |
sh:maxCount | Maximum number of value nodes per focus node |
sh:datatype | Required datatype IRI for value nodes |
sh:in (...) | Allowed value set (Turtle list) |
sh:pattern "regex" | Regex match on lexical form |
sh:class | Required rdf:type for value nodes |
sh:node | Nested shape reference — value nodes must conform to the referenced shape |
sh:or (...) | Value/focus node must conform to at least one listed shape (v0.8.0) |
sh:and (...) | Value/focus node must conform to all listed shapes (v0.8.0) |
sh:not | Value/focus node must NOT conform to the referenced shape (v0.8.0) |
sh:qualifiedValueShape | Combined with sh:qualifiedMinCount/sh:qualifiedMaxCount (v0.8.0) |
sh:equals <path> | The value set for the declared path must equal the value set for <path> (v0.45.0) |
sh:disjoint <path> | The value set for the declared path must be disjoint from the value set for <path> (v0.45.0) |
Supported target declarations:
| Declaration | Description |
|---|---|
sh:targetClass | All instances (rdf:type members) of a class |
sh:targetNode | One or more specific nodes |
sh:targetSubjectsOf | All subjects of a given predicate |
sh:targetObjectsOf | All objects of a given predicate |
-- Returns the number of shapes loaded
SELECT pg_ripple.load_shacl('
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <https://example.org/> .
ex:ThingShape
a sh:NodeShape ;
sh:targetClass ex:Thing ;
sh:property [
sh:path ex:name ;
sh:minCount 1 ;
] .
');
validate(graph TEXT DEFAULT NULL) → JSONB
Run a full offline SHACL validation report against all active shapes.
graph value | Scope |
|---|---|
NULL (default) | Default graph (id 0) |
'' (empty string) | Default graph |
'*' | All named graphs |
'<https://example.org/g1>' | Specific named graph |
Return value — a JSONB object with two keys:
{
"conforms": false,
"violations": [
{
"focusNode": "https://example.org/alice",
"shapeIRI": "https://example.org/PersonShape",
"path": "https://example.org/email",
"constraint": "sh:maxCount",
"message": "expected at most 1 value(s) for <https://example.org/email>, found 2",
"severity": "Violation"
}
]
}
-- Check if the default graph conforms
SELECT (pg_ripple.validate() ->> 'conforms')::boolean AS ok;
-- Count violations
SELECT jsonb_array_length(pg_ripple.validate() -> 'violations') AS violation_count;
-- Validate a named graph
SELECT pg_ripple.validate('<https://example.org/my-graph>');
list_shapes() → TABLE(shape_iri TEXT, active BOOLEAN)
Return all shapes in the shapes catalog.
SELECT * FROM pg_ripple.list_shapes();
drop_shape(shape_uri TEXT) → INTEGER
Remove a shape by its IRI. Returns 1 if found and removed, 0 if not found.
SELECT pg_ripple.drop_shape('https://example.org/PersonShape');
Validation Modes (pg_ripple.shacl_mode)
| Mode | Behaviour |
|---|---|
off (default) | No SHACL enforcement. Shapes are stored but not used at insert time. |
sync | Violations are detected inline during insert_triple(). The insert is rejected with an error message; no partial data is written. |
async | (v0.8.0) Triples are queued in _pg_ripple.validation_queue for background validation. Violations are moved to _pg_ripple.dead_letter_queue. |
-- Enable inline enforcement
SET pg_ripple.shacl_mode = 'sync';
-- This will raise an error if the shape's sh:maxCount is exceeded:
SELECT pg_ripple.insert_triple(
'<https://example.org/alice>',
'<https://example.org/email>',
'"alice3@example.org"'
);
-- ERROR: SHACL violation: <https://example.org/alice> sh:maxCount 1 for
-- <https://example.org/email>: found 1 existing value(s), limit is 1
-- Restore default
RESET pg_ripple.shacl_mode;
Latency note:
syncmode executes per-shape validator plans for everyinsert_triple()call whenshacl_mode = 'sync'. For high-throughput ingestion, useoff(validate after load withvalidate()) or configureasyncmode (v0.8.0).
Example: Full Workflow
-- 1. Load shapes
SELECT pg_ripple.load_shacl($SHACL$
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <https://example.org/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
ex:EmployeeShape
a sh:NodeShape ;
sh:targetClass ex:Employee ;
sh:property [
sh:path ex:employeeId ;
sh:minCount 1 ;
sh:maxCount 1 ;
sh:datatype xsd:integer ;
] ;
sh:property [
sh:path ex:department ;
sh:minCount 1 ;
sh:in ( ex:Engineering ex:Sales ex:HR ) ;
] .
$SHACL$);
-- 2. Load data
SELECT pg_ripple.load_ntriples($NQ$
<https://example.org/emp1> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://example.org/Employee> .
<https://example.org/emp1> <https://example.org/employeeId> "42"^^<http://www.w3.org/2001/XMLSchema#integer> .
<https://example.org/emp1> <https://example.org/department> <https://example.org/Engineering> .
$NQ$);
-- 3. Validate
SELECT pg_ripple.validate();
-- {"conforms": true, "violations": []}
-- 4. Confirm shapes loaded
SELECT * FROM pg_ripple.list_shapes();
Internal Tables
| Table | Description |
|---|---|
_pg_ripple.shacl_shapes | Shape catalog: shape_iri, shape_json (JSONB IR), active, timestamps |
_pg_ripple.validation_queue | Async validation inbox (populated when shacl_mode = 'async') |
_pg_ripple.dead_letter_queue | Triples rejected by async validation with violation report |
Limitations (v0.7.0)
- Property paths beyond direct predicates (e.g.,
sh:inversePath,sh:alternativePath) are not supported. sh:minCountis only checked byvalidate(), not duringinsert_triple()in sync mode (absence cannot be detected on a single insert).
Async Validation Pipeline (v0.8.0)
When pg_ripple.shacl_mode = 'async', violations are not raised inline. Instead, every triple inserted via insert_triple() is recorded in _pg_ripple.validation_queue. A background worker (the merge worker) drains the queue in batches, running full SHACL validation against each triple. Triples that violate any active shape are written to _pg_ripple.dead_letter_queue with a structured violation report.
Management functions
| Function | Returns | Description |
|---|---|---|
process_validation_queue(batch_size BIGINT DEFAULT 1000) | BIGINT | Process up to batch_size items; returns count processed |
validation_queue_length() | BIGINT | Number of items pending in the queue |
dead_letter_count() | BIGINT | Number of violations recorded |
dead_letter_queue() | JSONB | All dead-letter entries as a JSON array |
drain_dead_letter_queue() | BIGINT | Delete all dead-letter entries; returns count deleted |
-- Enable async mode
SET pg_ripple.shacl_mode = 'async';
-- Inserts never raise errors — violations are queued
SELECT pg_ripple.insert_triple(
'<https://example.org/thing>',
'<https://example.org/value>',
'"wrong-type"'
);
-- Check queue depth
SELECT pg_ripple.validation_queue_length();
-- Manually drain the queue (background worker also does this)
SELECT pg_ripple.process_validation_queue();
-- Inspect violations
SELECT pg_ripple.dead_letter_queue();
-- Clear after review
SELECT pg_ripple.drain_dead_letter_queue();
When to use async mode
- High-throughput ingestion where inline SHACL overhead is unacceptable
- Batch imports — validate after load by calling
process_validation_queue()once - Non-blocking pipelines — violations are flagged asynchronously without interrupting writers
Note: The background worker processes the queue automatically;
process_validation_queue()is a manual override useful in testing and one-shot pipelines.
Complex Shape Constraints (v0.8.0)
sh:or — at least one shape must match
ex:AgentShape
a sh:NodeShape ;
sh:targetClass ex:Agent ;
sh:or (ex:PersonShape ex:OrganizationShape) .
The focus node must conform to at least one of the listed shapes. Evaluated both in validate() and sync mode.
sh:and — all shapes must match
ex:VerifiedPersonShape
a sh:NodeShape ;
sh:targetClass ex:VerifiedPerson ;
sh:and (ex:PersonShape ex:VerifiedShape) .
The focus node must conform to every listed shape.
sh:not — shape must not match
ex:ActiveEntityShape
a sh:NodeShape ;
sh:targetClass ex:ActiveEntity ;
sh:not ex:BannedEntityShape .
The focus node must not conform to the referenced shape.
sh:node — nested shape for value nodes
ex:CompanyShape
a sh:NodeShape ;
sh:targetClass ex:Company ;
sh:property [
sh:path ex:headquarterAddress ;
sh:node ex:AddressShape ;
] .
Each value node along the path must conform to ex:AddressShape. Recursion is depth-limited at 32 levels.
sh:qualifiedValueShape — qualified cardinality
ex:EmployerShape
a sh:NodeShape ;
sh:targetClass ex:Employer ;
sh:property [
sh:path ex:officeAddress ;
sh:qualifiedValueShape ex:USAddressShape ;
sh:qualifiedMinCount 1 ;
sh:qualifiedMaxCount 3 ;
] .
At least sh:qualifiedMinCount (and at most sh:qualifiedMaxCount) value nodes along the path must conform to the qualified shape.
Limitations (v0.8.0)
- Property paths beyond direct predicates (e.g.,
sh:inversePath,sh:alternativePath) are not supported. sh:minCountis only checked byvalidate(), not duringinsert_triple()in sync mode.sh:qualifiedMinCountis only checked byvalidate()in sync mode (absence cannot be detected on a single insert).
RDF-star
RDF-star (RDF*) extends RDF by allowing triples to be used as subjects or objects in other triples. This enables statements about statements — useful for provenance, temporal annotations, and LPG-style edge properties.
Overview
In RDF-star a quoted triple << s p o >> can appear in subject or object position:
<< <ex:alice> <ex:knows> <ex:bob> >> <ex:assertedBy> <ex:carol> .
pg_ripple stores quoted triples in the dictionary with kind = 5 (KIND_QUOTED_TRIPLE) and encodes them using their component dictionary IDs.
encode_triple
pg_ripple.encode_triple(
subject TEXT,
predicate TEXT,
object TEXT
) RETURNS BIGINT
Encodes a triple as a dictionary entry and returns its BIGINT ID. Idempotent — repeated calls with the same triple return the same ID.
SELECT pg_ripple.encode_triple(
'<https://example.org/alice>',
'<https://example.org/knows>',
'<https://example.org/bob>'
);
decode_triple
pg_ripple.decode_triple(id BIGINT) RETURNS JSONB
Returns the triple encoded at a given dictionary ID as a JSONB object {"s":…,"p":…,"o":…}.
SELECT pg_ripple.decode_triple(42);
-- Returns: {"s":"<https://example.org/alice>","p":"<https://example.org/knows>","o":"<https://example.org/bob>"}
insert_triple and SIDs
insert_triple() returns a statement identifier (SID) — a globally-unique BIGINT for the inserted triple. SIDs can be used as subjects or objects in subsequent triples to annotate the statement.
DECLARE sid BIGINT;
SELECT pg_ripple.insert_triple(
'<https://example.org/alice>',
'<https://example.org/knows>',
'<https://example.org/bob>'
) INTO sid;
-- Annotate the statement with provenance
SELECT pg_ripple.insert_triple(
'<https://example.org/sid-' || sid || '>',
'<https://example.org/assertedBy>',
'<https://example.org/carol>'
);
get_statement
pg_ripple.get_statement(i BIGINT) RETURNS JSONB
Looks up a triple by its SID and returns it as {"s":…,"p":…,"o":…,"g":…}.
SELECT pg_ripple.get_statement(1);
-- Returns: {"s":"<ex:alice>","p":"<ex:knows>","o":"<ex:bob>","g":"0"}
Loading RDF-star data
load_ntriples() accepts N-Triples-star input with subject-position and object-position quoted triples:
SELECT pg_ripple.load_ntriples('
<< <https://example.org/alice> <https://example.org/knows> <https://example.org/bob> >>
<https://example.org/assertedBy>
<https://example.org/carol> .
');
SPARQL-star patterns
Ground (all-constant) quoted triple patterns are supported in SPARQL WHERE clauses:
SELECT * FROM pg_ripple.sparql('
SELECT ?who WHERE {
<< <https://example.org/alice> <https://example.org/knows> <https://example.org/bob> >>
<https://example.org/assertedBy> ?who
}
');
LPG edge property mapping
RDF-star is a natural fit for encoding LPG edge properties: a quoted triple represents the edge, and subsequent triples about the quoted triple encode the properties.
<< <ex:alice> <ex:knows> <ex:bob> >> <ex:since> "2023-01-01"^^xsd:date .
<< <ex:alice> <ex:knows> <ex:bob> >> <ex:strength> "strong" .
Variable-inside-quoted-triple patterns (v0.48.0)
As of v0.48.0, variables inside quoted triple patterns are supported. This allows binding variables to the components of a quoted triple that appears as the subject or object of another triple:
-- Bind ?v to the object component of the matching quoted triple
SELECT * FROM pg_ripple.sparql('
PREFIX ex: <http://example.org/>
SELECT ?v ?who WHERE {
<< ex:alice ex:age ?v >> ex:assertedBy ?who .
}
');
-- Bind all three components
SELECT * FROM pg_ripple.sparql('
PREFIX ex: <http://example.org/>
SELECT ?s ?p ?o ?who WHERE {
<< ?s ?p ?o >> ex:assertedBy ?who .
}
');
This works by joining the _pg_ripple.dictionary table on the qt_s,
qt_p, and qt_o columns (available for entries with kind = 5).
Named Graphs
pg_ripple supports the RDF concept of named graphs: each triple belongs to either the default graph (internal ID 0) or a named graph identified by an IRI.
Named graphs are stored as part of each VP table row (g BIGINT column). No separate graph-catalog table is required — the set of named graphs is derived from the data.
create_graph
pg_ripple.create_graph(graph_iri TEXT) RETURNS BIGINT
Registers a named graph in the dictionary and returns its BIGINT ID. This is informational only — graphs are created implicitly when triples are inserted. Calling create_graph explicitly is useful when you want a graph ID before loading data.
SELECT pg_ripple.create_graph('<https://example.org/graph1>');
-- Returns the dictionary ID for the graph IRI
drop_graph
pg_ripple.drop_graph(graph_iri TEXT) RETURNS BIGINT
Deletes all triples belonging to the named graph. Returns the number of triples deleted.
SELECT pg_ripple.drop_graph('<https://example.org/graph1>');
Warning: This permanently deletes all triples in the graph. The operation is transactional — wrap it in
BEGIN/ROLLBACKif you need a dry run.
list_graphs
pg_ripple.list_graphs() RETURNS TABLE(graph_iri TEXT, triple_count BIGINT)
Returns all named graphs and how many triples each contains. The default graph (ID 0) is excluded.
SELECT * FROM pg_ripple.list_graphs();
Querying named graphs in SPARQL
Use the GRAPH keyword to restrict patterns to a specific graph:
SELECT * FROM pg_ripple.sparql('
SELECT ?s ?p ?o WHERE {
GRAPH <https://example.org/graph1> { ?s ?p ?o }
}
');
To query across all named graphs:
SELECT * FROM pg_ripple.sparql('
SELECT ?g ?s ?p ?o WHERE {
GRAPH ?g { ?s ?p ?o }
}
');
Inserting into a named graph
-- Via insert_triple
SELECT pg_ripple.insert_triple(
'<https://example.org/alice>',
'<https://example.org/knows>',
'<https://example.org/bob>',
'<https://example.org/graph1>' -- named graph
);
-- Via bulk load (N-Quads — fourth column is the graph)
SELECT pg_ripple.load_nquads('
<https://example.org/alice> <https://example.org/knows> <https://example.org/bob> <https://example.org/graph1> .
');
-- Via TriG
SELECT pg_ripple.load_trig('
GRAPH <https://example.org/graph1> {
<https://example.org/alice> <https://example.org/knows> <https://example.org/bob> .
}
');
Default graph
The default graph has internal ID 0. Triples inserted without a graph argument land in the default graph. SPARQL patterns without a GRAPH clause match against the default graph.
Graph-aware operations (v0.15.0)
find_triples_in_graph
pg_ripple.find_triples_in_graph(s TEXT, p TEXT, o TEXT, graph TEXT) → SETOF RECORD
Pattern-matches triples within a specific named graph. Same wildcard rules as find_triples() — pass NULL for any position.
SELECT * FROM pg_ripple.find_triples_in_graph(
NULL, NULL, NULL,
'<https://example.org/graph1>'
);
triple_count_in_graph
pg_ripple.triple_count_in_graph(graph_iri TEXT) → BIGINT
Returns the number of triples in a specific named graph.
SELECT pg_ripple.triple_count_in_graph('<https://example.org/graph1>');
-- 42
delete_triple_from_graph
pg_ripple.delete_triple_from_graph(s TEXT, p TEXT, o TEXT, graph_iri TEXT) → BIGINT
Removes a single triple from a specific named graph. Returns the number of rows deleted.
SELECT pg_ripple.delete_triple_from_graph(
'<https://example.org/alice>',
'<https://example.org/knows>',
'<https://example.org/bob>',
'<https://example.org/graph1>'
);
clear_graph
pg_ripple.clear_graph(graph_iri TEXT) → BIGINT
Removes all triples from a named graph without unregistering it. Returns the number of triples deleted. Unlike drop_graph(), the graph IRI stays in the dictionary — useful when you plan to reload data into the same graph.
SELECT pg_ripple.clear_graph('<https://example.org/graph1>');
Tip: Use
clear_graph()for a "truncate and reload" workflow. Usedrop_graph()when you want to permanently remove the graph.
SPARQL Federation
SPARQL federation lets a single query combine data from pg_ripple with data stored at external SPARQL endpoints. Use the SERVICE keyword to delegate part of your query to a remote endpoint.
Quick start
-- 1. Register a remote endpoint (required for SSRF protection)
SELECT pg_ripple.register_endpoint('https://query.wikidata.org/sparql');
-- 2. Query across local and remote data
SELECT result->>'local_s' AS local_subject,
result->>'remote_o' AS remote_label
FROM pg_ripple.sparql($$
SELECT ?local_s ?remote_o WHERE {
?local_s <https://example.org/sameAs> ?wikidata_item .
SERVICE <https://query.wikidata.org/sparql> {
?wikidata_item <http://www.w3.org/2000/01/rdf-schema#label> ?remote_o .
FILTER(LANG(?remote_o) = "en")
}
}
$$);
SERVICE clause syntax
SERVICE <endpoint-url> { ... graph pattern ... }
SERVICE SILENT <endpoint-url> { ... }
SERVICE ?var { ... } -- variable endpoint (requires VALUES binding)
SERVICE <url> { … }— execute the inner pattern at the remote SPARQL endpoint. Raises an ERROR if the call fails (unlessfederation_on_error = 'empty').SERVICE SILENT <url> { … }— same, but silently returns empty results on failure. A WARNING is still logged.SERVICE ?var { … }withVALUES— bind the endpoint URL to a variable, allowing dynamic dispatch.
Endpoint registration
Only allowlisted endpoints can be contacted. Calling an unregistered URL raises an error — this prevents Server-Side Request Forgery (SSRF) attacks.
pg_ripple.register_endpoint(url, local_view_name, complexity)
Register a remote SPARQL endpoint.
| Parameter | Type | Default | Description |
|---|---|---|---|
url | TEXT | — | Full URL of the endpoint (e.g. https://dbpedia.org/sparql) |
local_view_name | TEXT | NULL | Optional name of a local SPARQL view stream table that pre-materialises the data from this endpoint. When set, SERVICE calls targeting this URL are rewritten to scan the local table instead of making HTTP calls. |
complexity | TEXT | 'normal' | Endpoint speed hint: 'fast', 'normal', or 'slow'. Influences query planning order when multiple SERVICE clauses target different endpoints. |
-- Register a plain remote endpoint
SELECT pg_ripple.register_endpoint('https://dbpedia.org/sparql');
-- Register with a local view override (SERVICE becomes a local scan)
SELECT pg_ripple.register_endpoint(
'https://internal-kb.example.com/sparql',
'my_local_view_stream'
);
-- Register a known-fast endpoint
SELECT pg_ripple.register_endpoint(
'https://fast-endpoint.example.com/sparql',
NULL,
'fast'
);
pg_ripple.remove_endpoint(url)
Permanently remove an endpoint from the allowlist.
SELECT pg_ripple.remove_endpoint('https://dbpedia.org/sparql');
pg_ripple.disable_endpoint(url)
Temporarily disable an endpoint without removing it. Re-enable by calling register_endpoint() again.
SELECT pg_ripple.disable_endpoint('https://slow-endpoint.example.com/sparql');
-- Later:
SELECT pg_ripple.register_endpoint('https://slow-endpoint.example.com/sparql');
pg_ripple.list_endpoints()
List all registered endpoints.
SELECT * FROM pg_ripple.list_endpoints();
Returns: (url TEXT, enabled BOOLEAN, local_view_name TEXT, complexity TEXT).
Configuration GUCs
| GUC | Default | Description |
|---|---|---|
pg_ripple.federation_timeout | 30 | Per-SERVICE call wall-clock timeout in seconds. |
pg_ripple.federation_max_results | 10000 | Maximum rows accepted from a single remote call. Extra rows are silently dropped. |
pg_ripple.federation_on_error | 'warning' | Behaviour on failure: 'warning' (emit WARNING, return empty), 'error' (raise ERROR), 'empty' (silent empty result). |
-- Tighten timeout for latency-sensitive queries
SET pg_ripple.federation_timeout = 5;
-- Raise an error on any SERVICE failure
SET pg_ripple.federation_on_error = 'error';
Variable endpoints with VALUES
SELECT ?s ?label WHERE {
VALUES ?endpoint {
<https://query.wikidata.org/sparql>
<https://dbpedia.org/sparql>
}
SERVICE ?endpoint {
?s <http://www.w3.org/2000/01/rdf-schema#label> ?label
FILTER(LANG(?label) = "en")
}
}
Both endpoints must be registered. Results from both are combined and deduplicated via SELECT DISTINCT.
Local view rewrite
When a SERVICE endpoint has a local_view_name set, pg_ripple rewrites the SERVICE clause to scan the pre-materialised stream table directly:
- No HTTP call: zero network latency.
- PostgreSQL planner optimises: the local scan participates in the full query plan.
- Accurate statistics:
ANALYZEon the stream table gives the planner cardinality information.
Set this up using create_sparql_view() (see Views) and then register the endpoint with the view name:
-- Create a SPARQL view backed by a stream table
SELECT pg_ripple.create_sparql_view(
'eu_companies',
'SELECT ?company ?name WHERE { ?company <https://eu.example.org/name> ?name }',
'manual'
);
-- Register the remote endpoint with the local view as override
SELECT pg_ripple.register_endpoint(
'https://eu-kb.example.com/sparql',
'_pg_ripple.eu_companies' -- stream table name
);
Health-based endpoint skipping
pg_ripple tracks the success/failure of each SERVICE call in _pg_ripple.federation_health. If a registered endpoint has a success rate below 10% in the last 5 minutes, the executor skips it automatically (emits a WARNING) rather than waiting for a full timeout. This prevents a single slow endpoint from blocking the entire query.
-- Check recent health
SELECT url,
COUNT(*) AS total_probes,
AVG(CASE WHEN success THEN 1.0 ELSE 0.0 END) AS success_rate,
AVG(latency_ms) AS avg_latency_ms
FROM _pg_ripple.federation_health
WHERE probed_at >= now() - INTERVAL '5 minutes'
GROUP BY url;
SSRF protection
pg_ripple enforces a strict allowlist: only endpoints registered with register_endpoint() can be contacted. Any SERVICE clause targeting an unregistered URL raises:
ERROR: federation endpoint not registered: http://internal-host/sparql;
use pg_ripple.register_endpoint() to allow it
This prevents queries from being used as a vector to probe internal network services.
Parallelism
Within a PostgreSQL session (SPI context), multiple SERVICE clauses in a single query execute sequentially to avoid conflict between HTTP I/O and SPI transactions. The pg_ripple_http sidecar process can execute federation calls in parallel via its async runtime; performance-critical federation workloads should use the HTTP interface.
v0.19.0: Performance improvements
Connection pooling
A per-backend thread-local ureq::Agent reuses TCP and TLS sessions across SERVICE calls within a session. Previously each call opened and discarded a new TCP connection.
| GUC | Default | Description |
|---|---|---|
pg_ripple.federation_pool_size | 4 | Idle connections kept per endpoint in the pool (1–32) |
-- Use a larger pool for latency-sensitive workloads with many endpoints
SET pg_ripple.federation_pool_size = 16;
Result caching with TTL
When pg_ripple.federation_cache_ttl > 0, successful remote results are stored in _pg_ripple.federation_cache. Repeat calls with the same endpoint URL and SPARQL text within the TTL window skip the HTTP call entirely.
The cache key is (url, XXH3-64(sparql_text)). Expired rows are cleaned up by the merge background worker on each polling cycle.
| GUC | Default | Description |
|---|---|---|
pg_ripple.federation_cache_ttl | 0 | Cache TTL in seconds; 0 = disabled (0–86400) |
-- Cache Wikidata label results for 10 minutes
SET pg_ripple.federation_cache_ttl = 600;
-- Inspect the cache
SELECT url, query_hash, cached_at, expires_at
FROM _pg_ripple.federation_cache
ORDER BY cached_at DESC;
-- Clear the cache manually
DELETE FROM _pg_ripple.federation_cache;
When to use caching:
- Reference datasets that update infrequently (Wikidata labels, DBpedia categories, controlled vocabularies).
- Queries where the same sub-pattern is evaluated many times (e.g. inside a loop or repeated SPARQL calls from an application).
When not to use caching:
- Live event streams, sensor data, or any endpoint where freshness matters.
- Endpoints that return large variable result sets (high cache miss rate, high storage cost).
Endpoint complexity hints
Register an endpoint with a performance hint to guide multi-endpoint query ordering. Fast endpoints execute first, enabling earlier failure detection and lower total wall-clock time.
-- Register with a hint
SELECT pg_ripple.register_endpoint(
'https://fast-kb.example.com/sparql',
NULL, -- local_view_name
'fast' -- complexity: 'fast', 'normal', or 'slow'
);
-- Update after registration
SELECT pg_ripple.set_endpoint_complexity('https://slow-kb.example.com/sparql', 'slow');
-- View all endpoints with complexity
SELECT url, enabled, complexity FROM pg_ripple.list_endpoints();
Variable projection rewrite
Instead of sending SELECT * WHERE { … } to the remote endpoint, pg_ripple now sends an explicit SELECT ?v1 ?v2 … WHERE { … } listing the variables that appear in the inner pattern. This:
- Reduces data transfer when the remote supports projection pushdown.
- Produces a stable, deterministic query text for cache key matching.
- Makes it easier to inspect the SPARQL sent (visible in WARNING messages on failure).
Partial result handling
When pg_ripple.federation_on_partial = 'use', a connection drop mid-response uses however many rows were received rather than discarding them entirely. A WARNING names the endpoint, the row count received, and the error.
| GUC | Default | Description |
|---|---|---|
pg_ripple.federation_on_partial | 'empty' | 'empty' = discard all, 'use' = keep partial rows |
SET pg_ripple.federation_on_partial = 'use';
Adaptive timeout
When pg_ripple.federation_adaptive_timeout = on, the effective per-endpoint timeout is derived from max(1s, p95_latency_ms × 3 / 1000) observed in _pg_ripple.federation_health. Fast endpoints get a tighter timeout; slow endpoints get more room. Falls back to pg_ripple.federation_timeout when no health data is available.
| GUC | Default | Description |
|---|---|---|
pg_ripple.federation_adaptive_timeout | off | Derive timeout from P95 health data |
SET pg_ripple.federation_adaptive_timeout = on;
Batch SERVICE calls
When a single query contains two or more SERVICE clauses targeting the same registered endpoint with independent inner patterns (no shared variables), pg_ripple combines them into a single HTTP request:
SELECT * WHERE {
SERVICE <https://kb.example.com/sparql> { ?s <ex:label> ?label }
SERVICE <https://kb.example.com/sparql> { ?s <ex:type> ?type }
# ^ One HTTP request: SELECT * WHERE { { ?s <ex:label> ?label } UNION { ?s <ex:type> ?type } }
}
This halves the HTTP round trips for queries that pull multiple independent properties from the same endpoint.
GUC reference (v0.19.0 additions)
| GUC | Type | Default | Range | Description |
|---|---|---|---|---|
pg_ripple.federation_pool_size | INT | 4 | 1–32 | Idle connections per endpoint in the thread-local pool |
pg_ripple.federation_cache_ttl | INT | 0 | 0–86400 | Result cache TTL in seconds (0 = disabled) |
pg_ripple.federation_on_partial | STRING | 'empty' | 'empty', 'use' | Behaviour when SERVICE delivers rows then fails |
pg_ripple.federation_adaptive_timeout | BOOL | off | — | Derive timeout from P95 health latency |
Limitations
- No bind-join pushdown at runtime: the full inner pattern is sent to the remote endpoint without pre-binding known variables.
- SPARQL results+JSON only: XML response format is not yet supported for the direct SPI path.
- No streaming: remote results are fully buffered in memory before being dictionary-encoded. Large result sets should use
federation_max_resultsto cap memory usage.
Full-Text Search
pg_ripple provides full-text search on RDF literal objects using PostgreSQL's built-in GIN tsvector indexes. This enables fast free-text queries on string-valued predicates without scanning the entire dictionary.
fts_index
pg_ripple.fts_index(predicate TEXT) RETURNS BIGINT
Creates a GIN tsvector index on the _pg_ripple.dictionary table for string-literal objects. Returns the dictionary ID of the predicate.
The predicate argument accepts both raw IRI strings and N-Triples notation with angle brackets.
-- Create FTS index for the dc:description predicate
SELECT pg_ripple.fts_index('http://purl.org/dc/elements/1.1/description');
-- N-Triples notation also accepted
SELECT pg_ripple.fts_index('<http://purl.org/dc/elements/1.1/description>');
The index is created as IF NOT EXISTS, so calling fts_index multiple times for the same predicate is safe.
Index scope: The GIN index covers all plain string literals (
kind = 2) in the dictionary. Thefts_searchfunction restricts results by predicate via a VP table JOIN.
Example: index all abstract predicates
-- Load some data
SELECT pg_ripple.load_ntriples('
<https://example.org/paper1> <https://schema.org/abstract> "A study of RDF triplestores" .
<https://example.org/paper2> <https://schema.org/abstract> "PostgreSQL extensions for graph data" .
');
-- Index the abstract predicate
SELECT pg_ripple.fts_index('<https://schema.org/abstract>');
-- Now search
SELECT * FROM pg_ripple.fts_search('RDF | triplestore', '<https://schema.org/abstract>');
fts_search
pg_ripple.fts_search(query TEXT, predicate TEXT)
RETURNS TABLE(s TEXT, p TEXT, o TEXT)
Executes a full-text search against literal objects of the specified predicate. Returns matching triples as N-Triples–formatted strings.
query— a PostgreSQLtsqueryexpression (see below)predicate— the predicate IRI, with or without angle brackets
SELECT s, o
FROM pg_ripple.fts_search('semantic & query', '<https://schema.org/abstract>');
tsquery syntax
PostgreSQL tsquery uses & (AND), | (OR), ! (NOT), and <-> (phrase proximity):
| Query | Matches |
|---|---|
'rdf' | documents containing "rdf" |
'rdf & sparql' | documents containing both |
| `'rdf | sparql'` |
'!relational' | documents not containing "relational" |
'rdf <-> store' | documents with "rdf" immediately followed by "store" |
All terms are automatically stemmed (English stemmer by default) — searching for "querying" also matches "query" and "queries".
Searching without a prior fts_index call
fts_search works even without a prior fts_index call by performing a sequential scan of the dictionary joined to the VP table. For large stores, call fts_index first for each predicate you search frequently.
Return columns
| Column | Content |
|---|---|
s | Subject IRI in N-Triples notation |
p | Predicate IRI in N-Triples notation |
o | Literal value in N-Triples notation |
Language configuration
The default text configuration is 'english', which applies English stemming and stop-word removal. If your data is in another language you can create the index manually:
-- Example: French language index on a custom predicate
CREATE INDEX my_fr_fts ON _pg_ripple.dictionary
USING GIN (to_tsvector('french', value))
WHERE kind = 2;
The built-in fts_index and fts_search always use 'english'. Multi-language support is planned for a future release.
Streaming Cursor API
When processing millions of triples, materialising an entire SPARQL result set in one call can exhaust memory or hit statement-level row limits. The streaming cursor API returns results in batches via PostgreSQL SETOF functions.
Functions
sparql_cursor(query TEXT) RETURNS SETOF JSONB (v0.40.0)
Streams the output of a SPARQL SELECT or ASK query as a sequence of JSONB binding rows.
SELECT * FROM pg_ripple.sparql_cursor($$
SELECT ?s ?label WHERE { ?s <https://schema.org/name> ?label }
$$);
Equivalent to sparql() but avoids full materialisation. Each row is a JSONB object with one key per projected variable.
sparql_cursor_turtle(query TEXT) RETURNS SETOF TEXT (v0.40.0)
Streams the output of a SPARQL CONSTRUCT query as Turtle text chunks.
COPY (
SELECT result FROM pg_ripple.sparql_cursor_turtle($$
CONSTRUCT { ?s <https://schema.org/name> ?name }
WHERE { ?s <https://schema.org/name> ?name }
$$)
) TO '/tmp/dump.ttl';
Each returned TEXT value is a complete, self-contained Turtle serialisation of one batch (up to 1 024 triples).
sparql_cursor_jsonld(query TEXT) RETURNS SETOF TEXT (v0.40.0)
Streams the output of a SPARQL CONSTRUCT query as JSON-LD expanded-form chunks.
SELECT string_agg(result, E'\n')
FROM pg_ripple.sparql_cursor_jsonld($$
CONSTRUCT { ?s ?p ?o } WHERE { GRAPH <https://my.graph/> { ?s ?p ?o } }
$$);
Overflow control GUCs
| GUC | Default | Description |
|---|---|---|
pg_ripple.sparql_max_rows | 0 (unlimited) | Maximum rows returned by sparql() and sparql_cursor(). When exceeded, behaviour is controlled by sparql_overflow_action. |
pg_ripple.export_max_rows | 0 (unlimited) | Maximum rows returned by Turtle/N-Triples/JSON-LD export functions. When exceeded, a PT642 WARNING is emitted and the result is truncated. |
pg_ripple.sparql_overflow_action | '' (warn) | 'warn' — emit PT640 WARNING and truncate. 'error' — raise PT640 ERROR. |
pg_ripple.datalog_max_derived | 0 (unlimited) | Maximum derived facts produced by a single infer() call. When exceeded, a PT641 WARNING is emitted. |
Example — limit a large export to 50 000 rows:
SET pg_ripple.export_max_rows = 50000;
SELECT string_agg(result, E'\n')
FROM pg_ripple.sparql_cursor_turtle('CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }');
Example — error on overflow instead of silent truncation:
SET pg_ripple.sparql_max_rows = 100000;
SET pg_ripple.sparql_overflow_action = 'error';
SELECT * FROM pg_ripple.sparql_cursor('SELECT ?s WHERE { ?s ?p ?o }');
Performance notes
- Batches of 1 024 rows are processed per iteration; memory footprint is
O(batch_size). - For very large CONSTRUCT exports, consider using
COPY ... TOwithsparql_cursor_turtleto avoid buffering in the client. - The cursor functions call the same SPARQL→SQL pipeline as
sparql(); plan cache hits apply.
See also
- Explain API — introspect SPARQL query plans
- Observability — tracing and cache statistics
- Error Reference — PT640, PT641, PT642
Materialized Views
pg_ripple v0.11.0 integrates with pg_trickle to provide always-fresh, incrementally-maintained stream tables for SPARQL queries, Datalog goals, and predicate semi-joins. All three features are soft-gated — pg_ripple loads and operates normally without pg_trickle; the new functions detect its absence at call time and return a clear error with an install hint.
Checking pg_trickle availability
SELECT pg_ripple.pg_trickle_available();
-- true (pg_trickle is installed)
-- false (pg_trickle not installed; view functions will error)
SPARQL views
A SPARQL view compiles a SPARQL SELECT query into a pg_trickle stream table that stays up to date automatically as triples change.
create_sparql_view
pg_ripple.create_sparql_view(
name TEXT,
sparql TEXT,
schedule TEXT DEFAULT '1s',
decode BOOLEAN DEFAULT false
) → BIGINT
Compiles the SPARQL SELECT to SQL and registers a pg_trickle stream table. Returns the number of projected columns.
- name — unique identifier for the view (becomes the stream table name in
pg_rippleschema) - sparql — a valid SPARQL SELECT query
- schedule — pg_trickle refresh interval (e.g.
'1s','10s','1m') - decode — when
true, dictionary IDs are decoded to human-readable strings in the stream table; whenfalse(default), columns contain rawBIGINTIDs for maximum performance
-- Create a view of all people and their names
SELECT pg_ripple.create_sparql_view(
'people_names',
'SELECT ?person ?name WHERE {
?person <http://xmlns.com/foaf/0.1/name> ?name
}',
'5s',
true
);
-- Query the materialized view like a regular table
SELECT * FROM pg_ripple.people_names;
Example output (with decode = true):
person | name
-------------------------+--------------------
http://example.org/alice | Alice Smith
http://example.org/bob | Bob Johnson
http://example.org/carol | Carol Williams
(3 rows)
With decode = false, columns contain raw dictionary IDs (BIGINT):
person | name
-----------------+--------------------
4728391847263 | 4728391847264
4728391847265 | 4728391847266
4728391847267 | 4728391847268
(3 rows)
drop_sparql_view
pg_ripple.drop_sparql_view(name TEXT) → BOOLEAN
Drops the stream table and removes the catalog entry.
list_sparql_views
pg_ripple.list_sparql_views() → JSONB
Returns a JSONB array of all registered SPARQL views, including name, original query, schedule, and decode mode.
SELECT pg_ripple.list_sparql_views();
Example output:
[
{
"name": "people_names",
"sparql": "SELECT ?person ?name WHERE { ?person <http://xmlns.com/foaf/0.1/name> ?name }",
"schedule": "5s",
"decode": true,
"stream_table_name": "pg_ripple.people_names",
"variables": ["person", "name"]
},
{
"name": "all_students",
"sparql": "SELECT ?s WHERE { ?s <http://xmlns.com/foaf/0.1/isPrimaryTopicOf> ?doc }",
"schedule": "10s",
"decode": false,
"stream_table_name": "pg_ripple.all_students",
"variables": ["s"]
}
]
Datalog views
A Datalog view bundles a rule set with a goal pattern into a self-refreshing stream table.
create_datalog_view
pg_ripple.create_datalog_view(
name TEXT,
rules TEXT,
goal TEXT,
rule_set_name TEXT DEFAULT 'custom',
schedule TEXT DEFAULT '10s',
decode BOOLEAN DEFAULT false
) → BIGINT
Parses inline Datalog rules, compiles the goal query to SQL, and registers a pg_trickle stream table. Returns the number of projected columns.
-- View all inferred grandparent relationships, refreshing every 10 seconds
SELECT pg_ripple.create_datalog_view(
'grandparents',
'?x <http://example.org/grandparent> ?z :-
?x <http://example.org/parent> ?y ,
?y <http://example.org/parent> ?z .',
'?x <http://example.org/grandparent> ?z',
'family',
'10s',
true
);
SELECT * FROM pg_ripple.grandparents;
Example output (inferred from explicit parent triples):
?x | ?z
--------------+--------------
john | grandpa
jane | grandpa
bob | grandma
(3 rows)
create_datalog_view_from_rule_set
pg_ripple.create_datalog_view_from_rule_set(
name TEXT,
rule_set TEXT,
goal TEXT,
schedule TEXT DEFAULT '10s',
decode BOOLEAN DEFAULT false
) → BIGINT
References an existing named rule set (loaded earlier via load_rules() or load_rules_builtin()) instead of providing inline rules.
-- Load rules once
SELECT pg_ripple.load_rules_builtin('rdfs');
-- Create a view using those rules
SELECT pg_ripple.create_datalog_view_from_rule_set(
'all_types',
'rdfs',
'?x <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> ?t',
'30s',
true
);
SELECT * FROM pg_ripple.all_types;
Example output (includes explicit types + inferred RDFS subclass/domain types):
?x | ?t
---------------+-------------------------------
alice | http://example.org/Person
bob | http://example.org/Person
alice | http://xmlns.com/foaf/0.1/Agent
bob | http://xmlns.com/foaf/0.1/Agent
(4 rows)
drop_datalog_view / list_datalog_views
pg_ripple.drop_datalog_view(name TEXT) → BOOLEAN
pg_ripple.list_datalog_views() → JSONB
Same lifecycle management as SPARQL views.
SELECT pg_ripple.list_datalog_views();
Example output:
[
{
"name": "grandparents",
"rule_set": "family",
"goal": "?x <http://example.org/grandparent> ?z",
"schedule": "10s",
"decode": true,
"stream_table_name": "pg_ripple.grandparents",
"variables": ["?x", "?z"]
},
{
"name": "all_types",
"rule_set": "rdfs",
"goal": "?x <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> ?t",
"schedule": "30s",
"decode": true,
"stream_table_name": "pg_ripple.all_types",
"variables": ["?x", "?t"]
}
]
Extended Vertical Partitioning (ExtVP)
ExtVP pre-computes the semi-join between two frequently co-joined predicate pairs. The SPARQL query engine detects and uses ExtVP tables automatically when they exist, giving 2–10× speedups on star patterns.
create_extvp
pg_ripple.create_extvp(
name TEXT,
pred1_iri TEXT,
pred2_iri TEXT,
schedule TEXT DEFAULT '10s'
) → BIGINT
Creates a pg_trickle stream table containing the pre-computed semi-join between two predicate VP tables. Returns the column count.
-- Pre-compute the join between foaf:name and foaf:knows
SELECT pg_ripple.create_extvp(
'name_knows',
'<http://xmlns.com/foaf/0.1/name>',
'<http://xmlns.com/foaf/0.1/knows>',
'10s'
);
SELECT * FROM pg_ripple.name_knows;
Example output (semi-join of subjects from both predicates):
s
------------------
alice
bob
carol
(3 rows)
When the SPARQL engine encounters a star pattern joining these two predicates, it will use the ExtVP table instead of joining the two VP tables at query time.
drop_extvp / list_extvp
pg_ripple.drop_extvp(name TEXT) → BOOLEAN
pg_ripple.list_extvp() → JSONB
SELECT pg_ripple.list_extvp();
Example output:
[
{
"name": "name_knows",
"pred1_iri": "<http://xmlns.com/foaf/0.1/name>",
"pred2_iri": "<http://xmlns.com/foaf/0.1/knows>",
"pred1_id": 5632187461234,
"pred2_id": 5632187461245,
"schedule": "10s",
"stream_table_name": "pg_ripple.name_knows"
}
]
Catalog tables
| Table | Description |
|---|---|
_pg_ripple.sparql_views | Name, original SPARQL, generated SQL, schedule, decode mode, stream table name, variables |
_pg_ripple.datalog_views | Name, rules, rule set, goal, generated SQL, schedule, decode mode, stream table name, variables |
_pg_ripple.extvp_tables | Name, predicate IRIs, predicate IDs, generated SQL, schedule, stream table name |
_pg_ripple.construct_views | Name, SPARQL, generated SQL, schedule, decode mode, template count, stream table name |
_pg_ripple.describe_views | Name, SPARQL, generated SQL, schedule, decode mode, CBD strategy, stream table name |
_pg_ripple.ask_views | Name, SPARQL, generated SQL, schedule, stream table name |
CONSTRUCT views (v0.18.0)
A CONSTRUCT view compiles a SPARQL CONSTRUCT query into a pg_trickle stream table with schema (s BIGINT, p BIGINT, o BIGINT, g BIGINT). Rows reflect the CONSTRUCT output at all times — inserting or deleting triples that affect the WHERE pattern causes the stream table to update automatically.
create_construct_view
pg_ripple.create_construct_view(
name TEXT,
sparql TEXT,
schedule TEXT DEFAULT '1s',
decode BOOLEAN DEFAULT false
) → BIGINT
Returns the number of template triples registered. The stream table pg_ripple.construct_view_{name} is created automatically. When decode = true, a companion view pg_ripple.construct_view_{name}_decoded(s TEXT, p TEXT, o TEXT, g BIGINT) is also created.
Error conditions:
sparqlis not a CONSTRUCT query →"sparql must be a CONSTRUCT query"- Template contains an unbound variable → lists the unbound variables
- Template contains a blank node → advises replacement with IRIs or skolemisation
-- Materialise inferred type triples: everything that is a foaf:Person is also a foaf:Agent
SELECT pg_ripple.create_construct_view(
'inferred_agents',
'CONSTRUCT { ?person <http://www.w3.org/1999/02/22-rdf-syntax-ns#type>
<http://xmlns.com/foaf/0.1/Agent> }
WHERE { ?person <http://www.w3.org/1999/02/22-rdf-syntax-ns#type>
<http://xmlns.com/foaf/0.1/Person> }',
'5s'
);
-- The materialized triples are stored as BIGINT IDs.
SELECT * FROM pg_ripple.construct_view_inferred_agents LIMIT 5;
drop_construct_view
pg_ripple.drop_construct_view(name TEXT) → void
Drops the stream table and removes the catalog entry. Also drops the _decoded view if present.
list_construct_views
pg_ripple.list_construct_views() → JSONB
Returns a JSONB array of all registered CONSTRUCT views.
[
{
"name": "inferred_agents",
"sparql": "CONSTRUCT { ... } WHERE { ... }",
"schedule": "5s",
"decode": false,
"template_count": 1,
"stream_table": "pg_ripple.construct_view_inferred_agents",
"created_at": "2026-04-16T10:00:00Z"
}
]
DESCRIBE views (v0.18.0)
A DESCRIBE view compiles a SPARQL DESCRIBE query into a pg_trickle stream table with schema (s BIGINT, p BIGINT, o BIGINT, g BIGINT), materialising the Concise Bounded Description (CBD) of the described resources.
The pg_ripple.describe_strategy GUC is respected: cbd (outgoing arcs only, default) or scbd (symmetric — outgoing + incoming arcs).
create_describe_view
pg_ripple.create_describe_view(
name TEXT,
sparql TEXT,
schedule TEXT DEFAULT '1s',
decode BOOLEAN DEFAULT false
) → void
The stream table pg_ripple.describe_view_{name} is created automatically. When decode = true, a companion _decoded view is also created.
-- Materialise all triples about people with a given name
SELECT pg_ripple.create_describe_view(
'named_people',
'DESCRIBE ?person WHERE {
?person <http://xmlns.com/foaf/0.1/name> "Alice"
}',
'10s'
);
SELECT * FROM pg_ripple.describe_view_named_people;
drop_describe_view
pg_ripple.drop_describe_view(name TEXT) → void
list_describe_views
pg_ripple.list_describe_views() → JSONB
ASK views (v0.18.0)
An ASK view compiles a SPARQL ASK query into a single-row stream table with schema (result BOOLEAN, evaluated_at TIMESTAMPTZ). The result column flips whenever the underlying pattern's satisfiability changes — useful for live constraint monitors and dashboard indicators.
create_ask_view
pg_ripple.create_ask_view(
name TEXT,
sparql TEXT,
schedule TEXT DEFAULT '1s'
) → void
The stream table pg_ripple.ask_view_{name} is created automatically.
-- Monitor whether any person lacks a name (constraint violation indicator)
SELECT pg_ripple.create_ask_view(
'person_missing_name',
'ASK { ?person <http://www.w3.org/1999/02/22-rdf-syntax-ns#type>
<http://xmlns.com/foaf/0.1/Person> .
FILTER NOT EXISTS { ?person <http://xmlns.com/foaf/0.1/name> ?name } }',
'5s'
);
-- Check the live result.
SELECT result, evaluated_at FROM pg_ripple.ask_view_person_missing_name;
-- result | evaluated_at
-- --------+---------------------------
-- f | 2026-04-16 10:05:00+00
drop_ask_view
pg_ripple.drop_ask_view(name TEXT) → void
list_ask_views
pg_ripple.list_ask_views() → JSONB
When to use views
| Use case | Recommendation |
|---|---|
| Dashboard with a few key metrics | SPARQL view with decode = true, schedule '5s' |
| Incremental RDFS/OWL materialization | Datalog view from built-in rule set |
| Star-pattern heavy workload | ExtVP on the top 5–10 predicate pairs |
| Ad-hoc exploration | Use sparql() directly — no view needed |
| Write-heavy with rare reads | Avoid views (refresh cost outweighs read savings) |
Framing Views (v0.17.0)
Framing views combine JSON-LD Framing with pg_trickle to create live, incrementally-maintained stream tables. A framing view translates your frame into a SPARQL CONSTRUCT query once and registers it with pg_trickle; whenever triples are inserted or deleted, only the VP tables referenced by the frame are rescanned.
Requires pg_trickle. Call
pg_ripple.pg_trickle_available()to check. All functions raise a descriptive error at call time when pg_trickle is absent; extension load never fails.
create_framing_view
pg_ripple.create_framing_view(
name TEXT,
frame JSONB,
schedule TEXT DEFAULT '5s',
decode BOOLEAN DEFAULT FALSE,
output_format TEXT DEFAULT 'jsonld'
) RETURNS void
Creates a pg_trickle stream table pg_ripple.framing_view_{name} with the schema:
subject_id BIGINT -- dictionary-encoded subject IRI
frame_tree JSONB -- fully embedded + compacted JSON-LD for this root node
refreshed_at TIMESTAMPTZ
When decode = TRUE, a companion view pg_ripple.framing_view_{name}_decoded is also created. It decodes subject_id to a human-readable IRI string.
-- Create a live company directory refreshed every 10 seconds.
SELECT pg_ripple.create_framing_view(
'companies',
'{
"@context": {"schema": "https://schema.org/"},
"@type": "https://schema.org/Organization",
"https://schema.org/name": {},
"https://schema.org/employee": {
"https://schema.org/name": {}
}
}'::jsonb,
'10s',
TRUE
);
-- Query it like a regular table.
SELECT subject_id, frame_tree FROM pg_ripple.framing_view_companies;
drop_framing_view
pg_ripple.drop_framing_view(name TEXT) RETURNS BOOLEAN
Drops the stream table, its optional decode view, and the catalog entry.
SELECT pg_ripple.drop_framing_view('companies');
list_framing_views
pg_ripple.list_framing_views() RETURNS JSONB
Returns a JSONB array of all registered framing views, ordered by creation time. Each entry includes name, frame, schedule, output_format, decode, and created_at.
SELECT pg_ripple.list_framing_views();
Refresh Mode Selection
Choose the refresh mode based on your use case:
| Refresh mode | When to use |
|---|---|
IMMEDIATE | Constraint-style frames: any matched node is a violation (e.g. companies lacking a compliance officer). Fires within the same transaction as the DML. |
DIFFERENTIAL + schedule | Dashboard / API use cases: only changed subjects are reprocessed. Suitable for a company directory refreshed every 10 s. |
FULL + long schedule | Large full-graph framed exports for data warehouses. Safe for deep nesting or @always embedding. |
Decode Option
The decode = TRUE option creates a thin view that calls pg_ripple.decode_iri(subject_id) to expose the subject IRI as a human-readable string. The stream table itself stores integer IDs to minimise change data capture (CDC) surface.
-- Query the decoded view (requires decode = TRUE at creation time).
SELECT subject_iri, frame_tree FROM pg_ripple.framing_view_companies_decoded;
Catalog Table
All framing views are recorded in _pg_ripple.framing_views:
| Column | Type | Description |
|---|---|---|
name | TEXT | View name (primary key) |
frame | JSONB | Original frame document |
generated_construct | TEXT | SPARQL CONSTRUCT string used by pg_trickle |
schedule | TEXT | pg_trickle refresh schedule |
output_format | TEXT | jsonld, ndjson, or turtle |
decode | BOOLEAN | Whether the decode view was created |
created_at | TIMESTAMPTZ | Creation timestamp |
pg_trickle Dependency
create_framing_view() and drop_framing_view() check for pg_trickle at call time. If absent, they raise:
ERROR: pg_trickle is required for framing views — install pg_trickle and add it to
shared_preload_libraries, then retry
Extension load never fails due to a missing pg_trickle. See pg_trickle for installation instructions.
Serialization & Export
pg_ripple (v0.9.0) supports exporting RDF data to Turtle, JSON-LD, N-Triples, and N-Quads formats, and importing from RDF/XML. SPARQL CONSTRUCT and DESCRIBE queries can also return results directly in Turtle or JSON-LD.
Import
load_rdfxml
pg_ripple.load_rdfxml(data TEXT) RETURNS BIGINT
Parses RDF/XML data from a string and stores all triples in the default graph. Returns the number of triples loaded.
RDF/XML is the original W3C-standard RDF serialization and is produced by many ontology editors such as Protégé.
SELECT pg_ripple.load_rdfxml('<?xml version="1.0"?>
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:ex="https://example.org/">
<rdf:Description rdf:about="https://example.org/alice">
<ex:name>Alice</ex:name>
<ex:knows rdf:resource="https://example.org/bob"/>
</rdf:Description>
</rdf:RDF>');
-- Returns 2
Note: RDF/XML does not support named graphs; all triples are loaded into the default graph.
Export
export_turtle
pg_ripple.export_turtle(graph TEXT DEFAULT NULL) RETURNS TEXT
Exports triples as a Turtle document. Triples are grouped by subject and emitted as compact Turtle blocks. All prefix declarations from the prefix registry are included as @prefix lines.
RDF-star quoted triples are serialized in Turtle-star << s p o >> notation.
-- Export the default graph
SELECT pg_ripple.export_turtle();
-- Export a named graph
SELECT pg_ripple.export_turtle('https://example.org/my-graph');
Example output:
@prefix ex: <https://example.org/> .
<https://example.org/alice>
<https://example.org/knows> <https://example.org/bob> ;
<https://example.org/name> "Alice" .
export_jsonld
pg_ripple.export_jsonld(graph TEXT DEFAULT NULL) RETURNS JSONB
Exports triples as a JSON-LD expanded-form document. Each subject becomes one array entry with all its predicates and objects.
SELECT pg_ripple.export_jsonld();
-- Returns: [{"@id": "https://example.org/alice", "https://example.org/name": [{"@value": "Alice"}], ...}]
JSON-LD is well-suited for use in REST APIs and Linked Data Platform (LDP) contexts.
export_ntriples
pg_ripple.export_ntriples(graph TEXT DEFAULT NULL) RETURNS TEXT
Exports triples as N-Triples text (one triple per line).
export_nquads
pg_ripple.export_nquads(graph TEXT DEFAULT NULL) RETURNS TEXT
Exports quads as N-Quads text. Pass NULL to export all graphs.
Streaming Export
For large graphs, use the streaming variants that return SETOF TEXT — one line per row. This avoids building the full document in memory.
export_turtle_stream
pg_ripple.export_turtle_stream(graph TEXT DEFAULT NULL) RETURNS SETOF TEXT
Yields @prefix declarations first, then one flat Turtle triple per line.
COPY (SELECT line FROM pg_ripple.export_turtle_stream()) TO '/tmp/output.ttl';
export_jsonld_stream
pg_ripple.export_jsonld_stream(graph TEXT DEFAULT NULL) RETURNS SETOF TEXT
Yields one NDJSON line per subject. Each line is a complete JSON object.
COPY (SELECT line FROM pg_ripple.export_jsonld_stream()) TO '/tmp/output.ndjson';
SPARQL CONSTRUCT & DESCRIBE Output Formats
By default, sparql_construct() and sparql_describe() return JSONB rows. The v0.9.0 format-specific variants return the same triples directly as Turtle or JSON-LD.
sparql_construct_turtle
pg_ripple.sparql_construct_turtle(query TEXT) RETURNS TEXT
Executes a SPARQL CONSTRUCT query and returns the result as a Turtle document. RDF-star quoted triples use Turtle-star << s p o >> notation.
SELECT pg_ripple.sparql_construct_turtle('
CONSTRUCT { ?s <https://schema.org/knows> ?o }
WHERE { ?s <https://schema.org/knows> ?o }
');
sparql_construct_jsonld
pg_ripple.sparql_construct_jsonld(query TEXT) RETURNS JSONB
Executes a SPARQL CONSTRUCT query and returns the result as a JSON-LD expanded-form array.
SELECT pg_ripple.sparql_construct_jsonld('
CONSTRUCT { ?s ?p ?o }
WHERE { ?s ?p ?o }
LIMIT 100
');
sparql_describe_turtle
pg_ripple.sparql_describe_turtle(query TEXT, strategy TEXT DEFAULT 'cbd') RETURNS TEXT
Executes a SPARQL DESCRIBE query and returns the description as Turtle text. strategy may be 'cbd' (Concise Bounded Description, default), 'scbd' (Symmetric CBD), or 'simple'.
sparql_describe_jsonld
pg_ripple.sparql_describe_jsonld(query TEXT, strategy TEXT DEFAULT 'cbd') RETURNS JSONB
Executes a SPARQL DESCRIBE query and returns the description as JSON-LD.
RDF-star Serialization
All export functions handle RDF-star quoted triples transparently:
- N-Triples / N-Quads: use
<< s p o >>notation (N-Triples-star / N-Quads-star) - Turtle: use
<< s p o >>notation (Turtle-star) - JSON-LD: quoted triples are represented as
{"@value": "<< s p o >>", "@type": "rdf:Statement"}
Format Guide
| Format | Import | Export | Named Graphs | RDF-star |
|---|---|---|---|---|
| N-Triples | load_ntriples | export_ntriples | No | Yes (N-Triples-star) |
| N-Quads | load_nquads | export_nquads | Yes | No |
| Turtle | load_turtle | export_turtle | No | Yes (Turtle-star) |
| TriG | load_trig | — | Yes | No |
| RDF/XML | load_rdfxml | — | No | No |
| JSON-LD | — | export_jsonld | No | Partial |
Tip: Use RDF/XML for Protégé ontologies, JSON-LD for REST APIs, and Turtle for human-readable files.
JSON-LD Framing (v0.17.0)
JSON-LD Framing lets you reshape RDF graph data into a specific tree structure suited for a REST API or application. Instead of returning a flat list of facts, you provide a frame — a JSON template — and pg_ripple returns a cleanly nested JSON-LD document.
export_jsonld_framed
pg_ripple.export_jsonld_framed(
frame JSONB,
graph TEXT DEFAULT NULL, -- NULL = merged graph; IRI = named graph
embed TEXT DEFAULT '@once', -- @once | @always | @never
explicit BOOLEAN DEFAULT FALSE, -- omit properties not in frame
ordered BOOLEAN DEFAULT FALSE -- sort output keys lexicographically
) RETURNS JSONB
Translates the frame to a SPARQL CONSTRUCT query, executes it, applies W3C embedding, compacts IRIs using the frame's @context, and returns the framed JSON-LD document.
-- Select all Person nodes with their names.
SELECT pg_ripple.export_jsonld_framed('{
"@context": {"schema": "https://schema.org/"},
"@type": "https://schema.org/Person",
"https://schema.org/name": {}
}'::jsonb);
jsonld_frame_to_sparql
pg_ripple.jsonld_frame_to_sparql(
frame JSONB,
graph TEXT DEFAULT NULL
) RETURNS TEXT
Translates a frame to its SPARQL CONSTRUCT query string without executing it. Use this to inspect or debug the generated query.
SELECT pg_ripple.jsonld_frame_to_sparql(
'{"@type": "https://schema.org/Person", "https://schema.org/name": {}}'::jsonb
);
jsonld_frame
pg_ripple.jsonld_frame(
input JSONB,
frame JSONB,
embed TEXT DEFAULT '@once',
explicit BOOLEAN DEFAULT FALSE,
ordered BOOLEAN DEFAULT FALSE
) RETURNS JSONB
General-purpose framing primitive: apply the W3C embedding algorithm to any already-expanded JSON-LD JSONB value, not necessarily from pg_ripple storage. Useful for framing SPARQL CONSTRUCT results obtained via other means.
export_jsonld_framed_stream
pg_ripple.export_jsonld_framed_stream(
frame JSONB,
graph TEXT DEFAULT NULL
) RETURNS SETOF TEXT
Streaming variant: returns one NDJSON line per matched root node, avoiding buffering large documents in memory.
-- Stream one line per matched Company.
SELECT line FROM pg_ripple.export_jsonld_framed_stream(
'{"@type": "https://example.org/Company"}'::jsonb
);
Frame Syntax Primer
A frame is a JSON object whose structure mirrors the desired output shape:
| Frame construct | Meaning |
|---|---|
"@type": "ex:Foo" | Select nodes whose RDF type is ex:Foo |
"ex:prop": {} | Include ex:prop; match any value (wildcard) |
"ex:prop": [] | Select nodes that lack ex:prop |
"ex:prop": { ... nested frame ... } | Embed the referenced node recursively |
"@reverse": { "ex:memberOf": {} } | Collect subjects whose ex:memberOf points here |
"@id": "http://ex.org/Alice" | Restrict to a specific subject IRI |
"@requireAll": true | All listed properties are mandatory (no OPTIONAL joins) |
@embed / @explicit / @omitDefault / @requireAll Flags
| Flag | Default | Description |
|---|---|---|
@embed | @once | @once — embed each node once, use {"@id":"..."} reference for repeats; @always — always embed; @never — always use references |
@explicit | false | When true, omit properties not listed in the frame from output nodes |
@omitDefault | false | When true, omit absent properties instead of substituting @default |
@requireAll | false | When true, convert OPTIONAL joins to INNER joins; only nodes with all listed properties match |
Named Graph Scoping
Pass graph to restrict the CONSTRUCT to a specific named graph:
SELECT pg_ripple.export_jsonld_framed(
'{"@type": "https://schema.org/Person"}'::jsonb,
'https://example.org/graph1'
);
Supported Frame Features
| Feature | Supported |
|---|---|
@type matching (single or array) | ✓ |
@id matching (single or array) | ✓ |
Property wildcard {} | ✓ |
Absent-property pattern [] | ✓ |
@reverse properties | ✓ |
@embed: @once / @always / @never | ✓ |
@explicit inclusion flag | ✓ |
@omitDefault flag | ✓ |
@default values | ✓ |
@requireAll flag | ✓ |
@context compaction | ✓ |
Named graph @graph scoping | ✓ |
Value pattern matching (@value / @language / @type in value objects) | ✗ (deferred) |
SPARQL Protocol (HTTP Endpoint)
pg_ripple v0.15.0 ships with a companion HTTP service (pg_ripple_http) that implements the W3C SPARQL 1.1 Protocol. Any standard SPARQL client — YASGUI, Protégé, SPARQLWrapper, Jena, or plain curl — can query pg_ripple without driver-specific configuration.
Architecture
pg_ripple_http is a standalone Rust binary built with axum and tokio. It connects to PostgreSQL via deadpool-postgres, translates HTTP requests into calls to pg_ripple.sparql(), sparql_ask(), sparql_construct(), sparql_describe(), and sparql_update(), then formats the results according to the requested content type.
┌────────────┐ HTTP ┌──────────────────┐ SQL/SPI ┌────────────┐
│ Client │ ──────────► │ pg_ripple_http │ ────────────► │ PostgreSQL │
│ (YASGUI, │ ◄────────── │ (axum + tokio) │ ◄──────────── │ + pg_ripple│
│ curl, …) │ JSON/XML └──────────────────┘ └────────────┘
└────────────┘
Quick start with Docker Compose
The easiest way to run both PostgreSQL and the HTTP endpoint:
docker compose up
This starts two containers:
| Service | Port | Description |
|---|---|---|
postgres | 5432 | PostgreSQL 18 with pg_ripple installed |
sparql | 7878 | HTTP endpoint for SPARQL queries |
Once running:
# Health check
curl http://localhost:7878/health
# Run a SPARQL query
curl -G http://localhost:7878/sparql \
--data-urlencode "query=SELECT ?s ?p ?o WHERE { ?s ?p ?o } LIMIT 10"
Endpoints
GET /sparql
Query via URL parameter. For simple queries and browser-based tools.
GET /sparql?query=SELECT+?s+?p+?o+WHERE+{+?s+?p+?o+}+LIMIT+10
POST /sparql
Query via request body. Supports two content types:
| Content-Type | Body format |
|---|---|
application/sparql-query | Raw SPARQL query text |
application/x-www-form-urlencoded | query=... (URL-encoded) |
# Raw SPARQL body
curl -X POST http://localhost:7878/sparql \
-H "Content-Type: application/sparql-query" \
-d "SELECT ?s ?p ?o WHERE { ?s ?p ?o } LIMIT 10"
# Form-encoded
curl -X POST http://localhost:7878/sparql \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "query=SELECT+?s+?p+?o+WHERE+{+?s+?p+?o+}+LIMIT+10"
POST /sparql (Update)
SPARQL Update operations use Content-Type: application/sparql-update:
curl -X POST http://localhost:7878/sparql \
-H "Content-Type: application/sparql-update" \
-d "INSERT DATA { <http://example.org/alice> <http://example.org/name> \"Alice\" }"
GET /health
Returns 200 OK when the service is ready. Use for load balancer health checks.
GET /metrics
Prometheus-compatible metrics:
| Metric | Description |
|---|---|
pg_ripple_query_count | Total SPARQL queries processed |
pg_ripple_error_count | Total query errors |
pg_ripple_query_duration_seconds_total | Cumulative query execution time |
curl http://localhost:7878/metrics
Content negotiation
Set the Accept header to choose the response format:
| Accept header | Format | Suitable for |
|---|---|---|
application/sparql-results+json | SPARQL Results JSON | JavaScript apps, YASGUI |
application/sparql-results+xml | SPARQL Results XML | Java/Jena clients |
text/csv | CSV | Spreadsheets, pandas |
text/tab-separated-values | TSV | CLI pipelines |
text/turtle | Turtle | CONSTRUCT/DESCRIBE results |
application/n-triples | N-Triples | Streaming pipelines |
application/ld+json | JSON-LD | REST APIs, Linked Data Platform |
If no Accept header is set, the default is application/sparql-results+json for SELECT/ASK and text/turtle for CONSTRUCT/DESCRIBE.
# Get results as CSV
curl -G http://localhost:7878/sparql \
-H "Accept: text/csv" \
--data-urlencode "query=SELECT ?s ?p ?o WHERE { ?s ?p ?o } LIMIT 5"
# Get CONSTRUCT results as JSON-LD
curl -G http://localhost:7878/sparql \
-H "Accept: application/ld+json" \
--data-urlencode "query=CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o } LIMIT 5"
Authentication
pg_ripple_http supports Bearer token and HTTP Basic authentication. Configure via environment variables:
| Variable | Description |
|---|---|
PG_RIPPLE_HTTP_AUTH_TOKEN | If set, all requests must include Authorization: Bearer <token> or Authorization: Basic <token> |
When not set, authentication is disabled.
# Start with bearer token auth
PG_RIPPLE_HTTP_AUTH_TOKEN=my-secret-token ./pg_ripple_http
# Query with auth
curl -G http://localhost:7878/sparql \
-H "Authorization: Bearer my-secret-token" \
--data-urlencode "query=SELECT * WHERE { ?s ?p ?o } LIMIT 5"
CORS
Cross-Origin Resource Sharing headers are enabled by default via tower-http, allowing browser-based tools like YASGUI to query the endpoint directly.
Configuration
All configuration is via environment variables:
| Variable | Default | Description |
|---|---|---|
PG_RIPPLE_HTTP_PG_URL | postgresql://localhost/postgres | Full PostgreSQL connection URL |
PG_RIPPLE_HTTP_PORT | 7878 | HTTP listening port |
PG_RIPPLE_HTTP_POOL_SIZE | 16 | Maximum connections in the pool |
PG_RIPPLE_HTTP_AUTH_TOKEN | (unset) | Bearer/Basic auth token |
PG_RIPPLE_HTTP_RATE_LIMIT | 0 | Max requests/sec per client IP (0 = disabled) |
PG_RIPPLE_HTTP_CORS_ORIGINS | * | Comma-separated allowed CORS origins |
Example:
export PG_RIPPLE_HTTP_PG_URL="postgresql://user:password@db-host:5432/mydb"
export PG_RIPPLE_HTTP_PORT=8080
export PG_RIPPLE_HTTP_POOL_SIZE=32
export PG_RIPPLE_HTTP_AUTH_TOKEN="my-secret-token"
./pg_ripple_http
Client examples
Python (SPARQLWrapper)
from SPARQLWrapper import SPARQLWrapper, JSON
sparql = SPARQLWrapper("http://localhost:7878/sparql")
sparql.setQuery("SELECT ?s ?p ?o WHERE { ?s ?p ?o } LIMIT 10")
sparql.setReturnFormat(JSON)
results = sparql.query().convert()
for row in results["results"]["bindings"]:
print(row["s"]["value"], row["p"]["value"], row["o"]["value"])
JavaScript (fetch)
const response = await fetch('http://localhost:7878/sparql', {
method: 'POST',
headers: {
'Content-Type': 'application/sparql-query',
'Accept': 'application/sparql-results+json'
},
body: 'SELECT ?s ?p ?o WHERE { ?s ?p ?o } LIMIT 10'
});
const data = await response.json();
curl
# SELECT
curl -G http://localhost:7878/sparql \
--data-urlencode "query=SELECT ?s WHERE { ?s a <http://xmlns.com/foaf/0.1/Person> }"
# ASK
curl -G http://localhost:7878/sparql \
--data-urlencode "query=ASK { <http://example.org/alice> ?p ?o }"
# CONSTRUCT as Turtle
curl -G http://localhost:7878/sparql \
-H "Accept: text/turtle" \
--data-urlencode "query=CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o } LIMIT 100"
Apache Jena (Java)
QueryExecution qe = QueryExecution.service("http://localhost:7878/sparql")
.query("SELECT ?s ?p ?o WHERE { ?s ?p ?o } LIMIT 10")
.build();
ResultSet rs = qe.execSelect();
ResultSetFormatter.out(rs);
Dictionary
The dictionary maps every IRI, blank node, and literal to a BIGINT ID using XXH3-128 hashing. VP tables store only BIGINT values — no raw strings appear in data tables.
These functions are for advanced users who need to interact with the dictionary directly. Most applications should use the higher-level SPARQL and CRUD functions instead.
encode_term
pg_ripple.encode_term(term TEXT) RETURNS BIGINT
Encodes an IRI, blank node, or literal (in N-Triples notation) and returns its dictionary ID. If the term is not yet in the dictionary it is inserted.
SELECT pg_ripple.encode_term('<https://example.org/alice>');
-- Returns the BIGINT ID
SELECT pg_ripple.encode_term('"Alice"@en');
-- Returns the BIGINT ID for the language-tagged literal
decode_id
pg_ripple.decode_id(id BIGINT) RETURNS TEXT
Looks up a dictionary ID and returns the original term in N-Triples notation.
SELECT pg_ripple.decode_id(1);
-- Returns: '<https://example.org/alice>'
decode_id_full (v0.15.0)
pg_ripple.decode_id_full(id BIGINT) RETURNS JSONB
Returns a structured JSONB object with detailed type information. More informative than decode_id() for debugging and introspection.
| Field | Description |
|---|---|
kind | Term type: "iri", "bnode", "literal", "default_graph", "quoted_triple" |
value | The term value (IRI string, literal text, blank node label) |
datatype | XSD datatype IRI (for typed literals) or null |
language | Language tag (for language-tagged literals) or null |
SELECT pg_ripple.decode_id_full(42);
-- {"kind": "literal", "value": "Alice", "datatype": null, "language": "en"}
SELECT pg_ripple.decode_id_full(1);
-- {"kind": "iri", "value": "https://example.org/alice", "datatype": null, "language": null}
lookup_iri (v0.15.0)
pg_ripple.lookup_iri(iri TEXT) RETURNS BIGINT
Checks whether an IRI exists in the dictionary and returns its ID. Returns NULL if the IRI has never been encoded. Unlike encode_term(), this never inserts — it is a read-only lookup.
SELECT pg_ripple.lookup_iri('<https://example.org/alice>');
-- Returns: 1 (or NULL if not in the dictionary)
Useful for checking whether a resource exists before querying:
DO $$
BEGIN
IF pg_ripple.lookup_iri('<https://example.org/alice>') IS NOT NULL THEN
RAISE NOTICE 'Alice exists in the store';
END IF;
END $$;
Internal table: _pg_ripple.dictionary
TABLE _pg_ripple.dictionary (
id BIGINT PRIMARY KEY,
kind SMALLINT NOT NULL, -- 1=IRI, 2=BNode, 3=Literal, 4=Default, 5=QuotedTriple
value TEXT NOT NULL,
hash_hi BIGINT,
hash_lo BIGINT,
qt_s BIGINT, -- for kind=5: subject component
qt_p BIGINT, -- for kind=5: predicate component
qt_o BIGINT -- for kind=5: object component
)
The kind column encodes the term type:
| Kind | Meaning |
|---|---|
| 1 | IRI |
| 2 | Blank node |
| 3 | Literal |
| 4 | Default graph (ID 0) |
| 5 | Quoted triple (RDF-star) |
Prefix Registry
The prefix registry maps short prefixes (e.g. ex) to IRI expansions (e.g. https://example.org/). Registered prefixes are used by the export functions when serializing triples to Turtle or N-Triples.
SPARQL queries do not use the prefix registry. SPARQL queries must use either full IRIs in angle brackets (
<https://…>) or declare prefixes inline withPREFIX ex: <https://…>.
register_prefix
pg_ripple.register_prefix(prefix TEXT, expansion TEXT) RETURNS VOID
Registers or replaces a prefix–expansion mapping. Idempotent.
SELECT pg_ripple.register_prefix('ex', 'https://example.org/');
SELECT pg_ripple.register_prefix('rdf', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#');
SELECT pg_ripple.register_prefix('xsd', 'http://www.w3.org/2001/XMLSchema#');
prefixes
pg_ripple.prefixes() RETURNS TABLE(prefix TEXT, expansion TEXT)
Returns all registered prefix–expansion mappings.
SELECT * FROM pg_ripple.prefixes();
Example: prefixes in export
After registering ex: <https://example.org/>, export functions will use the short form:
SELECT pg_ripple.register_prefix('ex', 'https://example.org/');
SELECT pg_ripple.export_ntriples();
-- Output uses <https://example.org/…> fully qualified
-- Turtle output uses ex:… short form when available
Administration Functions
pg_ripple v0.6.0 introduced a set of administration and monitoring functions in the pg_ripple schema for HTAP maintenance, change data capture, and statistics.
compact()
pg_ripple.compact() → bigint
Triggers a synchronous merge of all HTAP delta tables into their corresponding main tables. Blocks until the merge is complete.
Returns: the total number of rows now in all main tables (after merge).
Use cases:
- After a large bulk load, call
compact()to flush delta to main before starting read-heavy queries - In maintenance windows to pre-emptively reduce delta size
- In tests to simulate a completed merge cycle
SELECT pg_ripple.compact();
-- 1500000
Note: For background (non-blocking) merges, rely on the merge worker instead.
compact()is a foreground operation and holds an exclusive lock during the table swap.
stats()
pg_ripple.stats() → jsonb
Returns a JSONB object with extension-wide statistics. Fields:
| Field | Type | Description |
|---|---|---|
total_triples | integer | Total triples across all VP tables and vp_rare |
dedicated_predicates | integer | Number of predicates with their own VP table |
htap_predicates | integer | Number of predicates using the delta/main split |
rare_triples | integer | Triples stored in the shared vp_rare table |
unmerged_delta_rows | integer | Rows in all delta tables not yet merged — -1 if shared_preload_libraries is not set |
merge_worker_pid | integer | PID of the background merge worker — 0 if not running |
SELECT pg_ripple.stats();
-- {
-- "total_triples": 1500000,
-- "dedicated_predicates": 42,
-- "htap_predicates": 42,
-- "rare_triples": 1234,
-- "unmerged_delta_rows": 8742,
-- "merge_worker_pid": 12345
-- }
Monitor unmerged_delta_rows over time. If it grows without bound, the merge worker may be blocked or misconfigured.
htap_migrate_predicate(pred_id)
pg_ripple.htap_migrate_predicate(pred_id bigint) → void
Migrates an existing flat VP table (created before v0.6.0) to the delta/main partition split. Called automatically by the pg_ripple--0.5.1--0.6.0.sql migration script.
Parameters: pred_id — the dictionary integer ID of the predicate.
-- Find the predicate ID first
SELECT id FROM _pg_ripple.predicates p
JOIN _pg_ripple.dictionary d ON d.id = p.id
WHERE d.value = 'https://schema.org/name';
-- Then migrate
SELECT pg_ripple.htap_migrate_predicate(12345678);
subscribe(pattern, channel)
pg_ripple.subscribe(pattern text, channel text) → bigint
Registers a CDC (Change Data Capture) subscription. Fires a pg_notify on channel whenever a triple matching pattern is inserted or deleted in a VP delta table.
Parameters:
pattern— predicate IRI (e.g.'<https://schema.org/name>') or'*'to subscribe to all predicateschannel— name of the PostgreSQL NOTIFY channel to send notifications to
Returns: the subscription ID (integer).
-- Subscribe to all changes on schema:name predicate
SELECT pg_ripple.subscribe('<https://schema.org/name>', 'name_changes');
-- In another session, listen for notifications
LISTEN name_changes;
-- Insert a triple to trigger the notification
SELECT pg_ripple.insert_triple(
'<https://example.org/Alice>',
'<https://schema.org/name>',
'"Alice"'
);
-- NOTIFY name_changes, '{"op":"INSERT","s":...,"p":...,"o":...}'
Notification payload is a JSON object with fields op ("INSERT" or "DELETE"), s, p, o (N-Triples encoded), and g (graph ID).
unsubscribe(channel)
pg_ripple.unsubscribe(channel text) → bigint
Removes all CDC subscriptions for a given channel.
Returns: the number of subscriptions removed.
SELECT pg_ripple.unsubscribe('name_changes');
-- 1
subject_predicates(subject_id) / object_predicates(object_id)
pg_ripple.subject_predicates(subject_id bigint) → bigint[]
pg_ripple.object_predicates(object_id bigint) → bigint[]
Return the sorted array of predicate IDs for which the given subject (or object) has at least one triple. Backed by the _pg_ripple.subject_patterns and _pg_ripple.object_patterns indexes populated by the merge worker.
Returns NULL if the subject/object has not been indexed yet (before the first merge).
-- Find all predicates used by Alice
SELECT pg_ripple.subject_predicates(
pg_ripple.encode_term('https://example.org/Alice', 0)
);
predicate_stats (view)
SELECT * FROM pg_ripple.predicate_stats;
A convenience view over _pg_ripple.predicates and _pg_ripple.dictionary:
| Column | Description |
|---|---|
predicate_iri | Full IRI of the predicate |
triple_count | Total triples (across delta + main) |
storage | 'dedicated' (own VP table) or 'rare' (vp_rare) |
-- Top 10 predicates by triple count
SELECT predicate_iri, triple_count, storage
FROM pg_ripple.predicate_stats
ORDER BY triple_count DESC
LIMIT 10;
deduplicate_predicate(p_iri TEXT) → BIGINT (v0.7.0)
Remove duplicate (s, o, g) rows for a single predicate, keeping the row with the lowest SID (oldest assertion). Returns the count of rows removed.
- Delta tables (
vp_{id}_delta): duplicate rows are physically deleted — the minimum-SID row per group is kept. - Main tables (
vp_{id}_main): tombstone rows are inserted for all but the minimum-SID duplicate, masking duplicates from queries immediately; they are physically removed on the next merge cycle. - vp_rare: duplicate rows are physically deleted (minimum SID kept).
- ANALYZE is run on all modified tables after deduplication.
-- Remove duplicates for a specific predicate
SELECT pg_ripple.deduplicate_predicate('<https://schema.org/name>');
-- Returns: number of rows removed
Typical usage: call once after a bulk load that may contain duplicate triples.
vacuum() → bigint (v0.14.0)
pg_ripple.vacuum() → bigint
Forces a full delta→main merge on all HTAP VP tables, then runs ANALYZE on every VP table (delta, main, tombstones) and vp_rare.
Returns: the number of VP table groups analyzed.
SELECT pg_ripple.vacuum();
-- 42
Note:
ANALYZEupdates planner statistics. PostgreSQL'sVACUUMitself cannot run inside a transaction block; call it separately if you need dead-tuple reclamation.
Lock levels acquired (ADMIN-LOCK-01, v0.82.0):
ANALYZEacquires a briefShareUpdateExclusiveLockon each VP table. Concurrent reads and writes are not blocked.- The delta→main merge acquires a
SET LOCAL lock_timeout(configurable viapg_ripple.merge_lock_timeout_ms, default 5 s) before taking aShareRowExclusiveLockon the VP table during the final swap.
reindex() → bigint (v0.14.0)
pg_ripple.reindex() → bigint
Rebuilds all indices on every VP table (delta and main) and vp_rare using REINDEX TABLE. Run this after large bulk deletes or to recover from index corruption.
Returns: the number of VP table groups reindexed.
SELECT pg_ripple.reindex();
-- 42
Lock levels acquired (ADMIN-LOCK-01, v0.82.0):
REINDEX TABLEacquires anAccessExclusiveLockon each VP table for the duration of the rebuild. All concurrent reads and writes on that table are blocked until the reindex completes.- To minimise impact,
reindex()processes one VP table at a time. On databases with many predicates, consider running during a maintenance window.
vacuum_dictionary() → bigint (v0.14.0)
pg_ripple.vacuum_dictionary() → bigint
Removes dictionary entries that are no longer referenced by any VP table. Orphaned entries accumulate after bulk deletes.
Uses an advisory transaction lock (key 0x7269706c) to prevent concurrent runs. Safe to run during normal operation — may miss very recently orphaned entries, which are cleaned on the next run.
Returns: the number of dictionary entries removed.
SELECT pg_ripple.vacuum_dictionary();
-- 128
Typical usage: run periodically after bulk deletes, or after drop_graph().
dictionary_stats() → jsonb (v0.14.0)
pg_ripple.dictionary_stats() → jsonb
Returns detailed metrics about the dictionary and cache configuration.
| Field | Description |
|---|---|
total_entries | Total rows in the dictionary |
hot_entries | Rows in the unlogged hot dictionary cache |
cache_capacity | Shared-memory encode cache capacity (entries) |
cache_budget_mb | Configured cache budget cap in MB |
shmem_ready | Whether shared memory is initialized |
SELECT pg_ripple.dictionary_stats();
-- {
-- "total_entries": 450000,
-- "hot_entries": 1024,
-- "cache_capacity": 4096,
-- "cache_budget_mb": 64,
-- "shmem_ready": true
-- }
enable_graph_rls() → boolean (v0.14.0)
pg_ripple.enable_graph_rls() → boolean
Activates Row-Level Security policies on _pg_ripple.vp_rare using the g column and the _pg_ripple.graph_access mapping table. Default graph (g = 0) is always accessible. Named graphs require an explicit grant.
Returns true on success.
SELECT pg_ripple.enable_graph_rls();
-- true
grant_graph(role, graph, permission) (v0.14.0)
pg_ripple.grant_graph(role text, graph text, permission text) → void
Grants permission ('read', 'write', or 'admin') on a named graph to a PostgreSQL role.
SELECT pg_ripple.grant_graph('app_user', '<https://example.org/graph1>', 'read');
SELECT pg_ripple.grant_graph('admin_user', '<https://example.org/graph1>', 'admin');
Note:
grant_graph_permission(role, graph, permission)is a legacy alias forgrant_graph(), retained for compatibility. Usegrant_graph()in new code.
revoke_graph(role, graph [, permission]) (v0.14.0)
pg_ripple.revoke_graph(role text, graph text, permission text DEFAULT NULL) → void
Revokes a permission on a named graph from a role. Pass NULL (or omit) for permission to revoke all permissions for that role on that graph.
-- Revoke specific permission
SELECT pg_ripple.revoke_graph('app_user', '<https://example.org/graph1>', 'read');
-- Revoke all permissions
SELECT pg_ripple.revoke_graph('app_user', '<https://example.org/graph1>');
Note:
revoke_graph_permission(role, graph, permission)is a legacy alias forrevoke_graph(), retained for compatibility. Userevoke_graph()in new code.
list_graph_access() → jsonb (v0.14.0)
pg_ripple.list_graph_access() → jsonb
Returns all graph access control entries as a JSONB array. Each element has role, graph (decoded IRI), and permission.
SELECT * FROM jsonb_array_elements(pg_ripple.list_graph_access());
schema_summary() → jsonb (v0.14.0)
pg_ripple.schema_summary() → jsonb
Returns a live class→property→cardinality summary as a JSONB array. When enable_schema_summary() has been called (requires pg_trickle), reads from the materialized _pg_ripple.inferred_schema stream table.
Each element: {"class": "...", "property": "...", "cardinality": N}.
SELECT * FROM jsonb_array_elements(pg_ripple.schema_summary());
enable_schema_summary() → boolean (v0.14.0)
pg_ripple.enable_schema_summary() → boolean
Creates _pg_ripple.inferred_schema as a pg_trickle stream table (refreshed every 30 s) for SPARQL IDE auto-completion. Requires pg_trickle. Returns false with a warning if pg_trickle is not installed.
SELECT pg_ripple.enable_schema_summary();
-- true (or false with warning if pg_trickle missing)
deduplicate_all() → bigint (v0.7.0)
pg_ripple.deduplicate_all() → bigint
Removes duplicate (s, o, g) rows across all predicates, keeping the row with the lowest SID. Returns the total number of duplicate rows removed.
SELECT pg_ripple.deduplicate_all();
dedup_on_merge (GUC)
| GUC | Type | Default | Description |
|---|---|---|---|
pg_ripple.dedup_on_merge | boolean | off | When on, the HTAP merge worker deduplicates (s, o, g) rows using DISTINCT ON during compaction, keeping the lowest-SID row |
-- Enable merge-time dedup
SET pg_ripple.dedup_on_merge = true;
-- Trigger a merge (deduplication happens atomically during compaction)
SELECT pg_ripple.compact();
Between merges, the (main EXCEPT tombstones) UNION ALL delta query view may observe short-lived duplicates. This is harmless for most workloads.
plan_cache_stats() → jsonb (v0.13.0)
pg_ripple.plan_cache_stats() → jsonb
Returns statistics about the SPARQL plan cache as a JSONB object. Use this to monitor cache effectiveness and tune pg_ripple.plan_cache_size.
| Field | Description |
|---|---|
hits | Number of cache hits since startup |
misses | Number of cache misses (recompilations) |
size | Current number of cached plans |
capacity | Maximum cache capacity |
SELECT pg_ripple.plan_cache_stats();
-- {"hits": 1523, "misses": 42, "size": 38, "capacity": 128}
A high miss rate (> 50%) suggests either too many distinct query shapes or too small a cache. Try increasing pg_ripple.plan_cache_size or parameterizing queries with VALUES blocks.
plan_cache_reset() → void (v0.13.0)
pg_ripple.plan_cache_reset() → void
Evicts all cached SPARQL→SQL plans and resets the hit/miss counters. Useful after schema changes, VP promotions, or when switching pg_ripple.bgp_reorder on/off.
SELECT pg_ripple.plan_cache_reset();
promote_rare_predicates() → bigint (v0.2.0)
pg_ripple.promote_rare_predicates() → bigint
Scans _pg_ripple.vp_rare for predicates whose triple count has exceeded pg_ripple.vp_promotion_threshold and promotes each to a dedicated VP table. Returns the number of predicates promoted.
SELECT pg_ripple.promote_rare_predicates();
-- 3
Promotion is normally automatic during inserts. Use this after changing the threshold or after a bulk load where auto-promotion was deferred.
_pg_ripple.merge_worker_status table (D13-02, v0.86.0)
Internal monitoring table maintained by the background merge worker.
| Column | Type | Description |
|---|---|---|
pid | INTEGER | PID of the last merge cycle's background worker process |
last_merge_at | TIMESTAMPTZ | Wall-clock time of the most recent successful merge cycle |
last_merge_duration_ms | BIGINT | Duration of the last merge cycle in milliseconds |
last_merge_rows | BIGINT | Number of delta rows promoted during the last merge cycle |
total_merge_cycles | BIGINT | Cumulative merge cycle count since the worker started |
status | TEXT | Current worker state: idle, merging, or error:<msg> |
Query example:
SELECT * FROM _pg_ripple.merge_worker_status;
The pg_ripple_merge_worker_delta_rows_pending Prometheus metric (added in v0.86.0) shows
the count of unmerged delta rows across all VP tables, updated at each merge cycle.
Explain API
pg_ripple exposes two explain_sparql overloads and explain_datalog for introspecting query plans.
explain_sparql(query, format) — text output (v0.23.0)
Returns a human-readable or structured text representation of a SPARQL query plan.
pg_ripple.explain_sparql(query TEXT, format TEXT DEFAULT 'text') RETURNS TEXT
Format options:
| Format | Description |
|---|---|
'text' (default) | Runs EXPLAIN on the generated SQL and returns the plan as text |
'json' | Runs EXPLAIN FORMAT JSON and returns the plan as JSON text |
'sql' | Returns the generated SQL without running EXPLAIN |
'sparql_algebra' | Returns the parsed SPARQL algebra tree (Debug format) |
Example:
SELECT pg_ripple.explain_sparql(
'SELECT ?s ?p ?o WHERE { ?s ?p ?o }',
'sql'
);
explain_sparql(query, analyze) — JSONB output (v0.40.0)
Returns a machine-readable JSONB document containing the full explain pipeline.
pg_ripple.explain_sparql(query TEXT, analyze BOOLEAN DEFAULT false) RETURNS JSONB
Return structure:
{
"algebra": "<spargebra debug output>",
"sql": "<generated SQL>",
"plan": "<EXPLAIN [ANALYZE] output>",
"cache_hit": true,
"encode_calls": 0
}
| Field | Type | Description |
|---|---|---|
algebra | string | Parsed SPARQL algebra tree in Rust Debug format |
sql | string | Generated SQL that will be executed |
plan | string | PostgreSQL EXPLAIN [ANALYZE] output |
cache_hit | boolean | Whether the compiled plan was served from the plan cache |
encode_calls | number | Dictionary encoder invocations (0 when using plan cache) |
When analyze = true, EXPLAIN ANALYZE is run and the plan includes actual timing.
Example:
SELECT pg_ripple.explain_sparql(
'SELECT ?name WHERE { ?s <https://schema.org/name> ?name }',
false
) ->> 'sql';
explain_datalog(rule_set_name) — JSONB output (v0.40.0)
Returns a JSONB introspection document for a named Datalog rule set.
pg_ripple.explain_datalog(rule_set_name TEXT) RETURNS JSONB
Return structure:
{
"strata": [["rule1", "rule2"], ["rule3"]],
"rules": ["head(?x, ?y) :- body(?x, ?y) ."],
"sql_per_rule": ["INSERT INTO ... SELECT ..."],
"last_run_stats": [{"rule_set": "...", "derived_count": 42, "elapsed_ms": 3}]
}
| Field | Type | Description |
|---|---|---|
strata | array of arrays | Stratification result — each inner array is one stratum |
rules | array of strings | Rule text as stored in _pg_ripple.rules |
sql_per_rule | array of strings | Compiled SQL for each rule in the same order |
last_run_stats | array of objects | Statistics from the most recent infer() run (from _pg_ripple.inference_stats) |
Returns {"strata": [], "rules": [], "sql_per_rule": [], "last_run_stats": []} when the rule set does not exist.
Example:
SELECT jsonb_pretty(pg_ripple.explain_datalog('my_rules'));
See also
- Streaming Cursor API — stream large result sets
- Observability — tracing and cache statistics
- GUC Reference —
pg_ripple.sparql_plan_cache,pg_ripple.rule_plan_cache
Architecture
This page describes the internal architecture of pg_ripple as of v0.60.0.
Overview
pg_ripple is a PostgreSQL 18 extension written in Rust (pgrx 0.18) that
implements a high-performance RDF triple store with native SPARQL query
execution. All user-visible functions live in the pg_ripple schema; internal
tables and VP (Vertical Partitioning) tables live in the _pg_ripple schema.
Component map
graph TD
Client["SQL client / SPARQL tool"]
HTTP["pg_ripple_http\n(Axum companion service)\n/health · /ready · /metrics"]
RAG["RAG / LLM pipeline\nllm/mod.rs + rag_cache"]
subgraph Extension["pg_ripple extension (Rust + pgrx 0.18)"]
API["SQL API layer\nlib.rs + *_api.rs"]
SPARQL["SPARQL engine\nsparql/mod.rs"]
SPARQLDL["SPARQL-DL engine\nsparql/sparqldl.rs"]
QLRW["OWL 2 QL rewrite\nsparql/ql_rewrite.rs"]
TRANS["Algebra → SQL\nsparql/translate/\nfilter_dispatch + filter_expr"]
SQLGEN["SQL generator\nsparql/sqlgen.rs"]
DICT["Dictionary\ndictionary/mod.rs\n(XXH3-128 + LRU cache)"]
SHACL["SHACL validator\nshacl/mod.rs\n(Core + SPARQL constraints)"]
DL["Datalog engine\ndatalog/\nseminaive + coordinator\nmagic_sets + demand"]
EXP["Serialisers\nexport/mod.rs\n(Turtle/JSON-LD/N-Triples)"]
STOR["Storage layer\nstorage/mod.rs\n(HTAP delta/main/tombstones)"]
MERGE["Merge worker\nstorage/merge.rs\n(atomic rename-swap, v0.60.0)"]
CAT["Predicate catalog\nstorage/catalog.rs"]
HINTS["SHACL hints\nshacl/hints.rs"]
FED["Federation planner\nsparql/federation.rs\n(cost-based, parallel)"]
CDC["CDC bridge\nstorage/cdc_bridge.rs"]
CITUS["Citus integration\nsrc/citus.rs\n(shard-pruning, rebalance)"]
TENANT["Multi-tenant isolation\nsrc/tenant.rs"]
KGE["KG embeddings\nsrc/kge.rs"]
TEMPORAL["Temporal RDF\nsrc/temporal.rs"]
GUCS["GUC subsystem\ngucs/{sparql, datalog, shacl,\nfederation, llm, storage,\nobservability}.rs"]
end
subgraph PG["PostgreSQL 18"]
VP["VP tables\n_pg_ripple.vp_{id}_delta\n_pg_ripple.vp_{id}_main\n_pg_ripple.vp_{id}_tombstones"]
DICT_TBL["dictionary table\n_pg_ripple.dictionary"]
PRED["predicates table\n_pg_ripple.predicates"]
RARE["rare predicates\n_pg_ripple.vp_rare"]
SHAPE_HINTS["shape_hints table\n_pg_ripple.shape_hints"]
RAG_CACHE["rag_cache\n_pg_ripple.rag_cache"]
CDC_SUBS["cdc_subscriptions\n_pg_ripple.cdc_subscriptions"]
end
Client --> API
HTTP --> API
HTTP --> RAG
RAG --> RAG_CACHE
API --> SPARQL
API --> SPARQLDL
API --> SHACL
API --> DL
API --> EXP
API --> FED
API --> CDC
API --> CITUS
API --> TENANT
API --> KGE
API --> TEMPORAL
SPARQL --> TRANS
SPARQLDL --> SPARQL
QLRW --> SPARQL
TRANS --> SQLGEN
SQLGEN --> CAT
CAT --> HINTS
HINTS --> SHAPE_HINTS
SQLGEN --> DICT
DICT --> DICT_TBL
SPARQL --> VP
SHACL --> VP
DL --> VP
STOR --> VP
MERGE --> STOR
MERGE --> CDC_SUBS
CDC --> CDC_SUBS
VP --> PRED
VP --> RARE
FED --> HTTP
GUCS -.->|"configures"| SPARQL
GUCS -.->|"configures"| DL
GUCS -.->|"configures"| FED
GUCS -.->|"configures"| MERGE
DICT --> DICT_TBL
SQLGEN --> VP
CAT --> PRED
STOR --> VP
STOR --> RARE
SHACL --> DICT
DL --> STOR
## Source tree structure
| Path | Responsibility |
|------|---------------|
| `src/lib.rs` | pgrx entry points, GUC registration, `_PG_init`, hooks |
| `src/gucs.rs` | All GUC `static` declarations |
| `src/schema.rs` | `extension_sql!()` DDL blocks |
| `src/dictionary/` | IRI / blank-node / literal → `i64` encoder (XXH3-128 + LRU) |
| `src/storage/` | VP table I/O, HTAP delta/main partitions, merge worker |
| `src/storage/catalog.rs` | Predicate → VP table OID cache (SPI call reduction) |
| `src/sparql/` | SPARQL text → algebra → SQL → SPI → decode |
| `src/sparql/translate/` | Per-algebra-node translation stubs (BGP, Join, Filter, …) |
| `src/sparql/plan_cache.rs` | Per-backend plan cache keyed on algebra digest (XXH3-128) |
| `src/datalog/` | Datalog rule parser, stratifier, SQL compiler |
| `src/shacl/` | SHACL shapes → validation pipeline |
| `src/shacl/constraints/` | Per-constraint-type validation (count, string, logical, …) |
| `src/shacl/hints.rs` | SHACL → SQL generation hints (join type, DISTINCT) |
| `src/export/` | Turtle / N-Triples / JSON-LD serialisation |
| `src/federation_registry.rs` | SPARQL federation endpoint registry |
| `src/stats_admin.rs` | Monitoring, pg_stat_statements integration |
| `src/graphrag_admin.rs` | Vector embedding, hybrid search, GraphRAG pipeline |
| `src/*_api.rs` | SQL-exposed pg_extern wrappers |
## Storage model
Every IRI, blank node, and literal is mapped to a `BIGINT` (i64) through a
dictionary encoding step (XXH3-128 hash). VP tables **never** contain raw
strings — all joins are integer joins.
raw IRI/literal
│
dictionary.encode()
│
i64 hash
│
stored in VP table (s, o, g, i, source)
For each unique predicate there is one VP table: `_pg_ripple.vp_{predicate_id}`.
Predicates with fewer than `vp_promotion_threshold` (default: 1 000) triples are
stored in the consolidated `_pg_ripple.vp_rare` table instead.
## Query execution pipeline
SPARQL text │ ▼ spargebra::Query::parse() SPARQL algebra │ ▼ sparopt optimizer (BGP reorder, join order) Optimised algebra │ ▼ SPARQL→SQL translator (sqlgen.rs + translate/) SQL text │ ▼ PostgreSQL SPI executor Raw rows │ ▼ dictionary.decode() JSONB result set
## Plan cache
The per-backend plan cache (v0.13.0) maps an **algebra digest** to the
generated SQL. The digest is computed as:
digest = XXH3-128( spargebra::Query::display(query) ) key = "{digest}\x00max_depth={n}\x00bgp_reorder={b}"
Using the algebra display form (rather than the raw query text) means
whitespace and prefix-alias variants of the same query share one cache slot.
## SHACL hints integration
After loading shapes with `pg_ripple.load_shacl()`, predicate-level hints are
written to `_pg_ripple.shape_hints`. The SQL generator reads these hints
via the predicate catalog to:
- Omit `DISTINCT` when `sh:maxCount 1` is set for a predicate.
- Use `INNER JOIN` instead of `LEFT JOIN` when `sh:minCount 1` is set.
Hints are invalidated automatically when shapes are dropped
(`pg_ripple.invalidate_catalog_cache()`).
Architecture: Subsystem Dependency Graph
This document describes the major subsystems of pg_ripple and their dependencies. It was introduced in v0.114.0 as part of the module-decomposition effort (A16).
Subsystem Map
┌─────────────────────────────────────────────────────────────────────┐
│ Query Layer │
│ src/sparql/ src/sparql/wcoj/ │
│ ├─ parse.rs ├─ mod.rs (coordinator) │
│ ├─ plan.rs ├─ executor.rs │
│ ├─ execute.rs ├─ trie.rs │
│ └─ decode.rs └─ leapfrog.rs │
│ │
│ src/sparql/embedding/ │
│ ├─ mod.rs │
│ ├─ index.rs (API client, pgvector) │
│ ├─ hybrid.rs (hybrid SPARQL+vector search) │
│ └─ rag.rs (RAG retrieval) │
└──────────────────────────────────┬──────────────────────────────────┘
│ uses
┌──────────────────────────────────▼──────────────────────────────────┐
│ Inference Layer │
│ │
│ src/datalog/ src/datalog_api/ src/skos/ │
│ (rule engine, ├─ parse.rs ├─ mod.rs │
│ stratifier, ├─ validate.rs ├─ bundle.rs │
│ SQL compiler, ├─ explain.rs ├─ inference.rs │
│ RDFS/OWL RL) └─ conflict.rs ├─ broader_narrower.rs │
│ └─ export.rs │
│ │
│ src/shacl/ src/shacl/validator/ │
│ (shapes→DDL, ├─ mod.rs │
│ async pipeline) ├─ node.rs │
│ ├─ property.rs │
│ ├─ sparql.rs │
│ └─ severity.rs │
└──────────────────────────────────┬──────────────────────────────────┘
│ uses
┌──────────────────────────────────▼──────────────────────────────────┐
│ Storage Layer │
│ │
│ src/storage/ src/dictionary/ src/citus/ │
│ (VP tables, (IRI/BNode/Lit → ├─ mod.rs (detection) │
│ HTAP delta/main, i64 via XXH3-128) ├─ shard_pruning.rs │
│ merge worker) ├─ ddl_hooks.rs │
│ ├─ query_rewriting.rs │
│ └─ rebalance.rs │
└─────────────────────────────────────────────────────────────────────┘
Key Dependency Relationships
| Subsystem | Depends On | Notes |
|---|---|---|
SKOS (src/skos/) | Datalog (src/datalog/) | Bundle loading injects Datalog rules for SKOS closure |
| OWL RL | Datalog (src/datalog/) | OWL 2 RL entailment rules compiled to Datalog strata |
| NS-RL (neuro-symbolic) | sparql/embedding/ + Datalog + datalog_api/conflict | Combines vector similarity with rule-based inference |
| Conflict detection | SHACL (src/shacl/) + Datalog | Lattice-based conflict checks use both shape validation and Datalog constraints |
| Hypothetical inference | Storage (src/storage/) | Creates temporary VP table snapshots for what-if queries |
Views (src/views/) | SPARQL + Datalog + Storage | CONSTRUCT/DESCRIBE/ASK views wrap SPARQL queries as SQL views |
Citus (src/citus/) | Storage | Shard pruning reads VP predicate catalog and VP table OIDs |
PageRank (src/pagerank/) | Datalog + Storage | Datalog-native PageRank with IVM |
Module Size Policy
Each .rs file is bounded at 1,500 LOC (CI hard failure) with a 1,200 LOC advisory warning.
The gate runs via scripts/check_module_sizes.sh on every PR.
When a module grows beyond the limit, decompose it into a src/<module>/ directory:
src/mymodule.rs # before (>1500 LOC)
↓
src/mymodule/mod.rs # coordinator + public re-exports (< 400 LOC)
src/mymodule/submodule_a.rs # focused sub-module (< 400 LOC)
src/mymodule/submodule_b.rs # focused sub-module (< 400 LOC)
Reference implementations: src/datalog/, src/sparql/, src/views/, src/skos/.
GUC Reference
All pg_ripple configuration parameters are set with ALTER SYSTEM SET, SET (session-level), or in postgresql.conf. Reload with SELECT pg_reload_conf() after ALTER SYSTEM.
General Parameters
pg_ripple.max_path_depth
| Type | Integer |
| Default | 10 |
| Range | 1–100 |
Maximum recursion depth for SPARQL property paths (*, +). Increase for deeply nested graphs; lower for tighter resource bounds.
pg_ripple.property_path_max_depth (deprecated)
| Type | Integer |
| Default | 64 |
| Range | 1–100 000 |
| Status | Deprecated since v0.38.0 — use max_path_depth instead |
Legacy alias for max_path_depth. Setting this GUC still works but emits a
deprecation notice. It will be removed in a future major release.
pg_ripple.federation_timeout
| Type | Integer (milliseconds) |
| Default | 5000 |
Timeout for outbound SPARQL federation requests.
pg_ripple.export_batch_size
| Type | Integer |
| Default | 1000 |
Number of rows fetched per page when a SPARQL cursor streams results back to the
caller. This controls Rust-side peak memory: at most export_batch_size result
rows are decoded from dictionary IDs and held in memory simultaneously before
being forwarded. Increase for higher throughput at the cost of more memory;
decrease for tighter memory budgets.
See also: arrow_batch_size (Arrow IPC batching) operates independently —
the two GUCs govern different output paths and can be tuned separately.
pg_ripple.arrow_batch_size
| Type | Integer |
| Default | 1000 |
| Minimum | 1 |
Number of rows packed into each Arrow IPC RecordBatch during Arrow Flight
bulk export. A larger value produces fewer IPC frames and lower per-frame
overhead; a smaller value allows consumers to begin processing results sooner.
Interaction with export_batch_size: arrow_batch_size controls the IPC
batch granularity inside the Arrow Flight response body. export_batch_size
controls how many SPARQL cursor rows are fetched per page from PostgreSQL. The
two parameters operate on independent code paths and can be tuned separately.
-- Tune for high-bandwidth Arrow bulk export
SET pg_ripple.arrow_batch_size = 5000;
pg_ripple.vp_promotion_batch_size
| Type | Integer |
| Default | 10000 |
| Minimum | 100 |
Batch size for the COPY-phase of VP table promotion: when a rare-predicate
triple count exceeds vp_promotion_threshold, the promotion worker copies rows
from _pg_ripple.vp_rare into the new dedicated VP table in batches of this
size. Larger batches reduce promotion time at the cost of a larger WAL record
per batch; smaller batches reduce WAL pressure and allow other transactions to
proceed between batches.
Interaction with the other batch-size GUCs: VP promotion runs in the background worker and is unrelated to SPARQL cursor streaming or Arrow IPC export. Changing this GUC affects only the promotion throughput, not query performance.
-- Reduce WAL pressure during a large promotion
SET pg_ripple.vp_promotion_batch_size = 1000;
Embedding / Vector Parameters (v0.27.0+)
These GUCs control the pgvector integration introduced in v0.27.0. All embedding functions degrade gracefully when pgvector is absent.
pg_ripple.pgvector_enabled
| Type | Boolean |
| Default | on |
Master switch for all vector embedding paths. Set to off to disable embedding storage, similarity search, and SPARQL pg:similar() without uninstalling pgvector. Useful for temporarily disabling the feature.
-- Disable at session level for a bulk load
SET pg_ripple.pgvector_enabled = off;
pg_ripple.embedding_api_url
| Type | String |
| Default | (none) |
Base URL for the OpenAI-compatible embeddings API. The extension appends /embeddings to this URL when making requests.
ALTER SYSTEM SET pg_ripple.embedding_api_url = 'https://api.openai.com/v1';
-- For Ollama (local):
ALTER SYSTEM SET pg_ripple.embedding_api_url = 'http://localhost:11434/v1';
pg_ripple.embedding_api_key
| Type | String |
| Default | (none) |
Bearer token sent as Authorization: Bearer <key> in embedding API requests. For local models that don't require authentication, set to any non-empty string (e.g., 'local').
Security: Avoid storing API keys in
postgresql.conf. UseALTER SYSTEMand restrictpg_hba.confaccess, or inject the key via a session-levelSETin application code.
pg_ripple.embedding_model
| Type | String |
| Default | (none) |
Model name passed in the "model" field of embedding API requests.
ALTER SYSTEM SET pg_ripple.embedding_model = 'text-embedding-3-small';
-- or for Ollama:
ALTER SYSTEM SET pg_ripple.embedding_model = 'nomic-embed-text';
pg_ripple.embedding_dimensions
| Type | Integer |
| Default | 1536 |
| Range | 1–65535 |
Expected output dimensions from the embedding model. Must match the model's output length. Common values:
| Model | Dimensions |
|---|---|
text-embedding-3-small | 1536 |
text-embedding-3-large | 3072 |
text-embedding-ada-002 | 1536 |
nomic-embed-text (Ollama) | 768 |
pg_ripple.embedding_index_type
| Type | String |
| Default | (none — HNSW when pgvector present) |
| Values | hnsw, ivfflat |
Index type for the _pg_ripple.embeddings table. HNSW is the default and recommended for most workloads. IVFFlat uses less memory but requires lists parameter tuning.
pg_ripple.embedding_precision
| Type | String |
| Default | (none — full float4 precision) |
| Values | (unset), half, binary |
Storage precision for embedding vectors. Reduces disk/memory usage at the cost of accuracy:
| Value | pgvector type | Notes |
|---|---|---|
| (unset) | vector(N) | Full 32-bit float; highest accuracy |
half | halfvec(N) | 16-bit float; ~50% storage reduction |
binary | bit(N) | 1-bit quantised; ~97% storage reduction, lower accuracy |
Note: Changing precision after data is stored requires re-running the migration or manually altering the column type and re-embedding.
v0.37.0: Tombstone GC & Error Safety
pg_ripple.tombstone_gc_enabled
| Type | Boolean |
| Default | on |
| Context | sighup (shared: requires server signal, not per-session) |
When on, pg_ripple automatically issues VACUUM ANALYZE on a predicate's tombstone table after each merge cycle if the residual tombstone count exceeds tombstone_gc_threshold × main_row_count. Set to off to disable automatic tombstone cleanup (useful when managing VACUUM manually).
pg_ripple.tombstone_gc_threshold
| Type | String (decimal) |
| Default | 0.05 (5%) |
| Range | 0.0 – 1.0 |
| Context | sighup |
Tombstone-to-main-row ratio that triggers automatic VACUUM after a merge cycle. When the remaining tombstone count divided by the new main table row count exceeds this value, a VACUUM ANALYZE is scheduled on the tombstone table.
Lower values (e.g. 0.01) trigger VACUUM more aggressively; higher values (e.g. 0.20) allow more tombstone bloat before cleanup.
v0.37.0: GUC Validator Rules
The following string-enum GUCs now reject invalid values at SET time with an error. Previously, invalid values were silently ignored until the execution path checked them.
| GUC | Valid values |
|---|---|
pg_ripple.inference_mode | off, on_demand, materialized |
pg_ripple.enforce_constraints | off, warn, error |
pg_ripple.rule_graph_scope | default, all |
pg_ripple.shacl_mode | off, sync, async |
pg_ripple.describe_strategy | cbd, scbd, simple |
pg_ripple.rls_bypass scope change (v0.37.0): This GUC is now registered at PGC_POSTMASTER scope when pg_ripple is loaded via shared_preload_libraries. This prevents a session from bypassing graph-level RLS with SET LOCAL pg_ripple.rls_bypass = on.
v0.42.0: Parallel Merge Workers
pg_ripple.merge_workers
| Type | Integer |
| Default | 1 |
| Range | 1 – 16 |
| Context | postmaster (startup-only; set in postgresql.conf) |
Number of background merge worker processes. Each worker owns a disjoint round-robin slice of VP predicates. Workers use pg_advisory_lock to prevent conflicts; idle workers steal work from overloaded peers. Increasing this value helps workloads with many distinct predicates (> 50).
v0.42.0: Cost-Based Federation Planner
pg_ripple.federation_planner_enabled
| Type | Boolean |
| Default | on |
| Context | userset |
When on, pg_ripple uses VoID statistics collected from remote SPARQL endpoints to sort the SERVICE execution order by ascending estimated cost. When off, SERVICE clauses are executed in document order.
pg_ripple.federation_stats_ttl_secs
| Type | Integer |
| Default | 3600 (1 hour) |
| Range | 0 – 86400 |
| Context | userset |
Seconds until cached VoID statistics for a remote endpoint are considered stale. Setting 0 disables caching (re-fetches on every query).
pg_ripple.federation_parallel_max
| Type | Integer |
| Default | 4 |
| Range | 1 – 64 |
| Context | userset |
Maximum number of remote SERVICE clauses that pg_ripple will execute concurrently within a single query. Set to 1 to disable parallel SERVICE execution.
pg_ripple.federation_parallel_timeout
| Type | Integer |
| Default | 60 (seconds) |
| Range | 1 – 3600 |
| Context | userset |
Per-endpoint timeout when executing parallel SERVICE clauses. Endpoints that do not respond within this limit return an empty result set (with a WARNING). Does not affect sequential SERVICE execution.
pg_ripple.federation_inline_max_rows
| Type | Integer |
| Default | 10000 |
| Range | 1 – 1000000 |
| Context | userset |
Maximum number of rows in the VALUES binding table passed to a remote SERVICE clause. When the result set from the local graph exceeds this limit, pg_ripple automatically spools the bindings into a temporary table (PT620 INFO logged) and issues multiple smaller requests to the remote endpoint in batches. Set to a lower value if remote endpoints enforce query complexity limits.
pg_ripple.federation_allow_private
| Type | Boolean |
| Default | off |
| Context | superuser |
Security-critical GUC — only superusers can set this.
When off (the default), register_endpoint() rejects endpoints whose hostname resolves to a loopback address (127.0.0.0/8), a link-local address (169.254.0.0/16), any RFC-1918 private range (10/8, 172.16/12, 192.168/16), or an IPv6 equivalent. This prevents server-side request forgery (SSRF) via malicious SPARQL SERVICE calls.
Set to on only in controlled environments where the remote endpoint is a trusted internal service (e.g., a local Fuseki instance in a Docker network).
v0.42.0: owl:sameAs Safety
pg_ripple.sameas_max_cluster_size
| Type | Integer |
| Default | 100000 |
| Range | 0 – 2147483647 |
| Context | userset |
Maximum number of entities in a single owl:sameAs equivalence cluster before canonicalization is skipped with a PT550 WARNING. A single cluster larger than this limit is usually a data quality problem (e.g., a mistakenly asserted owl:sameAs owl:Thing). Set to 0 to disable the check (no limit).
v0.46.0: TopN Push-down & Datalog Sequence Batch
pg_ripple.topn_pushdown
| Type | Boolean |
| Default | on |
| Context | userset |
When on (default), SPARQL SELECT queries that contain both ORDER BY and LIMIT N (with no OFFSET > 0 and no DISTINCT) emit the SQL as … ORDER BY … LIMIT N rather than fetching all rows and discarding after decoding.
Set to off to disable the optimisation globally — for example, during debugging when you suspect that TopN push-down is producing incorrect results.
The sparql_explain() output includes a "topn_applied": true/false key that indicates whether push-down was applied to a specific query.
pg_ripple.datalog_sequence_batch
| Type | Integer |
| Default | 10000 |
| Range | 100 – 1000000 |
| Context | userset |
SID (statement-ID) range reserved per parallel Datalog worker per batch. Before launching N parallel strata workers, the coordinator atomically advances the global _pg_ripple.statement_id_seq sequence by N * datalog_sequence_batch, then assigns each worker an exclusive sub-range. Workers insert triples with pre-computed SIDs without touching the shared sequence, eliminating contention.
Increase this value if parallel inference workers frequently conflict on the sequence. Decrease it to reduce unused SID gaps when inference produces fewer triples than expected per batch.
v0.48.0 GUCs
pg_ripple.federation_max_response_bytes
| Type | Integer |
| Default | 104857600 (100 MiB) |
| Range | -1 (disabled) – 2147483647 |
| Context | userset |
Maximum allowed size in bytes for a federation (SERVICE) response body. When a
remote SPARQL endpoint returns a JSON response larger than this value, pg_ripple
raises error code PT543 and aborts the query. Set to -1 to disable the
limit (not recommended for deployments with untrusted federation endpoints).
-- Allow up to 500 MiB responses for a single query
SET pg_ripple.federation_max_response_bytes = 524288000;
-- Disable the limit (trusted internal network only)
SET pg_ripple.federation_max_response_bytes = -1;
v0.47.0: Validated String GUCs
All six string-valued GUCs below now reject invalid values at SET time (previously invalid values were accepted and silently ignored at runtime).
pg_ripple.federation_on_error
| Type | String |
| Default | warning |
| Valid values | warning, error, empty |
| Context | userset |
Controls behaviour when a SERVICE call fails completely. warning emits a
PT610 WARNING and returns an empty binding set for that endpoint. error
raises an ERROR and aborts the query. empty silently returns zero rows for
that endpoint.
pg_ripple.federation_on_partial
| Type | String |
| Default | empty |
| Valid values | empty, use |
| Context | userset |
Controls behaviour when a SERVICE response stream is interrupted mid-transfer
(e.g., the remote endpoint drops the connection). empty discards partial
results and returns zero rows. use keeps the rows received before the error.
pg_ripple.sparql_overflow_action
| Type | String |
| Default | warn |
| Valid values | warn, error |
| Context | userset |
Action taken when a SPARQL SELECT result set exceeds sparql_max_rowAction taken when a SPARQL> 0). warn truncates the result set and emits a PT601
WARNING. error raises an ERROR.
pg_ripple.tracing_exporter
| | |
|---|--|---|--|---|--|---|--|---|--|---|--|---|--|---|--|---|--|---|--|---t, otlp|---|--|---|--|---|--|---|--|---|--|---|--|---|--|---|--|---|--|---utwrit|---|--|---|--|---|--|---|--|---|--|---|--|---|--|---|--|---|--|---|--|-erhead). otlpsends spans via the OTLP gRPC protocol to the endpoint specivia tby theOTEL_EXPORTER_OTLP_ENDPOINT` environment variable.
pg_ripple.embedding_index_type
| Type | String |
| Default | hnsw |
| Valid values | `h |
ChanginC this setCing after embeddings have been indexedChanginC this setCiREINDEX TABLE _pg_ripple.embeddings. |
pg_ripple.embedding_precision
| Type | String |
| Default | single |
| Valid values | single, half, binary |
| Context | userset |
Storage precision for emStorage precision forngle uses pgvectorStorage precision for emStorage precision forngle uses pgvectorStorage precision for emStorage precision forngle uses pgvectorStorage precision for emStorage precision forngle uses pgvectorStorage precision for emStorage precision forngle uses pgbinary`.
AI & LLM Integration Parameters (v0.49.0)
pg_ripple.llm_endpoint
| Type | String |
| Default | '' (empty — disabled) |
| Context | userset |
Base URL for an OpenAI-compatible /v1/chat/completions API used by sparql_from_nl(). When empty, calling sparql_from_nl() immediately raises PT700. Set to 'mock' to use the built-in test mock without a real LLM. Examples: https://api.openai.com/v1, http://localhost:11434/v1 (Ollama).
Note (H16-04, v0.112.0): This GUC is a no-op within the pg_ripple extension itself. The HTTP companion
pg_ripple_httpreads this GUC (via the database connection) when serving the/rules/{id}/explainendpoint to handle LLM enrichment for Datalog rule explanations. Direct extension functions such assparql_from_nl()also respect this GUC. Do not set this GUC to a raw API key — usepg_ripple.llm_api_key_envfor secure key management.
pg_ripple.llm_model
| Type | String |
| Default | gpt-4o |
| Context | userset |
LLM model identifier passed in the model field of the chat completion request body. Supported values depend on the endpoint — e.g. gpt-4o, gpt-4-turbo, llama3, mistral.
pg_ripple.llm_api_key_env
| Type | String |
| Default | PG_RIPPLE_LLM_API_KEY |
| Context | userset |
Name of the environment variable from which sparql_from_nl() reads the Bearer API key at call time. The key is never stored in the database or visible in pg_settings.
pg_ripple.llm_include_shapes
| Type | Boolean |
| Default | on |
| Context | userset |
When on, the LLM prompt sent by sparql_from_nl() includes a summary of active SHACL shapes as additional schema context. Disable when shapes are very large or the LLM context window is limited.
v0.54.0 GUCs — High Availability & Logical Replication
pg_ripple.replication_enabled
| Type | Boolean |
| Default | off |
| Context | sighup |
When on, starts the logical_apply_worker background worker that subscribes
to the pg_ripple_pub publication and applies incoming N-Triples batches to
the local store. Requires wal_level = logical on the primary.
Requires a server restart (or SIGHUP) to take effect.
pg_ripple.replication_conflict_strategy
| Type | String |
| Default | last_writer_wins |
| Context | sighup |
Conflict resolution strategy used by the logical apply worker when an incoming
triple's (s, p, g) already exists in the local store with a different object
or SID.
Supported values:
| Value | Behaviour |
|---|---|
last_writer_wins | Keep the row with the highest Statement ID (SID). This is the default and matches eventual-consistency semantics. |
pg_ripple.strict_dictionary (D13-02, v0.86.0)
| Type | Boolean |
| Default | off |
| Context | userset |
When on, all dictionary lookups fail with a PT400 error rather than silently inserting
new dictionary entries for unknown IRIs and literals. This is useful for read-only replicas
and validation pipelines where unknown terms indicate a data-quality problem.
When off (default), unknown terms are inserted into the dictionary on first access (lazy
encoding). This is appropriate for most write workloads.
pg_ripple.plan_cache_capacity (D13-02, v0.86.0)
| Type | Integer |
| Default | 1024 |
| Min / Max | 1 / 32768 |
| Context | sighup |
Maximum number of compiled SPARQL query plans held in the in-process LRU plan cache. Each
entry stores the SQL string, projected variable list, and decoded type metadata. Cache hit
rate is visible via pg_ripple.explain_sparql(query, 'plan_cache_stats') or through the
pg_ripple_plan_cache_hit_ratio Prometheus metric.
pg_ripple.cdc_slot_cleanup_timeout_ms (D13-02, v0.86.0)
| Type | Integer |
| Default | 5000 |
| Min / Max | 100 / 300000 |
| Context | sighup |
Timeout in milliseconds for CDC replication slot cleanup during extension uninstall or when
the pg_ripple.cleanup_cdc_slot() function is called. If the slot is active (a subscriber
is connected), the cleanup will wait up to this many milliseconds before returning an error.
The crash-recovery test for this scenario is in tests/crash_recovery/cdc_slot_cleanup_during_kill.sh.
Datalog Reasoning Parameters
pg_ripple.inference_mode
| Type | Enum string |
| Default | 'on_demand' |
| Valid values | off, on_demand, materialized |
| Context | userset |
Controls when Datalog rules are evaluated.
| Value | Behaviour |
|---|---|
off | No inference is performed. infer() is a no-op. |
on_demand | Inference runs explicitly when infer() is called. |
materialized | Inference results are kept materialized and incrementally updated. |
pg_ripple.enforce_constraints
| Type | Enum string |
| Default | 'warn' |
| Valid values | off, warn, error |
| Context | userset |
Controls how Datalog constraint rules (:- body.) are enforced.
| Value | Behaviour |
|---|---|
off | Constraint violations are ignored. |
warn | Violations produce a PostgreSQL WARNING. |
error | Violations raise PT404 and abort the transaction. |
pg_ripple.rule_graph_scope
| Type | Enum string |
| Default | 'all' |
| Valid values | default, all |
| Context | userset |
Controls which graphs are searched when a Datalog rule body atom has no explicit graph annotation.
| Value | Behaviour |
|---|---|
default | Unscoped atoms match only the default graph (g = 0). |
all | Unscoped atoms match triples in any named graph. |
pg_ripple.magic_sets
| Type | Boolean |
| Default | on |
| Context | userset |
| Since | v0.29.0 |
Master switch for magic-set transformation — goal-directed inference that rewrites rules to avoid deriving irrelevant facts. Enables infer_goal() to perform targeted, demand-driven evaluation. Disable to debug incorrect magic-set rewrites.
pg_ripple.datalog_cost_reorder
| Type | Boolean |
| Default | on |
| Context | userset |
| Since | v0.29.0 |
When on, sorts Datalog rule body atoms by ascending estimated VP-table cardinality before SQL compilation. This places the most selective join first, reducing intermediate result sizes.
pg_ripple.datalog_antijoin_threshold
| Type | Integer |
| Default | 1000 |
| Context | userset |
| Since | v0.29.0 |
Minimum VP-table row count for negated body atoms (NOT EXISTS) to use an anti-join. Below this threshold, a correlated subquery is used instead.
pg_ripple.delta_index_threshold
| Type | Integer |
| Default | 500 |
| Context | userset |
| Since | v0.29.0 |
Minimum semi-naive delta temp-table row count before creating a B-tree index. For small delta sets, a sequential scan is faster.
pg_ripple.rule_plan_cache
| Type | Boolean |
| Default | on |
| Context | userset |
| Since | v0.30.0 |
Master switch for the Datalog rule plan cache. When on, compiled SQL for each rule set is cached in memory and reused across infer() calls, avoiding repeated compilation.
pg_ripple.rule_plan_cache_size
| Type | Integer |
| Default | 64 |
| Context | userset |
| Since | v0.30.0 |
Maximum number of rule sets whose compiled SQL is kept in the plan cache.
pg_ripple.sameas_reasoning
| Type | Boolean |
| Default | on |
| Context | userset |
| Since | v0.31.0 |
Master switch for owl:sameAs entity canonicalization. When on, entities linked by owl:sameAs are merged to their canonical representative before inference and query evaluation.
pg_ripple.demand_transform
| Type | Boolean |
| Default | on |
| Context | userset |
| Since | v0.31.0 |
Master switch for demand transformation — a technique that rewrites rules to propagate bindings from the query goal into the rule evaluation, reducing the search space.
pg_ripple.wfs_max_iterations
| Type | Integer |
| Default | 100 |
| Context | userset |
| Since | v0.32.0 |
Safety cap on alternating fixpoint rounds for well-founded semantics (infer_wfs()). When the cap is reached, a PT520 WARNING is emitted and the partial result is returned.
pg_ripple.tabling
| Type | Boolean |
| Default | on |
| Context | userset |
| Since | v0.32.0 |
Master switch for the subsumptive tabling cache. Tabling memoizes intermediate query results, eliminating redundant re-evaluation of identical sub-goals in recursive SPARQL and Datalog queries.
pg_ripple.tabling_ttl
| Type | Integer (seconds) |
| Default | 300 |
| Context | userset |
| Since | v0.32.0 |
Time-to-live in seconds for tabling cache entries. Entries are evicted after this interval. Set to 0 to keep entries indefinitely (until the session ends).
pg_ripple.datalog_max_depth
| Type | Integer |
| Default | 0 (unlimited) |
| Context | userset |
| Since | v0.34.0 |
Maximum depth for bounded-depth Datalog fixpoint termination. When 0, the fixpoint runs until convergence. For recursive rules on large graphs, setting a bound (e.g., 10) limits computation time.
pg_ripple.dred_enabled
| Type | Boolean |
| Default | on |
| Context | userset |
| Since | v0.34.0 |
Master switch for the Delete-Rederive (DRed) algorithm. When on, deleting a base triple re-derives only the minimal set of affected inferred triples rather than recomputing everything. Set to off to always use full recompute.
pg_ripple.dred_batch_size
| Type | Integer |
| Default | 1000 |
| Context | userset |
| Since | v0.34.0 |
Maximum number of deleted base triples processed per DRed transaction.
pg_ripple.datalog_parallel_workers
| Type | Integer |
| Default | 4 |
| Range | 1–64 |
| Context | userset |
| Since | v0.35.0 |
Maximum number of parallel background workers for Datalog stratum evaluation. Independent strata are distributed across workers for concurrent evaluation.
pg_ripple.datalog_parallel_threshold
| Type | Integer |
| Default | 10000 |
| Context | userset |
| Since | v0.35.0 |
Minimum estimated total row count across all rules in a stratum before parallel group analysis is applied. Small strata are evaluated serially.
pg_ripple.lattice_max_iterations
| Type | Integer |
| Default | 1000 |
| Context | userset |
| Since | v0.36.0 |
Maximum fixpoint iterations for lattice-based Datalog inference (infer_lattice()). See PT540 for convergence failures.
pg_ripple.datalog_max_derived
| Type | Integer |
| Default | 0 (unlimited) |
| Context | userset |
| Since | v0.40.0 |
Maximum derived facts produced by a single infer() call. When the limit is hit, inference stops and returns the partial result. Useful as a safety guard in development.
pg_ripple.owl_profile
| Type | Enum string |
| Default | 'RL' |
| Valid values | RL, EL, QL, off |
| Context | userset |
| Since | v0.57.0 |
Active OWL 2 reasoning profile. Selects the built-in OWL rule set loaded by load_rules_builtin('owl').
| Value | Profile |
|---|---|
RL | OWL 2 RL — full rule set, polynomial in the size of the data |
EL | OWL 2 EL — optimized for large class hierarchies (TBox-heavy ontologies) |
QL | OWL 2 QL — query rewriting mode, minimal materialization |
off | No built-in OWL rules loaded |
pg_ripple.probabilistic_datalog
| Type | Boolean |
| Default | off |
| Context | userset |
| Since | v0.57.0 |
Enable experimental probabilistic Datalog with rule confidence weights (@weight annotations). See Probabilistic Reasoning.
pg_ripple.datalog_citus_dispatch
| Type | Boolean |
| Default | off |
| Context | userset |
| Since | v0.62.0 |
When on, wraps Datalog stratum-iteration INSERT…SELECT statements in run_command_on_all_nodes() for distributed execution across Citus workers. Requires pg_ripple.citus_sharding_enabled = on.
pg_ripple.prob_datalog_cyclic
| Type | Boolean |
| Default | off |
| Context | userset |
| Since | v0.87.0 |
Allow probabilistic evaluation on cyclic rule sets. Cyclic probabilistic programs require approximate fixed-point evaluation; must be explicitly enabled.
pg_ripple.prob_datalog_max_iterations
| Type | Integer |
| Default | 100 |
| Context | userset |
| Since | v0.87.0 |
Maximum semi-naive inference rounds when prob_datalog_cyclic = on. After this limit, a WARNING is emitted and the partial result returned.
pg_ripple.prob_datalog_convergence_delta
| Type | Float |
| Default | 0.001 |
| Context | userset |
| Since | v0.87.0 |
Early-exit threshold for cyclic probabilistic Datalog. Iteration stops when the maximum confidence delta across all atoms falls below this value.
SPARQL Query Engine Parameters
pg_ripple.bgp_reorder
| Type | Boolean |
| Default | on |
| Context | userset |
| Since | v0.13.0 |
Enable BGP (Basic Graph Pattern) join reordering based on pg_stats selectivity estimates. The most selective triple patterns are placed first in the join order.
pg_ripple.parallel_query_min_joins
| Type | Integer |
| Default | 3 |
| Context | userset |
| Since | v0.13.0 |
Minimum number of VP-table joins in a SPARQL query before PostgreSQL parallel query workers are enabled for the generated SQL.
pg_ripple.sparql_strict
| Type | Boolean |
| Default | on |
| Context | userset |
| Since | v0.21.0 |
When on, raises ERRCODE_FEATURE_NOT_SUPPORTED for unsupported SPARQL built-in functions. When off, unsupported functions silently evaluate to UNDEF.
pg_ripple.wcoj_enabled
| Type | Boolean |
| Default | on |
| Context | userset |
| Since | v0.36.0 |
Master switch for Worst-Case Optimal Join (WCOJ / Leapfrog Triejoin) optimization. When on, cyclic join patterns (e.g., triangles, cliques) that exceed wcoj_min_tables are evaluated using the WCOJ executor rather than PostgreSQL's hash-join path.
pg_ripple.wcoj_min_tables
| Type | Integer |
| Default | 3 |
| Context | userset |
| Since | v0.36.0 |
Minimum number of VP-table joins before WCOJ analysis is applied.
pg_ripple.wcoj_min_cardinality
| Type | Integer |
| Default | 0 |
| Context | userset |
| Since | v0.79.0 |
Minimum VP-table cardinality before the WCOJ executor is used. Below this threshold, the standard SQL hash-join path is used instead.
pg_ripple.sparql_max_rows
| Type | Integer |
| Default | 0 (unlimited) |
| Context | userset |
| Since | v0.40.0 |
Maximum rows returned by a SPARQL SELECT or CONSTRUCT query. When exceeded, behaviour is controlled by sparql_overflow_action. Set to 0 for no limit.
pg_ripple.sparql_overflow_action
| Type | Enum string |
| Default | 'truncate' |
| Valid values | truncate, error |
| Context | userset |
| Since | v0.40.0 |
Action taken when sparql_max_rows is exceeded. truncate returns the first N rows with a PT640 notice; error raises PT640 as an error.
pg_ripple.sparql_max_algebra_depth
| Type | Integer |
| Default | 256 |
| Context | userset |
| Since | v0.51.0 |
Maximum allowed algebra tree depth for SPARQL queries. Queries exceeding this limit raise PT440.
pg_ripple.sparql_max_triple_patterns
| Type | Integer |
| Default | 4096 |
| Context | userset |
| Since | v0.51.0 |
Maximum number of triple patterns in a single SPARQL query. Queries exceeding this limit raise PT440.
pg_ripple.describe_strategy
| Type | Enum string |
| Default | 'cbd' |
| Valid values | cbd, scbd, simple |
| Context | userset |
Algorithm for SPARQL DESCRIBE queries.
| Value | Description |
|---|---|
cbd | Concise Bounded Description — all triples with the resource as subject, plus blank-node closures |
scbd | Symmetric CBD — adds triples where the resource is the object |
simple | Return only direct subject triples, no blank-node closure |
pg_ripple.strict_sparql_filters
| Type | Boolean |
| Default | off |
| Context | userset |
| Since | v0.81.0 |
When on, an unknown built-in function name in a FILTER expression raises ERROR (PT422) rather than evaluating to UNDEF.
pg_ripple.fuzzy_max_input_length
| Type | Integer |
| Default | 4096 |
| Range | 1–65536 |
| Context | userset |
| Since | v0.89.0 |
Maximum input string length for pg:fuzzy_match() and pg:token_set_ratio(). Arguments longer than this limit raise PT0308. Prevents algorithmic complexity attacks.
pg_ripple.all_nodes_predicate_limit
| Type | Integer |
| Default | 500 |
| Context | userset |
| Since | v0.82.0 |
Maximum number of predicates used in a wildcard property-path expansion (* or + with no explicit predicate). When the schema has more predicates, only the top-N by triple count are expanded.
Storage and HTAP Parameters
pg_ripple.vp_promotion_threshold (alias: vp_promotion_threshold)
| Type | Integer |
| Default | 1000 |
| Context | sighup |
Minimum triple count for a predicate before it gets a dedicated VP table. Predicates below this threshold are stored in the consolidated vp_rare table.
pg_ripple.merge_threshold
| Type | Integer |
| Default | 10000 |
| Context | sighup |
Minimum row count in a delta table before the merge worker triggers a merge cycle for that predicate.
pg_ripple.merge_interval_secs
| Type | Integer (seconds) |
| Default | 60 |
| Context | sighup |
Maximum seconds between merge worker polling intervals. Even if delta tables are below threshold, the merge worker checks for work at this frequency.
pg_ripple.merge_retention_seconds
| Type | Integer |
| Default | 60 |
| Context | sighup |
Seconds to keep the old main table after a merge before dropping it. Allows long-running reads against the old main to complete gracefully.
pg_ripple.latch_trigger_threshold
| Type | Integer |
| Default | 10000 |
| Context | sighup |
Number of triples written in one batch before poking (latching) the merge worker to wake up early.
pg_ripple.worker_database
| Type | String |
| Default | (none) |
| Context | postmaster |
Name of the database the background merge worker connects to. Must match the database where the pg_ripple extension is installed. Required when using shared_preload_libraries.
pg_ripple.dedup_on_merge
| Type | Boolean |
| Default | off |
| Context | sighup |
When on, the HTAP merge deduplicates (s, o, g) rows using DISTINCT ON, keeping the row with the lowest SID. Useful for write workloads that may insert duplicates.
pg_ripple.cache_budget_mb
| Type | Integer (MiB) |
| Default | 64 |
| Context | postmaster |
Shared-memory budget cap in megabytes for the dictionary encode cache. Must be increased in step with dictionary_cache_size.
pg_ripple.auto_analyze
| Type | Boolean |
| Default | on |
| Context | sighup |
When on, the background merge worker runs ANALYZE on each VP main table immediately after a successful merge cycle, keeping planner statistics fresh.
pg_ripple.named_graph_optimized
| Type | Boolean |
| Default | off |
| Context | postmaster |
When on, adds a (g, s, o) B-tree index to every dedicated VP table for fast named-graph–scoped queries. Off by default to avoid index bloat. Enable if most queries use GRAPH patterns.
pg_ripple.normalize_iris
| Type | Boolean |
| Default | on |
| Context | sighup |
| Since | v0.55.0 |
When on, normalizes IRI strings to NFC Unicode before dictionary encoding. Ensures that semantically equivalent IRIs differing only in Unicode normalization form map to the same dictionary entry.
pg_ripple.rls_bypass
| Type | Boolean |
| Default | off |
| Context | postmaster (requires shared_preload_libraries) |
| Access | Superuser only |
| Since | v0.37.0 |
Superuser override to bypass graph-level Row-Level Security policies. Registered at PGC_POSTMASTER scope — a session-level SET is rejected, preventing privilege escalation.
pg_ripple.predicate_cache_enabled
| Type | Boolean |
| Default | on |
| Context | userset |
| Since | v0.38.0 |
Enable the backend-local predicate OID cache, which stores the mapping from predicate IDs to VP table OIDs. Disable to debug catalog lookup issues.
pg_ripple.cdc_bridge_enabled
| Type | Boolean |
| Default | off |
| Context | sighup |
| Since | v0.52.0 |
Master switch for the CDC → pg_tide outbox bridge worker. When on, triple inserts and deletes are serialized as JSON-LD events and published to the configured pg_tide outbox.
pg_ripple.cdc_bridge_batch_size
| Type | Integer |
| Default | 100 |
| Context | sighup |
| Since | v0.52.0 |
Maximum number of CDC notifications batched before flushing to the outbox table.
pg_ripple.cdc_bridge_flush_ms
| Type | Integer (ms) |
| Default | 200 |
| Context | sighup |
| Since | v0.52.0 |
Maximum milliseconds between CDC bridge worker flush cycles.
pg_ripple.cdc_bridge_outbox_table
| Type | String |
| Default | (none) |
| Context | sighup |
| Since | v0.52.0 |
Legacy name for the pg_tide outbox that the CDC bridge worker publishes JSON-LD events to. The outbox must already exist via tide.outbox_create(...) when cdc_bridge_enabled = on.
pg_ripple.trickle_integration
| Type | Boolean |
| Default | on |
| Context | userset |
| Since | v0.52.0 |
Legacy master switch for relay bridge integration features. When off, CDC bridge code paths are disabled even when pg_tide is installed. Use pg_ripple.pg_trickle_available() separately for IVM-backed live views.
pg_ripple.replication_enabled
| Type | Boolean |
| Default | off |
| Context | sighup |
| Since | v0.54.0 |
Enable the RDF logical replication consumer worker. When on, the worker subscribes to a logical replication slot and applies triple changes from a primary server.
pg_ripple.prov_enabled
| Type | Boolean |
| Default | off |
| Context | sighup |
| Since | v0.58.0 |
When on, emits PROV-O provenance triples for all ingest operations, linking each triple to its prov:Activity, prov:Agent, and timestamp.
pg_ripple.citus_sharding_enabled
| Type | Boolean |
| Default | off |
| Context | postmaster |
| Since | v0.58.0 |
Enable Citus horizontal sharding of VP tables. When on, new VP tables are created with REPLICA IDENTITY FULL and distributed via create_distributed_table(s).
pg_ripple.citus_trickle_compat
| Type | Boolean |
| Default | off |
| Context | userset |
| Since | v0.58.0 |
When on, create_distributed_table uses colocate_with = 'none' for legacy CDC/IVM compatibility. Prevents cross-shard tombstone deletes.
pg_ripple.approx_distinct
| Type | Boolean |
| Default | off |
| Context | userset |
| Since | v0.68.0 |
When on, routes COUNT(DISTINCT …) aggregates through Citus HLL (hll_add_agg) when the hll extension is available. Provides approximate but highly scalable distinct counts on distributed VP tables.
pg_ripple.citus_service_pruning
| Type | Boolean |
| Default | off |
| Context | userset |
| Since | v0.68.0 |
When on, the SPARQL federation translator rewrites SERVICE subqueries targeting Citus workers to include shard-constraint annotations, pruning irrelevant shards.
pg_ripple.columnar_threshold
| Type | Integer |
| Default | -1 (disabled) |
| Context | sighup |
| Since | v0.57.0 |
Triple count threshold above which the HTAP merge converts vp_{id}_main from heap to columnar storage (via pg_columnar). -1 disables columnar conversion.
pg_ripple.adaptive_indexing_enabled
| Type | Boolean |
| Default | off |
| Context | sighup |
| Since | v0.57.0 |
Enable automatic adaptive index creation based on query access patterns. When on, the planner creates additional indexes on VP tables for frequently-queried predicate combinations.
pg_ripple.arrow_flight_secret
| Type | String |
| Default | (none — tickets unsigned) |
| Context | sighup |
| Access | Superuser only |
| Since | v0.66.0 |
HMAC-SHA256 secret for signing Arrow Flight export tickets. In production, set to a long random value. Empty string = unsigned tickets (rejected by default in pg_ripple_http).
ALTER SYSTEM SET pg_ripple.arrow_flight_secret = 'your-long-random-secret-here';
SELECT pg_reload_conf();
pg_ripple.arrow_flight_expiry_secs
| Type | Integer (seconds) |
| Default | 3600 |
| Context | sighup |
| Since | v0.66.0 |
Arrow Flight ticket validity period. Tickets older than this many seconds are rejected.
pg_ripple.arrow_unsigned_tickets_allowed
| Type | Boolean |
| Default | off |
| Context | sighup |
| Access | Superuser only |
| Since | v0.67.0 |
When on, unsigned Arrow Flight tickets are accepted. For local development only — never enable in production.
pg_ripple.merge_lock_timeout_ms
| Type | Integer (ms) |
| Default | 5000 |
| Context | sighup |
| Since | v0.82.0 |
Fence lock timeout for the merge worker. If the lock is not acquired within this interval, the merge cycle is skipped.
pg_ripple.merge_batch_size
| Type | Integer |
| Default | 1000000 |
| Range | 100–100000000 |
| Context | sighup |
| Since | v0.82.0 |
Maximum rows processed in a single merge INSERT…SELECT batch. Tune downward to reduce transaction duration at the cost of more merge passes.
pg_ripple.describe_max_depth
| Type | Integer |
| Default | 16 |
| Range | 1–256 |
| Context | userset |
| Since | v0.85.0 |
Maximum recursion depth for DESCRIBE CBD traversal. Prevents runaway recursion on cyclic or deeply nested graphs.
pg_ripple.bulk_load_use_copy
| Type | Boolean |
| Default | off |
| Context | userset |
| Since | v0.94.0 |
When on, bulk loaders use COPY ... FROM STDIN BINARY for dictionary-encoded triple stream insertion instead of batched INSERTs. May improve throughput for very large loads.
pg_ripple.cdc_watermark_batch_size
| Type | Integer |
| Default | 100 |
| Context | sighup |
| Since | v0.91.0 |
Number of CDC events to accumulate before flushing the LSN watermark. Reduces per-event write amplification.
pg_ripple.bidi_relay_max_inflight
| Type | Integer |
| Default | 1000 |
| Range | 1–100000 |
| Context | userset |
| Since | v0.94.0 |
Maximum concurrent in-flight bidirectional relay operations per process. When the limit is reached, new relay dispatch calls are dropped (drop-oldest) and pg_ripple_bidi_relay_dropped_total is incremented.
Federation Parameters
pg_ripple.federation_timeout
| Type | Integer (seconds) |
| Default | 30 |
| Context | userset |
Per-SERVICE-call wall-clock timeout. Endpoints that do not respond within this interval return an empty result (with PT214 WARNING).
pg_ripple.federation_max_results
| Type | Integer |
| Default | 10000 |
| Context | userset |
Maximum rows accepted from a single remote SERVICE call.
pg_ripple.federation_on_error
| Type | Enum string |
| Default | 'empty' |
| Valid values | empty, error |
| Context | userset |
Behaviour when a SERVICE call fails completely. empty returns an empty result set; error raises an exception.
pg_ripple.federation_pool_size
| Type | Integer |
| Default | 4 |
| Context | sighup |
| Since | v0.19.0 |
Number of idle HTTP connections to keep per remote federation endpoint (connection pooling).
pg_ripple.federation_cache_ttl
| Type | Integer (seconds) |
| Default | 0 (disabled) |
| Context | userset |
| Since | v0.19.0 |
TTL for cached SERVICE results. When 0, federation results are not cached. Set to a positive value to enable result caching for idempotent queries.
pg_ripple.federation_adaptive_timeout
| Type | Boolean |
| Default | off |
| Context | userset |
| Since | v0.19.0 |
When on, derives the effective per-endpoint timeout from observed P95 latency, adapting to endpoint responsiveness.
pg_ripple.federation_endpoint_policy
| Type | Enum string |
| Default | 'default-deny' |
| Valid values | default-deny, allowlist, open |
| Context | sighup |
| Since | v0.55.0 |
Network security policy for federation endpoints.
| Value | Behaviour |
|---|---|
default-deny | Blocks RFC-1918 private, loopback, and link-local addresses (SSRF protection) |
allowlist | Only endpoints listed in federation_allowed_endpoints are permitted |
open | All endpoints allowed — development only, never use in production |
pg_ripple.federation_allowed_endpoints
| Type | String (comma-separated URLs) |
| Default | (none) |
| Context | sighup |
| Since | v0.55.0 |
Comma-separated list of allowed federation endpoint URLs. Consulted only when federation_endpoint_policy = 'allowlist'.
pg_ripple.federation_circuit_breaker_threshold
| Type | Integer |
| Default | 5 |
| Context | userset |
| Since | v0.56.0 |
Consecutive failures before opening the circuit breaker for a remote endpoint. Once tripped, the endpoint is bypassed for federation_circuit_breaker_reset_seconds.
pg_ripple.federation_circuit_breaker_reset_seconds
| Type | Integer (seconds) |
| Default | 60 |
| Context | userset |
| Since | v0.56.0 |
Seconds until a tripped circuit breaker half-opens and allows a retry probe.
pg_ripple.federation_connect_timeout_secs
| Type | Integer (seconds) |
| Default | 10 |
| Context | userset |
| Since | v0.96.0 |
TCP/TLS connection timeout for federation SERVICE endpoints. Separate from federation_timeout (which covers the query body). If the endpoint does not accept the connection within this window, the request is rejected immediately.
pg_ripple.federation_allow_unregistered_service_endpoints
| Type | Boolean |
| Default | off |
| Context | sighup |
| Since | v0.98.0 |
When off (default), executing a SERVICE clause against an endpoint not registered in pg_ripple.federation_endpoints raises PT-SSRF-01. Set to on only for development or testing.
Observability Parameters
pg_ripple.tracing_enabled
| Type | Boolean |
| Default | off |
| Context | userset |
| Since | v0.40.0 |
Master switch for OpenTelemetry distributed tracing. When on, SPARQL and Datalog query executions emit spans to the configured exporter.
pg_ripple.tracing_exporter
| Type | Enum string |
| Default | 'stdout' |
| Valid values | stdout, otlp |
| Context | sighup |
| Since | v0.40.0 |
OpenTelemetry exporter backend. Use otlp with tracing_otlp_endpoint to send spans to an OTLP collector (Jaeger, Tempo, etc.).
pg_ripple.tracing_otlp_endpoint
| Type | String |
| Default | (none) |
| Context | sighup |
| Since | v0.51.0 |
OTLP collector endpoint URL for span export. Example: http://localhost:4317.
pg_ripple.audit_log_enabled
| Type | Boolean |
| Default | off |
| Context | userset |
| Since | v0.56.0 |
Enable SPARQL write-operation audit logging into _pg_ripple.audit_log. Records all INSERT/DELETE/UPDATE operations with timestamp, query text, and role.
pg_ripple.audit_retention_days
| Type | Integer (days) |
| Default | 90 |
| Context | sighup |
| Since | v0.78.0 |
Retention period for _pg_ripple.event_audit rows. A background worker sweep prunes rows older than this many days once per hour. Set to 0 to disable automatic pruning.
pg_ripple.export_max_rows
| Type | Integer |
| Default | 0 (unlimited) |
| Context | userset |
| Since | v0.40.0 |
Maximum rows returned by export functions (export_turtle(), export_ntriples(), export_jsonld()). Truncated exports emit PT642.
PageRank Parameters
pg_ripple.pagerank_enabled
| Type | Boolean |
| Default | off |
| Context | sighup |
| Since | v0.88.0 |
Master switch for the Datalog-native PageRank engine. Must be on to use pagerank_run(), centrality_run(), and related functions.
pg_ripple.pagerank_damping
| Type | Float |
| Default | 0.85 |
| Range | 0.0–1.0 |
| Context | userset |
| Since | v0.88.0 |
PageRank damping factor (teleportation probability = 1 − damping). Standard value is 0.85 (matching Google's original paper).
pg_ripple.pagerank_max_iterations
| Type | Integer |
| Default | 100 |
| Context | userset |
| Since | v0.88.0 |
Maximum PageRank iteration count. Stops early when convergence is reached.
pg_ripple.pagerank_convergence_delta
| Type | Float |
| Default | 0.0001 |
| Context | userset |
| Since | v0.88.0 |
Convergence threshold for PageRank. Iteration stops when the maximum score change across all nodes falls below this value.
pg_ripple.pagerank_convergence_norm
| Type | Enum string |
| Default | 'l1' |
| Valid values | l1, l2, linf |
| Context | userset |
| Since | v0.90.0 |
Convergence norm for PageRank iteration. l1 matches NetworkX; l2 matches igraph; linf is most conservative.
pg_ripple.pagerank_partition
| Type | Boolean |
| Default | on |
| Context | userset |
| Since | v0.88.0 |
Enable graph-partitioned parallel PageRank computation. When on, the number of partitions is auto-tuned to min(num_cpus, count(named_graphs)). Set to off for single-partition (debugging) mode.
pg_ripple.pagerank_incremental
| Type | Boolean |
| Default | off |
| Context | sighup |
| Since | v0.88.0 |
Enable pg-trickle incremental K-hop refresh. When on, only the K-hop neighborhood of changed nodes is re-evaluated after each write, rather than a full recompute.
pg_ripple.pagerank_khop_limit
| Type | Integer |
| Default | 30 |
| Context | userset |
| Since | v0.88.0 |
Maximum K-hop propagation depth for incremental PageRank updates.
pg_ripple.pagerank_rules
| Type | String (comma-separated IRIs) |
| Default | (empty — all object-valued predicates) |
| Context | userset |
| Since | v0.88.0 |
Comma-separated IRI list of edge predicates to include in the PageRank graph. Empty string means all object-valued predicates.
pg_ripple.pagerank_confidence_weighted
| Type | Boolean |
| Default | off |
| Context | userset |
| Since | v0.88.0 |
When on, multiplies edge weights by confidence scores from _pg_ripple.confidence during PageRank computation. Higher-confidence edges contribute more to PageRank propagation.
pg_ripple.pagerank_full_recompute_threshold
| Type | Float |
| Default | 0.01 |
| Range | 0.0–1.0 |
| Context | userset |
| Since | v0.90.0 |
Fraction of stale pagerank_scores rows that triggers a full recompute instead of incremental refresh.
pg_ripple.pagerank_max_seeds
| Type | Integer |
| Default | 1024 |
| Range | 1–1048576 |
| Context | userset |
| Since | v0.89.0 |
Maximum number of seed IRIs accepted by pagerank_run(..., seed_iris). Arrays longer than this raise PT0411.
pg_ripple.pagerank_katz_alpha
| Type | Float |
| Default | 0.01 |
| Context | userset |
| Since | v0.89.0 |
Attenuation factor for Katz centrality computation via centrality_run().
LLM / AI Parameters
pg_ripple.llm_endpoint
| Type | String |
| Default | (none) |
| Context | userset |
| Since | v0.49.0 |
LLM API base URL for natural-language → SPARQL generation via sparql_from_nl(). Must be an OpenAI-compatible chat completions endpoint.
ALTER SYSTEM SET pg_ripple.llm_endpoint = 'https://api.openai.com/v1';
-- Or for Ollama:
ALTER SYSTEM SET pg_ripple.llm_endpoint = 'http://localhost:11434/v1';
pg_ripple.llm_model
| Type | String |
| Default | (none) |
| Context | userset |
| Since | v0.49.0 |
LLM model identifier used for NL → SPARQL generation. Example: 'gpt-4o', 'llama3'.
pg_ripple.llm_api_key_env
| Type | String |
| Default | (none) |
| Context | sighup |
| Since | v0.49.0 |
Name of the OS environment variable that holds the LLM API key. The extension reads the key from this environment variable at query time.
pg_ripple.llm_include_shapes
| Type | Boolean |
| Default | on |
| Context | userset |
| Since | v0.49.0 |
When on, active SHACL shapes are included as semantic context in the prompt sent to the LLM for NL → SPARQL generation.
pg_ripple.kge_enabled
| Type | Boolean |
| Default | off |
| Context | sighup |
| Since | v0.57.0 |
Enable the knowledge-graph embedding (KGE) background worker. When on, the worker trains TransE/RotatE embeddings on the graph structure.
pg_ripple.kge_model
| Type | Enum string |
| Default | 'transe' |
| Valid values | transe, rotate |
| Context | sighup |
| Since | v0.57.0 |
Knowledge-graph embedding model. transe is faster; rotate handles relation composition better.
pg_ripple.auto_embed
| Type | Boolean |
| Default | off |
| Context | sighup |
| Since | v0.28.0 |
Master switch for trigger-based auto-embedding of new dictionary entries. When on, newly loaded entities are queued for background embedding.
pg_ripple.embedding_batch_size
| Type | Integer |
| Default | 100 |
| Context | sighup |
| Since | v0.28.0 |
Number of entities dequeued and embedded per background worker batch.
pg_ripple.use_graph_context
| Type | Boolean |
| Default | off |
| Context | userset |
| Since | v0.28.0 |
When on, serializes each entity's RDF neighborhood before passing it to the embedding model. Produces graph-contextual embeddings at the cost of longer input strings.
pg_ripple.vector_federation_timeout_ms
| Type | Integer (ms) |
| Default | 5000 |
| Context | userset |
| Since | v0.28.0 |
HTTP timeout for calls to external vector service endpoints.
SHACL Parameters
pg_ripple.shacl_mode
| Type | Enum string |
| Default | 'off' |
| Valid values | off, sync, async |
| Context | userset |
SHACL validation mode.
| Value | Behaviour |
|---|---|
off | SHACL enforcement disabled — validate() still works on demand |
sync | Violations are caught inline during triple insert; violating triples are rejected with PT301 |
async | Violations are queued for background validation; inserts are not blocked |
pg_ripple.shacl_rule_max_iterations
| Type | Integer |
| Default | 100 |
| Context | userset |
| Since | v0.79.0 |
Maximum fixpoint iterations for sh:SPARQLRule evaluation per validation cycle. Prevents infinite loops when rules fire each other.
pg_ripple.shacl_rule_cwb
| Type | Boolean |
| Default | off |
| Context | userset |
| Since | v0.79.0 |
When on, sh:SPARQLRule rules whose target graph matches an existing CONSTRUCT writeback pipeline are registered as CWB rules rather than standalone SPARQL evaluations.
Quick-Reference Table
For convenience, here is a summary of all parameters grouped by the most common tuning need:
Performance Tuning
| Parameter | Default | Purpose |
|---|---|---|
dictionary_cache_size | 65536 | In-memory encode/decode LRU cache size |
merge_threshold | 10000 | Rows before merge worker fires |
merge_workers | 1 | Parallel merge background workers |
export_batch_size | 10000 | SPARQL cursor batch size |
wcoj_enabled | on | Worst-case optimal joins for cyclic patterns |
bgp_reorder | on | Join reordering by selectivity |
topn_pushdown | on | ORDER BY … LIMIT N optimization |
arrow_batch_size | 1000 | Arrow IPC record batch size |
auto_analyze | on | Post-merge ANALYZE |
Security
| Parameter | Default | Purpose |
|---|---|---|
federation_endpoint_policy | default-deny | SSRF protection for SERVICE clauses |
federation_allow_private | off | Block private-IP federation targets |
rls_bypass | off | Superuser graph-RLS bypass |
arrow_flight_secret | (none) | Arrow Flight ticket signing |
arrow_unsigned_tickets_allowed | off | Development-only unsigned tickets |
Inference Tuning
| Parameter | Default | Purpose |
|---|---|---|
inference_mode | on_demand | When inference runs |
magic_sets | on | Goal-directed demand evaluation |
dred_enabled | on | Incremental deletion re-derivation |
datalog_parallel_workers | 4 | Parallel stratum evaluation |
owl_profile | RL | OWL 2 rule set selection |
wfs_max_iterations | 100 | Well-founded semantics cap |
JSON Mapping Writeback (v0.128.0)
| Parameter | Default | Purpose |
|---|---|---|
json_writeback_batch_size | 100 | Queue rows drained per background merge-worker tick |
pg_ripple.json_writeback_batch_size
Controls how many _pg_ripple.json_writeback_queue rows the background merge
worker processes per tick.
| Attribute | Value |
|---|---|
| Type | Integer |
| Default | 100 |
| Range | 0–10000 |
| Context | suset |
| Since | v0.128.0 |
Set to 0 to disable automatic background draining (rows will accumulate until
processed manually or via a direct pg_ripple.writeback_json_row() call).
Higher values increase writeback throughput at the cost of longer merge-worker transactions. The default of 100 is suitable for most workloads.
Deprecated GUCs
A13-04 (v0.86.0): this page lists all deprecated GUC parameters in pg_ripple, their replacement names, and their scheduled removal versions.
Setting a deprecated GUC raises PT501 with a descriptive message pointing to the replacement.
Currently Deprecated
| Deprecated GUC | Introduced | Replacement | Removal |
|---|---|---|---|
pg_ripple.property_path_max_depth | v0.24.0 | pg_ripple.max_path_depth | v0.56.0 (already removed) |
Removed GUCs
These GUCs have already been removed. Attempting to set them raises a PT501 error.
| Removed GUC | Removed In | Replacement |
|---|---|---|
pg_ripple.property_path_max_depth | v0.56.0 | pg_ripple.max_path_depth |
Planned Deprecations
| GUC | Planned Deprecation | Reason | Replacement |
|---|---|---|---|
pg_ripple.describe_strategy | v0.86.0 (soft) | Superseded by describe_form with W3C-aligned values | pg_ripple.describe_form |
Migration Notes
pg_ripple.property_path_max_depth → pg_ripple.max_path_depth
Renamed in v0.56.0. Update postgresql.conf and ALTER SYSTEM SET calls:
-- Before (raises PT501):
SET pg_ripple.property_path_max_depth = 200;
-- After:
SET pg_ripple.max_path_depth = 200;
pg_ripple.describe_strategy → pg_ripple.describe_form
The describe_strategy GUC uses values cbd, scbd, simple. The new describe_form GUC
uses W3C-aligned names cbd, scbd, symmetric (symmetric is an alias for scbd).
When describe_form is set, it takes precedence over describe_strategy.
-- Before:
SET pg_ripple.describe_strategy = 'scbd';
-- After (preferred):
SET pg_ripple.describe_form = 'symmetric'; -- same as 'scbd'
Both GUCs remain supported in v0.86.0. describe_strategy may be formally deprecated in v0.87.0.
Feature Status Taxonomy
pg_ripple.feature_status() returns one row per major capability. Each row
includes a status column drawn from a fixed vocabulary of seven values. This
page documents what each status means, its promotion criteria, and a concrete
example from the codebase.
Status values
| Status | Meaning | Promotion criteria | Example |
|---|---|---|---|
planned | Roadmap item exists; no user-facing implementation. | First code lands; at least one happy-path test passes; CHANGELOG entry added. → experimental | sparql_12 (tracked in plans/sparql12_tracking.md) |
experimental | Implementation exists; documented limits; may change. | Full behavior test matrix added (positive + negative + error cases); docs updated; CI gate added. → implemented | arrow_flight — real IPC streaming works; nonce replay protection added; tickets require HMAC signing |
stub | API exists (SQL function registered) but production behavior is not implemented. | Real implementation replaces stub; existing callers continue to work; regression test covers the new behavior. → experimental | Any future function that requires external infrastructure not yet wired |
implemented | Full execution path wired, tested in CI, and documented. | This is the steady state. May regress to degraded if a required dependency is removed. | sparql_select, datalog_inference, construct_writeback |
planner_hint | An optimization hint or guidance is emitted, but no custom executor handles the path. | Full custom executor implemented; validated against reference results. → implemented | wcoj — cyclic-BGP join reordering; a true Leapfrog Triejoin executor is not implemented |
manual_refresh | Feature produces correct output only when a manual action is performed (e.g., SELECT pg_ripple.refresh_view(...)). | Automatic refresh or change-detection wired; no manual step required. → implemented | Materialized SPARQL views before cron-based refresh was added |
degraded | A required dependency or configuration is missing; a fallback or no-op is active. | Dependency is installed and the feature falls back to implemented automatically. | vector_hybrid_search without pgvector installed |
Promotion process
-
planned→experimental: open a PR that adds the first implementation and at least one happy-path regression test. Update CHANGELOG under[Unreleased]. Update thefeature_status()row insrc/feature_status.rs. -
experimental→implemented: add the full behavior test matrix (see the pg_regress test for the feature), update the docs page underdocs/src/, and add a CI gate entry in theci_gatefield of thefeature_status()row. -
stub→experimental: ensure existing callers still pass (no breaking API change); add a regression test that exercises the real path.
Adding a new feature to feature_status()
- Add an entry to the
vec!insrc/feature_status.rs. - Choose the correct initial
statusfrom the table above. - Fill in
ci_gatewith the test that verifies the feature (e.g.,"ci/regress: my_feature.sql"). - Fill in
docs_pathwith the primary documentation page. - If an evidence file exists (e.g., a test output or benchmark baseline),
fill in
evidence_path. - Update CHANGELOG.
See also
pg_ripple.feature_status()SQL function —docs/src/reference/sql-functions.mdsrc/feature_status.rsin the repository
Plan Cache
pg_ripple maintains a backend-local SPARQL plan cache to avoid redundant SPARQL → SQL translation for repeated queries. This document describes the cache contract, key construction, and isolation properties.
Cache key construction
The cache key is derived from the SPARQL query text alone:
- The query text is parsed by
spargebrainto a canonical algebra tree. - The algebra tree is printed with
spargebra'sDisplayimplementation to produce a canonical string. - Two GUC values are appended:
pg_ripple.max_path_depthandpg_ripple.bgp_reorder.
The resulting canonical string is used as the hash map key.
What is not in the cache key
| Omitted item | Rationale |
|---|---|
| Current role / session user | PostgreSQL enforces Row Level Security at executor level, not at plan level. The cached SQL string is safe to execute under any role; the RLS policy filters rows after the plan runs. |
Named-graph binding (GRAPH <iri>) | Named graphs are encoded as integer IDs at translation time. The graph IRI is embedded in the SQL WHERE g = <id> clause, so the query text already includes graph identity. |
| RLS-relevant session variables | Session-level GUCs that affect RLS policies (e.g. app.current_tenant) are evaluated by the PostgreSQL executor, not by pg_ripple's query planner. |
Why this is safe
PostgreSQL's RLS mechanism applies row filters at execution time, after the query plan is resolved. A cached plan that omits the role from its key produces correct (RLS-filtered) results when executed under any role. The plan may not be optimal for a role-specific workload (a role-specialized plan could use tighter index scans), but it cannot return rows the caller should not see.
SERVICE clause exclusion
Queries containing SERVICE clauses (federated SPARQL) are never cached. Remote endpoint availability and result shapes may vary between invocations; caching such plans would produce stale or incorrect results.
Cache capacity and eviction
The plan cache is a LruCache with a fixed capacity set by pg_ripple.plan_cache_capacity (default: 256 plans). When the cache is full, the least-recently-used plan is evicted.
Manual invalidation
-- Evict all cached plans and reset hit/miss counters.
SELECT pg_ripple.plan_cache_reset();
-- Inspect cache statistics.
SELECT pg_ripple.plan_cache_stats();
plan_cache_stats() returns a JSONB object with keys:
hits— number of cache hits since last reset.misses— number of cache misses (translation performed) since last reset.size— current number of cached plans.capacity— maximum number of plans the cache can hold.
Role isolation regression test
The pg_regress file tests/pg_regress/sql/plan_cache_rls.sql verifies that two
roles with different RLS grants see only their own data when running the same
SPARQL query text:
- Role
ripple_alicehas access to<https://example.org/graph_a>. - Role
ripple_bobhas access to<https://example.org/graph_b>only. - The same SPARQL query is executed as each role.
ripple_alicereceives triples from graph A;ripple_bobreceives triples from graph B.
This test confirms that plan cache sharing between roles does not bypass PostgreSQL's RLS enforcement.
Audit Log
When pg_ripple.audit_log_enabled = on, every SPARQL UPDATE — INSERT DATA, DELETE DATA, INSERT { … } WHERE, DELETE { … } WHERE, LOAD, CLEAR, MOVE, COPY, ADD — is captured to _pg_ripple.audit_log. The audit log is the workhorse of compliance, debugging, and tenant attribution.
For a higher-level view of the audit story (PROV-O, RDF-star, point-in-time replay), see Temporal & Provenance.
Schema
CREATE TABLE _pg_ripple.audit_log (
id BIGSERIAL PRIMARY KEY,
ts TIMESTAMPTZ NOT NULL DEFAULT now(),
role NAME NOT NULL DEFAULT current_user,
txid BIGINT NOT NULL DEFAULT txid_current(),
operation TEXT NOT NULL, -- 'INSERT DATA' | 'DELETE DATA' | …
query TEXT NOT NULL,
triple_delta INTEGER NOT NULL -- net change in triple count
);
The table is partitioned by month if pg_ripple.audit_log_partition_monthly = on is set at extension creation time.
Configuration
| GUC | Default | Effect |
|---|---|---|
pg_ripple.audit_log_enabled | off | Master switch |
pg_ripple.audit_log_payload_max | 8192 | Truncate captured queries longer than this many characters |
pg_ripple.audit_log_partition_monthly | off | Create monthly child partitions on extension create |
Toggle the GUC per database according to the compliance posture of that database. There is no global on/off — different tenants on the same instance can have different policies.
Querying the log
-- Recent activity by role.
SELECT role, count(*) AS ops, sum(triple_delta) AS net_triples
FROM _pg_ripple.audit_log
WHERE ts > now() - interval '24 hours'
GROUP BY role
ORDER BY ops DESC;
-- All deletions in a transaction.
SELECT id, ts, role, query
FROM _pg_ripple.audit_log
WHERE txid = 12345678
ORDER BY id;
-- Find the SPARQL UPDATE that introduced a specific triple.
-- (Combined with point_in_time(), reconstructs the change history.)
SELECT id, ts, role, query
FROM _pg_ripple.audit_log
WHERE query ILIKE '%<https://example.org/secret>%'
ORDER BY ts DESC
LIMIT 10;
Retention and cleanup
purge_audit_log(before TIMESTAMPTZ) removes old entries:
SELECT pg_ripple.purge_audit_log(before := now() - interval '90 days');
-- Returns the count of rows deleted.
For partitioned audit logs, drop whole partitions instead:
DROP TABLE _pg_ripple.audit_log_2025_q4;
Shipping to a SIEM
The audit log is a regular PostgreSQL table — every shipping mechanism that works for tables works for it:
- Logical replication to a centralised audit warehouse.
- Foreign data wrapper to expose it inside a SIEM.
- Trigger +
pg_notifyto push entries into a SOC pipeline in real time. - CDC subscription with
pg_ripple_http's WebSocket exposure for real-time streaming.
Performance notes
- Logging is synchronous within the UPDATE statement. The cost is dominated by the
INSERTinto_pg_ripple.audit_log— roughly the same as one ordinary triple insert. - For OLTP workloads with high UPDATE rates, enable partitioning and add a
BRIN(ts)index. The defaultid PRIMARY KEYis sufficient for most cases. - The log is written even when the SPARQL UPDATE itself is later rolled back — to capture attempted changes for forensics. Use
pg_ripple.audit_log_only_committed = onto log only on commit.
See also
Embedding Functions Reference
These functions implement vector embedding storage and similarity search in pg_ripple. All functions require the pgvector extension to perform real work; without it they degrade gracefully (WARNING + empty result).
pg_ripple.store_embedding
pg_ripple.store_embedding(
entity_iri TEXT,
embedding FLOAT8[],
model TEXT DEFAULT NULL
) RETURNS VOID
Upserts a float vector for the given entity IRI into _pg_ripple.embeddings. If the entity already has an embedding it is replaced.
Parameters:
| Parameter | Description |
|---|---|
entity_iri | Full IRI of the entity (must exist in the triple store dictionary) |
embedding | Float8 array; length must match pg_ripple.embedding_dimensions |
model | Model name label stored alongside the vector; defaults to pg_ripple.embedding_model |
Returns: VOID
Error codes: PT602 (dimension mismatch), PT603 (pgvector not installed)
Example:
SELECT pg_ripple.store_embedding(
'https://pharma.example/aspirin',
ARRAY[0.12, -0.34, 0.56, ...]::float8[]
);
pg_ripple.similar_entities
pg_ripple.similar_entities(
query_text TEXT,
k INT DEFAULT 10,
model TEXT DEFAULT NULL
) RETURNS TABLE(entity_id BIGINT, entity_iri TEXT, score FLOAT8)
Embeds query_text via the configured embedding API and returns the k entities with the smallest cosine distance.
Parameters:
| Parameter | Description |
|---|---|
query_text | Free-form text to embed and use as query |
k | Number of nearest neighbors to return (clamped to 1–1000) |
model | Override the model from pg_ripple.embedding_model |
Returns: Table of (entity_id, entity_iri, score) ordered by ascending cosine distance.
Error codes: PT601 (API URL not configured), PT603 (pgvector not installed)
Example:
SELECT entity_iri, score
FROM pg_ripple.similar_entities('anti-inflammatory drugs', k := 5)
ORDER BY score;
pg_ripple.embed_entities
pg_ripple.embed_entities(
graph_iri TEXT DEFAULT '',
model TEXT DEFAULT NULL,
batch_size INT DEFAULT 100
) RETURNS BIGINT
Batch-embeds entities that have an rdfs:label (or skos:prefLabel) but no embedding yet. Calls the embedding API in batches and stores results via store_embedding.
Parameters:
| Parameter | Description |
|---|---|
graph_iri | Named graph to scan; empty string = default graph |
model | Embedding model to use (falls back to GUC) |
batch_size | Number of entities to embed per API call (1–500) |
Returns: Number of entities successfully embedded.
Error codes: PT601 (API URL not configured), PT603 (pgvector not installed)
Example:
-- Embed all entities in the default graph
SELECT pg_ripple.embed_entities() AS embedded_count;
-- Embed entities in a specific named graph
SELECT pg_ripple.embed_entities(
graph_iri := 'https://myapp.org/graphs/products',
batch_size := 200
);
pg_ripple.refresh_embeddings
pg_ripple.refresh_embeddings(
graph_iri TEXT DEFAULT '',
model TEXT DEFAULT NULL,
force BOOLEAN DEFAULT FALSE
) RETURNS BIGINT
Re-embeds entities whose embeddings are stale (label changed) or missing. When force := TRUE, re-embeds all entities regardless of staleness.
Parameters:
| Parameter | Description |
|---|---|
graph_iri | Named graph to scan |
model | Embedding model to use |
force | When TRUE, re-embed everything; when FALSE (default) only re-embed stale entries |
Returns: Number of entities re-embedded.
Error codes: PT601 (API URL not configured), PT603 (pgvector not installed), PT606 (no stale embeddings found when not force)
Example:
-- Refresh only stale embeddings
SELECT pg_ripple.refresh_embeddings();
-- Force full re-embedding
SELECT pg_ripple.refresh_embeddings(force := TRUE);
Internal Tables
_pg_ripple.embeddings
Stores entity embeddings. When pgvector is installed:
| Column | Type | Description |
|---|---|---|
entity_id | BIGINT | Dictionary integer ID for the entity IRI |
embedding | vector(N) | Float vector, dimension = pg_ripple.embedding_dimensions |
model | TEXT | Model used to generate this embedding |
updated_at | TIMESTAMPTZ | When this embedding was last stored |
A HNSW index on embedding enables approximate nearest-neighbour search.
When pgvector is absent, the embedding column is BYTEA and all similarity functions return empty results.
SPARQL Integration
The pg:similar() function is callable from SPARQL BIND expressions. See Hybrid Search for usage.
Function IRI: http://pg-ripple.org/functions/similar
Signature (SPARQL):
pg:similar(?entity_variable, "query text", k_integer)
Returns a numeric cosine distance, or SPARQL unbound (NULL) when pgvector is absent.
v0.28.0 Functions
pg_ripple.hybrid_search
pg_ripple.hybrid_search(
sparql_query TEXT,
query_text TEXT,
k INT DEFAULT 10,
alpha FLOAT8 DEFAULT 0.5,
model TEXT DEFAULT NULL
) RETURNS TABLE (
entity_id BIGINT,
entity_iri TEXT,
rrf_score FLOAT8,
sparql_rank INT,
vector_rank INT
)
Fuses SPARQL candidate set with vector k-NN results using Reciprocal Rank Fusion (RRF). alpha = 0.0 is vector-only, 1.0 is SPARQL-only, 0.5 is equal weight.
Error codes: PT603 (pgvector not installed; returns empty result with WARNING)
pg_ripple.rag_retrieve
pg_ripple.rag_retrieve(
question TEXT,
sparql_filter TEXT DEFAULT NULL,
k INT DEFAULT 5,
model TEXT DEFAULT NULL,
output_format TEXT DEFAULT 'jsonb'
) RETURNS TABLE (
entity_iri TEXT,
label TEXT,
context_json JSONB,
distance FLOAT8
)
End-to-end RAG retrieval: vector search → optional SPARQL filter → contextualization → structured output. See RAG Retrieval for full documentation.
pg_ripple.contextualize_entity
pg_ripple.contextualize_entity(
entity_iri TEXT,
depth INT DEFAULT 1,
max_neighbors INT DEFAULT 20
) RETURNS TEXT
Serializes an entity's label, RDF types, and neighbor labels as plain text suitable for embedding. Example output: "aspirin. Type: NSAID, Drug. Related: headache, fever, inflammation".
pg_ripple.list_embedding_models
pg_ripple.list_embedding_models()
RETURNS TABLE (model TEXT, entity_count BIGINT, dimensions INT)
Enumerates all embedding models in _pg_ripple.embeddings with entity counts and vector dimensions.
pg_ripple.add_embedding_triples
pg_ripple.add_embedding_triples() RETURNS BIGINT
Materialises <entity> <http://pg-ripple.org/functions/hasEmbedding> "true"^^xsd:boolean triples for all entities that have embeddings. Use with SHACL sh:minCount 1 to validate embedding completeness.
pg_ripple.register_vector_endpoint
pg_ripple.register_vector_endpoint(
url TEXT,
api_type TEXT
) RETURNS VOID
Registers an external vector service for federation. See Vector Federation for full documentation.
api_type must be one of: pgvector, weaviate, qdrant, pinecone. An invalid type emits a WARNING and does not persist the endpoint.
Error codes: PT607 (invalid api_type)
GraphRAG Export Functions
These functions export GraphRAG knowledge-graph data from pg_ripple to Parquet files compatible with the Microsoft GraphRAG pipeline.
All functions require superuser and return the number of rows written.
export_graphrag_entities
pg_ripple.export_graphrag_entities(
graph_iri TEXT, -- named graph IRI, or '' for the default graph
output_path TEXT -- absolute path to the output .parquet file
) RETURNS BIGINT
Exports all gr:Entity instances.
Output columns:
| Column | Type | Description |
|---|---|---|
id | BYTE_ARRAY | Entity IRI |
title | BYTE_ARRAY | gr:title value |
type | BYTE_ARRAY | gr:type value |
description | BYTE_ARRAY | gr:description value |
text_unit_ids | BYTE_ARRAY | JSON array placeholder (always "[]") |
frequency | INT64 | gr:frequency value, default 0 |
degree | INT64 | gr:degree value, default 0 |
Example:
SELECT pg_ripple.export_graphrag_entities(
'https://myapp.org/graphs/graphrag',
'/var/data/entities.parquet'
);
export_graphrag_relationships
pg_ripple.export_graphrag_relationships(
graph_iri TEXT,
output_path TEXT
) RETURNS BIGINT
Exports all gr:Relationship instances.
Output columns:
| Column | Type | Description |
|---|---|---|
id | BYTE_ARRAY | Relationship IRI |
source | BYTE_ARRAY | gr:source entity IRI |
target | BYTE_ARRAY | gr:target entity IRI |
description | BYTE_ARRAY | gr:description value |
weight | DOUBLE | gr:weight value, default 0.0 |
combined_degree | INT64 | Placeholder, always 0 |
text_unit_ids | BYTE_ARRAY | JSON array placeholder |
export_graphrag_text_units
pg_ripple.export_graphrag_text_units(
graph_iri TEXT,
output_path TEXT
) RETURNS BIGINT
Exports all gr:TextUnit instances.
Output columns:
| Column | Type | Description |
|---|---|---|
id | BYTE_ARRAY | Text unit IRI |
text | BYTE_ARRAY | gr:text value |
n_tokens | INT64 | gr:tokenCount value, default 0 |
document_id | BYTE_ARRAY | gr:documentId value |
entity_ids | BYTE_ARRAY | JSON array placeholder |
relationship_ids | BYTE_ARRAY | JSON array placeholder |
Notes
- Graph IRI: Pass
''(empty string) to query the default graph (all triples without a named-graph assignment). Pass a full IRI to restrict to a named graph. - Path security: The output path must not contain
..components or null bytes. The directory must already exist. - Parquet encoding: Uses Snappy compression. Columns are
REQUIRED BYTE_ARRAYfor mandatory fields andOPTIONAL BYTE_ARRAY / INT64 / DOUBLEfor optional ones. - Superuser required: Because the function writes to the filesystem.
See also
GraphRAG Ontology Reference
The GraphRAG namespace https://graphrag.org/ns/ (prefix gr:) defines the
vocabulary used to represent GraphRAG knowledge graphs in pg_ripple.
Classes
gr:Entity
A named entity extracted from source text.
Required properties: gr:title
Optional properties: gr:type, gr:description, gr:frequency, gr:degree
gr:Relationship
A directed relationship between two entities.
Required properties: gr:source, gr:target
Optional properties: gr:description, gr:weight
gr:TextUnit
A chunk of source text from which entities were extracted.
Required properties: gr:text
Optional properties: gr:tokenCount, gr:documentId, gr:mentionsEntity
gr:Community
A community of related entities (from community detection).
Optional properties: gr:title, gr:description, gr:rank, gr:level
gr:CommunityReport
A natural-language summary report for a community.
Optional properties: gr:summary, gr:findings, gr:rating
Properties
Data properties
| Property | Domain | Range | Notes |
|---|---|---|---|
gr:title | gr:Entity, gr:Community | xsd:string | Display label |
gr:type | gr:Entity | xsd:string | E.g. "PERSON", "ORGANIZATION" |
gr:description | any | xsd:string | Free-text description |
gr:text | gr:TextUnit | xsd:string | Raw source text |
gr:tokenCount | gr:TextUnit | xsd:integer | Token count |
gr:frequency | gr:Entity | xsd:integer | Mention frequency |
gr:degree | gr:Entity | xsd:integer | Graph degree (# relationships) |
gr:weight | gr:Relationship | xsd:float | Relationship strength 0–1 |
gr:rank | gr:Community | xsd:integer | Community rank |
gr:level | gr:Community | xsd:integer | Hierarchical level |
gr:summary | gr:CommunityReport | xsd:string | Summary text |
gr:rating | gr:CommunityReport | xsd:float | Impact rating |
Object properties
| Property | Domain | Range | Notes |
|---|---|---|---|
gr:source | gr:Relationship | gr:Entity | Relationship source entity |
gr:target | gr:Relationship | gr:Entity | Relationship target entity |
gr:mentionsEntity | gr:TextUnit | gr:Entity | Entity mentioned in text unit |
gr:documentId | gr:TextUnit | gr:Document | Source document |
gr:hasReport | gr:Community | gr:CommunityReport | Community report |
Derived properties (Datalog)
These properties are not loaded directly but derived by the enrichment rule set:
| Property | Semantics |
|---|---|
gr:coworker | Symmetric — share a common relationship target |
gr:collaborates | Symmetric — both mentioned in the same text unit |
gr:indirectReport | Transitive closure of gr:manages |
gr:relatedOrg | Organizations bridged by a shared entity |
Ontology file
The full OWL ontology is shipped as sql/graphrag_ontology.ttl. Load it with:
SELECT pg_ripple.load_turtle(pg_read_file('/path/to/graphrag_ontology.ttl'));
SHACL shapes
Validation shapes are in sql/graphrag_shapes.ttl. Load with:
SELECT pg_ripple.load_shacl(pg_read_file('/path/to/graphrag_shapes.ttl'));
GeoSPARQL Reference
pg_ripple v0.25.0 implements a subset of GeoSPARQL 1.1 using PostGIS as the underlying geometry engine. All geo functions gracefully degrade (returning false or NULL) when PostGIS is not installed.
Requirements
| Requirement | Version |
|---|---|
| PostGIS | 2.5+ (3.x recommended) |
| PostGIS SQL extension | Must be installed in the same database |
Check availability:
SELECT EXISTS(SELECT 1 FROM pg_proc WHERE proname = 'st_geomfromtext') AS postgis_available;
Geometry Representation
Geometries are stored as WKT (Well-Known Text) literals in the RDF triple store. Use N-Triples or Turtle syntax to load them:
SELECT pg_ripple.load_ntriples(
'<https://geo.example/city/berlin> <https://www.w3.org/ns/locn#geometry>
"POINT(13.404954 52.520008)" .'
);
The geo:wktLiteral datatype IRI is stored as a regular literal; the WKT string is passed directly to ST_GeomFromText() at query time.
Topological Relation Functions (FILTER context)
These functions are used in FILTER(...) clauses and return true or false.
| SPARQL IRI | PostGIS equivalent | Description |
|---|---|---|
geo:sfIntersects(a, b) | ST_Intersects(…) | Geometries share at least one point |
geo:sfContains(a, b) | ST_Contains(…) | A completely contains B |
geo:sfWithin(a, b) | ST_Within(…) | A is completely within B |
geo:sfOverlaps(a, b) | ST_Overlaps(…) | Geometries overlap |
geo:sfTouches(a, b) | ST_Touches(…) | Geometries touch at boundary |
geo:sfCrosses(a, b) | ST_Crosses(…) | Geometries cross |
geo:sfDisjoint(a, b) | ST_Disjoint(…) | Geometries share no points |
geo:sfEquals(a, b) | ST_Equals(…) | Geometries are spatially equal |
geo:ehIntersects(a, b) | ST_Intersects(…) | Egenhofer intersection |
geo:ehContains(a, b) | ST_Contains(…) | Egenhofer contains |
geo:ehCoveredBy(a, b) | ST_CoveredBy(…) | A is covered by B |
geo:ehCovers(a, b) | ST_Covers(…) | A covers B |
Namespace prefix: http://www.opengis.net/def/function/geosparql/
Example: Find intersecting geometries
PREFIX geo: <http://www.opengis.net/def/function/geosparql/>
SELECT ?city WHERE {
?city <https://www.w3.org/ns/locn#geometry> ?geom .
FILTER(geo:sfIntersects(?geom, "POLYGON((13.0 52.0, 14.0 52.0, 14.0 53.0, 13.0 53.0, 13.0 52.0))"))
}
Measurement Functions (BIND context)
These functions are used in BIND(...) clauses and return numeric or WKT values.
| SPARQL IRI | Returns | Description |
|---|---|---|
geof:distance(a, b, unit) | xsd:double (metres) | Geodetic distance between two geometries |
geof:area(a, unit) | xsd:double (m²) | Surface area of a geometry |
geof:boundary(a) | WKT literal | Boundary geometry as WKT string |
The unit argument is accepted for API compatibility but all results are returned in SI base units (metres / square metres).
Example: Distance query
PREFIX geof: <http://www.opengis.net/def/function/geosparql/>
SELECT ?city ?dist WHERE {
?city <https://www.w3.org/ns/locn#geometry> ?geom .
BIND(geof:distance(?geom, "POINT(13.404954 52.520008)",
<http://www.opengis.net/def/uom/OGC/1.0/metre>) AS ?dist)
FILTER(?dist < 100000)
}
ORDER BY ?dist
Behaviour When PostGIS Is Absent
When PostGIS is not installed:
- Topological filter functions evaluate to
false— queries return zero rows - Measurement functions evaluate to
NULL—BINDvariables are unbound - No
ERRORis raised; the query completes normally
This allows geospatial queries to be deployed to environments where PostGIS is an optional component.
Limitations
- 3D geometries (
POINT Z,POLYGON Z) are stored as literals but PostGIS 2D functions will project them to 2D. - Coordinate reference system (CRS) handling: all geometries are assumed to be in WGS84 (SRID 4326) when passed to the
geographycast forgeof:distanceandgeof:area. - RDF-star quoted triples are not supported as geometry arguments.
GeoSPARQL Functions
SC13-03 (v0.86.0): this page documents the implementation status of all GeoSPARQL 1.1 functions in pg_ripple.
pg_ripple implements the GeoSPARQL 1.1 extension (OGC, 2022) via the PostGIS integration layer.
Geometry literals are stored as WKT strings with the geo:wktLiteral datatype.
Topological Relations
GeoSPARQL topological relation functions operate on geometry literals and return xsd:boolean.
| Function | Prefix | Status | Notes |
|---|---|---|---|
geo:sfEquals | geo: | ✅ Implemented | PostGIS ST_Equals |
geo:sfDisjoint | geo: | ✅ Implemented | PostGIS ST_Disjoint |
geo:sfIntersects | geo: | ✅ Implemented | PostGIS ST_Intersects |
geo:sfTouches | geo: | ✅ Implemented | PostGIS ST_Touches |
geo:sfCrosses | geo: | ✅ Implemented | PostGIS ST_Crosses |
geo:sfWithin | geo: | ✅ Implemented | PostGIS ST_Within |
geo:sfContains | geo: | ✅ Implemented | PostGIS ST_Contains |
geo:sfOverlaps | geo: | ✅ Implemented | PostGIS ST_Overlaps |
geo:ehEquals | geo: | ✅ Implemented | Egenhofer equals |
geo:ehDisjoint | geo: | ✅ Implemented | Egenhofer disjoint |
geo:ehMeet | geo: | ✅ Implemented | Egenhofer meet |
geo:ehOverlap | geo: | ✅ Implemented | Egenhofer overlap |
geo:ehCovers | geo: | ✅ Implemented | Egenhofer covers |
geo:ehCoveredBy | geo: | ✅ Implemented | Egenhofer covered by |
geo:ehInside | geo: | ✅ Implemented | Egenhofer inside |
geo:ehContains | geo: | ✅ Implemented | Egenhofer contains |
geo:rcc8dc | geo: | ✅ Implemented | RCC8 DC |
geo:rcc8ec | geo: | ✅ Implemented | RCC8 EC |
geo:rcc8po | geo: | ✅ Implemented | RCC8 PO |
geo:rcc8tppi | geo: | ✅ Implemented | RCC8 TPPi |
geo:rcc8tpp | geo: | ✅ Implemented | RCC8 TPP |
geo:rcc8ntppi | geo: | ✅ Implemented | RCC8 NTPPi |
geo:rcc8ntpp | geo: | ✅ Implemented | RCC8 NTPP |
geo:rcc8eq | geo: | ✅ Implemented | RCC8 EQ |
Non-topological Functions
| Function | Status | Returns | Notes |
|---|---|---|---|
geo:distance | ✅ Implemented | xsd:double | PostGIS ST_Distance; unit = degrees (EPSG:4326) |
geo:buffer | ✅ Implemented | geo:wktLiteral | PostGIS ST_Buffer |
geo:convexHull | ✅ Implemented | geo:wktLiteral | PostGIS ST_ConvexHull |
geo:intersection | ✅ Implemented | geo:wktLiteral | PostGIS ST_Intersection |
geo:union | ✅ Implemented | geo:wktLiteral | PostGIS ST_Union |
geo:difference | ✅ Implemented | geo:wktLiteral | PostGIS ST_Difference |
geo:symDifference | ✅ Implemented | geo:wktLiteral | PostGIS ST_SymDifference |
geo:envelope | ✅ Implemented | geo:wktLiteral | PostGIS ST_Envelope |
geo:boundary | ✅ Implemented | geo:wktLiteral | PostGIS ST_Boundary |
geo:area | ✅ Implemented | xsd:double | PostGIS ST_Area; unit = square degrees |
geo:length | ✅ Implemented | xsd:double | PostGIS ST_Length |
geo:asWKT | ✅ Implemented | geo:wktLiteral | PostGIS ST_AsText |
geo:asGeoJSON | ✅ Implemented | xsd:string | PostGIS ST_AsGeoJSON |
Known Gaps
| Function | Status | Reason |
|---|---|---|
geo:metricArea | ⏳ Planned | Requires CRS-aware unit conversion via GEOGRAPHY type |
geo:metricLength | ⏳ Planned | Requires CRS-aware unit conversion |
geo:metricPerimeter | ⏳ Planned | Requires CRS-aware unit conversion |
geo:metricDistance | ⏳ Planned | Requires CRS-aware unit conversion |
| DGGS functions | 🔜 Future | Discrete Global Grid Systems (DGGS) not yet implemented |
Enabling GeoSPARQL
GeoSPARQL requires the PostGIS extension:
CREATE EXTENSION postgis;
CREATE EXTENSION pg_ripple CASCADE;
See GeoSPARQL + PostGIS Integration for a full tutorial.
Lattice-Based Datalog Reference (v0.36.0)
Available since v0.36.0. Lattice-Based Datalog (Datalog^L) extends pg_ripple's Datalog engine with monotone lattice aggregation, enabling recursive aggregation without stratification constraints.
Background
Standard Datalog^agg stratifies aggregate functions: an aggregate can only appear at a strictly higher stratum than the predicate it aggregates over. This makes recursive aggregation (e.g., propagating minimum trust scores through a social graph) impossible to express without manual loop unrolling.
Lattice-Based Datalog lifts this restriction by requiring only that the aggregation operation is monotone with respect to a user-supplied lattice. A lattice is an algebraic structure (L, ⊔) where ⊔ is a commutative, associative, idempotent join operation with a bottom element ⊥. Fixpoint computation over a lattice terminates by the ascending chain condition — the lattice has no infinite strictly ascending chains.
Key references
- Abo Khamis et al., PODS 2017 — lattice-structured aggregation in Datalog
- Alvaro et al., CIDR 2011 — monotone logic programming (Bloom^L)
- Green et al., PODS 2007 — provenance semirings as a generalization of lattices
Built-in lattices
pg_ripple ships with four built-in lattice types that cover the most common use cases:
MinLattice (min)
join: LEAST(a, b) (PostgreSQL: min aggregate)
bottom: +∞ (encoded as 9223372036854775807 = i64::MAX)
Use cases: trust propagation, shortest-path weights, minimum-cost routing.
Example: propagate the minimum trust score along a path — the trustworthiness of a chain is limited by its weakest link.
MaxLattice (max)
join: GREATEST(a, b) (PostgreSQL: max aggregate)
bottom: −∞ (encoded as -9223372036854775808 = i64::MIN)
Use cases: reachability weights, longest-path annotation, maximum influence scores.
SetLattice (set)
join: UNION (array deduplication via array_agg)
bottom: {} (empty set)
Use cases: set-valued provenance annotation, multi-hop neighbourhood sets.
IntervalLattice (interval)
join: interval hull (max of lower bound, max of upper bound)
bottom: empty interval (0)
Use cases: temporal reasoning, numeric range propagation.
User-defined lattices
Register a custom lattice with any PostgreSQL aggregate function as the join:
-- Minimum-cost routing over decimal weights.
SELECT pg_ripple.create_lattice('route_cost', 'min', '1e308');
-- Custom bounded lattice (values 0–100, join = LEAST).
SELECT pg_ripple.create_lattice('reputation', 'min', '100');
The join_fn must be a registered PostgreSQL aggregate (verified via pg_proc). A warning is emitted at registration time if the function is not yet visible, but the lattice is still stored — this allows pre-registering lattices before their custom aggregates are created.
GUC parameters
| GUC | Type | Default | Description |
|---|---|---|---|
pg_ripple.lattice_max_iterations | integer | 1000 | Maximum fixpoint iterations before error code PT540 warning and partial-result return. Set to 0 for unlimited (not recommended). |
-- Change the iteration limit.
SET pg_ripple.lattice_max_iterations = 5000;
-- Check current setting.
SHOW pg_ripple.lattice_max_iterations;
SQL Functions
pg_ripple.create_lattice(name, join_fn, bottom) → boolean
Register a new lattice type in the _pg_ripple.lattice_types catalog.
| Parameter | Type | Description |
|---|---|---|
name | text | Unique lattice name (case-sensitive) |
join_fn | text | PostgreSQL aggregate function name |
bottom | text | Bottom element as a text string |
Returns true if newly registered, false if the name already exists (idempotent).
SELECT pg_ripple.create_lattice('trust', 'min', '100'); -- true
SELECT pg_ripple.create_lattice('trust', 'min', '100'); -- false (idempotent)
pg_ripple.list_lattices() → jsonb
Return a JSON array of all registered lattice types (built-in and user-defined).
SELECT jsonb_pretty(pg_ripple.list_lattices());
Each entry has: name, join_fn, bottom, builtin.
pg_ripple.infer_lattice(rule_set, lattice_name) → jsonb
Run a monotone fixpoint over all active rules in rule_set using the specified lattice.
| Parameter | Default | Description |
|---|---|---|
rule_set | 'custom' | Rule set name as used in load_rules() |
lattice_name | 'min' | Lattice type to use for head-predicate joins |
Returns JSONB:
{
"derived": 42,
"iterations": 5,
"lattice": "min",
"rule_set": "my_rules"
}
Errors:
infer_lattice: unknown lattice type '...'— lattice not registered; callcreate_lattice()first.PT540WARNING — fixpoint did not converge withinlattice_max_iterations.
Catalog table
Lattice types are stored in _pg_ripple.lattice_types:
SELECT * FROM _pg_ripple.lattice_types;
| Column | Type | Description |
|---|---|---|
name | text | Primary key; lattice identifier |
join_fn | text | PostgreSQL aggregate name |
bottom | text | Bottom element as text |
builtin | boolean | True for pre-registered lattices |
created_at | timestamptz | Registration timestamp |
Complete example: Trust propagation
This example propagates minimum trust scores through a social graph. The trustworthiness of an indirect connection is bounded by the weakest link on the path.
-- 1. Create extension and configure lattice.
CREATE EXTENSION IF NOT EXISTS pg_ripple;
SELECT pg_ripple.create_lattice('trust', 'min', '100');
-- 2. Insert direct trust relationships (score: 0=no trust, 100=full trust).
SELECT pg_ripple.load_ntriples($$
<https://trust.example/alice> <https://trust.example/directTrust> "90"^^<xsd:integer> .
<https://trust.example/bob> <https://trust.example/directTrust> "70"^^<xsd:integer> .
<https://trust.example/carol> <https://trust.example/directTrust> "85"^^<xsd:integer> .
<https://trust.example/alice> <https://trust.example/knows> <https://trust.example/bob> .
<https://trust.example/bob> <https://trust.example/knows> <https://trust.example/carol> .
$$);
-- 3. Write a trust-propagation rule (using Datalog syntax).
SELECT pg_ripple.load_rules($$
?y <https://trust.example/transitTrust> ?min_t :-
?x <https://trust.example/knows> ?y ,
?x <https://trust.example/directTrust> ?t1 ,
?y <https://trust.example/directTrust> ?t2 .
$$, 'trust_rules');
-- 4. Run lattice-based fixpoint.
SELECT pg_ripple.infer_lattice('trust_rules', 'trust');
-- 5. Query propagated trust values.
SELECT * FROM pg_ripple.sparql($$
SELECT ?x ?t WHERE { ?x <https://trust.example/transitTrust> ?t }
$$);
Error code PT540
Meaning: the lattice fixpoint did not converge within the configured iteration limit.
Trigger: emitted as a PostgreSQL WARNING (not ERROR) when pg_ripple.lattice_max_iterations is exceeded.
Resolution options:
-
Increase the limit:
SET pg_ripple.lattice_max_iterations = 10000; -
Verify your lattice is finite: every value domain used in rules must have a finite number of distinct elements reachable from the bottom.
-
Verify monotonicity: every operation in rule bodies must be monotone with respect to the lattice order. A non-monotone operation (e.g., negation) in a recursive rule violates the convergence guarantee.
Relationship to other pg_ripple inference modes
| Feature | Stratum requirement | Aggregation | Recursion |
|---|---|---|---|
infer() — standard Datalog | Stratified | Not supported | Restricted |
infer_wfs() — Well-Founded Semantics | None | Not supported | Full |
infer_lattice() — Datalog^L | None | Monotone lattice joins | Full |
Use infer_lattice() when you need recursive aggregation with a convergence guarantee, for example: shortest paths, trust propagation, or set-reachability annotations.
Introduced in v0.36.0.
HTTP API Reference
pg_ripple_http is a standalone Rust binary that exposes the full pg_ripple feature set over HTTP. It supports the W3C SPARQL 1.1 Protocol, Server-Sent Events streaming, Datalog REST operations, PageRank, probabilistic reasoning, Arrow Flight bulk export, and administrative endpoints.
Start the service:
PG_RIPPLE_HTTP_PG_URL=postgresql://localhost/mydb \
PG_RIPPLE_HTTP_PORT=7878 \
PG_RIPPLE_HTTP_AUTH_TOKEN=mysecret \
pg_ripple_http
Authentication
All endpoints that mutate state require a Bearer token when PG_RIPPLE_HTTP_AUTH_TOKEN is set.
Authorization: Bearer <token>
Read-only endpoints (GET /health, GET /metrics, GET /openapi.yaml) do not require authentication.
Configuration
| Environment Variable | Default | Description |
|---|---|---|
PG_RIPPLE_HTTP_PG_URL | postgresql://localhost/postgres | PostgreSQL connection URL |
PG_RIPPLE_HTTP_PORT | 7878 | TCP listening port |
PG_RIPPLE_HTTP_POOL_SIZE | 16 | PostgreSQL connection pool size |
PG_RIPPLE_HTTP_AUTH_TOKEN | (none) | Bearer token; set to enable auth |
PG_RIPPLE_HTTP_DATALOG_WRITE_TOKEN | (falls back to auth token) | Optional separate token for mutating Datalog/rule endpoints |
PG_RIPPLE_HTTP_RATE_LIMIT | 100 | Per-IP rate limit (req/s; 0 = unlimited) |
PG_RIPPLE_HTTP_CORS_ORIGINS | '' | Comma-separated allowed CORS origins; * enables permissive CORS |
PG_RIPPLE_HTTP_MAX_BODY_BYTES | 10485760 | Max request body size (10 MiB) |
PG_RIPPLE_HTTP_SKIP_COMPAT_CHECK | (unset) | Set to 1 to skip extension version check |
PG_RIPPLE_HTTP_STRICT_COMPAT | (unset) | Set to 1 to fail startup on incompatible extension versions |
PG_RIPPLE_HTTP_METRICS_TOKEN | (none) | Optional bearer token for /metrics |
PG_RIPPLE_HTTP_AUTH_REALM | pg_ripple | WWW-Authenticate realm for 401 responses |
PG_RIPPLE_HTTP_REPLICA_DSN | (none) | Optional read-replica PostgreSQL DSN |
PG_RIPPLE_HTTP_TRUST_PROXY | (none) | Trusted upstream proxy IP/CIDR list for forwarded headers |
PG_RIPPLE_HTTP_CA_BUNDLE | (system roots) | Extra CA bundle for outbound HTTPS clients |
PG_RIPPLE_HTTP_PIN_FINGERPRINTS | (none) | Optional pinned TLS certificate fingerprints |
PG_RIPPLE_HTTP_SHUTDOWN_TIMEOUT_SECS | 30 | Graceful shutdown timeout |
ARROW_FLIGHT_SECRET | (none) | HMAC secret for Arrow Flight tickets |
ARROW_UNSIGNED_TICKETS_ALLOWED | false | Allow unsigned Arrow tickets for local development |
ARROW_NONCE_CACHE_MAX | 10000 | Replay-protection nonce cache size |
Complete Endpoint Reference
Read and Write mean the endpoint calls the HTTP companion's read or write
authentication check when PG_RIPPLE_HTTP_AUTH_TOKEN is configured. None means
the endpoint is intentionally unauthenticated.
| Method | Path | Auth | Description |
|---|---|---|---|
GET | /sparql | Read | SPARQL 1.1 query via URL parameters |
POST | /sparql | Query-dependent | SPARQL 1.1 query/update via request body; updates require write auth |
POST | /sparql/stream | Read | Streaming SPARQL SELECT (SSE) |
POST | /rag | Read | RAG retrieval / NL-to-SPARQL |
GET | /health | None | Liveness probe |
GET | /ready | None | Kubernetes readiness probe |
GET | /health/ready | None | Deep extension readiness probe |
GET | /metrics | None or metrics token | Prometheus metrics |
GET | /metrics/extension | None | Extension-internal Prometheus metrics |
GET | /void | None | VoID dataset description |
GET | /service | None | SPARQL service description |
GET | /openapi.yaml | None | OpenAPI 3.1 specification |
GET | /explorer | Read | Web-based SPARQL explorer UI |
GET | /admin/bench-history | Write | Recent benchmark history from _pg_ripple.bench_history |
GET | /admin/diagnostic-snapshot | Write | Diagnostic bundle with schema, GUC, version, and metrics data |
POST | /flight/do_get | Write | Arrow Flight bulk export |
GET | /subscribe/{subscription_id} | Read | Live SPARQL subscription SSE stream |
GET | /datalog/rules | Read | List Datalog rule sets |
POST/DELETE | /datalog/rules/{rule_set} | Write | Load or drop a rule set |
POST | /datalog/rules/{rule_set}/builtin | Write | Load built-in rules for a rule set |
POST | /datalog/rules/{rule_set}/add | Write | Add a rule to a rule set |
PUT | /datalog/rules/{rule_set}/enable | Write | Enable a rule set |
PUT | /datalog/rules/{rule_set}/disable | Write | Disable a rule set |
DELETE | /datalog/rules/{rule_set}/{rule_id} | Write | Delete a specific rule |
POST | /datalog/infer/{rule_set} | Write | Run forward-chaining inference |
POST | /datalog/infer/{rule_set}/stats | Write | Run inference and return stats |
POST | /datalog/infer/{rule_set}/agg | Write | Run inference with aggregation |
POST | /datalog/infer/{rule_set}/wfs | Write | Run well-founded semantics inference |
POST | /datalog/infer/{rule_set}/demand | Write | Goal-directed demand inference |
POST | /datalog/infer/{rule_set}/lattice | Write | Lattice-based inference |
POST | /datalog/query/{rule_set} | Read | Goal-directed Datalog query |
GET | /datalog/constraints | Read | Check all constraint rules |
GET | /datalog/constraints/{rule_set} | Read | Check constraints for one rule set |
GET | /datalog/stats/cache | Read | Rule plan cache statistics |
GET | /datalog/stats/tabling | Read | Tabling cache statistics |
GET/POST | /datalog/lattices | Read/Write | List or create lattice structures |
GET/POST | /datalog/views | Read/Write | List or create Datalog-backed views |
DELETE | /datalog/views/{name} | Write | Drop a Datalog-backed view |
POST | /pagerank/run | Write | Start PageRank computation |
GET | /pagerank/status | Read | PageRank computation status |
GET | /pagerank/results | Read | Retrieve PageRank scores |
GET | /pagerank/export | Read | Export PageRank scores |
GET | /pagerank/explain/{node_iri} | Read | Explain PageRank score for a node |
GET | /pagerank/queue-stats | Read | PageRank queue statistics |
POST | /pagerank/vacuum-dirty | Write | Vacuum stale PageRank rows |
POST | /centrality/run | Write | Compute centrality metrics |
GET | /centrality/results | Read | Retrieve centrality results |
POST | /pagerank/find-duplicates | Read | Find near-duplicate nodes |
POST | /confidence/load | Write | Load triples with confidence scores |
GET | /confidence/shacl-score | Read | Get SHACL soft-validation scores |
GET | /confidence/shacl-report | Read | Get scored SHACL validation report |
POST | /confidence/vacuum | Write | Vacuum stale confidence rows |
POST | /confidence/update | Write | Run Bayesian confidence update |
POST | /confidence/bulk-update | Write | Bulk confidence update |
POST | /explain | Read | Natural-language explanation of a query or result |
GET | /explain | Read | Explanation endpoint metadata/query form |
POST | /hypothetical | Write | What-if reasoning against hypothetical facts |
GET | /rule-conflicts/{ruleset} | Read | Detect rule conflicts in a rule set |
GET | /rule-libraries | Read | List rule libraries |
GET | /rule-libraries/{name}/stream | Read | Stream a published rule library |
POST | /rule-libraries/{name}/subscribe | Write | Subscribe to a remote rule library stream |
POST | /rules/draft | Write | Draft rules from natural-language guidance |
POST | /rules/validate | Write | Validate a drafted rule |
GET | /rules/{id}/explain | Read | Explain a rule by ID |
GET/POST | /temporal/mark | Read/Write | List or mark temporal predicates |
POST | /temporal/point_in_time | Write | Set point-in-time temporal context |
GET | /temporal/facts | Read | List temporal facts |
GET | /temporal/graphs/{iri}/snapshot | Read | Materialize a point-in-time graph snapshot |
GET | /temporal/graphs/{iri}/diff | Read | Diff a named graph between two timestamps |
POST | /pprl/bloom_encode | Write | Privacy-preserving Bloom encoding |
POST | /pprl/dice_similarity | Read | Dice similarity over encoded values |
POST | /dp/noisy_count | Read | Differential privacy noisy count |
POST | /dp/noisy_histogram | Read | Differential privacy noisy histogram |
GET | /dp/budget/{dataset}/{principal} | Read | Privacy budget status |
POST | /entity-resolution/resolve | Write | Run entity resolution |
POST | /entity-resolution/evaluate | Read | Evaluate entity-resolution output |
POST | /entity-resolution/monitoring/enable | Write | Enable entity-resolution monitoring |
POST | /entity-resolution/monitoring/disable | Write | Disable entity-resolution monitoring |
GET | /proof-tree/{subject}/{predicate}/{object} | Read | Explain derivation/proof tree for a triple |
GET/POST | /tenants | Read/Write | List or create tenants |
GET/DELETE | /tenants/{name} | Read/Write | Get or delete a tenant |
GET/POST | /tenants/{name}/quota | Read/Write | Get or update tenant quota |
GET | /federation/{endpoint}/auth-status | Write | Per-endpoint federation credential status |
POST | /json-mapping/{name}/writeback | Write | Synchronous JSON mapping relational writeback |
GET | /json-mapping/{name}/writeback/status | Read | JSON mapping writeback queue status |
SPARQL Endpoints
GET /sparql
W3C SPARQL 1.1 Protocol query endpoint.
Query Parameters:
| Parameter | Required | Description |
|---|---|---|
query | Yes (query) | URL-encoded SPARQL SELECT/CONSTRUCT/ASK/DESCRIBE query |
update | Yes (update) | URL-encoded SPARQL UPDATE |
default-graph-uri | No | Default graph URI |
named-graph-uri | No | Named graph URI (repeatable) |
Accept header controls the result format:
| Accept | Format |
|---|---|
application/sparql-results+json | SPARQL JSON (default) |
application/sparql-results+xml | SPARQL XML |
text/turtle | Turtle (for CONSTRUCT/DESCRIBE) |
application/n-triples | N-Triples (for CONSTRUCT/DESCRIBE) |
application/ld+json | JSON-LD (for CONSTRUCT/DESCRIBE) |
Example:
curl -G "http://localhost:7878/sparql" \
--data-urlencode 'query=SELECT ?s ?p ?o WHERE { ?s ?p ?o } LIMIT 10' \
-H "Accept: application/sparql-results+json"
POST /sparql
Accepts the query either as an application/x-www-form-urlencoded body (form post) or as a raw application/sparql-query / application/sparql-update body.
Form body parameters: same as GET /sparql query parameters.
Example (SELECT):
curl -X POST http://localhost:7878/sparql \
-H "Content-Type: application/sparql-query" \
-d 'SELECT ?label WHERE { <https://example.org/Alice> rdfs:label ?label }'
Example (UPDATE, requires auth):
curl -X POST http://localhost:7878/sparql \
-H "Content-Type: application/sparql-update" \
-H "Authorization: Bearer $TOKEN" \
-d 'INSERT DATA { <:Alice> rdfs:label "Alice" }'
POST /sparql/stream
Streaming SPARQL SELECT using Server-Sent Events. Results are delivered as SSE data: events, one result row per event, as the query executes.
Request: Same as POST /sparql.
Response Content-Type: text/event-stream
Event format:
event: row
data: {"?name": "\"Alice\"^^xsd:string", "?age": "\"30\"^^xsd:integer"}
event: done
data: {"total_rows": 42}
Example:
curl -X POST http://localhost:7878/sparql/stream \
-H "Content-Type: application/sparql-query" \
-H "Accept: text/event-stream" \
-d 'SELECT ?s ?p ?o WHERE { ?s ?p ?o }' \
--no-buffer
RAG Endpoint
POST /rag
Execute a hybrid SPARQL + vector-similarity RAG retrieval query. The endpoint embeds the question, finds the nearest RDF entities by cosine similarity, and returns structured results with a plain-text context string suitable for use as an LLM prompt.
Content-Type: application/json
Request body:
{
"question": "what treats headaches?",
"sparql_filter": "?entity a <https://pharma.example/Drug>",
"k": 5,
"model": "text-embedding-3-small",
"output_format": "jsonb"
}
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
question | string | yes | — | Natural-language question |
sparql_filter | string | no | null | SPARQL WHERE fragment to filter candidates |
k | integer | no | 5 | Number of nearest neighbors |
model | string | no | (GUC) | Override pg_ripple.embedding_model |
output_format | string | no | "jsonb" | "jsonb" or "jsonld" |
Response (200 OK):
{
"results": [
{
"entity_iri": "https://pharma.example/aspirin",
"label": "aspirin",
"context_json": {
"label": "aspirin",
"types": ["Drug", "NSAID"],
"properties": [{"predicate": "treats", "object": "headache"}],
"contextText": "aspirin. Type: NSAID, Drug."
},
"distance": 0.12
}
],
"context": "aspirin. Type: NSAID, Drug.\n\nibuprofen. Type: Drug."
}
The context field is a concatenated plain-text summary ready for use as an LLM system prompt.
JSON Mapping Writeback
POST /json-mapping/{name}/writeback
Synchronously write one RDF subject back to the relational table configured for
the named JSON mapping. This calls pg_ripple.writeback_json_row(name, subject_iri) and requires write auth when authentication is enabled.
Content-Type: application/json
{
"subject_iri": "https://example.com/contacts/c001"
}
Response (200 OK):
{
"rows_affected": 1
}
Error mapping:
| Status | Error | Cause |
|---|---|---|
422 | writeback_target_not_configured | The mapping has no writeback table or key columns (PT0550) |
409 | writeback_conflict | Conflict policy is error and a conflicting row exists (PT0551) |
GET /json-mapping/{name}/writeback/status
Return queue depth, error count, and last processed timestamp for one mapping.
This is a filtered HTTP view over pg_ripple.json_writeback_status() and
requires read auth when authentication is enabled.
Response (200 OK):
{
"mapping_name": "contacts",
"pending": 0,
"errors": 0,
"last_error": null,
"last_processed_at": null
}
Arrow Flight Bulk Export
POST /flight/do_get
Bulk-export SPARQL query results as an Apache Arrow IPC record stream. Suitable for high-throughput extract pipelines (DuckDB, pandas, Spark).
Content-Type: application/json
Request body:
{
"ticket": "eyJhbGciOiJIUzI1NiJ9...",
"query": "SELECT ?s ?p ?o WHERE { ?s ?p ?o }"
}
| Field | Type | Required | Description |
|---|---|---|---|
ticket | string | yes | HMAC-signed JWT ticket issued by pg_ripple.arrow_flight_ticket(query) |
query | string | no | Inline query (only when pg_ripple.arrow_unsigned_tickets_allowed = on) |
Response: application/vnd.apache.arrow.stream — Arrow IPC stream.
Example (Python):
import requests, pyarrow as pa
# 1. Get a signed ticket from PostgreSQL
ticket = pg.execute("SELECT pg_ripple.arrow_flight_ticket('SELECT ?s ?p ?o WHERE { ?s ?p ?o }')")
# 2. Stream the Arrow IPC export
r = requests.post("http://localhost:7878/flight/do_get",
json={"ticket": ticket},
headers={"Authorization": f"Bearer {token}"},
stream=True)
reader = pa.ipc.open_stream(r.raw)
table = reader.read_all()
print(table.to_pandas())
Streaming Subscriptions
GET /subscribe/{subscription_id}
Subscribe to a live SPARQL result stream via Server-Sent Events. The subscription_id is returned by pg_ripple.subscribe(query).
Response Content-Type: text/event-stream
Event types:
| Event | Payload | Description |
|---|---|---|
row | JSON object | New result row |
retract | JSON object | Retracted row (triple deleted) |
heartbeat | {} | Keep-alive every 30 s |
error | {"message": "..."} | Subscription error |
Example:
# Create subscription in PostgreSQL
psql -c "SELECT pg_ripple.subscribe('SELECT ?s ?p ?o WHERE { ?s ?p ?o }')"
-- Returns: sub_abc123
# Connect SSE stream
curl -N "http://localhost:7878/subscribe/sub_abc123" \
-H "Accept: text/event-stream"
Datalog REST API
GET /datalog/rules
List all registered Datalog rule sets.
Response (200 OK):
[
{"rule_set": "rdfs_closure", "rule_count": 13, "enabled": true},
{"rule_set": "custom_rules", "rule_count": 5, "enabled": true}
]
POST /datalog/rules/{rule_set}/add
Add a Datalog rule to a rule set.
Content-Type: application/json
{
"rule": "ancestor(?x, ?z) :- ancestor(?x, ?y), parent(?y, ?z).",
"enabled": true
}
Response (200 OK):
{"rule_id": "rule_42", "rule_set": "custom_rules"}
POST /datalog/infer/{rule_set}
Run forward-chaining inference for a rule set.
Content-Type: application/json
{
"graph": "https://example.org/graph1",
"max_iterations": 100,
"dry_run": false
}
Response (200 OK):
{
"derived_count": 847,
"iterations": 3,
"elapsed_ms": 42,
"stratification_order": ["base_rules", "closure_rules"]
}
POST /datalog/infer/{rule_set}/wfs
Run well-founded semantics inference. Returns three-valued results (true / false / undefined) for recursive rules with negation.
POST /datalog/infer/{rule_set}/demand
Goal-directed magic-sets inference. Only derives facts relevant to a specific query goal.
Content-Type: application/json
{
"goal": "ancestor(<:Alice>, ?z)",
"graph": "https://example.org/graph1"
}
POST /datalog/query/{rule_set}
Run a single Datalog goal query against a materialized rule set without modifying the store.
Content-Type: application/json
{
"goal": "ancestor(<:Alice>, ?z)",
"limit": 100
}
Response (200 OK):
{
"results": [
{"?z": "<https://example.org/Bob>"},
{"?z": "<https://example.org/Carol>"}
],
"total": 2
}
GET /datalog/constraints
Check all constraint rules across all rule sets.
Response (200 OK):
{
"violations": [
{
"rule_set": "integrity_rules",
"constraint": "uniqueness_violation",
"violating_bindings": [{"?x": "<:entity1>"}]
}
],
"total_violations": 1
}
GET /datalog/stats/cache
Returns rule plan cache hit/miss statistics.
GET /datalog/stats/tabling
Returns tabling cache occupancy and eviction statistics.
PageRank API
POST /pagerank/run
Start a PageRank computation job.
Content-Type: application/json
{
"graph": "https://example.org/graph1",
"edge_predicates": ["schema:knows", "schema:worksFor"],
"damping": 0.85,
"max_iterations": 100,
"convergence_delta": 0.0001
}
Response (200 OK):
{
"job_id": "pr_20250503_001",
"status": "running",
"started_at": "2025-05-03T12:00:00Z"
}
GET /pagerank/status
Poll the status of the most recent PageRank job.
Response:
{
"job_id": "pr_20250503_001",
"status": "completed",
"iterations": 47,
"elapsed_ms": 1204,
"node_count": 50000,
"edge_count": 250000
}
GET /pagerank/export
Export PageRank scores as JSON or CSV.
Query parameters:
| Parameter | Default | Description |
|---|---|---|
format | json | json or csv |
limit | 1000 | Maximum nodes |
offset | 0 | Pagination offset |
graph | (all) | Filter by named graph |
Response (200 OK, JSON):
{
"scores": [
{"iri": "https://example.org/Alice", "score": 0.0423, "rank": 1},
{"iri": "https://example.org/Bob", "score": 0.0381, "rank": 2}
],
"total": 50000
}
GET /pagerank/explain/{node_iri}
Explain the PageRank score for a specific node, showing top contributing in-edges.
Path parameter: node_iri — URL-encoded IRI.
Response:
{
"iri": "https://example.org/Alice",
"score": 0.0423,
"rank": 1,
"top_contributors": [
{"from_iri": "https://example.org/Bob", "edge_weight": 0.012},
{"from_iri": "https://example.org/Carol", "edge_weight": 0.009}
]
}
POST /centrality/run
Compute centrality metrics (degree, betweenness, Katz).
Content-Type: application/json
{
"graph": "https://example.org/graph1",
"metrics": ["degree", "katz"],
"katz_alpha": 0.01
}
POST /pagerank/find-duplicates
Find near-duplicate nodes by PageRank score similarity and graph structure.
Confidence / Probabilistic API
POST /confidence/load
Load triples with associated confidence scores.
Content-Type: application/json
{
"triples": [
{
"subject": "https://example.org/Alice",
"predicate": "rdf:type",
"object": "schema:Person",
"confidence": 0.95,
"graph": "https://example.org/g1"
}
]
}
Response (200 OK):
{"inserted": 1, "updated": 0}
GET /confidence/shacl-score
Get SHACL soft-validation confidence scores for shapes.
Query parameters:
| Parameter | Default | Description |
|---|---|---|
shape_iri | (all) | Filter by specific shape |
min_score | 0.0 | Minimum score threshold |
GET /confidence/shacl-report
Get a full SHACL validation report with confidence scores per constraint.
POST /confidence/vacuum
Vacuum stale confidence score rows from _pg_ripple.confidence.
Administrative Endpoints
GET /health
Returns {"status": "ok"} when the service is running and the database connection pool is healthy.
Response: 200 OK — {"status": "ok"}
Response: 503 Service Unavailable — database unreachable.
GET /health/ready
Kubernetes-compatible readiness probe. Checks that the PostgreSQL connection pool has at least one available connection.
GET /metrics
Prometheus-format metrics. No authentication required.
Key metrics:
| Metric | Type | Description |
|---|---|---|
pg_ripple_http_requests_total | Counter | Total HTTP requests by method, path, status |
pg_ripple_http_request_duration_seconds | Histogram | Request latency |
pg_ripple_sparql_queries_total | Counter | SPARQL queries by type |
pg_ripple_sparql_query_duration_seconds | Histogram | SPARQL query latency |
pg_ripple_federation_calls_total | Counter | SERVICE clause calls by endpoint |
pg_ripple_datalog_infer_duration_seconds | Histogram | Inference run latency |
GET /metrics/extension
Extension-internal Prometheus metrics sourced directly from the pg_ripple extension within PostgreSQL.
GET /void
Returns a VoID (Vocabulary of Interlinked Datasets) dataset description, summarizing the RDF store contents: triple counts, predicate frequencies, and class distributions.
Accept: text/turtle or application/ld+json
GET /service
Returns a SPARQL 1.1 Service Description document describing the endpoint's capabilities.
Accept: text/turtle or application/ld+json
GET /openapi.yaml
Returns the full OpenAPI 3.1 specification for the pg_ripple_http API.
GET /explorer
Web-based SPARQL explorer UI — an embedded query editor with syntax highlighting, result table, and endpoint configuration.
Error Responses
All error responses use a consistent JSON envelope:
{
"error": "PT440",
"message": "query exceeds maximum algebra depth (256)",
"hint": "Simplify the query or raise pg_ripple.sparql_max_algebra_depth"
}
| HTTP Status | Meaning |
|---|---|
| 400 | Invalid request body or SPARQL syntax error |
| 401 | Missing or invalid Bearer token |
| 403 | Operation not permitted for this role |
| 404 | Resource not found |
| 422 | Semantic error (SHACL violation, constraint error) |
| 429 | Rate limit exceeded |
| 500 | Internal error (check PostgreSQL logs) |
| 503 | Database connection unavailable |
Security
- Bearer token authentication controls mutating endpoints
- SSRF protection: federation SERVICE endpoints are checked against
federation_endpoint_policy - SQL injection prevention: all queries use parameterized PostgreSQL SPI
- Arrow Flight tickets are HMAC-SHA256 signed; set
pg_ripple.arrow_flight_secret - Rate limiting: per-IP, configurable via
PG_RIPPLE_HTTP_RATE_LIMIT - HTTPS termination should be handled by a reverse proxy (nginx, Caddy, Traefik)
- Never expose
PG_RIPPLE_HTTP_AUTH_TOKENin URLs or logs
Limits and Quotas
This page documents the hard limits, default quotas, and tunable caps in pg_ripple. All GUC parameters listed here can be adjusted; see the GUC Reference for full details.
Query Limits
| Limit | Default | GUC | Error code |
|---|---|---|---|
| Max SPARQL result rows | unlimited | pg_ripple.sparql_max_rows | PT640 |
| SPARQL overflow action | truncate | pg_ripple.sparql_overflow_action | — |
| Max algebra tree depth | 256 | pg_ripple.sparql_max_algebra_depth | PT440 |
| Max triple patterns per query | 4096 | pg_ripple.sparql_max_triple_patterns | PT440 |
| Max DESCRIBE CBD depth | 16 | pg_ripple.describe_max_depth | — |
| Fuzzy match input length | 4096 chars | pg_ripple.fuzzy_max_input_length | PT0308 |
| All-nodes predicate expansion cap | 500 predicates | pg_ripple.all_nodes_predicate_limit | — |
Export Limits
| Limit | Default | GUC | Error code |
|---|---|---|---|
| Export function max rows (Turtle/N-Triples/JSON-LD) | unlimited | pg_ripple.export_max_rows | PT642 |
| Arrow Flight batch size | 1000 rows/batch | pg_ripple.arrow_batch_size | — |
Inference Limits
| Limit | Default | GUC | Error code |
|---|---|---|---|
| Datalog max depth | unlimited | pg_ripple.datalog_max_depth | — |
| Datalog max derived facts | unlimited | pg_ripple.datalog_max_derived | — |
| Well-founded semantics max rounds | 100 | pg_ripple.wfs_max_iterations | PT520 |
| SHACL rule max iterations | 100 | pg_ripple.shacl_rule_max_iterations | PT301 |
| Lattice inference max iterations | 1000 | pg_ripple.lattice_max_iterations | PT540 |
| Probabilistic Datalog max iterations | 100 | pg_ripple.prob_datalog_max_iterations | — |
| SHACL score log retention | 30 days | pg_ripple.shacl_score_log_retention_days | — |
Federation Limits
| Limit | Default | GUC | Error code |
|---|---|---|---|
| Per-SERVICE timeout | 30 s | pg_ripple.federation_timeout | PT214 |
| Connect timeout | 10 s | pg_ripple.federation_connect_timeout_secs | PT214 |
| Max rows per SERVICE call | 10,000 | pg_ripple.federation_max_results | — |
| Max response body per endpoint | 100 MiB | pg_ripple.federation_max_response_bytes | PT215 |
| Partial recovery max bytes | 64 KiB | pg_ripple.federation_partial_recovery_max_bytes | — |
| Circuit breaker threshold | 5 failures | pg_ripple.federation_circuit_breaker_threshold | PT217 |
| Parallel SERVICE workers | 4 | pg_ripple.federation_parallel_max | — |
PageRank Limits
| Limit | Default | GUC | Error code |
|---|---|---|---|
| Max PageRank iterations | 100 | pg_ripple.pagerank_max_iterations | — |
| Max seed IRIs per call | 1024 | pg_ripple.pagerank_max_seeds | PT0411 |
| Convergence check norm | L1 | pg_ripple.pagerank_convergence_norm | — |
Storage Limits
| Limit | Default | GUC | Notes |
|---|---|---|---|
| VP promotion threshold | 1,000 triples | pg_ripple.vp_promotion_threshold | Below threshold: stored in vp_rare |
| Merge batch size | 1,000,000 rows | pg_ripple.merge_batch_size | Per-merge INSERT…SELECT |
| Merge fence lock timeout | 5,000 ms | pg_ripple.merge_lock_timeout_ms | — |
| CDC watermark batch size | 100 events | pg_ripple.cdc_watermark_batch_size | — |
| VP promotion batch size | 10,000 rows | pg_ripple.vp_promotion_batch_size | — |
| Bidi relay max in-flight | 1,000 ops | pg_ripple.bidi_relay_max_inflight | Drop-oldest policy |
| Dictionary vacuum threshold | 10,000 terms | pg_ripple.dict_vacuum_threshold | Post-encode auto-VACUUM |
Dictionary and Encoding
| Limit | Default | GUC | Notes |
|---|---|---|---|
| Dictionary LRU cache size | 65,536 entries | pg_ripple.dictionary_cache_size | XXH3-128 hash map |
| Shared memory budget | 64 MiB | pg_ripple.cache_budget_mb | postmaster-scoped |
HTTP API Limits
| Limit | Default | Config | Notes |
|---|---|---|---|
| Max request body size | 10 MiB | PG_RIPPLE_HTTP_MAX_BODY_BYTES | Applies to SPARQL UPDATE body |
| Rate limit | unlimited | PG_RIPPLE_HTTP_RATE_LIMIT | Per source IP, req/s |
| Arrow Flight ticket expiry | 3,600 s | pg_ripple.arrow_flight_expiry_secs | Signed HMAC tickets |
| Connection pool size | 16 | PG_RIPPLE_HTTP_POOL_SIZE | Postgres connections |
Audit and Retention
| Limit | Default | GUC | Notes |
|---|---|---|---|
| Event audit retention | 90 days | pg_ripple.audit_retention_days | _pg_ripple.event_audit |
| SHACL score log retention | 30 days | pg_ripple.shacl_score_log_retention_days | _pg_ripple.shacl_score_log |
| CDC slot idle timeout | 3,600 s | pg_ripple.cdc_slot_idle_timeout_seconds | Orphan slot cleanup |
| VACUUM dict batch size | 200 entries | pg_ripple.vacuum_dict_batch_size | vacuum_dictionary() |
Hard Limits (Not Configurable)
These limits are baked into the implementation:
| Constraint | Value | Notes |
|---|---|---|
| Dictionary hash space | 2^64 (XXH3-128) | Collision probability negligible in practice |
| Maximum SID (statement ID) | 2^63 − 1 (i64) | PostgreSQL sequence maximum |
| Maximum named graph ID | 2^63 − 1 (i64) | Same sequence namespace |
| Maximum predicate ID | 2^63 − 1 (i64) | Same sequence namespace |
| SPARQL 1.1 spec compliance | Full | SELECT, CONSTRUCT, DESCRIBE, ASK, UPDATE, LOAD, CLEAR, DROP, ADD, MOVE, COPY |
| PostgreSQL target | 18.x only | pgrx 0.18, no older PG support |
Recommended Limits for Production
For a 100 GB RDF dataset on a 32-core / 128 GB RAM server:
# postgresql.conf overrides
pg_ripple.dictionary_cache_size = 1000000
pg_ripple.cache_budget_mb = 512
pg_ripple.sparql_max_rows = 100000
pg_ripple.export_max_rows = 500000
pg_ripple.merge_threshold = 50000
pg_ripple.merge_workers = 4
pg_ripple.datalog_parallel_workers = 8
pg_ripple.federation_timeout = 60
pg_ripple.federation_parallel_max = 8
pg_ripple.pagerank_max_iterations = 200
See Performance Tuning for a full tuning guide.
Approximate Aggregates (HLL COUNT DISTINCT)
pg_ripple supports approximate COUNT(DISTINCT ...) in SPARQL queries using the PostgreSQL
hll extension when it is available.
When HLL is used
HLL is used for COUNT(DISTINCT ...) when both conditions are met:
- The GUC
pg_ripple.approx_distinctis set toon(default:off). - The
hllPostgreSQL extension is installed in the current database.
When either condition is not met, pg_ripple falls back to exact COUNT(DISTINCT ...) SQL.
Example:
-- Enable approximate COUNT(DISTINCT)
SET pg_ripple.approx_distinct = on;
-- This SPARQL query uses HLL when the hll extension is available
SELECT *
FROM pg_ripple.sparql($$
SELECT (COUNT(DISTINCT ?s) AS ?n) WHERE { ?s ?p ?o }
$$);
Error bounds
The hll extension uses a HyperLogLog sketch with default precision log2m = 14
(approximately 16,384 register buckets). The resulting error characteristics are:
| Cardinality range | Standard error | Typical relative error |
|---|---|---|
| < 1,000 | ~3.5% | up to ±5% |
| 1,000 – 10,000 | ~1.5% | typically ±2% |
| ≥ 10,000 | ~0.81% | typically ±1% |
| ≥ 100,000 | ~0.025% | typically ±0.1% |
The standard error for log2m = 14 is approximately 1.04 / sqrt(2^14) ≈ 0.0081 (0.81%)
for cardinalities in the asymptotic regime (≥ 10,000 distinct values).
Fallback behavior
When pg_ripple.approx_distinct = off or the hll extension is not installed:
COUNT(DISTINCT ...)uses exact SQLCOUNT(DISTINCT ...)semantics.- No error bounds apply; results are exact.
- This is the default behavior.
Enabling HLL
-- Install the hll extension (requires superuser or pg_extension_owner)
CREATE EXTENSION IF NOT EXISTS hll;
-- Enable approximate aggregates for this session
SET pg_ripple.approx_distinct = on;
-- Or set it globally
ALTER SYSTEM SET pg_ripple.approx_distinct = on;
SELECT pg_reload_conf();
Citus / distributed COUNT DISTINCT
On Citus clusters, COUNT(DISTINCT ...) is not natively pushable to shards (because shards may
contain overlapping value sets). When approx_distinct = on and the hll extension is present,
pg_ripple rewrites COUNT(DISTINCT ?x) to:
hll_cardinality(hll_add_agg(hll_hash_bigint(x)))::bigint
This allows Citus to aggregate HLL sketches across shards using hll_union_agg, giving a
scalable distributed approximate count with the same error bounds as above.
Checking if HLL is active
-- Check current setting
SHOW pg_ripple.approx_distinct;
-- Check if hll extension is installed
SELECT count(*) > 0 AS hll_available
FROM pg_available_extensions
WHERE name = 'hll' AND installed_version IS NOT NULL;
Configuration reference
| GUC | Default | Description |
|---|---|---|
pg_ripple.approx_distinct | off | Use HLL for COUNT(DISTINCT ...) when hll is available |
Testing accuracy
The pg_regress test hll_accuracy.sql verifies that approximate COUNT(DISTINCT ...) returns
a result within 2% of the exact count for a dataset of 200 distinct subjects. Full accuracy
benchmarks at cardinality 100,000 can be run with the tests/http_integration/ suite.
See also: GUC Reference, Citus Integration.
Arrow Flight Reference
pg_ripple exposes an Apache Arrow Flight
bulk-export endpoint via the pg_ripple_http companion service.
Endpoint
GET /flight/do_get
Streams Arrow IPC record batches from VP tables (or a SPARQL SELECT query result) directly to the client using the Apache Arrow Flight protocol.
Authentication
Tickets are HMAC-SHA256 signed with an expiry timestamp and a random nonce to prevent replay
attacks. The secret key is configured via pg_ripple.arrow_flight_secret. Unsigned tickets are
rejected unless pg_ripple.arrow_unsigned_tickets_allowed = on (disabled by default).
Ticket Format
A valid Arrow Flight ticket is a JSON object:
{
"query": "SELECT * FROM pg_ripple.sparql_select($1)",
"exp": 1735689600,
"nonce": "a1b2c3d4e5f6",
"sig": "HMAC-SHA256 hex signature"
}
The sig field is computed over query + exp + nonce using the configured secret.
Streaming Behavior
The endpoint uses Transfer-Encoding: chunked HTTP streaming (via axum::body::Body::from_stream)
so that clients can begin decoding Arrow IPC record batches before the full export completes.
Response bytes are sent in 64 KiB chunks as the IPC buffer is produced.
Memory bound: The Arrow IPC buffer for the entire export is built in memory before streaming
begins. For very large result sets the RSS of pg_ripple_http scales with result-set size
(approximately 32 bytes per row in the IPC buffer plus ~200 bytes per row in PostgreSQL client
memory). The recommended upper bound for a single export call is 10 million rows (RSS ≲ 512 MB
on a host with 1 GB available to the HTTP companion). For larger exports, partition by named graph
or predicate and call the endpoint in batches.
Clients should use streaming reads (e.g., chunked IPC reader) rather than buffering the full response body.
Configuration
| Parameter | Default | Description |
|---|---|---|
pg_ripple.arrow_flight_secret | — | HMAC secret for ticket signing (required) |
pg_ripple.arrow_unsigned_tickets_allowed | off | Allow unsigned tickets (development only) |
ARROW_BATCH_SIZE env var | 1000 | Rows per Arrow IPC record batch |
Response Headers
| Header | Description |
|---|---|
Content-Type | application/vnd.apache.arrow.stream |
X-Arrow-Rows | Total number of triples exported |
X-Arrow-Batches | Number of Arrow IPC record batches sent |
Transfer-Encoding | chunked — response is streamed, not buffered |
Status
Arrow Flight bulk export is experimental in v0.71.0. The HMAC-SHA256 signing,
expiry and nonce checking are fully implemented (v0.67.0 FLIGHT-SEC-02). Chunked HTTP
streaming via Body::from_stream is confirmed and validated (v0.71.0 FLIGHT-STREAM-01).
v1 → v2 HMAC Ticket Migration (L16-05)
pg_ripple v0.72.0 introduced v2 tickets with a nonce-based replay-protection
cache (FLIGHT-NONCE-01). Older v1 tickets (pre-v0.72.0) lack a nonce field
and are therefore accepted only when arrow_unsigned_tickets_allowed = on.
What constitutes a valid v1 ticket
A v1 ticket is any ticket JSON that:
- Contains
queryandexpfields. - Does not contain a
noncefield. - Has a valid HMAC-SHA256
sigcomputed overquery + exp(no nonce in the signed payload).
v1 tickets are not subject to replay protection because the nonce cache does not apply to them. They are therefore inherently replayable until expiry.
Migration path
- Generate new v2 tickets — include a random
nonce(hex string, ≥ 12 bytes of entropy) in every new ticket and sign overquery + exp + nonce. - Transition window — v2 tickets are accepted immediately. v1 tickets continue to be
accepted for the remainder of their
expwindow when the server is runningarrow_unsigned_tickets_allowed = on. Set this tooffonce all active clients have migrated to v2 tickets. - Identify expired v1 tickets — a v1 ticket is expired when
exp < now()(Unix timestamp). Use the following to check a raw ticket JSON:echo '<ticket_json>' | python3 -c " import json, sys, time t = json.load(sys.stdin) exp = t.get('exp', 0) print('expired' if exp < time.time() else f'valid until {time.ctime(exp)}') print('v1 ticket (no nonce)' if 'nonce' not in t else 'v2 ticket (has nonce)') " - Disable v1 acceptance — once all tickets in flight have migrated, set
ARROW_UNSIGNED_TICKETS_ALLOWED=falsein thepg_ripple_httpenvironment and restart.
Summary
| Feature | v1 ticket | v2 ticket |
|---|---|---|
| Replay protection | None (replayable until expiry) | Nonce cache (one-time use) |
| HMAC input | query + exp | query + exp + nonce |
nonce field | Absent | Present (≥ 12 bytes hex) |
| Server requirement | arrow_unsigned_tickets_allowed = on | Always accepted (default) |
See also: HTTP API, Architecture, Compatibility Matrix.
Citus SERVICE Shard Pruning
When pg_ripple is deployed on a Citus distributed PostgreSQL
cluster, SPARQL SERVICE queries that target Citus worker shards can benefit from shard
annotation pruning. This reduces the number of shards queried when the subject IRI can be
used to identify the target shard.
What is SERVICE shard annotation?
In a SPARQL SERVICE query such as:
SELECT ?name WHERE {
SERVICE <https://worker-node.example/sparql> {
<https://example.org/Alice> schema:name ?name
}
}
When the subject IRI (<https://example.org/Alice>) is bound, pg_ripple can use the Citus
shard key to compute which shards contain triples for that subject. With pruning enabled,
only the relevant shards are queried, bypassing the rest.
How shard annotation works
- At query planning time, pg_ripple calls
citus_service_shard_annotation(endpoint_url)for eachSERVICEclause. - If the endpoint is identified as a Citus worker (
is_citus_worker_endpoint()returns true), and a bound subject IRI is present, the shard routing key is computed. - The generated SQL appends
WHERE dist_key = $shard_keyto prune the shard fan-out.
Configuration
| GUC | Default | Description |
|---|---|---|
pg_ripple.citus_service_pruning | off | Enable SERVICE shard annotation for Citus workers |
-- Enable for the session
SET pg_ripple.citus_service_pruning = on;
-- Or set globally (recommended for Citus deployments)
ALTER SYSTEM SET pg_ripple.citus_service_pruning = on;
SELECT pg_reload_conf();
Benchmark results
On a 3-node Citus cluster with 32 shards per table, bound-subject SPARQL SERVICE queries show:
| Condition | Shards queried | Latency (p50) |
|---|---|---|
citus_service_pruning = off | 32 | ~45 ms |
citus_service_pruning = on | 1 | ~5 ms |
10× latency improvement on a 10 M-triple dataset with 1,000 triples per subject.
To reproduce with EXPLAIN:
-- Without pruning
SET pg_ripple.citus_service_pruning = off;
SELECT pg_ripple.explain_sparql(
'SELECT ?name WHERE {
SERVICE <http://citus-worker-1:5432/sparql> {
<https://example.org/Alice> <https://schema.org/name> ?name
}
}',
false
);
-- With pruning
SET pg_ripple.citus_service_pruning = on;
SELECT pg_ripple.explain_sparql(
'SELECT ?name WHERE {
SERVICE <http://citus-worker-1:5432/sparql> {
<https://example.org/Alice> <https://schema.org/name> ?name
}
}',
false
);
The EXPLAIN output with pruning enabled includes shards: 1 in the Citus plan node,
compared to shards: 32 without pruning.
Status
citus_service_pruning is experimental in v0.71.0. The GUC and hook are wired;
multi-node benchmark validation is documented above (CITUS-BENCH-01). Single-node CI
tests verify the GUC and explain plumbing without requiring a Citus cluster.
See also: Approximate Aggregates (HLL), Citus Integration, Compatibility Matrix.
API Stability
This page defines the stability contract for the pg_ripple public API, effective from v0.20.0.
Stability tiers
pg_ripple functions and tables are divided into three tiers:
| Tier | Schema prefix | Contract |
|---|---|---|
| Stable | pg_ripple.* | Will not change in any incompatible way within the 1.x release series. |
| Internal | _pg_ripple.* | Private implementation detail. May change at any minor version. Do not depend on it in application code. |
| Experimental | Marked -- EXPERIMENTAL in source | Subject to change without notice. |
Stable public API (pg_ripple.*)
The following functions are stable as of v0.20.0 and will be maintained without incompatible change through v1.x:
Triple store core
| Function | Signature | Notes |
|---|---|---|
insert_triple | (s text, p text, o text) → bigint | Returns the statement ID (SID). |
delete_triple | (s text, p text, o text) → bigint | Returns deleted row count. |
find_triples | (s text, p text, o text) → setof jsonb | NULL = wildcard. |
triple_count | () → bigint | Total explicit triples in default graph. |
load_ntriples | (data text) → bigint | Bulk load from N-Triples string. |
load_turtle | (data text) → bigint | Bulk load from Turtle string. |
load_rdf_xml | (data text) → bigint | Bulk load from RDF/XML string. |
SPARQL
| Function | Signature | Notes |
|---|---|---|
sparql | (query text) → setof jsonb | SPARQL 1.1 SELECT. Returns one JSONB object per row. |
sparql_ask | (query text) → boolean | SPARQL 1.1 ASK. |
sparql_construct | (query text) → setof jsonb | SPARQL 1.1 CONSTRUCT. |
sparql_describe | (query text, strategy text) → setof jsonb | SPARQL 1.1 DESCRIBE. |
sparql_update | (update text) → bigint | SPARQL 1.1 Update. Returns affected triple count. |
sparql_explain | (query text, analyze boolean) → text | Query plan / execution trace. |
Dictionary
| Function | Signature | Notes |
|---|---|---|
encode_term | (value text, kind smallint) → bigint | Encode a term to dictionary ID. |
decode_id | (id bigint) → text | Decode a dictionary ID to its string form. |
encode_triple | (s text, p text, o text) → bigint | Encode a quoted triple to dictionary ID. |
decode_triple | (id bigint) → jsonb | Decode a quoted-triple ID to {s, p, o}. |
dictionary_stats | () → jsonb | Statistics about the dictionary table. |
vacuum_dictionary | () → bigint | Remove orphaned dictionary entries. |
SHACL validation
| Function | Signature | Notes |
|---|---|---|
load_shacl | (shapes text) → bigint | Parse and store a SHACL shapes graph. |
validate | () → jsonb | Run offline validation. Returns {conforms, violations}. |
list_shapes | () → setof record | Enumerate stored shapes. |
drop_shape | (shape_iri text) → bigint | Remove a shape. |
process_validation_queue | () → bigint | Process the async validation queue. |
dead_letter_queue | () → setof record | Items that failed validation processing. |
Datalog reasoning
| Function | Signature | Notes |
|---|---|---|
load_rules | (rules text, rule_set text DEFAULT 'custom') → bigint | Parse and store a Datalog rule set. |
load_rules_builtin | (name text) → bigint | Load a built-in rule set ('rdfs' or 'owl-rl'). |
add_rule | (rule_set text, rule_text text) → bigint | Add a single rule; returns new rule catalog ID. |
remove_rule | (rule_id bigint) → bigint | Remove a rule by ID; returns triples retracted. |
drop_rules | (rule_set text) → bigint | Drop all rules in a named rule set. |
infer | (rule_set text DEFAULT 'custom') → bigint | Materialise inferences; returns triple count. |
infer_with_stats | (rule_set text DEFAULT 'custom') → jsonb | Semi-naive inference with statistics. |
infer_goal | (rule_set text, goal text) → jsonb | Goal-directed magic-sets inference. |
retract_inferred | (rule_set text) → bigint | Delete all materialised triples for a rule set. |
list_rules | () → jsonb | List all stored rules with metadata. |
list_rule_sets | () → table | List all named rule sets with rule counts. |
Administration
| Function | Signature | Notes |
|---|---|---|
vacuum | () → bigint | Remove stale tombstones and orphaned delta rows. |
reindex | () → bigint | Rebuild VP table indices. |
promote_rare_predicates | () → bigint | Promote predicates from vp_rare to dedicated VP tables. |
trigger_merge | () → void | Signal the background merge worker to run immediately. |
get_statement | (sid bigint) → jsonb | Retrieve a triple by its statement ID. |
Named graphs
| Function | Signature | Notes |
|---|---|---|
insert_triple_graph | (s text, p text, o text, g text) → bigint | Insert into a named graph. |
delete_triple_graph | (s text, p text, o text, g text) → bigint | Delete from a named graph. |
find_triples_graph | (s text, p text, o text, g text) → setof jsonb | Query a named graph. |
Graph RLS
| Function | Signature | Notes |
|---|---|---|
enable_graph_rls | () → void | Enable graph-level Row-Level Security. |
grant_graph | (graph_iri text, role text) → void | Grant RLS SELECT access to a graph for a role. |
revoke_graph | (graph_iri text, role text) → void | Revoke RLS access for a role. |
grant_graph_access | (graph_iri text, role text, privilege text DEFAULT 'SELECT') → void | Grant with explicit privilege level. |
revoke_graph_access | (graph_iri text, role text) → void | Revoke access (detailed). |
Full-text search
| Function | Signature | Notes |
|---|---|---|
fts_index | (predicate text) → bigint | Add a predicate to the FTS index. |
fts_search | (query text) → setof jsonb | Full-text search over indexed literals. |
Export and serialisation
| Function | Signature | Notes |
|---|---|---|
export_ntriples | () → text | Export all triples as N-Triples. |
export_turtle | () → text | Export as Turtle. |
export_jsonld | () → jsonb | Export as JSON-LD. |
Plan cache
| Function | Signature | Notes |
|---|---|---|
plan_cache_stats | () → jsonb | Hit/miss statistics for the SPARQL plan cache. |
plan_cache_reset | () → void | Flush the plan cache. |
Internal schema (_pg_ripple.*)
Tables, sequences, and functions in the _pg_ripple schema are private implementation details. They may be renamed, restructured, or removed at any minor version.
Do not write application code that queries _pg_ripple.* tables directly. Use the pg_ripple.* API instead.
Examples of internal objects (non-exhaustive):
_pg_ripple.dictionary— the term encoding table_pg_ripple.predicates— the VP table catalog_pg_ripple.vp_rare— the rare-predicate consolidation table_pg_ripple.vp_{id}_main,_pg_ripple.vp_{id}_delta,_pg_ripple.vp_{id}_tombstones— HTAP partition tables_pg_ripple.statement_id_seq— the global SID sequence_pg_ripple.shacl_shapes,_pg_ripple.validation_queue,_pg_ripple.dead_letter_queue— SHACL internals
GUC stability
The following GUCs are part of the stable public API:
| GUC | Default | Type |
|---|---|---|
pg_ripple.dictionary_cache_size | 65536 | integer |
pg_ripple.vp_promotion_threshold | 1000 | integer |
pg_ripple.merge_threshold | 10000 | integer |
pg_ripple.shacl_mode | 'async' | enum: 'async', 'sync', 'off' |
pg_ripple.enforce_constraints | 'warn' | enum: 'error', 'warn', 'off' |
pg_ripple.rls_bypass | off | boolean |
pg_ripple.federation_timeout_ms | 5000 | integer |
pg_ripple.enable_plan_cache | on | boolean |
Internal GUCs (names starting with pg_ripple._) may change without notice.
Upgrade compatibility
pg_ripple provides a SQL migration script for every minor release (sql/pg_ripple--X.Y.Z--X.Y.(Z+1).sql). Upgrading is always possible via:
ALTER EXTENSION pg_ripple UPDATE;
No data migration is ever required for stable API changes. If a future release modifies the _pg_ripple.* schema, the migration script handles it automatically.
SPARQL Reference
This page is the reference for pg_ripple's SPARQL 1.1 query and update engine.
Overview
pg_ripple implements SPARQL 1.1 Query Language and SPARQL 1.1 Update as native
PostgreSQL SQL functions. All SPARQL execution is performed inside the
extension via the spargebra parser, an algebra optimizer (sparopt), and a
translation layer that converts SPARQL algebra to PostgreSQL SQL executed
through SPI. Results are decoded back through the dictionary to return RDF
terms as text.
Status
SELECT feature_name, status FROM pg_ripple.feature_status()
WHERE feature_name LIKE 'sparql%';
SQL Functions
| Function | Description |
|---|---|
pg_ripple.sparql(query TEXT) → SETOF record | Execute a SPARQL SELECT query |
pg_ripple.sparql_update(update TEXT) → void | Execute SPARQL 1.1 Update (INSERT DATA, DELETE DATA, DELETE/INSERT WHERE, CLEAR, DROP, COPY, MOVE, ADD) |
pg_ripple.sparql_construct(query TEXT) → TEXT | Execute SPARQL CONSTRUCT, return Turtle |
pg_ripple.sparql_describe(iri TEXT) → TEXT | Execute SPARQL DESCRIBE, return Turtle |
pg_ripple.sparql_ask(query TEXT) → BOOLEAN | Execute SPARQL ASK query |
pg_ripple.explain_sparql(query TEXT, analyze BOOLEAN) → TEXT | Return JSON explain plan for a SPARQL query |
pg_ripple.sparql_cursor(query TEXT, page_size INT) → TEXT | Open a server-side cursor for large result sets |
pg_ripple.sparql_cursor_next(cursor_id TEXT, page_size INT) → SETOF record | Fetch next page from cursor |
pg_ripple.sparql_cursor_close(cursor_id TEXT) → void | Close cursor and release resources |
pg_ripple.sparql_cursor_turtle(query TEXT, page_size INT) → TEXT | Open CONSTRUCT cursor returning Turtle pages |
pg_ripple.sparql_cursor_jsonld(query TEXT, page_size INT) → TEXT | Open CONSTRUCT cursor returning JSON-LD pages |
pg_ripple.subscribe_sparql(id TEXT, query TEXT, graph_iri TEXT) → void | Register a live subscription |
pg_ripple.unsubscribe_sparql(id TEXT) → void | Remove a live subscription |
pg_ripple.list_sparql_subscriptions() → SETOF record | List active subscriptions |
SPARQL 1.1 Feature Coverage
pg_ripple supports the full SPARQL 1.1 specification:
- SELECT with projection, DISTINCT, REDUCED, LIMIT, OFFSET, ORDER BY
- CONSTRUCT with graph patterns and template triples
- DESCRIBE returning a CBD (Concise Bounded Description)
- ASK returning boolean
- Graph patterns: BGP, OPTIONAL, UNION, MINUS, GRAPH, SERVICE, FILTER, BIND, VALUES
- Property paths:
|,/,^,?,*,+,!,{n},{n,},{n,m} - Aggregate functions: COUNT, SUM, MIN, MAX, AVG, GROUP_CONCAT, SAMPLE
- Built-in functions: All ~50+ SPARQL 1.1 scalar functions
- Subqueries: nested SELECT patterns
- SPARQL Update: all 10 update forms
RDF-star Support
Triple-quoted patterns <<s p o>> in both subject and object positions are
supported. The dictionary stores RDF-star terms as encoded triples (hash of the
quoted triple's subject, predicate, and object encoded together).
Performance Notes
- Integer joins: all SPARQL-to-SQL translation encodes bound terms to
BIGINTbefore generating SQL; no string comparisons occur inside VP table queries. - Filter pushdown: FILTER constants are encoded at translation time.
- Self-join elimination: star patterns on the same subject are collapsed into single-scan plans.
- The plan cache (
_pg_ripple.plan_cache) stores compiled SQL for reuse across repeated queries.
Related Pages
- SPARQL Query SQL Reference
- SPARQL Update SQL Reference
- SPARQL Compliance Matrix
- Plan Cache
- Query Optimization
- Feature Status Taxonomy
SPARQL Extension Function IRI Namespace (API-04, v0.91.0)
All pg_ripple SPARQL extension functions are defined under the canonical namespace:
http://pg-ripple.org/fn/
The shorthand pg: prefix maps to this namespace in all SPARQL queries
executed through pg_ripple. The prefix is auto-declared — queries do not need
to explicitly declare PREFIX pg: <http://pg-ripple.org/fn/>, though doing so
is harmless and is the recommended style for queries intended to run against
multiple SPARQL endpoints.
Available extension functions
| Short form | Full IRI | Since | Description |
|---|---|---|---|
pg:confidence(?s, ?p, ?o) | http://pg-ripple.org/fn/confidence | v0.87.0 | Highest confidence score across models for a triple |
pg:pagerank(?node) | http://pg-ripple.org/fn/pagerank | v0.88.0 | PageRank score for a node (default topic) |
pg:pagerank(?node, ?topic) | http://pg-ripple.org/fn/pagerank | v0.88.0 | PageRank score for a node in a named topic |
pg:similar(?a, ?b) | http://pg-ripple.org/fn/similar | v0.27.0 | Cosine similarity between embedding vectors |
pg:fuzzy_match(?a, ?b) | http://pg-ripple.org/fn/fuzzy_match | v0.87.0 | Trigram similarity (requires pg_trgm) |
pg:confPath(?pred, ?minConf) | http://pg-ripple.org/fn/confPath | v0.87.0 | Property path with confidence threshold filter |
Federation note
Federation partners that wish to invoke pg_ripple extension functions remotely must
use the full IRI form, as remote endpoints do not auto-declare the pg: prefix:
FILTER(<http://pg-ripple.org/fn/confidence>(?s, ?p, ?o) > 0.8)
Datalog Reference
This page is the reference for pg_ripple's Datalog inference engine.
Overview
pg_ripple includes a full Datalog engine that compiles Datalog rules to
recursive SQL (WITH RECURSIVE), executes inference in PostgreSQL, and
materializes derived triples back into the triple store. The engine supports
stratified negation, semi-naive evaluation, aggregation (Datalog^agg),
magic sets for goal-directed inference, owl:sameAs entity canonicalization,
well-founded semantics for cyclic ontologies, tabling, Delete-Rederive (DRed)
for retraction, and parallel stratum evaluation.
Status
SELECT feature_name, status FROM pg_ripple.feature_status()
WHERE feature_name LIKE 'datalog%';
SQL Functions
| Function | Description |
|---|---|
pg_ripple.load_rules(rules TEXT, rule_set TEXT DEFAULT 'custom') → BIGINT | Parse and store a Datalog rule set; returns rule count |
pg_ripple.load_rules_builtin(name TEXT) → BIGINT | Load a built-in rule set ('rdfs' or 'owl-rl') |
pg_ripple.add_rule(rule_set TEXT, rule_text TEXT) → BIGINT | Add a single rule to an existing rule set; returns new rule ID |
pg_ripple.remove_rule(rule_id BIGINT) → BIGINT | Remove a rule by catalog ID; returns triples retracted |
pg_ripple.drop_rules(rule_set TEXT) → BIGINT | Drop all rules in a named rule set; returns rule count |
pg_ripple.enable_rule_set(name TEXT) → void | Enable a rule set without re-loading |
pg_ripple.disable_rule_set(name TEXT) → void | Disable a rule set without dropping it |
pg_ripple.list_rules() → JSONB | List all stored rules with id, rule_set, rule_text, stratum, active |
pg_ripple.list_rule_sets() → TABLE(rule_set, active, rule_count, created_at) | List all named rule sets |
pg_ripple.infer(rule_set TEXT DEFAULT 'custom') → BIGINT | Run forward-chaining inference; returns triple count |
pg_ripple.infer_with_stats(rule_set TEXT DEFAULT 'custom') → JSONB | Run semi-naive inference with detailed statistics |
pg_ripple.infer_goal(rule_set TEXT, goal TEXT) → JSONB | Goal-directed inference using magic sets |
pg_ripple.infer_agg(rule_set TEXT DEFAULT 'custom') → JSONB | Run Datalog^agg inference for aggregate rules |
pg_ripple.infer_wfs(rule_set TEXT DEFAULT 'custom') → JSONB | Well-founded semantics inference for cyclic programs |
pg_ripple.infer_lattice(rule_set TEXT, lattice_name TEXT) → JSONB | Lattice-based monotone fixpoint inference |
pg_ripple.retract_inferred(rule_set TEXT) → BIGINT | Delete all materialised triples for a rule set; returns count |
pg_ripple.check_constraints(rule_set TEXT DEFAULT NULL) → JSONB | Run integrity constraint rules; returns violations |
pg_ripple.explain_inference(s TEXT, p TEXT, o TEXT, g TEXT DEFAULT NULL) → TABLE | Return derivation tree for an inferred triple |
pg_ripple.explain_datalog(rule_set_name TEXT) → JSONB | Full explain document: strata, rules, SQL, last run stats |
pg_ripple.dred_on_delete(pred_id BIGINT, s BIGINT, o BIGINT, g BIGINT) → BIGINT | Manual DRed retraction for a deleted base triple |
Rule Syntax
Rules use Turtle-style IRI notation or prefix-qualified names:
:ancestor(?x, ?z) :- :parent(?x, ?y), :ancestor(?y, ?z).
:ancestor(?x, ?y) :- :parent(?x, ?y).
Built-in RDFS/OWL RL rules are included and activated automatically when
pg_ripple.enable_owl_rl = true (default: false).
Inference Architecture
- Rules are parsed and stratified (negation-as-failure via WFS for cyclic rules).
- Each stratum is compiled to a
WITH RECURSIVESQL query. - Semi-naive evaluation tracks the delta between iterations.
- Magic sets transform rules for demand-driven (goal-directed) evaluation.
- Derived triples are inserted into the triple store with
source = 1.
OWL RL Support
The built-in OWL 2 RL rule set covers the complete set of ~100 OWL RL rules including:
- Class and property hierarchy (
rdfs:subClassOf,rdfs:subPropertyOf) - Inverse, symmetric, transitive, and functional properties
owl:allValuesFrom,owl:someValuesFrom,owl:hasValueowl:sameAscanonicalization
Related Pages
- Datalog SQL Reference
- Lattice-Based Datalog
- LUBM OWL RL Results
- OWL 2 RL Results
- Feature Status Taxonomy
SHACL Reference
This page is the reference for pg_ripple's SHACL (Shapes Constraint Language) engine.
Overview
pg_ripple implements the SHACL Core and SHACL-SPARQL constraint sets. Shape
definitions are loaded from the triple store using pg_ripple.load_shacl().
Constraints are compiled to DDL CHECK constraints (for synchronous validation)
and to an asynchronous validation pipeline backed by a background worker for
complex shape hierarchies.
Status
SELECT feature_name, status FROM pg_ripple.feature_status()
WHERE feature_name LIKE 'shacl%';
SQL Functions
| Function | Description |
|---|---|
pg_ripple.load_shacl(graph_iri TEXT) → void | Load SHACL shapes from a named graph |
pg_ripple.validate_shacl(graph_iri TEXT) → SETOF record | Validate a named graph against loaded shapes |
pg_ripple.list_shapes() → SETOF record | List all loaded SHACL shapes |
pg_ripple.drop_shape(shape_iri TEXT) → void | Remove a shape and its constraints |
SHACL Core Constraint Coverage
All 35 SHACL Core constraint components are implemented:
- Property constraints:
sh:minCount,sh:maxCount,sh:minLength,sh:maxLength,sh:pattern,sh:languageIn,sh:uniqueLang,sh:equals,sh:disjoint,sh:lessThan,sh:lessThanOrEquals - Value constraints:
sh:in,sh:hasValue,sh:class,sh:datatype,sh:nodeKind - Shape constraints:
sh:node,sh:property,sh:qualifiedValueShape,sh:qualifiedMinCount,sh:qualifiedMaxCount - Logical constraints:
sh:and,sh:or,sh:not,sh:xone - Closed shapes:
sh:closed,sh:ignoredProperties
SHACL-SPARQL Constraints
Custom constraint components using SPARQL SELECT and ASK are supported via
sh:sparql on shape nodes. The constraint query is compiled to a pg_trickle
stream table for continuous validation.
SHACL-AF Rule Execution
sh:rule triple rules (SHACL Advanced Features) are supported and compiled
to CONSTRUCT writeback rules, allowing shapes to derive new triples from
existing data.
Performance Notes
sh:maxCount 1hints are propagated to the SPARQL planner: DISTINCT is omitted.sh:minCount 1hints allow LEFT JOINs to be rewritten as INNER JOINs.- Shape hints are cached in
_pg_ripple.shape_hintsand consulted at plan time.
Related Pages
Storage Reference
This page is the reference for pg_ripple's HTAP storage layer.
Overview
pg_ripple uses Vertical Partitioning (VP) storage: one table per unique
predicate, with subjects and objects stored as BIGINT dictionary IDs. An
HTAP (Hybrid Transactional/Analytical Processing) design splits writes to a
delta table (heap + B-tree) and reads from a main table (BRIN-indexed).
A background merge worker periodically combines main + delta (minus tombstones)
into a fresh main table.
Status
SELECT feature_name, status FROM pg_ripple.feature_status()
WHERE feature_name LIKE '%htap%' OR feature_name LIKE '%storage%';
Table Layout
Each VP predicate gets three physical tables in _pg_ripple:
| Table | Description |
|---|---|
vp_{id}_delta | Recent writes (heap, B-tree indices on (s,o) and (o,s)) |
vp_{id}_main | Historical data (BRIN-indexed, read-optimized) |
vp_{id}_tombstones | Deleted rows from main (cleared after merge) |
The query path is: (main EXCEPT tombstones) UNION ALL delta.
Rare predicates (below pg_ripple.vp_promotion_threshold, default 1,000 triples)
are stored in _pg_ripple.vp_rare (p, s, o, g, i, source).
Dictionary Encoding
Every IRI, blank node, and literal is mapped to BIGINT (i64) via XXH3-128
hash before storage. The _pg_ripple.dictionary table stores the full
round-trip mapping. A shared-memory LRU cache (size controlled by
pg_ripple.dictionary_cache_size) avoids repeated database lookups.
VP Table Columns
| Column | Type | Description |
|---|---|---|
s | BIGINT | Subject dictionary ID |
o | BIGINT | Object dictionary ID |
g | BIGINT | Named graph dictionary ID (0 = default graph) |
i | BIGINT | Statement ID (SID) from shared sequence |
source | SMALLINT | 0 = explicit, 1 = inferred |
Merge Worker
The background merge worker runs continuously and:
- Detects VP tables whose delta exceeds
pg_ripple.merge_threshold. - Acquires an advisory lock on the predicate ID.
- Builds a new
vp_{id}_maintable (main EXCEPT tombstones UNION ALL delta). - Atomically renames tables (swap) using DDL lock.
- Truncates delta and tombstones.
SQL Functions
| Function | Description |
|---|---|
pg_ripple.vacuum_triples(graph_iri TEXT) → BIGINT | Deduplicate triples in a graph |
pg_ripple.reindex_vp(predicate_iri TEXT) → void | Rebuild indices on a VP table |
pg_ripple.force_merge(predicate_iri TEXT) → void | Trigger immediate merge for a predicate |
pg_ripple.promote_predicate(predicate_iri TEXT) → void | Promote from vp_rare to dedicated table |
pg_ripple.recover_interrupted_promotions() → BIGINT | Recover VP tables stuck in 'promoting' state |
Related Pages
CONSTRUCT Writeback Rules Reference
This page is the reference for pg_ripple's CONSTRUCT writeback rules (CWB).
Overview
CONSTRUCT writeback rules allow SPARQL CONSTRUCT queries to act as transformation pipelines: whenever triples are written to a source graph, the rule re-runs the CONSTRUCT query and writes the derived triples to a target graph. This enables live, incrementally-updated canonical views, raw-to-clean ETL pipelines, and schema-mapping layers entirely within PostgreSQL.
Status
SELECT feature_name, status FROM pg_ripple.feature_status()
WHERE feature_name LIKE 'construct%';
SQL Functions
| Function | Description |
|---|---|
pg_ripple.register_construct_rule(name TEXT, construct_query TEXT, source_graphs TEXT[], target_graph TEXT) → void | Register a CONSTRUCT writeback rule |
pg_ripple.drop_construct_rule(name TEXT) → void | Remove a rule and its derived triples |
pg_ripple.list_construct_rules() → SETOF record | List all registered rules |
pg_ripple.recompute_construct_rule(name TEXT) → BIGINT | Trigger full recompute for a rule |
How It Works
-
Registration:
register_construct_rule()stores the rule in_pg_ripple.construct_rules. The CONSTRUCT query is parsed and validated. Source and target graphs are encoded as dictionary IDs. -
Trigger: After any write to a source graph (via
mutation_journal::flush()),construct_rules::on_graph_write()is called for each affected graph. -
Delta maintenance: The engine re-executes the CONSTRUCT query on the affected source graph and computes the diff (new triples minus existing, deleted triples to retract) using Delete-Rederive (DRed).
-
Writeback: New triples are batch-inserted into the target graph. Retracted triples are deleted.
Provenance
Each derived triple is tracked in _pg_ripple.construct_rule_triples with
a reference to the rule that generated it. This enables precise retraction
when a rule is updated or dropped.
Pipeline Stratification
Rules are topologically sorted by their source/target graph dependencies. Cycles are detected and rejected at registration time. Rules fire in dependency order after each write.
Performance Notes
- Per-statement deferral: the mutation journal accumulates writes during a statement and flushes once at statement end, so N inserts in a single statement fire CWB rules exactly once, not N times.
- CONSTRUCT queries are compiled to SQL and cached in the plan cache.
Related Pages
IVM (Incremental View Maintenance) — Architecture and Boundary
Version: v0.91.0 (IVM-01)
pg_ripple implements two independent incremental view maintenance (IVM) mechanisms: CWB-IVM (CONSTRUCT Writeback IVM) and PageRank-IVM (K-hop dirty-edge propagation). This page explains each mechanism and their operational boundary.
IVM Boundary: CWB vs. PageRank
The two mechanisms operate on different tables and are triggered by different events. They do not interact: a CWB recompute does not automatically trigger PageRank recomputation, and a PageRank IVM update does not write inferred triples back to VP tables.
| Property | CWB-IVM | PageRank-IVM |
|---|---|---|
| What is maintained | Inferred triples derived from SPARQL CONSTRUCT rules | Approximate PageRank scores |
| Algorithm | Delete-Rederive (Z-set deltas) | K-hop local push from dirty edges |
| Source module | src/construct_rules/delta.rs | src/pagerank/ivm.rs |
| Queue table | _pg_ripple.cwb_queue | _pg_ripple.pagerank_dirty_edges |
| Triggered by | VP table delta INSERT/DELETE via cwb_queue | VP table delta INSERT/DELETE via pagerank_dirty_edges |
| Output | New inferred triples written to VP tables | Updated scores in _pg_ripple.pagerank_scores |
| Full recompute function | pg_ripple.run_full_recompute(rule_name) | pg_ripple.pagerank_run(...) |
CWB-IVM (CONSTRUCT Writeback)
CWB-IVM maintains inferred triples derived from SPARQL CONSTRUCT rules. When a VP table delta is modified (new triples inserted or deleted), the affected rules are re-evaluated using the Delete-Rederive algorithm:
- Compute which previously-inferred triples are no longer derivable (Z-set negatives).
- Compute which new triples are now derivable (Z-set positives).
- Apply the delta: retract the negatives, assert the positives.
The _pg_ripple.cwb_queue table holds the pending delta events. The queue is drained by
run_full_recompute() or by the incremental maintenance background worker.
Source: src/construct_rules/delta.rs
PageRank-IVM (K-hop Dirty-Edge Queue)
PageRank-IVM maintains approximate PageRank scores using bounded K-hop propagation from
recently-changed edges. When a VP table delta is modified, affected edges are added to
_pg_ripple.pagerank_dirty_edges. The IVM worker processes the queue by:
- Identifying dirty nodes (nodes whose in-edges changed).
- Re-computing scores for those nodes and their K-hop neighbourhood.
- Updating
_pg_ripple.pagerank_scoresfor affected nodes.
A full pagerank_run() is required after a large CWB recompute if edge predicates are
affected — the K-hop propagation cannot capture large-scale graph restructuring efficiently.
Source: src/pagerank/ivm.rs
Monitoring
CWB queue depth
SELECT COUNT(*) FROM _pg_ripple.cwb_queue;
PageRank queue depth
SELECT * FROM pg_ripple.pagerank_queue_stats();
-- Returns: queued_edges, max_delta, oldest_enqueue, estimated_drain_seconds
Both metrics are exposed via Prometheus (see Observability Reference):
pg_ripple_pagerank_queue_depth{topic=""}— dirty edges pending PageRank refreshpg_ripple_pagerank_queue_max_delta{topic=""}— largest pending score deltapg_ripple_pagerank_queue_oldest_enqueue_seconds{topic=""}— age of oldest dirty entry
GUC Parameters
| GUC | Default | Description |
|---|---|---|
pg_ripple.pagerank_incremental | off | Enable K-hop IVM (dirty-edge queue) |
pg_ripple.pagerank_queue_warn_threshold | 10000 | Log a WARNING when queue depth exceeds this |
pg_ripple.pagerank_ivm_k_hop | 3 | Number of hops to propagate from a dirty edge |
Cross-Module Dependency Scheduling (IVM-03, v0.92.0)
Rules writing to _pg_ripple.confidence (the probabilistic Datalog engine) are not
included in the CWB (CONSTRUCT Writeback) topological sort. The CWB scheduler only
processes SPARQL CONSTRUCT rules; Datalog inference (including confidence propagation)
is triggered via pg_ripple.infer() or the background inference worker.
Cross-module dependency edges — for example, a CONSTRUCT rule that reads from
_pg_ripple.confidence or a Datalog rule that depends on a CWB-maintained predicate —
would require an explicit registration call. This API is reserved for future use:
-- Future API (not yet exposed): register a cross-module dependency
-- SELECT pg_ripple.register_ivm_dependency(
-- source_module => 'construct_rules',
-- source_rule => 'my_pipeline',
-- target_module => 'datalog',
-- target_rule => 'confidence_propagation'
-- );
Until this API is available, applications that need CONSTRUCT → confidence or Datalog → CONSTRUCT dependencies must sequence the calls manually:
-- Manual sequencing: run CONSTRUCT pipeline first, then confidence inference
SELECT pg_ripple.run_construct_pipeline('my_pipeline');
SELECT pg_ripple.infer('confidence');
Related Pages
Federation Reference
This page is the reference for pg_ripple's SPARQL 1.1 federation engine.
Overview
pg_ripple implements SPARQL 1.1 Federated Query (SERVICE keyword) with a
cost-based planner, result caching, connection pooling, circuit-breaker
protection, and parallel remote execution. Local graphs can be mapped to
SERVICE endpoints so the planner transparently rewrites remote-looking queries
to local scans.
flowchart TD
Q[SPARQL query\nwith SERVICE clause] --> P[Cost-based planner\nVoID statistics]
P --> L[Local VP tables\nPostgreSQL scan]
P --> R1[Remote endpoint A\nHTTP/SPARQL]
P --> R2[Remote endpoint B\nHTTP/SPARQL]
L --> J[Result join\n& DISTINCT]
R1 --> C1[Cache\n_pg_ripple.federation_cache]
R2 --> C2[Cache\n_pg_ripple.federation_cache]
C1 --> J
C2 --> J
J --> Out[Final result set]
CB1[Circuit breaker] -. guards .-> R1
CB2[Circuit breaker] -. guards .-> R2
Status
SELECT feature_name, status FROM pg_ripple.feature_status()
WHERE feature_name LIKE '%federation%' OR feature_name = 'sparql_federation';
SQL Functions
| Function | Description |
|---|---|
pg_ripple.register_endpoint(url TEXT, name TEXT, complexity TEXT) → void | Register a remote SPARQL endpoint |
pg_ripple.drop_endpoint(name TEXT) → void | Remove an endpoint registration |
pg_ripple.list_endpoints() → SETOF record | List all registered endpoints |
pg_ripple.is_endpoint_healthy(url TEXT) → BOOLEAN | Check circuit-breaker health status |
pg_ripple.federation_stats() → SETOF record | Per-endpoint query latency and error statistics |
SERVICE Clause
Use the standard SPARQL SERVICE clause in queries:
SELECT ?s ?label WHERE {
?s a <http://example.org/Person> .
SERVICE <https://dbpedia.org/sparql> {
?s rdfs:label ?label .
FILTER(LANG(?label) = "en")
}
}
Cost-Based Planning
The federation planner uses VoID statistics (stored in _pg_ripple.endpoint_stats)
to estimate remote result sizes and order SERVICE sub-queries to minimize
total network round-trips. The complexity field (fast/normal/slow)
adjusts ordering.
Caching
Results from remote endpoints are cached in _pg_ripple.federation_cache
keyed by (endpoint_id, query_hash). The cache TTL is controlled by
pg_ripple.federation_cache_ttl (default: 60 seconds).
Circuit Breaker
Each endpoint tracks probe history in _pg_ripple.federation_health. When
an endpoint fails more than pg_ripple.federation_circuit_threshold times
within the probe window, it is marked unhealthy and future SERVICE calls
immediately return empty results until a successful probe.
Shard Pruning (Citus)
When Citus is installed, SERVICE clauses that resolve to a local shard are
rewritten to direct shard scans, eliminating coordinator overhead. Multi-hop
pruning is supported for chained SERVICE patterns.
Related Pages
- Federation SQL Reference
- HTTP API Reference
- Scalability
- Citus SERVICE Shard Pruning
- Feature Status Taxonomy
CDC (Change Data Capture) Reference
This page is the reference for pg_ripple's CDC (Change Data Capture) integration.
Overview
pg_ripple can subscribe to PostgreSQL logical replication streams and convert row-level change events into RDF triples. The CDC bridge supports:
- PostgreSQL logical decoding via
pg_logical_slot_get_changes() - Configurable table-to-RDF mapping (subject template, predicate template, object template)
- Outbox bridge worker for reliable at-least-once delivery
- CDC lifecycle events (insert, update, delete) mapped to named graphs
- JSON-LD event serialization for downstream consumers
All CDC-ingested triples are recorded in the mutation journal, ensuring CONSTRUCT writeback rules fire for CDC-derived data.
Status
SELECT feature_name, status FROM pg_ripple.feature_status()
WHERE feature_name LIKE 'cdc%';
SQL Functions
| Function | Description |
|---|---|
pg_ripple.register_cdc_subscription(slot_name TEXT, mapping_name TEXT, target_graph TEXT) → void | Register a CDC subscription |
pg_ripple.drop_cdc_subscription(slot_name TEXT) → void | Remove a CDC subscription |
pg_ripple.list_cdc_subscriptions() → SETOF record | List active CDC subscriptions |
pg_ripple.process_cdc_batch(slot_name TEXT, batch_size INT) → BIGINT | Process next batch from a slot |
Mapping Configuration
Each CDC subscription references a mapping_name that defines how table rows
are converted to triples. The mapping specifies:
- Subject template: URI pattern using column values (e.g.,
http://example.org/employee/{id}) - Predicate mapping: column name → predicate IRI
- Object type: literal (with datatype/lang), IRI, or blank node
Mappings are stored in _pg_ripple.cdc_subscriptions.
PG18 Logical Decoding
On PostgreSQL 18, the CDC bridge uses the built-in logical replication
infrastructure. A replication slot is created for each subscription, and
the bridge worker polls for changes using pg_logical_slot_get_changes().
Reliability and Delivery
The outbox bridge pattern ensures at-least-once delivery:
- Changes are first written to
_pg_ripple.cdc_outbox. - The bridge worker processes the outbox transactionally.
- Successfully processed entries are deleted; failures are retried.
Related Pages
GraphRAG Reference
This page is the reference for pg_ripple's GraphRAG (Retrieval-Augmented Generation) integration.
Overview
pg_ripple provides GraphRAG capabilities that combine RDF knowledge graph retrieval with large language model (LLM) generation. The system uses pgvector-based semantic search, SPARQL query generation, RAG context assembly, and entity alignment to support knowledge-graph-grounded question answering.
Status
SELECT feature_name, status FROM pg_ripple.feature_status()
WHERE feature_name LIKE '%rag%' OR feature_name LIKE 'graphrag%';
SQL Functions
| Function | Description |
|---|---|
pg_ripple.rag_context(question TEXT, k INT) → TEXT | Retrieve top-k relevant graph context for a question |
pg_ripple.sparql_from_nl(question TEXT) → TEXT | Generate SPARQL from natural language (requires LLM endpoint) |
pg_ripple.suggest_sameas(iri TEXT, k INT) → SETOF TEXT | Suggest owl:sameAs candidates via embedding similarity |
pg_ripple.graphrag_export(graph_iri TEXT, format TEXT) → TEXT | Export graph in GraphRAG-compatible format (Parquet or JSON-LD) |
RAG Pipeline
- Embedding: Subject IRIs are embedded via the configured LLM embedding endpoint.
- Retrieval: Given a question, its embedding is compared to entity embeddings
using HNSW/IVFFlat vector search (
pg_ripple.rag_context()). - Context assembly: Top-k entities are expanded to their triples via SPARQL
(
DESCRIBEor a custom query template). - Generation: The assembled context is passed to the LLM for answer generation.
Microsoft GraphRAG Integration
graphrag_export() produces output compatible with Microsoft GraphRAG:
- Parquet format: entity and relationship tables with embeddings
- JSON-LD format: context-annotated graph nodes
LLM Configuration
Set LLM endpoint parameters via GUCs:
| GUC | Default | Description |
|---|---|---|
pg_ripple.llm_endpoint | '' | Base URL for LLM API (OpenAI-compatible) |
pg_ripple.llm_api_key | '' | API key (use a secret, not plaintext) |
pg_ripple.llm_model | 'gpt-4o' | Model name for generation |
pg_ripple.embedding_model | 'text-embedding-3-small' | Model for embeddings |
Related Pages
Observability Reference
This page is the reference for pg_ripple's observability and monitoring features.
Overview
pg_ripple exposes monitoring data via:
- A Prometheus metrics endpoint (
/metrics) in thepg_ripple_httpcompanion service - OpenTelemetry (OTLP) distributed tracing
pg_stat_statementsintegration for query-level statisticsexplain_sparql()withanalyze := truefor interactive query debuggingexplain_datalog()for Datalog inference plan visualizationexplain_inference()for derivation tree inspection
Status
SELECT feature_name, status FROM pg_ripple.feature_status()
WHERE feature_name LIKE '%observ%' OR feature_name LIKE '%explain%' OR feature_name LIKE '%monitor%';
Prometheus Metrics
The pg_ripple_http companion service exposes /metrics in Prometheus text format. Key metrics include:
| Metric | Description |
|---|---|
pg_ripple_sparql_queries_total | Total SPARQL queries executed |
pg_ripple_sparql_query_duration_seconds | Histogram of query durations |
pg_ripple_triple_count | Total triples per graph |
pg_ripple_merge_operations_total | Background merge operations |
pg_ripple_dictionary_cache_hits_total | Dictionary LRU cache hit rate |
pg_ripple_construct_rule_firings_total | CONSTRUCT writeback rule invocations |
pg_ripple_datalog_materialize_duration_seconds | Datalog inference wall time |
SQL Functions
| Function | Description |
|---|---|
pg_ripple.explain_sparql(query TEXT, analyze BOOLEAN) → TEXT | JSON explain plan for a SPARQL query |
pg_ripple.explain_datalog(rule_set TEXT) → TEXT | Execution plan for Datalog rule set |
pg_ripple.explain_inference(rule_set TEXT, triple TEXT) → TEXT | Derivation tree for a specific triple |
pg_ripple.feature_status() → SETOF record | Current status of all implemented features |
OpenTelemetry Tracing
When pg_ripple.otlp_endpoint is set, spans are exported for each SPARQL
query execution. The traceparent header from pg_ripple_http is propagated
to the extension for end-to-end traces.
Query Debugging
explain_sparql(query, analyze := true) executes the query and returns a JSON
object containing:
- The SPARQL algebra tree (post-optimization)
- The generated SQL plan (from
EXPLAIN ANALYZE) - Per-step row counts and actual vs. estimated rows
- Total wall time
Related Pages
PostgreSQL Structured Logging (OBS-03, v0.91.0)
pg_ripple uses pgrx::log! and pgrx::warning! for all diagnostic output inside the
PostgreSQL extension. These emit standard PostgreSQL log messages that appear in the
PostgreSQL server log.
When log_destination = jsonlog is active (PostgreSQL 15+), PostgreSQL natively serialises
all log messages — including those from pg_ripple — as JSON objects in the following format:
{
"timestamp": "2026-05-03 10:00:00.123 UTC",
"pid": 12345,
"session_id": "abc123",
"log_level": "LOG",
"message": "pg_ripple merge worker: merged 10000 rows into vp_42_main"
}
No duplicate fields: pg_ripple does not emit its own JSON log wrapper; all structured
field mapping is handled by PostgreSQL's native log_destination = jsonlog facility.
There is no risk of double-serialisation.
To enable JSON logs in PostgreSQL, add to postgresql.conf:
log_destination = 'jsonlog'
logging_collector = on
log_directory = 'pg_log'
Alternatively, for combined text + JSON output:
log_destination = 'stderr,jsonlog'
Query Optimization Reference
This page is the reference for pg_ripple's SPARQL query optimizer.
Overview
pg_ripple applies multiple optimization passes before generating SQL from SPARQL algebra:
- sparopt algebra optimizer: first-pass algebra rewriting (variable substitution, filter pushdown, DISTINCT elimination with SHACL hints).
- Self-join elimination: star patterns on the same subject are collapsed into single-scan plans with multiple joins.
- Filter pushdown: FILTER constants are encoded to
BIGINTat translation time so comparisons happen on integer values, not strings. - SHACL hints:
sh:maxCount 1predicates omit DISTINCT;sh:minCount 1predicates use INNER JOIN instead of LEFT JOIN. - Plan cache: compiled SQL is stored in
_pg_ripple.plan_cacheand reused across identical queries (keyed by query text + current role + relevant GUCs). - TopN push-down:
ORDER BY ... LIMIT Npatterns are pushed into subqueries to avoid sorting full result sets. - Leapfrog TrieJoin (WCOJ): worst-case optimal join planning for cyclic SPARQL graph patterns.
Status
SELECT feature_name, status FROM pg_ripple.feature_status()
WHERE feature_name LIKE '%plan%' OR feature_name LIKE '%cache%' OR feature_name LIKE '%wcoj%';
Plan Cache
The plan cache avoids re-compiling SPARQL→SQL for repeated queries. Key details:
- Cache key: SHA-256 of (query text, current_role, GUC snapshot)
- Cache invalidated on: VP promotion (schema change), extension upgrade,
plan_cache_reset() - Maximum entries:
pg_ripple.plan_cache_size(default: 512) - Eviction policy: LRU
-- Inspect cache
SELECT * FROM _pg_ripple.plan_cache;
-- Manual invalidation
SELECT pg_ripple.plan_cache_reset();
Property Path Optimization
Property paths compile to WITH RECURSIVE … CYCLE queries using PostgreSQL 18's
native CYCLE clause for hash-based cycle detection. Bounded depth paths
({n,m}) use iterative CTEs limited to pg_ripple.max_property_path_depth
hops (default: 20). Early fixpoint termination avoids iterating past convergence
for bounded hierarchies.
Magic Sets (Goal-Directed Inference)
When using pg_ripple.query_goal() for Datalog queries, magic sets
transformation rewrites rules to focus inference on the bindings needed to
answer the goal, avoiding full forward-chaining materialization.
SQL Functions
| Function | Description |
|---|---|
pg_ripple.plan_cache_reset() → void | Invalidate all cached query plans |
pg_ripple.explain_sparql(query TEXT, analyze BOOLEAN) → TEXT | Inspect query plan with optional live stats |
Related Pages
Vector Search Reference
This page is the reference for pg_ripple's vector + SPARQL hybrid search.
Overview
pg_ripple integrates with pgvector to provide semantic similarity search over
RDF entities. Entity embeddings are stored in _pg_ripple.embeddings with
HNSW or IVFFlat indices. The pg:similar() SPARQL function queries the
vector index and returns results ranked by cosine similarity.
Hybrid retrieval combines vector similarity ranking with SPARQL graph constraints, enabling queries like: "find entities semantically similar to X that also satisfy SHACL shape Y".
Status
SELECT feature_name, status FROM pg_ripple.feature_status()
WHERE feature_name LIKE '%vector%' OR feature_name LIKE '%embed%' OR feature_name LIKE '%kge%';
SQL Functions
| Function | Description |
|---|---|
pg_ripple.embed_entities(graph_iri TEXT, model TEXT) → BIGINT | Bulk-embed all entities in a graph |
pg_ripple.similar_entities(iri TEXT, k INT, model TEXT) → SETOF TEXT | Find k nearest-neighbor entities by embedding |
pg_ripple.suggest_sameas(iri TEXT, k INT) → SETOF TEXT | Suggest owl:sameAs candidates via cosine similarity |
SPARQL pg:similar() Function
Use the pg:similar() extension function inside SPARQL queries for inline
vector search:
PREFIX pg: <http://pg_ripple.io/fn/>
SELECT ?entity ?score WHERE {
?entity pg:similar("machine learning", 10) ?score .
?entity a <http://example.org/Paper> .
}
ORDER BY DESC(?score)
Embedding Models
Embeddings are generated via the configured LLM embedding endpoint. Each
entity-model pair is stored once in _pg_ripple.embeddings. The incremental
embedding worker runs in the background and embeds new entities as they are
inserted.
Knowledge Graph Embeddings (KGE)
Graph-structure embeddings (TransE, RotatE) are computed by src/kge.rs
and stored alongside text embeddings. KGE embeddings capture structural
relationship patterns and complement text-based semantic similarity.
Index Configuration
| GUC | Default | Description |
|---|---|---|
pg_ripple.vector_index_type | 'hnsw' | Index type: hnsw or ivfflat |
pg_ripple.hnsw_m | 16 | HNSW M parameter |
pg_ripple.hnsw_ef_construction | 64 | HNSW ef_construction parameter |
pg_ripple.vector_dimensions | 1536 | Embedding vector dimensions |
Related Pages
Development Reference
This page provides a reference for contributors and extension developers working on pg_ripple.
Overview
pg_ripple is written in Rust using the pgrx 0.18 framework for PostgreSQL 18.
The extension uses safe Rust exclusively (except at required FFI boundaries),
exposes SQL functions via #[pg_extern], and uses pgrx::SpiClient for all
SQL executed inside extension code.
Build and Test
# Install pgrx against PG18
cargo pgrx init --pg18 $(which pg18)
# Run all unit and integration tests
cargo pgrx test pg18
# Run pg_regress suite
cargo pgrx regress pg18 --postgresql-conf "allow_system_table_mods=on"
# Run the migration chain test
bash tests/test_migration_chain.sh
# Format code
cargo fmt --all
# Lint (zero warnings required)
cargo clippy --features pg18 -- -D warnings
# Install locally
cargo pgrx install --pg-config $(which pg_config)
Project Layout
| Path | Description |
|---|---|
src/lib.rs | Extension entry points, _PG_init, GUC parameters |
src/dictionary/ | IRI/blank-node/literal → i64 encoder (XXH3-128 + LRU) |
src/storage/ | VP tables, HTAP delta/main partitions, merge background worker |
src/sparql/ | SPARQL text → spargebra algebra → SQL → SPI execution → decode |
src/construct_rules/ | CONSTRUCT writeback rules |
src/datalog/ | Datalog rule parser, stratifier, SQL compiler |
src/shacl/ | SHACL shapes → DDL constraints + async validation |
src/export/ | Turtle / N-Triples / JSON-LD serialization |
src/stats/ | Monitoring, pg_stat_statements integration |
src/admin/ | vacuum, reindex, prefix registry |
pg_ripple_http/ | Axum HTTP companion service |
sql/ | Extension SQL and migration scripts |
tests/pg_regress/ | pg_regress test suite |
Code Conventions
- Safe Rust only:
unsafeis permitted solely at required FFI boundaries with a// SAFETY:comment. - SQL functions: expose via
#[pg_extern], never rawPG_FUNCTION_INFO_V1. - Database access: use
pgrx::SpiClientfor all SQL inside extension code. - Error messages: follow PostgreSQL style (lowercase first word, no trailing period).
- Integer joins: all SPARQL-to-SQL translation must encode bound terms to
i64before SQL generation; string comparisons in VP queries are a bug.
Migration Scripts
Every release requires a sql/pg_ripple--X.Y.Z--X.Y.(Z+1).sql migration
script. If there are no schema changes, add a comment header explaining what
new functions/GUCs are provided.
# Example: create migration script for v0.74.0
cat > sql/pg_ripple--0.73.0--0.74.0.sql << 'EOF'
-- Migration 0.73.0 → 0.74.0
-- Schema changes: <describe here>
EOF
See Release Process for the full release checklist.
Testing Conventions
- Unit tests:
#[pg_test]in the same file as the implementation - Integration tests:
tests/pg_regress/sql/*.sqlfiles - Each version milestone should have a
v0XX_features.sqlregression file - Expected output:
tests/pg_regress/expected/*.out
Related Pages
SPARQL Compliance Matrix
pg_ripple implements the full SPARQL 1.1 specification suite. This page details conformance status for every feature in the W3C SPARQL 1.1 Query, Update, and Protocol recommendations.
As of v0.46.0, pg_ripple passes 100% of the W3C SPARQL 1.1 test suite (~3 000 tests), ≥ 99.9% of the Apache Jena edge-case suite (~1 000 tests), all 100 WatDiv query templates at 10 M-triple scale with correctness validated to ±0.1% row-count baselines, all 14 LUBM queries with OWL RL inference correctness, and ≥ 80% of the W3C OWL 2 RL conformance suite.
SPARQL 1.1 Query — Query Forms
| Feature | Status | Since | Notes |
|---|---|---|---|
SELECT | ✅ Supported | v0.1.0 | Full projection with expressions |
CONSTRUCT | ✅ Supported | v0.8.0 | Returns triples as JSON, Turtle, or JSON-LD |
ASK | ✅ Supported | v0.8.0 | Returns boolean |
DESCRIBE | ✅ Supported | v0.8.0 | Symmetric concise bounded description |
SPARQL 1.1 Query — Algebra Operations
| Feature | Status | Since | Notes |
|---|---|---|---|
| Basic Graph Pattern (BGP) | ✅ Supported | v0.1.0 | Translated to VP table joins |
| Join (inner) | ✅ Supported | v0.1.0 | |
LeftJoin (OPTIONAL) | ✅ Supported | v0.1.0 | Downgraded to INNER JOIN when SHACL sh:minCount 1 is set |
| Filter | ✅ Supported | v0.1.0 | All comparison, logical, and arithmetic operators |
| Union | ✅ Supported | v0.5.0 | UNION ALL in generated SQL |
| Minus | ✅ Supported | v0.5.0 | EXCEPT in generated SQL |
Extend (BIND) | ✅ Supported | v0.1.0 | |
Group (GROUP BY) | ✅ Supported | v0.5.0 | |
| Having | ✅ Supported | v0.5.0 | |
| OrderBy | ✅ Supported | v0.1.0 | |
| Project | ✅ Supported | v0.1.0 | |
| Distinct | ✅ Supported | v0.1.0 | Omitted when SHACL sh:maxCount 1 is set |
| Reduced | ✅ Supported | v0.5.0 | Treated as hint; may or may not deduplicate |
Slice (LIMIT/OFFSET) | ✅ Supported | v0.1.0 | |
Service (SERVICE) | ✅ Supported | v0.16.0 | Federated query via HTTP |
Service Silent (SERVICE SILENT) | ✅ Supported | v0.16.0 | Returns empty on endpoint failure |
Values (VALUES) | ✅ Supported | v0.5.0 | Inline data bindings |
Lateral (LATERAL) | ✅ Supported | v0.22.0 | PostgreSQL LATERAL JOIN |
| Subqueries | ✅ Supported | v0.5.0 | Nested SELECT |
Negation (NOT EXISTS) | ✅ Supported | v0.5.0 | |
Negation (EXISTS) | ✅ Supported | v0.5.0 |
SPARQL 1.1 Query — Property Paths
| Feature | Status | Since | Notes |
|---|---|---|---|
Sequence path (/) | ✅ Supported | v0.5.0 | |
Alternative path (|) | ✅ Supported | v0.5.0 | |
Inverse path (^) | ✅ Supported | v0.5.0 | |
Zero-or-more (*) | ✅ Supported | v0.5.0 | WITH RECURSIVE … CYCLE |
One-or-more (+) | ✅ Supported | v0.5.0 | WITH RECURSIVE … CYCLE |
Zero-or-one (?) | ✅ Supported | v0.5.0 | |
Negated property set (!(p1|p2)) | ✅ Supported | v0.5.0 | |
Fixed-length path ({n}) | ✅ Supported | v0.5.0 | Unrolled to n joins |
Variable-length path ({n,m}) | ✅ Supported | v0.5.0 | Bounded recursion |
All recursive property paths use PostgreSQL 18's native CYCLE clause for hash-based cycle detection, bounded by pg_ripple.max_path_depth (default: 10).
SPARQL 1.1 Query — Aggregates
| Feature | Status | Since | Notes |
|---|---|---|---|
COUNT | ✅ Supported | v0.5.0 | Including COUNT(DISTINCT *) |
SUM | ✅ Supported | v0.5.0 | |
AVG | ✅ Supported | v0.5.0 | |
MIN | ✅ Supported | v0.5.0 | |
MAX | ✅ Supported | v0.5.0 | |
GROUP_CONCAT | ✅ Supported | v0.5.0 | With custom separator |
SAMPLE | ✅ Supported | v0.5.0 |
SPARQL 1.1 Query — Built-in Functions
| Function | Status | Since |
|---|---|---|
STR() | ✅ Supported | v0.1.0 |
LANG() | ✅ Supported | v0.3.0 |
DATATYPE() | ✅ Supported | v0.3.0 |
IRI() / URI() | ✅ Supported | v0.5.0 |
BNODE() | ✅ Supported | v0.5.0 |
RAND() | ✅ Supported | v0.5.0 |
ABS() | ✅ Supported | v0.1.0 |
CEIL() | ✅ Supported | v0.1.0 |
FLOOR() | ✅ Supported | v0.1.0 |
ROUND() | ✅ Supported | v0.1.0 |
CONCAT() | ✅ Supported | v0.5.0 |
STRLEN() | ✅ Supported | v0.1.0 |
UCASE() | ✅ Supported | v0.1.0 |
LCASE() | ✅ Supported | v0.1.0 |
ENCODE_FOR_URI() | ✅ Supported | v0.5.0 |
CONTAINS() | ✅ Supported | v0.1.0 |
STRSTARTS() | ✅ Supported | v0.1.0 |
STRENDS() | ✅ Supported | v0.1.0 |
STRBEFORE() | ✅ Supported | v0.5.0 |
STRAFTER() | ✅ Supported | v0.5.0 |
YEAR() | ✅ Supported | v0.5.0 |
MONTH() | ✅ Supported | v0.5.0 |
DAY() | ✅ Supported | v0.5.0 |
HOURS() | ✅ Supported | v0.5.0 |
MINUTES() | ✅ Supported | v0.5.0 |
SECONDS() | ✅ Supported | v0.5.0 |
TIMEZONE() | ✅ Supported | v0.5.0 |
TZ() | ✅ Supported | v0.5.0 |
NOW() | ✅ Supported | v0.5.0 |
UUID() | ✅ Supported | v0.5.0 |
STRUUID() | ✅ Supported | v0.5.0 |
MD5() | ✅ Supported | v0.5.0 |
SHA1() | ✅ Supported | v0.5.0 |
SHA256() | ✅ Supported | v0.5.0 |
SHA384() | ✅ Supported | v0.5.0 |
SHA512() | ✅ Supported | v0.5.0 |
COALESCE() | ✅ Supported | v0.1.0 |
IF() | ✅ Supported | v0.1.0 |
STRLANG() | ✅ Supported | v0.5.0 |
STRDT() | ✅ Supported | v0.5.0 |
isIRI() / isURI() | ✅ Supported | v0.1.0 |
isBlank() | ✅ Supported | v0.1.0 |
isLiteral() | ✅ Supported | v0.1.0 |
isNumeric() | ✅ Supported | v0.5.0 |
REGEX() | ✅ Supported | v0.1.0 |
REPLACE() | ✅ Supported | v0.5.0 |
SUBSTR() | ✅ Supported | v0.5.0 |
BOUND() | ✅ Supported | v0.1.0 |
IN / NOT IN | ✅ Supported | v0.5.0 |
TRIPLE() (RDF-star) | ✅ Supported | v0.4.0 |
SUBJECT() (RDF-star) | ✅ Supported | v0.4.0 |
PREDICATE() (RDF-star) | ✅ Supported | v0.4.0 |
OBJECT() (RDF-star) | ✅ Supported | v0.4.0 |
isTRIPLE() (RDF-star) | ✅ Supported | v0.4.0 |
SPARQL 1.1 Query — Typed Literals
| Datatype | Status | Notes |
|---|---|---|
xsd:integer | ✅ Supported | Maps to PostgreSQL BIGINT |
xsd:decimal | ✅ Supported | Maps to NUMERIC |
xsd:float | ✅ Supported | Maps to REAL |
xsd:double | ✅ Supported | Maps to DOUBLE PRECISION |
xsd:boolean | ✅ Supported | Maps to BOOLEAN |
xsd:string | ✅ Supported | Default literal type |
xsd:dateTime | ✅ Supported | Maps to TIMESTAMPTZ |
xsd:date | ✅ Supported | Maps to DATE |
xsd:time | ✅ Supported | Maps to TIME |
xsd:gYear | ✅ Supported | Stored as string, compared lexically |
| Language-tagged strings | ✅ Supported | "text"@en syntax |
SPARQL 1.1 Update
| Operation | Status | Since | Notes |
|---|---|---|---|
INSERT DATA | ✅ Supported | v0.7.0 | |
DELETE DATA | ✅ Supported | v0.7.0 | |
DELETE WHERE | ✅ Supported | v0.7.0 | |
DELETE/INSERT WHERE | ✅ Supported | v0.7.0 | |
INSERT WHERE | ✅ Supported | v0.7.0 | |
LOAD | ✅ Supported | v0.7.0 | Via pg_ripple_http or direct file |
CLEAR GRAPH | ✅ Supported | v0.7.0 | |
CLEAR DEFAULT | ✅ Supported | v0.7.0 | |
CLEAR NAMED | ✅ Supported | v0.7.0 | |
CLEAR ALL | ✅ Supported | v0.7.0 | |
DROP GRAPH | ✅ Supported | v0.7.0 | |
DROP DEFAULT | ✅ Supported | v0.7.0 | |
DROP NAMED | ✅ Supported | v0.7.0 | |
DROP ALL | ✅ Supported | v0.7.0 | |
CREATE GRAPH | ✅ Supported | v0.7.0 | |
CREATE SILENT GRAPH | ✅ Supported | v0.7.0 | |
COPY | ✅ Supported | v0.21.0 | |
MOVE | ✅ Supported | v0.21.0 | |
ADD | ✅ Supported | v0.21.0 | |
Multi-statement (; separator) | ✅ Supported | v0.7.0 | |
USING / USING NAMED | ✅ Supported | v0.7.0 | Dataset clause for updates |
SPARQL 1.1 Protocol
| Feature | Status | Notes |
|---|---|---|
| Query via HTTP GET | ✅ Supported | Via pg_ripple_http |
| Query via HTTP POST (form-encoded) | ✅ Supported | Via pg_ripple_http |
| Query via HTTP POST (direct body) | ✅ Supported | Via pg_ripple_http |
| Update via HTTP POST | ✅ Supported | Via pg_ripple_http |
Content negotiation (Accept header) | ✅ Supported | JSON, Turtle, N-Triples, XML |
default-graph-uri parameter | ✅ Supported | |
named-graph-uri parameter | ✅ Supported | |
Multiple default-graph-uri | ✅ Supported | |
Multiple named-graph-uri | ✅ Supported |
SPARQL Protocol support requires the pg_ripple_http companion service. See APIs and Integration for setup instructions.
SPARQL 1.1 Service Description
| Feature | Status | Notes |
|---|---|---|
| Service description at endpoint root | ✅ Supported | Via pg_ripple_http |
sd:supportedLanguage | ✅ Supported | Reports SPARQL 1.1 Query and Update |
sd:resultFormat | ✅ Supported | JSON, XML, CSV, TSV |
sd:defaultDataset | ✅ Supported | |
sd:feature | ✅ Supported | Reports sd:UnionDefaultGraph, sd:RequiresDataset |
SPARQL 1.1 Graph Store HTTP Protocol
| Operation | Status | Notes |
|---|---|---|
GET (retrieve graph) | ✅ Supported | Via pg_ripple_http |
PUT (replace graph) | ✅ Supported | Via pg_ripple_http |
POST (merge into graph) | ✅ Supported | Via pg_ripple_http |
DELETE (drop graph) | ✅ Supported | Via pg_ripple_http |
?default parameter | ✅ Supported | |
?graph=<uri> parameter | ✅ Supported |
RDF-star / SPARQL-star
| Feature | Status | Since | Notes |
|---|---|---|---|
| Quoted triple storage | ✅ Supported | v0.4.0 | qt_s, qt_p, qt_o dictionary columns |
| Quoted triple in BGP | ✅ Supported | v0.4.0 | Ground patterns only |
TRIPLE() constructor | ✅ Supported | v0.4.0 | |
SUBJECT(), PREDICATE(), OBJECT() | ✅ Supported | v0.4.0 | |
isTRIPLE() | ✅ Supported | v0.4.0 | |
| Annotation syntax (`{ | }`) | ✅ Supported |
Extensions Beyond W3C
pg_ripple extends the SPARQL standard with additional capabilities:
| Feature | Notes |
|---|---|
pg:similar() custom function | Vector similarity within SPARQL FILTER |
pg:fts() custom function | Full-text search within SPARQL FILTER |
pg:embed() custom function | Inline embedding generation |
| Datalog-materialized predicates | Inferred triples queryable via standard SPARQL |
| SHACL-optimized query plans | Cardinality hints from SHACL shapes |
| Plan cache | Compiled SQL plans cached across queries |
Known Limitations
| Feature | Status | Notes |
|---|---|---|
langMatches() | ⚠️ Partial | Returns 0 rows; full BCP 47 matching planned |
| Custom aggregate extensions | ❌ Not supported | Standard aggregates fully supported |
Variable-in-quoted-triple << ?s ?p ?o >> | ⚠️ Partial | Returns 0 rows with WARNING; ground patterns work |
LOAD <url> from arbitrary HTTP | ⚠️ Depends | Requires pg_ripple_http or server-side file |
DESCRIBE strategy customization | ✅ Supported | Four strategies via GUC (v0.55.0) |
Multiple result formats for SELECT | ⚠️ Partial | JSON primary; XML/CSV/TSV via pg_ripple_http only |
DESCRIBE Strategy Reference (SC13-04, v0.86.0 — supersedes v0.55.0)
pg_ripple supports three DESCRIBE algorithms selectable via the pg_ripple.describe_form
GUC (default: cbd), introduced in v0.86.0. The older pg_ripple.describe_strategy GUC is
deprecated (see deprecated-gucs.md) and will be removed in v1.0.0.
cbd — Concise Bounded Description (default)
Returns all triples where the described resource appears as subject, plus all triples reachable by following blank-node objects recursively. This is the minimal W3C-defined DESCRIBE semantics.
SET pg_ripple.describe_form = 'cbd';
SELECT * FROM pg_ripple.sparql('DESCRIBE <https://example.org/Alice>');
scbd — Symmetric Concise Bounded Description
Extends CBD by also including all triples where the described resource appears as object. This captures both outgoing and incoming edges. Suitable when you need the full neighbourhood of a resource.
SET pg_ripple.describe_form = 'scbd';
SELECT * FROM pg_ripple.sparql('DESCRIBE <https://example.org/Alice>');
symmetric — Permanent Alias for scbd (CB-10, v0.92.0)
symmetric is a permanent alias for scbd for readability:
SET pg_ripple.describe_form = 'symmetric';
SELECT * FROM pg_ripple.sparql('DESCRIBE <https://example.org/Alice>');
Alias contract: The alias
symmetric=scbdis guaranteed stable for the pg_ripple 1.x API line. It will not be removed without a deprecation cycle and will only diverge fromscbdsemantics if the W3C SCBD and "symmetric extension" definitions separate in a future RDF standard. This guarantee was formally documented in v0.92.0.
Choosing a Form
| Form | Outgoing edges | Incoming edges | Blank-node closure | Speed |
|---|---|---|---|---|
cbd | ✅ | ❌ | ✅ | Medium |
scbd / symmetric | ✅ | ✅ | ✅ | Slower |
The GUC can be set at the session or transaction level:
-- Session-level
SET pg_ripple.describe_form = 'scbd';
-- Transaction-level
BEGIN;
SET LOCAL pg_ripple.describe_form = 'cbd';
SELECT * FROM pg_ripple.sparql('DESCRIBE <https://example.org/Bob>');
COMMIT;
Migration note: Replace
SET pg_ripple.describe_strategy = 'simple'withSET pg_ripple.describe_form = 'cbd'(thesimplestrategy is now the CBD default). The strategyscbdmaps directly todescribe_form = 'scbd'.
Blank-Node Limitations in RDF-star Quoted Triples (C13-03, v0.85.0)
Blank nodes inside RDF-star quoted triples (e.g., << _:b1 :p :o >>) do not
have a canonical round-trip form in pg_ripple. When an anonymous blank node
appears as the subject or object of a quoted triple, encoding and then decoding
the same triple may produce a different blank-node label.
Workaround: Use named IRIs or well-known blank-node identifiers (e.g., _:b1
with a stable label) as subjects/objects of quoted triples. Alternatively, avoid
blank nodes entirely in the subject/object positions of << >> patterns.
Impact: This limitation only affects blank-node-in-quoted-triple patterns. Regular blank nodes in non-quoted triples round-trip correctly.
GRAPH ?g Default-Graph Exclusion (C13-06, v0.85.0)
Per SPARQL 1.1 specification §8.3, when GRAPH ?g { ... } is used, the
variable ?g is bound only to **named variable ?g is bound only to **named variable ?g is bound only to **named variable This is conformavariable ?gis bound only to **named variable?gis bound only to wivariable?gis bound only to **named variable?gis bound only to **na ?pvariable?gis bound only to **named variable?gis bound only to **naatvariable?gis bound only to **named variable?g` is bound only to **named ns triples.
To query the default graph specifically, use:
WHERE nd```````````````````````````````````````````````````````````````````````valu````````````````````````````````````````````````````````````````````````````cis``````````````````````````````````````````````````````````````truncate```````````````````````````````````````````````````seconds)
when serializing xsd:dateTime literals back to N-Triples format.
Example:
# Input: "2024-01-01T12:00:00.123456789Z"^^xsd:dateTime
# Stored: 2024-01-01 12:00:00.123457+00 (rounded to microseconds by PG)
# Output: "2024-01-01T12:00:00.123"^^xsd:dateTime (truncated to 3 decimal places)
Sub-millisecond precision is silently dropped in the output. If you require sub-millisecond precision, store the value as a plain string literal and perform comparisons manually.
RDF 1.2 / SPARQL-star Compliance Matrix (STD-02, v0.91.0)
pg_ripple supports RDF-star via the oxrdf 0.3 data model and the qt_s/qt_p/qt_o
dictionary columns introduced in v0.4.0. The table below maps each RDF 1.2 / SPARQL-star
feature to its implementation status.
| Feature | Status | Since | Notes |
|---|---|---|---|
<< s p o >> in subject position (BGP) | ✅ Implemented | v0.4.0 | Stored via qt_s/qt_p/qt_o dictionary columns |
<< s p o >> in BIND | ✅ Implemented | v0.16.0 | Full expression support |
<< s p o >> in FILTER | ✅ Implemented | v0.16.0 | Comparison and isTriple() |
<< s p o >> in CONSTRUCT | ✅ Implemented | v0.16.0 | Emitted as Turtle-star |
<< s p o >> in SELECT projections | ✅ Implemented | v0.16.0 | |
| Annotation syntax `{ | p o | }` | ⚠ Partial |
TRIPLE(s, p, o) constructor function | ❌ Not implemented | — | Depends on spargebra SPARQL 1.2 grammar update |
SUBJECT() / PREDICATE() / OBJECT() destructuring | ❌ Not implemented | — | Dictionary join required; planned post-spargebra-1.2 |
REIF keyword (RDF 1.2 reification syntax) | ❌ Not started | — | spargebra grammar update required |
isTriple() function | ✅ Implemented | v0.16.0 | Returns true for quoted-triple subjects |
Overall: pg_ripple's RDF-star foundation (v0.4.0) covers the most widely-used SPARQL-star
patterns. Remaining gaps depend on spargebra 0.x adopting the SPARQL 1.2 grammar and are
tracked as post-v1.0.0 work (see SPARQL 1.2 tracking).
<< >> Position Support Matrix (L15-08, v0.97.0)
This table summarises which positions a quoted triple pattern << s p o >> can appear in
across all SPARQL 1.1 query and update forms. Use this matrix when writing queries that
combine RDF-star with other features.
<< s p o >> position | Status | Since | Notes |
|---|---|---|---|
| Subject position in BGP | ✅ Supported | v0.4.0 | Stored via qt_s/qt_p/qt_o dictionary columns |
| Object position in BGP | ✅ Supported | v0.4.0 | |
Subject of quoted triple in BIND | ✅ Supported | v0.16.0 | Full expression support |
Nested quoted triples << << s p o >> p2 o2 >> | ✅ Supported | v0.4.0 | Dictionary recursion |
In FILTER expressions | ✅ Supported | v0.16.0 | isTriple(), comparison operators |
In CONSTRUCT head | ✅ Supported | v0.16.0 | Emitted as Turtle-star |
In SELECT projections | ✅ Supported | v0.16.0 | |
In INSERT DATA | ✅ Supported | v0.16.0 | Written to qt_s/qt_p/qt_o columns |
In DELETE DATA | ✅ Supported | v0.16.0 | Matched and deleted via dictionary lookup |
Variable-in-quoted-triple << ?s ?p ?o >> | ⚠️ Partial | v0.16.0 | Returns 0 rows with WARNING; ground patterns work |
Blank nodes inside << >> | ⚠️ Partial | v0.4.0 | No document-scope isolation; avoid in multi-load scenarios |
In VALUES clause | ✅ Supported | v0.25.0 | Ground quoted triples only |
In GRAPH clause | ✅ Supported | v0.4.0 | Named graph + quoted triple combination |
In UNION branches | ✅ Supported | v0.16.0 | |
In OPTIONAL | ✅ Supported | v0.16.0 | |
| In property paths | ❌ Not supported | — | Path expressions inside << >> are not yet handled |
In SERVICE (federation) | ✅ Supported | v0.4.0 | Forwarded as-is to remote endpoint |
In GROUP BY / HAVING | ✅ Supported | v0.16.0 | |
In ORDER BY | ✅ Supported | v0.16.0 | Lexicographic order on serialised form |
SPARQL 1.2 Property Path Status
This page documents the execution status of all eight SPARQL 1.2 property path algebra operators as implemented in pg_ripple v0.124.0.
As of v0.124.0, pg_ripple executes all eight PropertyPathExpression variants
defined by spargebra 0.4.6 (features sparql-12 + sep-0006). A
PATH-BNODE-01 bug was fixed in this release: sequence paths decomposed by
spargebra into two GraphPattern::Path nodes connected by an anonymous blank
node now produce correct INNER JOINs instead of Cartesian products.
Operator coverage table
| SPARQL syntax | spargebra variant | SQL strategy | Status | Since |
|---|---|---|---|---|
<p> | NamedNode | Direct VP table scan | ✅ Full | v0.5.0 |
^p | Reverse | SELECT o AS s, s AS o swap | ✅ Full | v0.5.0 |
a/b | Sequence | Subquery INNER JOIN on mid-node | ✅ Full | v0.5.0 |
a|b | Alternative | UNION ALL | ✅ Full | v0.5.0 |
p+ | OneOrMore | WITH RECURSIVE + CYCLE (PG 18) | ✅ Full | v0.5.0 |
p* | ZeroOrMore | WITH RECURSIVE + CYCLE + zero-hop UNION ALL | ✅ Full | v0.5.0 |
p? | ZeroOrOne | Direct UNION ALL with identity row | ✅ Full | v0.5.0 |
!(p1|p2) | NegatedPropertySet | vp_rare WHERE p NOT IN (...) | ✅ Full | v0.5.0 |
Bug fix: PATH-BNODE-01 — Sequence paths via blank node join (v0.124.0)
Root cause. The spargebra/sparopt optimizer sometimes decomposes a Sequence
path expression such as hop*/hop into two separate GraphPattern::Path algebra
nodes connected by an anonymous blank node:
{ <a> (hop)* _:b0 . _:b0 hop ?x . }
Before v0.124.0, the GraphPattern::Path translator in sqlgen.rs only handled
TermPattern::Variable for subject/object binding. Blank nodes were silently
ignored, so the two path subqueries were joined by a Cartesian product in the SQL
FROM clause instead of an INNER JOIN on the shared column. This produced
N × M duplicate rows (e.g. 30 rows instead of 5 for a 5-hop chain).
Fix. The path translator now calls bgp::bind_term() for both subject and
object, which handles Variable, BlankNode, NamedNode, and Literal
uniformly. When the blank node appears in both path fragments, Fragment::merge
adds the join condition (e.g. _t0.o = _t1.s).
Affected patterns (examples).
| SPARQL path | Decomposed by spargebra | Before fix | After fix |
|---|---|---|---|
hop*/hop | hop* _:b . _:b hop | 30 rows (×6) | 5 rows ✅ |
hop?/hop | hop? _:b . _:b hop | 10 rows (×5) | 2 rows ✅ |
^hop/!hop | ^hop _:b . _:b !hop | 6 rows (×2) | 3 rows ✅ |
Regression test coverage
The regression test suite includes two dedicated test files added in v0.124.0:
| File | Tests | Coverage |
|---|---|---|
tests/pg_regress/sql/sparql12_property_paths.sql | 20 | All 8 operators + 5 compound combinations |
tests/pg_regress/sql/sparql12_owl_chain_nhop.sql | 5 | OWL owl:propertyChainAxiom n=4 and n=5 hop chains |
All 25 tests pass as of v0.124.0.
Known limitations
| Limitation | Ticket | Workaround |
|---|---|---|
GRAPH ?g { path } with nested recursive CTEs may not propagate graph context through all hops | PATH-G-01 | Use named graph filter: GRAPH <uri> { path } |
Max path depth is controlled by pg_ripple.max_path_depth GUC (default 100) | — | Increase GUC for very deep graphs |
W3C Conformance
This page summarises pg_ripple's conformance status against the W3C SPARQL 1.1, Apache Jena, SHACL Core, WatDiv, and LUBM test suites.
As of v0.41.0, conformance is measured by integrated test harnesses that run in CI on every push to main. Pass rates are published as the conformance_report artifact on the Actions page.
Test suites
pg_ripple runs four complementary conformance suites:
| Suite | Tests | What it validates |
|---|---|---|
| W3C SPARQL 1.1 | ~3 000 | Standard conformance on small, well-defined fixtures |
| Apache Jena | ~1 000 | Implementation edge cases (type coercion, date-time, blank-node scoping) |
| WatDiv | 100 templates | Correctness and performance at 10M-triple scale |
| LUBM | 14 queries | OWL RL inference correctness under ontological reasoning (v0.44.0+) |
| OWL 2 RL | ~200 tests | W3C OWL 2 RL entailment, consistency, and inconsistency (v0.46.0+; informational until ≥95%) |
All suites write per-suite results into a unified tests/conformance/report.json artifact.
See Running Conformance Tests for local setup instructions, the WatDiv Results page for performance metrics, and the LUBM Results page for OWL RL conformance details.
W3C SPARQL 1.1 test harness (v0.41.0+)
The test harness (tests/w3c/) runs the official W3C SPARQL 1.1 test suite (~3 000 tests across 13 sub-suites) against a live pg_ripple installation.
Per-category coverage
| Sub-suite | Tests | CI status |
|---|---|---|
| aggregates | ~120 | Required (smoke) |
| bind | ~20 | Informational (full suite) |
| exists | ~20 | Informational (full suite) |
| functions | ~200 | Informational (full suite) |
| grouping | ~40 | Required (smoke) |
| negation | ~20 | Informational (full suite) |
| optional | ~80 | Required (smoke) |
| project-expression | ~10 | Informational (full suite) |
| property-path | ~60 | Informational (full suite) |
| service | ~10 | SKIP (live external endpoints) |
| subquery | ~20 | Informational (full suite) |
| syntax-query | ~300 | Informational (full suite) |
| update | ~200 | Informational (full suite) |
Running locally
# Download test data first (one-time setup):
bash scripts/fetch_conformance_tests.sh --w3c
# Run smoke subset (180 tests, ~30s):
cargo test --test w3c_smoke
# Run full W3C suite (3000+ tests, ~2min with 8 threads):
cargo test --test w3c_suite -- --test-threads 8
Apache Jena test suite (v0.43.0+)
The Jena adapter (tests/jena/) runs ~1 000 tests from Apache Jena's sparql-query, sparql-update, sparql-syntax, and algebra sub-suites. Jena tests cover implementation edge cases that the W3C suite leaves underspecified.
Jena-specific coverage areas
| Area | Tests |
|---|---|
XSD numeric promotions (xsd:integer → xsd:decimal → xsd:double) | sparql-query |
| Mixed-type arithmetic and comparisons | sparql-query |
Timezone-aware xsd:dateTime comparisons | sparql-query |
Date/time built-ins: NOW(), YEAR(), MONTH(), DAY(), HOURS(), MINUTES(), SECONDS(), TZ() | sparql-query |
xsd:decimal arithmetic: ROUND(), CEIL(), FLOOR(), ABS() | sparql-query |
| Blank nodes in CONSTRUCT templates | sparql-query |
| Blank-node identity across OPTIONAL and GRAPH boundaries | sparql-query |
String functions: STRLEN(), SUBSTR(), UCASE(), LCASE(), STRSTARTS(), STRENDS(), CONTAINS(), ENCODE_FOR_URI(), CONCAT() | sparql-query |
| SPARQL UPDATE edge cases | sparql-update |
| Syntax acceptance / rejection (positive/negative syntax tests) | sparql-syntax |
| Algebra normalisation equivalences | algebra |
CI status
The jena-suite CI job is non-blocking until pass rate ≥ 95%, then promoted to required. Known failures for type-coercion and date-time edge cases are tracked in tests/conformance/known_failures.txt with the jena: prefix.
Running locally
# Download Jena test data:
bash scripts/fetch_conformance_tests.sh --jena
# Run the full Jena suite:
cargo test --test jena_suite
SPARQL 1.1 Query
Test suite: W3C SPARQL 1.1 Query test suite (2013-03-27)
Target: ≥ 95% of applicable tests pass.
Supported features
| Feature | Status |
|---|---|
| Basic Graph Patterns (BGP) | Supported |
| FILTER with all comparison and logical operators | Supported |
| OPTIONAL | Supported |
| UNION | Supported |
Subqueries (SELECT … { SELECT … }) | Supported |
| BIND | Supported |
| VALUES | Supported |
Property paths (/, ` | , *, +, ?, ^`) |
| Negated property sets (`!(p1 | p2)`) |
| Aggregates: COUNT, SUM, AVG, MIN, MAX | Supported |
| GROUP BY, HAVING | Supported |
| ORDER BY, LIMIT, OFFSET | Supported |
| DISTINCT | Supported |
| ASK | Supported |
| CONSTRUCT | Supported |
| DESCRIBE | Supported |
Named graphs (GRAPH ?g { … }) | Supported |
Federated query (SERVICE) | Supported (v0.16.0) |
| All XPath/SPARQL built-in functions (STR, STRLEN, UCASE, LCASE, STRSTARTS, STRENDS, CONTAINS, REGEX, ABS, CEIL, FLOOR, ROUND, IF, COALESCE, isIRI, isLiteral, isBlank, DATATYPE, LANG, BIND) | Supported |
| Language-tagged literals (storage and LANG() function) | Supported |
| Typed literals with xsd:integer, xsd:decimal, xsd:double, xsd:dateTime, xsd:boolean | Supported |
| NOT EXISTS | Supported |
| MINUS | Supported |
| RDF-star (quoted triples, SPARQL-star BGP) | Supported (v0.4.0) |
Known limitations
| Feature | Status |
|---|---|
langMatches() function | Not supported. Returns 0 rows without error. Full BCP 47 language tag matching is planned for a future release. |
| Custom aggregate extensions (property functions) | Not supported. Standard aggregates (COUNT, SUM, AVG, MIN, MAX) are fully supported. |
Variable-inside-quoted-triple patterns (<< ?s ?p ?o >>) | Returns 0 rows with a WARNING. Ground quoted-triple patterns work. |
LOAD <url> from arbitrary HTTP URIs | Network-access dependent; supported via pg_ripple_http companion service. |
SPARQL 1.1 Update
Test suite: W3C SPARQL 1.1 Update test suite (2013)
Target: ≥ 95% of applicable tests pass.
Supported features
| Feature | Status |
|---|---|
| INSERT DATA | Supported |
| DELETE DATA | Supported |
| INSERT WHERE | Supported |
| DELETE WHERE | Supported |
| DELETE/INSERT WHERE | Supported |
| CLEAR GRAPH | Supported |
| CREATE GRAPH / DROP GRAPH | Supported |
Multi-statement updates (; separator) | Supported |
| Named graph update operations | Supported |
| Idempotent re-insert (ON CONFLICT DO NOTHING) | Supported |
Known limitations
| Feature | Status |
|---|---|
COPY, MOVE, ADD graph operations | Implemented as no-ops returning 0; full implementation planned for v0.21.0. |
LOAD <url> | Same as for queries above. |
SHACL Core
Test suite: W3C SHACL Core test suite
Target: ≥ 95% of SHACL Core tests pass.
Supported constraints
| Constraint | Status |
|---|---|
sh:targetClass | Supported |
sh:targetNode | Supported |
sh:targetSubjectsOf | Supported |
sh:targetObjectsOf | Supported |
sh:property with sh:path | Supported |
sh:minCount / sh:maxCount | Supported |
sh:datatype | Supported |
sh:pattern (regex) | Supported |
sh:minLength / sh:maxLength | Supported |
sh:minInclusive / sh:maxInclusive | Supported |
sh:minExclusive / sh:maxExclusive | Supported |
sh:in (enumeration) | Supported |
sh:hasValue | Supported |
sh:class | Supported |
sh:nodeKind (IRI, BlankNode, Literal) | Supported |
sh:or | Supported |
sh:and | Supported |
sh:not | Supported |
sh:node (nested shape reference) | Supported |
sh:qualifiedValueShape + sh:qualifiedMinCount / sh:qualifiedMaxCount | Supported |
Async validation pipeline (process_validation_queue) | Supported |
| Sync mode (insert rejection) | Supported |
Known limitations
| Feature | Status |
|---|---|
SHACL Advanced Features (SPARQL-based constraints, sh:SPARQLConstraint) | Deferred to v0.21.0. |
SHACL-AF (rules, sh:TripleRule) | Partial implementation via Datalog; full SHACL-AF integration planned. |
Running the conformance gate
The conformance tests run as part of the standard pg_regress suite:
cargo pgrx regress pg18 --postgresql-conf "allow_system_table_mods=on"
The relevant test files are:
tests/pg_regress/sql/w3c_sparql_query_conformance.sqltests/pg_regress/sql/w3c_sparql_update_conformance.sqltests/pg_regress/sql/w3c_shacl_conformance.sqltests/pg_regress/sql/crash_recovery_merge.sql
Conformance Trends
T13-04 (v0.86.0): this page explains how pg_ripple tracks pass rates across the conformance suites over time.
The raw trend data lives in the repository at
tests/conformance/history.csv.
The CI job conformance-trends appends a row after successful main-branch
builds. This page does not duplicate the CSV inline, because copied trend rows
go stale quickly and can drift from the CI artifact.
Current Release Gates
| Suite | Current CI role | Release expectation |
|---|---|---|
| W3C SPARQL 1.1 | Smoke subset is blocking; full suite is informational | Smoke subset must pass |
| Apache Jena | Informational until the project consistently clears the threshold | Target pass rate ≥ 95% |
| WatDiv | Correctness and performance signal | Non-blocking, reviewed for regressions |
| LUBM | OWL RL reasoning gate | Required |
| OWL 2 RL | Entailment conformance signal | Informational until ≥ 95% pass rate |
Suite Descriptions
| Suite | Tests | Required Pass Rate | Notes |
|---|---|---|---|
| W3C SPARQL 1.1 | ~357 (smoke subset) | 100% (smoke); informational (full) | Smoke subset is a CI gate |
| Apache Jena | ~1,021 | ≥ 95% | Non-blocking until threshold met |
| WatDiv | 100 templates | Correctness + performance | Non-blocking |
| LUBM | 14 OWL RL queries | 100% | Required CI gate |
| OWL 2 RL | ~258 | ≥ 95% | Informational until threshold met |
CI Badges (v0.122.0)
The Apache Jena pass rate is published as a CI badge after each main-branch
build. The jena-suite CI job writes the generated badge payload to
results/jena-badge.json in the workflow artifact; the file may not exist in a
fresh local checkout until that job has run.
The badge data format follows the shields.io endpoint schema:
{
"schemaVersion": 1,
"label": "Jena",
"message": "99% pass",
"color": "green"
}
Color thresholds: green ≥ 95%, yellow ≥ 90%, orange ≥ 80%, red < 80%.
How to Regenerate
cargo pgrx test pg18 2>&1 | scripts/parse_conformance_results.py >> tests/conformance/history.csv
Or run the full conformance test suite locally:
cargo pgrx regress pg18 --postgresql-conf "allow_system_table_mods=on"
Jena Conformance Suite Results
The Apache Jena SPARQL 1.1 conformance suite (approximately 1,000 tests) covers the full SPARQL 1.1 query and update specification. pg_ripple targets ≥95% pass rate on this suite as of v0.55.0.
What the Jena suite tests
The Jena conformance tests are derived from the W3C SPARQL 1.1 test suite and supplemented by Apache Jena's own test cases. They cover:
| Category | Tests (approx.) | What it exercises |
|---|---|---|
| Basic Graph Patterns | ~150 | BGP matching, triple patterns, blank nodes |
| SPARQL Algebra | ~200 | UNION, OPTIONAL, MINUS, JOIN |
| Aggregates | ~80 | COUNT, SUM, AVG, GROUP BY, HAVING |
| Property paths | ~100 | *, +, ?, ` |
| SPARQL Update | ~150 | INSERT DATA, DELETE DATA, MODIFY, COPY, MOVE |
| SPARQL Protocol | ~50 | Content negotiation, result formats |
| Numeric/String functions | ~120 | All SPARQL built-in functions |
| RDF-star | ~50 | Quoted triples, TRIPLE(), isTriple() |
| Subqueries / VALUES | ~60 | Correlated sub-SELECTs, VALUES rows |
| Other | ~40 | Miscellaneous edge cases |
Pass rate
The pass rate is measured by CI on each push to main using
cargo test --test jena_suite. The current pass rate is ≥95%.
Badge status is updated automatically by the CI jena-suite job.
Known exclusions
Some tests in the Jena suite exercise features that are non-blocking until the full suite requirement is enforced at ≥95% pass rate:
- Tests requiring named-graph write operations via SPARQL UPDATE that conflict with PostgreSQL transaction semantics (platform-specific)
- Tests for features gated on
spargebraSPARQL 1.2 grammar support
Running locally
# Fetch Jena test data (requires network access)
bash scripts/fetch_conformance_tests.sh --jena
# Run the suite
cargo test --test jena_suite -- --nocapture
The suite generates a report at tests/conformance/report.json.
OWL 2 RL Conformance Baseline (v0.48.0)
This page documents the OWL 2 RL conformance baseline for pg_ripple v0.48.0, as measured against the OWL 2 RL rule suite. The CI gate was upgraded to required at ≥ 95% in v0.48.0 (previously informational).
Summary
| Category | Rules Tested | Passing | XFAIL | Notes |
|---|---|---|---|---|
| cls (class axioms) | 12 | 12 | 0 | Full pass |
| prp (property axioms) | 18 | 17 | 1 | prp-spo2 (complex chain) XFAIL |
| cax (class axiom entailments) | 8 | 8 | 0 | Full pass |
| scm (schema entailments) | 14 | 13 | 1 | scm-sco (cyclical subclass) XFAIL |
| eq (equality reasoning) | 10 | 9 | 1 | eq-diff1 with owl:differentFrom XFAIL |
| dt (datatype reasoning) | 4 | 3 | 1 | dt-type2 (xs:double precision) XFAIL |
| Total | 66 | 62 | 4 | 93.9% pass rate |
New in v0.48.0
The following OWL 2 RL rules were added or completed:
- cax-sco: Full
rdfs:subClassOftransitive closure (previously single-step only) - prp-spo1:
rdfs:subPropertyOffull chain (previously binary case only) - prp-ifp: Inverse-functional-property derived
owl:sameAspropagation - cls-avf: Chained
owl:allValuesFrominteraction with subclass hierarchy - owl:minCardinality, owl:maxCardinality, owl:cardinality entailment rules
Known Failures (XFAIL)
These failures are documented in tests/owl2rl/known_failures.txt and tracked
release-to-release for regression detection.
prp-spo2 — Complex sub-property chain
OWL 2 RL rule prp-spo2 requires owl:propertyChainAxiom with two hops.
pg_ripple supports two-hop chains but the standard test case uses a three-hop
chain that requires recursive sub-property expansion not yet implemented.
Impact: Low — three-hop chains are rare in practice. Target: v0.49.0
scm-sco — Cyclical subclass entailment
The test graph contains a subclass cycle (A rdfs:subClassOf B, B rdfs:subClassOf A).
pg_ripple's WFS-based Datalog engine handles this correctly but the OWL 2 RL
test harness expects a specific owl:equivalentClass entailment that our
inferencer does not currently emit.
Impact: Low — owl:equivalentClass assertion from subclass cycles is a non-essential derived fact for most workloads. Target: v0.49.0
eq-diff1 — owl:differentFrom combined with owl:sameAs
The test requires detecting inconsistency when a node is asserted both
owl:sameAs and owl:differentFrom another node and emitting the resulting
owl:Nothing entailment. pg_ripple detects the inconsistency (emits PT550
WARNING) but does not propagate the owl:Nothing conclusion to the triple store.
Impact: Very low — inconsistency detection is present; the inferred owl:Nothing is rarely queried directly. Target: v0.50.0
dt-type2 — xs:double precision rounding
The OWL 2 RL test for xs:double datatype entailment requires
"1.0E0"^^xsd:double to be recognised as equal to "1"^^xsd:integer under
numeric promotion rules. pg_ripple's dictionary encodes each literal verbatim
and does not currently perform XSD numeric promotion on store.
Impact: Low — affects only mixed-type numeric comparison assertions. Target: v0.51.0 (XSD numeric tower)
Pass Rate History
| Version | Passing / Total | Pass Rate |
|---|---|---|
| v0.46.0 | n/a (suite added) | — |
| v0.47.0 | 62 / 66 | 93.9% |
| v0.119.0 | 66 / 66 | 100% (propertyChainAxiom prp-spo2 now passing) |
owl:propertyChainAxiom Support (v0.119.0)
OWL 2 RL rule prp-spo2 (owl:propertyChainAxiom) is now fully implemented.
A chain axiom of the form:
ex:ancestor owl:propertyChainAxiom ( ex:parent ex:parent ) .
is compiled at inference time into a Datalog chain rule:
ancestor(X, Z) :- parent(X, Y), parent(Y, Z).
Cycle safety is guaranteed by PG 18's WITH RECURSIVE … CYCLE clause, which
uses hash-based cycle detection rather than a separate visited-set join.
Ten canonical pg_regress tests
The suite tests/pg_regress/sql/owl_property_chain_axiom.sql covers:
- FOAF
foaf:knows2-hop acquaintance chain - SKOS
skos:broaderTransitiveclosure - PROV-O
prov:wasInfluencedByderivation chain - 3-hop family chain (
parent/parent/sibling) - Multiple concurrent chain axioms
- Chain combined with
owl:inverseOf - Cycle safety (no infinite loop)
- FOAF
foaf:knows3-hop acquaintance chain rdfs:subPropertyOfcombined withowl:propertyChainAxiom- LUBM-style
indirectAdvisorchain
Running the Suite
# Requires: cargo pgrx start pg18 first
cargo pgrx regress pg18 -- tests/pg_regress/sql/owl2rl_*.sql
Or with the justfile recipe:
just test-regress
The known-failure list is maintained in tests/owl2rl/known_failures.txt.
Any regression (a previously-passing test now failing) is a blocking CI
failure regardless of the overall pass rate.
WatDiv Benchmark Results
WatDiv (Waterloo SPARQL Diversity Test Suite) tests pg_ripple's correctness and query performance under realistic data distributions.
What WatDiv tests
WatDiv generates a synthetic e-commerce dataset at configurable scale and defines 100 query templates across four structural classes, each exercising different join patterns:
| Class | Templates | What it stresses |
|---|---|---|
| Star (S1–S7) | 7 | Same subject, multiple predicates — VP table scan and star-join optimisation |
| Chain (C1–C3) | 3 | Linear predicate path — join ordering |
| Snowflake (F1–F5) | 5 | Star + chain hybrid — mixed join strategies |
| Complex (B1–B12, L1–L5) | 17 | Multi-hop with OPTIONAL and UNION — full algebra |
Correctness criterion
Each template is run against a 10M-triple dataset and the result row count is compared to a pre-computed baseline. A template passes when its row count is within ±0.1% of the baseline. Row-count failures indicate SQL planner regressions or VP table correctness bugs.
Performance criterion
Median query latency per template is recorded and compared to the previous release baseline. A regression > 20% triggers a CI warning (not a failure). The WatDiv suite is always non-blocking because performance naturally varies with hardware.
Running locally
# 1. Fetch WatDiv templates and generate the 10M-triple dataset:
bash scripts/fetch_conformance_tests.sh --watdiv
# 2. Load the dataset into pg_ripple (requires a running instance):
cargo pgrx start pg18
psql -c "SELECT pg_ripple.load_ntriples(pg_read_file('tests/watdiv/data/watdiv-10M.nt'), false);"
# 3. Run the suite:
cargo test --test watdiv_suite
CI job
The watdiv-suite CI job runs on every push to main and:
- Checks correctness (row count ±0.1% per template)
- Records per-template median latency
- Writes results to
tests/conformance/report.jsonas a CI artifact
The job is non-blocking (performance regressions are warnings, not failures).
Results table (v0.46.0, 10M triples, 8-core CI runner)
Results are updated automatically on each release. The table below reflects
the v0.46.0 baseline; updated figures appear in the conformance_report CI artifact.
| Template | Class | Expected rows | Status |
|---|---|---|---|
| S1 | Star | — | — |
| S2 | Star | — | — |
| S3 | Star | — | — |
| S4 | Star | — | — |
| S5 | Star | — | — |
| S6 | Star | — | — |
| S7 | Star | — | — |
| C1 | Chain | — | — |
| C2 | Chain | — | — |
| C3 | Chain | — | — |
| F1 | Snowflake | — | — |
| F2 | Snowflake | — | — |
| F3 | Snowflake | — | — |
| F4 | Snowflake | — | — |
| F5 | Snowflake | — | — |
| B1–B12 | Complex | — | — |
| L1–L5 | Complex | — | — |
Note: Row counts and latency baselines are populated on first run against a freshly generated WatDiv 10M dataset. The
—entries above are filled in by the CI artifacttests/watdiv/baselines.jsonafter the first run.
Known limitations
- Templates that use
%var%substitution markers require concrete IRI bindings sampled from the dataset. Templates without substitution markers run as-is. - The WatDiv data generator (
watdivbinary or Docker image) must be available to generate the 10M-triple dataset. CI uses the pre-cached artifact from the first successful run.
See also
- Running Conformance Tests — how to fetch data and run all suites
- W3C Conformance — W3C SPARQL 1.1 and Jena suite results
LUBM Conformance Results
This page summarises pg_ripple's results on the Lehigh University Benchmark (LUBM), a canonical benchmark for OWL RL knowledge-base systems.
Overview
LUBM (Guo et al., 2005) defines 14 canonical SPARQL queries over a synthetic university-domain ontology (univ-bench.owl). The queries exercise:
rdf:typelookups with OWL RL subclass/subproperty entailment- Multi-hop property chains (
memberOf+subOrganizationOf+undergraduateDegreeFrom) - Domain and range reasoning
- Conjunctive patterns over asserted and inferred triples
As of v0.44.0, all 14 LUBM queries pass against the bundled univ1 synthetic fixture with 0 known failures.
Test Fixture
pg_ripple uses a self-contained synthetic fixture (tests/lubm/fixtures/univ1.ttl) rather than the original Java UBA generator. The fixture models 1 university, 1 department, 1 research group, 4 faculty, 7 graduate students, 5 undergraduate students, 6 graduate courses, 1 undergraduate course, and 4 publications — all with explicit supertype assertions so that no OWL RL inference pass is required to match the reference counts.
A complementary Datalog validation sub-suite (tests/lubm/datalog/) separately validates that running pg_ripple.load_rules_builtin('owl-rl') and pg_ripple.infer('owl-rl') on an implicit-type-only version of the same data produces identical query results.
Query Results (univ1 fixture)
| Query | Description | Inference rules exercised | Expected | Result | Status |
|---|---|---|---|---|---|
| Q1 | Graduate students taking GraduateCourse0 | rdf:type + subclass entailment | 3 | 3 | ✅ PASS |
| Q2 | Graduate students whose department is part of their undergrad university | Multi-hop join | 2 | 2 | ✅ PASS |
| Q3 | Publications by AssistantProfessor0 | Direct lookup | 2 | 2 | ✅ PASS |
| Q4 | Professors in Dept0 with name/email/phone | Property star pattern | 4 | 4 | ✅ PASS |
| Q5 | Persons in Dept0 taking GraduateCourse0 | ub:Person superclass | 3 | 3 | ✅ PASS |
| Q6 | All students | ub:Student superclass | 12 | 12 | ✅ PASS |
| Q7 | Students taking GC0 advised by a FullProfessor | Advisor + course conjunction | 3 | 3 | ✅ PASS |
| Q8 | Students in Dept0/University0 with email | Full join pattern | 12 | 12 | ✅ PASS |
| Q9 | Students taking courses from AssistantProfessors | 3-way join | 7 | 7 | ✅ PASS |
| Q10 | Graduate students in ResearchGroup0 | ub:memberOf lookup | 3 | 3 | ✅ PASS |
| Q11 | Sub-organizations of Department0 | ub:subOrganizationOf | 1 | 1 | ✅ PASS |
| Q12 | Professors heading Department0 | ub:headOf | 1 | 1 | ✅ PASS |
| Q13 | Professors acting as teaching assistants | ub:teachingAssistantOf | 1 | 1 | ✅ PASS |
| Q14 | All undergraduate students | ub:UndergraduateStudent subclass | 5 | 5 | ✅ PASS |
Datalog Validation Sub-suite
The Datalog sub-suite validates the OWL RL inference engine independently of the SPARQL translator.
| Test | What it validates | Status |
|---|---|---|
| Rule compilation | load_rules_builtin('owl-rl') compiles ≥ 20 rules | ✅ PASS |
| Inference iteration | infer_with_stats() reaches fixpoint in 1–10 iterations | ✅ PASS |
| Inferred triple counts | Key supertype entailments produce correct row counts | ✅ PASS |
| Goal queries | infer_goal() and SPARQL counts agree for Q1/Q6/Q14 | ✅ PASS |
| Materialization perf | infer('owl-rl') completes in < 5 s on univ1 | ✅ PASS |
| Custom rules | User-defined transitive-closure rule works correctly | ✅ PASS |
Running LUBM Locally
# Start pg_ripple (uses pgrx default port 28818)
cargo pgrx start pg18
# Run the LUBM suite (self-contained — no data download required)
cargo test --test lubm_suite -- --nocapture
To run the Datalog sub-suite SQL files manually:
# Assumes pg_ripple is installed and running
psql -c "SELECT pg_ripple.load_turtle(pg_read_file('tests/lubm/fixtures/univ1.ttl'), false)"
psql -f tests/lubm/datalog/rule_compilation.sql
psql -f tests/lubm/datalog/inference_iterations.sql
psql -f tests/lubm/datalog/inferred_triples.sql
Adding Known Failures
If a LUBM query fails, add a lubm:Q{N} entry to tests/conformance/known_failures.txt:
# Example — Q2 fails due to multi-hop join bug, fix in progress
lubm:Q2 multi-hop memberOf/subOrganizationOf join returns wrong count
Remove the entry once the underlying bug is fixed.
See Also
- Running Conformance Tests — local setup and all suites
- W3C Conformance — W3C SPARQL 1.1, Jena, WatDiv results
- WatDiv Results — performance benchmark details
Vector Index Trade-offs
pg_ripple supports hybrid SPARQL + semantic search via the
pg_ripple.hybrid_search() function, which uses pgvector's ANN (Approximate
Nearest Neighbour) indexes. Two index types are available — HNSW and
IVFFlat — at three precision levels: single (32-bit float),
half (16-bit float), and binary (1-bit).
This page presents reference benchmarks measured on a 100,000-embedding fixture with 128-dimensional vectors. Use these figures to choose the right combination for your workload.
Benchmark Setup
| Parameter | Value |
|---|---|
| Dataset size | 100,000 embeddings |
| Dimensions | 128 (use 1536 for text-embedding-3-small) |
| Queries | 1,000 random query vectors |
| k (neighbours) | 10 |
| PostgreSQL | 18 |
| pgvector | 0.7.4 |
| Hardware | 8-core CPU, 32 GB RAM |
Run the benchmark yourself:
psql -U postgres -f benchmarks/vector_index_compare.sql
Reference Results
HNSW (m=16, ef_construction=64)
| Precision | Build time | Recall p50 | Recall p95 | Latency p50 | Latency p95 |
|---|---|---|---|---|---|
| single (32-bit) | ~45 s | 99.2% | 98.1% | 0.4 ms | 0.8 ms |
| half (16-bit) | ~30 s | 98.7% | 97.5% | 0.3 ms | 0.6 ms |
| binary (1-bit) | ~8 s | 91.3% | 88.0% | 0.08 ms | 0.15 ms |
IVFFlat (lists=100)
| Precision | Build time | Recall p50 | Recall p95 | Latency p50 | Latency p95 |
|---|---|---|---|---|---|
| single (32-bit) | ~5 s | 96.4% | 94.2% | 0.9 ms | 2.1 ms |
| half (16-bit) | ~4 s | 95.8% | 93.7% | 0.7 ms | 1.8 ms |
| binary (1-bit) | ~1.5 s | 85.2% | 81.0% | 0.2 ms | 0.5 ms |
Recommendations
| Scenario | Recommended |
|---|---|
| High-accuracy semantic search (RAG) | HNSW, single precision |
| Latency-sensitive real-time search | HNSW, half precision |
| Very large datasets (> 10 M embeddings), memory-constrained | IVFFlat, half precision |
| Coarse pre-filtering before exact reranking | HNSW or IVFFlat, binary |
| Fast prototyping / development | IVFFlat, single precision (fast build) |
Configuring the Index Type
Control the index type and precision via GUCs:
SET pg_ripple.embedding_index_type = 'hnsw'; -- or 'ivfflat'
SET pg_ripple.embedding_precision = 'single'; -- or 'half' or 'binary'
Rebuild the index after changing:
SELECT pg_ripple.rebuild_embedding_index();
Memory Footprint
| Precision | Memory per 1 M 1536-dim vectors |
|---|---|
| single (32-bit) | ~6 GB |
| half (16-bit) | ~3 GB |
| binary (1-bit) | ~190 MB |
For production deployments with millions of embeddings, half precision offers the best recall-to-memory trade-off.
Related
- Embedding Functions — full API reference
- GUC Reference —
embedding_index_type,embedding_precision - Hybrid Vector Search example
Running Conformance Tests
pg_ripple ships four complementary conformance suites that can be run locally or in CI. This page covers how to set up data, run each suite, and interpret results.
Prerequisites
- A working pg_ripple development environment
cargo pgrxinstalled and initialised for PostgreSQL 18curlorwgetfor downloading test data- Docker (optional) for generating the WatDiv dataset
One-command setup
Download all test data for all three suites at once:
bash scripts/fetch_conformance_tests.sh
Or fetch individual suites:
bash scripts/fetch_conformance_tests.sh --w3c # W3C SPARQL 1.1 only
bash scripts/fetch_conformance_tests.sh --jena # Apache Jena only
bash scripts/fetch_conformance_tests.sh --watdiv # WatDiv only
bash scripts/fetch_conformance_tests.sh --force # re-download everything
W3C SPARQL 1.1 suite
Data location
tests/w3c/data/ (default) or the directory in W3C_TEST_DIR.
Running
# Start pg_ripple
cargo pgrx start pg18
# Smoke subset (180 tests, ~30s — fastest feedback):
cargo test --test w3c_smoke -- --nocapture
# Full suite (3 000+ tests, ~2min with 8 threads):
W3C_THREADS=8 cargo test --test w3c_suite -- --nocapture
Known failures
Edit tests/conformance/known_failures.txt with lines prefixed w3c::
# Example — property-path regression, fix in progress
w3c:http://www.w3.org/2009/sparql/docs/tests/data-sparql11/property-path/manifest#pp35 pp inside GRAPH
Remove entries when the underlying bug is fixed.
Apache Jena suite
Data location
tests/jena/data/ (default) or the directory in JENA_TEST_DIR.
Running
# Download Jena test data (one-time):
bash scripts/fetch_conformance_tests.sh --jena
# Run the full suite (~1 000 tests, target < 3 minutes):
JENA_THREADS=8 cargo test --test jena_suite -- --nocapture
Coverage
Jena tests focus on implementation edge cases:
- Type coercion — XSD numeric promotions, mixed-type arithmetic
- Date/time — timezone-aware comparisons,
YEAR(),MONTH(),DAY(),HOURS(),MINUTES(),SECONDS(),TZ() - Blank-node scoping — CONSTRUCT templates, GRAPH boundaries, OPTIONAL
- String functions —
STRLEN(),SUBSTR(),UCASE(),LCASE(),STRSTARTS(),STRENDS(),CONTAINS(),ENCODE_FOR_URI(),CONCAT() - Numeric precision —
xsd:decimalarithmetic,ROUND(),CEIL(),FLOOR(),ABS()
Known failures
Prefix entries with jena: in tests/conformance/known_failures.txt:
# Example — timezone-aware dateTime comparison
jena:http://jena.example.org/tests/sparql-query/manifest#dateTime-tz-offset TZ offset handling
The CI job is non-blocking until pass rate ≥ 95%.
WatDiv benchmark suite
Data location
- Templates:
tests/watdiv/templates/(orWATDIV_TEMPLATE_DIR) - RDF data:
tests/watdiv/data/(orWATDIV_DATA_DIR) - Baselines:
tests/watdiv/baselines.json(orWATDIV_BASELINE_FILE)
Data generation
The WatDiv 10M-triple dataset is generated once and cached as a CI artifact.
# Using Docker:
docker run --rm dcslab/watdiv -s 1 -t 10000000 > tests/watdiv/data/watdiv-10M.nt
# Using a local binary:
WATDIV_BINARY=/usr/local/bin/watdiv bash scripts/fetch_conformance_tests.sh --watdiv
Loading the dataset
Before running the WatDiv suite, load the dataset into pg_ripple:
cargo pgrx start pg18
psql -d postgres -c "SELECT pg_ripple.load_ntriples(pg_read_file('tests/watdiv/data/watdiv-10M.nt'), false);"
Running
# Run all 100 templates (target < 5 min on 8-core runner):
WATDIV_THREADS=8 cargo test --test watdiv_suite -- --nocapture
Interpreting results
- Correctness pass: row count within ±0.1% of baseline
- Performance warning: median latency > 20% above baseline (non-blocking)
- Baselines: stored in
tests/watdiv/baselines.json— update after intentional performance changes
Known failures
Prefix entries with watdiv: in tests/conformance/known_failures.txt:
# Example — complex template with OPTIONAL cardinality edge case
watdiv:B7 known cardinality mismatch with OPTIONAL
LUBM benchmark suite (v0.44.0+)
The LUBM (Lehigh University Benchmark) suite validates OWL RL inference correctness through 14 canonical SPARQL queries over a university-domain ontology.
Data location
The LUBM suite is self-contained — no download or external data generation is needed.
The synthetic fixture is bundled at tests/lubm/fixtures/univ1.ttl.
Running
# Start pg_ripple
cargo pgrx start pg18
# Run all 14 LUBM queries + Datalog validation sub-suite (< 30s):
cargo test --test lubm_suite -- --nocapture
What is tested
- 14 canonical queries (
tests/lubm/queries/q01.sparql–q14.sparql) against the bundled univ1 fixture — exact row-count validation. - OWL RL rule loading via
pg_ripple.load_rules_builtin('owl-rl'). - Inference materialization via
pg_ripple.infer('owl-rl')— verifies fixpoint is reached in ≤ 10 iterations and completes in < 5 s. - Goal queries via
pg_ripple.infer_goal()— validates inference engine results match SPARQL query results. - Custom Datalog rules — defines ad-hoc rules on LUBM data and validates correctness.
Known failures
Prefix entries with lubm: in tests/conformance/known_failures.txt:
# Example — Q2 multi-hop join returns wrong count
lubm:Q2 multi-hop memberOf/subOrganizationOf join bug
Regenerating baselines
If the fixture is changed, regenerate the baseline counts:
cargo pgrx start pg18
# Run the suite once, observe the actual counts in the output,
# then update tests/lubm/baselines/univ1.json accordingly.
See also
- LUBM Results — full conformance table and Datalog sub-suite results
Unified report
All suites write results to tests/conformance/report.json:
{
"w3c": { "suite": "w3c", "total": 3100, "passed": 3097, "failed": 0, ... },
"jena": { "suite": "jena", "total": 1000, "passed": 983, "failed": 0, ... },
"watdiv": { "suite": "watdiv", "total": 100, "passed": 100, "failed": 0, ... }
}
This file is uploaded as the conformance_report CI artifact after each run.
(The LUBM suite writes pass/fail results to stdout; a JSON report artifact is planned for v0.45.0.)
Updating baselines
After intentional performance improvements, regenerate the WatDiv baselines:
# Run the suite to populate baselines.json:
cargo test --test watdiv_suite -- --nocapture
# Then commit the updated baselines.json.
Updating the known-failures manifest
The unified known-failures file lives at tests/conformance/known_failures.txt.
Format:
# Comment lines are ignored.
# Each entry: <suite>:<test-key> <optional reason>
w3c:http://... reason
jena:http://... reason
watdiv:S3 reason
lubm:Q2 reason
Any test listed here that unexpectedly passes (XPASS) triggers a CI notice to remove the entry.
See also
- W3C Conformance — per-category pass rates
- LUBM Results — OWL RL conformance table
- WatDiv Results — benchmark metrics and results table
Error Code Registry
A13-03 (v0.86.0): this page is the authoritative registry for all
PTerror codes used in pg_ripple. Every production error path must reference a code from this list. CI enforcesPTcodes on production error paths.
pg_ripple uses structured error codes in the range PT001–PT799 (extension) and PT400–PT503 (HTTP companion). Error messages follow PostgreSQL conventions: lowercase first word, no trailing period.
See also: Error Message Catalog for the full list of error messages by subsystem.
Code Ranges
| Range | Subsystem |
|---|---|
| PT001–PT099 | Dictionary encoding |
| PT100–PT199 | VP storage |
| PT200–PT299 | SPARQL query engine |
| PT300–PT399 | Datalog inference |
| PT400–PT499 | Input validation / HTTP |
| PT500–PT599 | Internal execution |
| PT600–PT699 | SHACL validation |
| PT700–PT799 | External services (LLM, federation) |
Uncertain Knowledge (v0.87.0) — PT0301–PT0307
| Code | Feature | Condition |
|---|---|---|
| PT0301 | Uncertain Knowledge | Fuzzy SPARQL input exceeds pg_ripple.fuzzy_max_input_length characters |
| PT0302 | Uncertain Knowledge | pg_trgm extension is not installed |
| PT0303 | Uncertain Knowledge | Invalid confidence value (NaN, Inf, or outside [0.0, 1.0]) |
| PT0304 | Uncertain Knowledge | pg:confidence() called with all three arguments unbound |
| PT0305 | Uncertain Knowledge | pg:confidence() used inside a SERVICE clause (not supported) |
| PT0306 | Uncertain Knowledge | SHACL score-log table _pg_ripple.shacl_score_log does not exist |
| PT0307 | Uncertain Knowledge | Confidence bulk-loader: file path outside allowed directory |
PageRank (v0.88.0) — PT0401–PT0423
| Code | Feature | Condition |
|---|---|---|
| PT0401 | PageRank | Invalid damping factor — must be in (0, 1) exclusive |
| PT0402 | PageRank | max_iterations must be a positive integer |
| PT0403 | PageRank | topic label exceeds pg_ripple.pagerank_max_topic_length characters |
| PT0404 | PageRank | seed_nodes list exceeds pg_ripple.pagerank_max_seeds limit |
| PT0405 | PageRank | edge_predicates list is empty (at least one required) |
| PT0406 | PageRank | convergence_threshold must be positive and finite |
| PT0407 | PageRank | No pagerank scores found — run pagerank_run() first |
| PT0408 | PageRank | pagerank_scores table does not exist — run pagerank_run() first |
| PT0409 | PageRank | pagerank_dirty_edges table does not exist |
| PT0410 | PageRank | Export format unsupported — valid values: turtle, jsonld, csv, ntriples |
| PT0411 | PageRank | Centrality metric unsupported — valid values: betweenness, closeness, eigenvector, katz |
| PT0412 | PageRank | katz_beta (attenuation factor) must be positive |
| PT0413 | PageRank | explain_pagerank top_k must be a positive integer |
| PT0414 | PageRank | PageRank run aborted — convergence not achieved within max_iterations |
| PT0415 | PageRank | Concurrent PageRank run already in progress for this topic |
| PT0416 | PageRank | IRI escaping error in export (malformed IRI in pagerank_scores) |
| PT0417 | PageRank | pg:pagerank() in SPARQL query triggered on-demand run; timed out |
| PT0418 | PageRank | Betweenness centrality requires at least 3 nodes |
| PT0419 | PageRank | pagerank_partition value out of range [1, 1024] |
| PT0420 | PageRank | k_hop_depth for incremental refresh is out of range [1, 20] |
| PT0421 | PageRank | Confidence-weighted PageRank requested but no confidence scores exist |
| PT0422 | PageRank | Temporal decay half_life_days must be positive |
| PT0423 | PageRank | federation_minimum_confidence outside [0.0, 1.0] |
HTTP Companion PT Codes
| Code | HTTP Status | Meaning | Source |
|---|---|---|---|
| PT400 | 400 | Missing or malformed query parameter | routing/sparql_handlers.rs |
| PT400_SPARQL_PARSE | 400 | SPARQL syntax error — parse failed | routing/sparql_handlers.rs, spi_bridge.rs |
| PT401 | 401 | Unauthorized — missing or invalid Bearer token | common.rs |
| PT403 | 403 | Forbidden — path outside allowed directory | bulk_load.rs |
| PT404 | 413 | Request body exceeds maximum allowed size | routing/sparql_handlers.rs |
| PT413 | 413 | Arrow Flight export result is too large | arrow_encode.rs |
| PT503 | 503 | Database connection unavailable | common.rs, stream.rs |
Extension PT Codes (selected)
| Code | Message | Subsystem |
|---|---|---|
| PT001 | dictionary encode failed: hash collision detected | Dictionary |
| PT002 | dictionary decode failed: id not found | Dictionary |
| PT003 | invalid term kind: expected 0/1/2 | Dictionary |
| PT008 | malformed IRI: <detail> | Dictionary |
| PT400 | SPARQL parse error: <detail> | SPARQL |
| PT403 | file path outside allowed directory | Bulk load |
| PT501 | deprecated GUC: use <replacement> | Storage GUC |
| PT512 | strict_dictionary: unknown dictionary id | Dictionary |
| PT600 | SHACL constraint violation: <detail> | SHACL |
| PT700 | LLM endpoint unreachable or returned HTTP error | LLM |
CI Enforcement
The CI job check-pt-codes (added in v0.86.0) scans all pgrx::error! and tracing::error! call sites in production code to verify that each one references a PT code in either:
- The error message body (e.g.,
"... (PT400)"), or - The error code argument (e.g.,
json_error("PT400", ..., StatusCode::BAD_REQUEST)).
Internal-only errors that use "internal: <description> — please report" format are exempted.
To add a new error code, update this file first, then reference the code in the implementation.
Error Message Catalog
pg_ripple uses structured error codes in the range PT001–PT799, organized by subsystem. Error messages follow PostgreSQL conventions: lowercase first word, no trailing period.
Error codes appear in the DETAIL field of PostgreSQL error messages. Use \errverbose in psql to see the full error context including the code.
PT001–PT099: Dictionary
Errors from the IRI/literal/blank-node → integer encoding subsystem.
| Code | Message | Cause | Fix |
|---|---|---|---|
| PT001 | dictionary encode failed: hash collision detected | Two distinct terms produced the same XXH3-128 hash (extremely rare) | Report to maintainers with the two colliding terms |
| PT002 | dictionary decode failed: id not found | The integer ID does not exist in _pg_ripple.dictionary | Data may be corrupt; run pg_ripple.vacuum_dictionary() and check VP tables |
| PT003 | invalid term kind: expected 0 (IRI), 1 (literal), 2 (blank node) | Wrong kind integer passed to encode_term() | Use 0 for IRIs, 1 for literals, 2 for blank nodes |
| PT004 | quoted triple components not found | A quoted-triple ID references qt_s/qt_p/qt_o values that are missing from the dictionary | Re-load the RDF-star data; may indicate a partial load failure |
| PT005 | inline-encoded literal decode failed | Internal decoding error for small inline-encoded literals | Report to maintainers with the literal value |
| PT006 | dictionary batch insert failed | The ON CONFLICT DO NOTHING … RETURNING batch insert encountered an unexpected error | Check PostgreSQL logs for disk space or permission issues |
| PT007 | dictionary lookup: NULL term | A NULL value was passed where an IRI, literal, or blank node was expected | Ensure all arguments are non-NULL |
| PT008 | malformed IRI: <detail> | The IRI string does not conform to RFC 3987 | Fix the IRI syntax; IRIs must be wrapped in angle brackets <…> |
| PT009 | malformed literal: <detail> | The literal string cannot be parsed | Use N-Triples syntax: "value", "value"@lang, or "value"^^<datatype> |
| PT010 | malformed blank node: <detail> | The blank node label is invalid | Blank nodes must start with _: followed by a valid label |
| PT011 | dictionary cache full, eviction failed | The LRU cache could not evict entries | Increase pg_ripple.dictionary_cache_size |
| PT012 | prewarm_dictionary_hot: table not found | The dictionary table does not exist | Run CREATE EXTENSION pg_ripple first |
PT100–PT199: Storage
Errors from the VP table storage layer, HTAP partitions, and rare-predicate management.
| Code | Message | Cause | Fix |
|---|---|---|---|
| PT100 | insert_triple: predicate IRI required | The predicate argument is NULL or empty | Provide a valid predicate IRI |
| PT101 | VP table creation failed | DDL error when creating a new VP table | Check pg_log for the underlying PostgreSQL error |
| PT102 | htap_migrate_predicate: predicate not found | The predicate ID does not exist in _pg_ripple.predicates | Verify the predicate IRI and that triples exist for it |
| PT103 | merge: lock_timeout exceeded during main table swap | Another transaction held a lock on the VP table for too long | Retry; consider increasing lock_timeout for maintenance windows |
| PT104 | rare-predicate promotion failed | Error promoting a predicate from vp_rare to a dedicated VP table | Check disk space and user permissions |
| PT105 | delete_triple: predicate not found in catalog | The triple's predicate has no VP table | The predicate may never have been used, or was already compacted |
| PT106 | VP table not found: <table_name> | The VP table referenced in _pg_ripple.predicates does not exist on disk | Run pg_ripple.compact() to reconcile the catalog |
| PT107 | delta table insert failed | Error writing to the HTAP delta partition | Check PostgreSQL logs for tablespace or permission issues |
| PT108 | tombstone insert failed | Error recording a deletion in the tombstones table | Check PostgreSQL logs |
| PT109 | merge worker: unexpected state | The background merge worker encountered an inconsistent state | Restart PostgreSQL; check pg_log for crash details |
| PT110 | statement_id_seq: sequence exhausted | The global statement ID sequence has reached its maximum | This is unlikely with BIGINT; contact maintainers |
| PT111 | vp_rare: row limit exceeded | The rare-predicate table has too many rows for a single predicate | Manually promote with promote_rare_predicates() or lower pg_ripple.vp_promotion_threshold |
| PT112 | deduplicate: advisory lock not acquired | Another deduplication operation is already running | Wait and retry |
| PT113 | create_graph: invalid graph IRI | The graph IRI is malformed | Graph IRIs must be valid absolute IRIs in angle brackets |
| PT114 | drop_graph: graph not found | The named graph does not exist | Use list_graphs() to check available graphs |
PT200–PT299: SPARQL
Errors from the SPARQL parser, algebra optimizer, and SQL code generator.
| Code | Message | Cause | Fix |
|---|---|---|---|
| PT200 | SPARQL parse error: <detail> | The SPARQL query has a syntax error | Fix the syntax; use sparql_explain() to validate without executing |
| PT201 | unsupported SPARQL algebra node: <type> | The query uses a feature not yet implemented | Check the compliance matrix for supported features |
| PT202 | SPARQL SELECT: no projected variables | The SELECT clause has no variables | Add at least one ?variable to the SELECT clause |
| PT203 | property path depth exceeded max_path_depth | A recursive property path exceeded the configured depth limit | Increase pg_ripple.max_path_depth or simplify the path expression |
| PT204 | SPARQL federated SERVICE: endpoint not reachable | The remote SPARQL endpoint did not respond | Check the endpoint URL and network connectivity |
| PT205 | SPARQL VALUES clause: column count mismatch | The number of values in a VALUES row does not match the variable list | Ensure each VALUES row has the same number of columns as variables |
| PT206 | SPARQL type error: <detail> | A type mismatch in a FILTER expression | Check operand types; e.g., comparing a string to an integer |
| PT207 | SPARQL CONSTRUCT: template variable not in WHERE | A variable in the CONSTRUCT template is not bound in the WHERE clause | Bind all template variables in the WHERE clause |
| PT208 | SPARQL DESCRIBE: no resource specified | DESCRIBE requires at least one resource or variable | Add a resource IRI or variable to the DESCRIBE clause |
| PT209 | SPARQL aggregate: variable not grouped | A non-aggregated variable is used outside GROUP BY | Add the variable to GROUP BY or wrap it in an aggregate function |
| PT210 | SPARQL HAVING: refers to non-aggregate | The HAVING clause references a variable that is not an aggregate result | Use an aggregate function in HAVING |
| PT211 | generated SQL execution failed: <detail> | The SQL generated from SPARQL failed to execute | Check pg_log for the underlying error; report if reproducible |
| PT212 | plan cache: entry evicted during execution | A cached plan was evicted while the query was still running | Increase pg_ripple.plan_cache_size |
| PT213 | SPARQL SERVICE: response parse error | The federated endpoint returned malformed results | Check the remote endpoint's response format |
| PT214 | SPARQL SERVICE: timeout after <N> ms | The federated request exceeded pg_ripple.federation_timeout | Increase the timeout or simplify the SERVICE query |
| PT215 | SPARQL UPDATE parse error: <detail> | The SPARQL Update statement has a syntax error | Fix the syntax |
| PT216 | SPARQL UPDATE: LOAD failed for <url> | The LOAD operation could not retrieve the remote resource | Check URL, network, and pg_ripple.federation_timeout |
| PT217 | SPARQL UPDATE: unsupported content type <type> | The LOAD target serves an unrecognized RDF format | The URL must serve Turtle, N-Triples, N-Quads, TriG, or RDF/XML |
| PT218 | SPARQL UPDATE: CREATE GRAPH already exists | The graph already exists | Use CREATE SILENT GRAPH to suppress this error |
| PT219 | SPARQL UPDATE: DROP GRAPH not found | The graph does not exist | Use DROP SILENT GRAPH to suppress this error |
PT300–PT399: SHACL
Errors from the SHACL shapes loader, validator, and async monitoring pipeline.
| Code | Message | Cause | Fix |
|---|---|---|---|
| PT300 | SHACL parse error: <detail> | The Turtle-encoded SHACL shapes have a syntax error | Fix the Turtle syntax in the shapes definition |
| PT301 | SHACL sync validation failed: <shape> — <message> | A triple violates a SHACL constraint during synchronous validation | Fix the data to conform to the shape, or modify the shape |
| PT302 | SHACL shape not found: <iri> | The referenced shape has not been loaded | Load the shape with load_shacl() first |
| PT303 | SHACL DAG monitor: pg_trickle not installed | DAG-aware monitors require the pg_trickle extension | Install pg_trickle or use enable_shacl_monitors() for trigger-based validation |
| PT304 | SHACL: unsupported constraint component <type> | The shape uses a SHACL-AF or SHACL-JS constraint | Only SHACL Core constraints are supported |
| PT305 | SHACL: sh:path too complex | The property path in the shape exceeds supported complexity | Simplify the sh:path expression |
| PT306 | SHACL: validation queue overflow | The async validation queue has exceeded its capacity | Process the queue with process_validation_queue() or increase the queue size |
| PT307 | SHACL: dead letter queue threshold reached | Too many validation failures have accumulated | Inspect with dead_letter_queue() and address the failures |
| PT308 | SHACL: sh:targetClass not found | The target class IRI is not present in the data | Load data with the target class, or fix the class IRI |
| PT309 | SHACL: circular shape reference | A shape references itself through sh:node or sh:qualifiedValueShape | Break the circular reference |
| PT310 | SHACL: drop_shape: shape has active monitors | Cannot drop a shape that has active monitors | Disable monitors first with disable_shacl_dag_monitors() |
PT400–PT499: Datalog — Rules
Errors from the Datalog rule parser, stratifier, and rule management.
| Code | Message | Cause | Fix |
|---|---|---|---|
| PT400 | rule parse error: <detail> | The Datalog rule has a syntax error | Fix the rule syntax; see Reasoning and Inference for syntax reference |
| PT401 | rule stratification failed: unstratifiable program | The rule set contains a cycle through negation that prevents stratification | Rewrite rules to break the negation cycle, or use infer_wfs() for well-founded semantics |
| PT402 | rule set not found: <name> | The referenced rule set has not been loaded | Load it with load_rules() or load_rules_builtin() |
| PT403 | inference: maximum iteration depth exceeded | Semi-naive evaluation did not converge within the iteration limit | Simplify the rule set or increase statement_timeout |
| PT404 | constraint violation detected: <rule> | A constraint rule (:- body.) fired | Check the data against the constraint body |
| PT405 | rule set already exists: <name> | A rule set with this name is already loaded | Drop it first with drop_rules(), or choose a different name |
| PT406 | rule: unsafe variable <var> | A variable appears in the head but not in a positive body literal | Ensure every head variable also appears in a positive body literal |
| PT407 | rule: built-in predicate not recognized: <name> | An unknown built-in predicate was used | Check available built-ins: =, !=, <, >, <=, >=, +, -, *, / |
| PT408 | rule: aggregation variable not in group-by | An aggregated variable is used outside the grouping context | Add the variable to the group-by list |
| PT440 | SPARQL query exceeds algebra depth or pattern limit | The query is too complex: its algebra tree is deeper than pg_ripple.sparql_max_algebra_depth or contains more triple patterns than pg_ripple.sparql_max_triple_patterns | Simplify the query, break it into smaller queries, or increase the limits |
| PT480 | SHACL-AF sh:rule not compiled | sh:rule triples were found in the shapes but Datalog inference is disabled or the bridge failed | Enable inference with pg_ripple.datalog_inference = 'on', or remove sh:rule triples |
| PT481 | SHACL-SPARQL constraint query failed | A sh:sparql constraint's embedded SPARQL query could not be executed | Check the embedded SPARQL syntax and that all prefixes are declared in the shapes document |
| PT482 | SHACL-AF sh:rule compilation failed | A sh:rule body could not be compiled into a Datalog rule and was skipped | Review the rule body for unsupported constructs; the rule was not registered |
PT500–PT599: Datalog — Inference Engine
Errors from the materialization engine, magic sets optimizer, WFS evaluator, and tabling.
| Code | Message | Cause | Fix |
|---|---|---|---|
| PT500 | infer: no enabled rule sets | infer() was called with no rule sets enabled | Enable at least one rule set with enable_rule_set() |
| PT501 | infer: SPI execution failed during iteration <N> | The SQL generated for a rule body failed | Check pg_log for the underlying error |
| PT502 | infer_demand: magic set rewriting failed | The demand transformation could not be applied | Simplify the goal pattern or rule set |
| PT503 | infer_demand: goal pattern too broad | The goal has no bound arguments, defeating the purpose of demand-driven evaluation | Bind at least one argument in the goal |
| PT504 | infer_wfs: unfounded set computation exceeded limit | The well-founded semantics fixpoint did not converge | Simplify the rule set or check for unusual negation patterns |
| PT505 | infer_wfs: three-valued model contains undefined atoms | Some atoms could not be classified as true or false | This is expected in WFS; query the undefined result set to see which atoms |
| PT506 | tabling: memo store overflow | The tabling memo store exceeded its size limit | Increase pg_ripple.tabling_memo_size |
| PT507 | infer_agg: aggregation cycle detected | An aggregation rule depends on its own aggregate result | Rewrite to break the cycle |
| PT508 | infer_goal: predicate not in any rule set | The goal predicate is not defined by any loaded rule | Load a rule set that defines the predicate |
| PT509 | owl:sameAs canonicalization: cycle limit exceeded | The owl:sameAs equivalence class merging exceeded the iteration limit | Check for very large owl:sameAs clusters |
| PT510 | infer_agg: aggregation-stratification violation | A rule's aggregate depends (directly or through recursion) on its own derived result | Rewrite rules so that aggregated predicates are not re-used in the rule set that derives them |
| PT511 | infer_agg: unsupported aggregate function | The rule uses an aggregate not supported by the SQL compiler | Use one of COUNT, SUM, MIN, MAX, AVG |
| PT520 | infer_wfs: iteration cap reached (<N> iterations) | The WFS alternating fixpoint did not converge within pg_ripple.wfs_max_iterations | Emitted as WARNING; partial result is returned with "stratifiable": false; increase the cap or simplify the rule set |
| PT530 | DRed cycle detected: <rule_set> | Delete-Rederive detected a cycle in the rule derivation graph that it cannot safely resolve; the system falls back to full recompute | This is a WARNING, not an error; the operation still succeeds. Reduce cyclic dependencies in your rule set, or set pg_ripple.dred_enabled = off to always use full recompute |
| PT540 | lattice: fixpoint did not converge after <N> iterations | The lattice fixpoint did not stabilise within pg_ripple.lattice_max_iterations | Increase pg_ripple.lattice_max_iterations or verify that the join function is monotone |
| PT541 | lattice: join_fn <name> could not be resolved | The user-supplied join function name could not be resolved via regprocedure | Check the function name, schema, and argument types; use a fully-qualified name |
| PT542 | federation: result decoder received unparseable XML/JSON | The SPARQL results response from a remote SERVICE endpoint could not be parsed | Check the endpoint's response format; ensure it returns application/sparql-results+xml or +json |
| PT543 | federation response body exceeds federation_max_response_bytes | The HTTP response body from a remote SERVICE endpoint is larger than the configured limit | Increase pg_ripple.federation_max_response_bytes, or set it to -1 to disable; consider filtering at the remote endpoint instead |
| PT550 | owl:sameAs cluster too large: <size> members | An owl:sameAs equivalence class exceeds pg_ripple.sameas_max_cluster_size; canonicalization is skipped for this cluster | Investigate the data for spurious owl:sameAs triples; increase the limit if the cluster is legitimate |
PT600–PT699: Export / HTTP
Errors from export serializers, GraphRAG export, and the HTTP companion service.
| Code | Message | Cause | Fix |
|---|---|---|---|
| PT600 | export: serialization failed for triple <sid> | A triple could not be serialized to the target format | Check that the triple's dictionary entries are intact |
| PT601 | export: unsupported format <format> | An unrecognized export format was requested | Use ntriples, nquads, turtle, or jsonld |
| PT602 | export_turtle_stream: batch_size must be > 0 | Invalid batch size | Use a positive integer |
| PT603 | export_jsonld: framing failed | The JSON-LD framing algorithm encountered an error | Check the frame structure; see JSON-LD Framing |
| PT604 | export_graphrag_entities: no entities found | No entities match the GraphRAG export criteria | Load data or adjust the GraphRAG ontology |
| PT605 | jsonld_frame_to_sparql: invalid frame | The JSON-LD frame could not be converted to SPARQL | Check the frame JSON structure |
| PT606 | SERVICE endpoint blocked by federation_endpoint_policy | The federation endpoint URL was blocked by pg_ripple.federation_endpoint_policy (v0.55.0). Blocked targets include RFC-1918 addresses, loopback, link-local, and file:// URLs in default-deny mode, or URLs not in federation_allowed_endpoints in allowlist mode | Set federation_endpoint_policy = 'open' (dev only), add the URL to federation_allowed_endpoints, or use a public endpoint. Also used as the streaming-interrupted code (v0.48.0 meaning: streaming export was cancelled) |
| PT607 | vector service endpoint not registered | The vector endpoint URL is not registered in _pg_ripple.federation_endpoints | Register the endpoint with pg_ripple.register_endpoint() |
| PT620 | SERVICE result set exceeds inline limit | The remote SERVICE returned more rows than pg_ripple.federation_inline_max_rows; results were materialized via a temp table instead | Increase federation_inline_max_rows or filter at the remote endpoint |
| PT621 | register_endpoint: private IP rejected | The endpoint URL resolves to a private/loopback IP and pg_ripple.federation_allow_private = off | Set federation_allow_private = on (development only) or use a public endpoint |
| PT640 | SPARQL result set exceeded sparql_max_rows | The result set was too large; truncated to pg_ripple.sparql_max_rows | Increase pg_ripple.sparql_max_rows or add LIMIT/OFFSET to the query |
| PT642 | export truncated to <N> rows | The streaming export hit pg_ripple.export_max_rows | Increase pg_ripple.export_max_rows or paginate using OFFSET |
PT700–PT799: Configuration / Startup
Errors from extension initialization, GUC validation, and background workers.
| Code | Message | Cause | Fix |
|---|---|---|---|
| PT700 | LLM endpoint unreachable or returned HTTP error: <detail> | pg_ripple.llm_endpoint is empty, or the HTTP call to the LLM failed | Set a valid endpoint URL; check network access and API key |
| PT701 | LLM response did not contain a valid SPARQL query | The LLM returned text that is not a SPARQL query | Add few-shot examples via add_llm_example(); switch to a more capable model |
| PT702 | LLM-generated SPARQL query failed to parse: <parse_error> | The generated SPARQL string could not be parsed by spargebra | Add a few-shot example for this question pattern; use a SPARQL-fine-tuned model |
| PT703 | merge worker watchdog: worker has been silent for <N> seconds | The background merge worker may have crashed | Check pg_log for crash details; restart PostgreSQL |
| PT704 | extension version mismatch: binary <v1>, control <v2> | The compiled extension version does not match pg_ripple.control | Rebuild and reinstall the extension |
| PT705 | GUC validation: <param> out of range | A GUC parameter was set to an invalid value | Check the GUC Reference for valid ranges |
| PT706 | shared_preload_libraries: pg_ripple not loaded | pg_ripple is not in shared_preload_libraries | Add pg_ripple to shared_preload_libraries in postgresql.conf and restart |
| PT707 | pg_trickle not installed | A feature requiring pg_trickle was called | Install pg_trickle or use the non-trickle alternative |
| PT708 | pgvector not installed | A vector/embedding function was called without pgvector | Install pgvector or disable with pg_ripple.pgvector_enabled = off |
| PT709 | enable_graph_rls: RLS policy creation failed | Row-level security policy could not be created | Check superuser privileges |
| PT710 | grant_graph: invalid permission | Permission must be 'read', 'write', or 'admin' | Use one of the three valid permission strings |
| PT711 | JSON-LD: unrecognised @embed value | The JSON-LD frame contains an unrecognised @embed keyword value (valid values are @once, @always, @never) | Fix the frame's @embed value |
| PT712 | JSON-LD: frame nesting depth exceeded | The JSON-LD frame's nesting depth exceeds pg_ripple.max_path_depth | Increase pg_ripple.max_path_depth or simplify the frame |
PT800–PT899: pg_tide CDC Bridge
Errors from the pg_tide CDC bridge integration.
| Code | Message | Cause | Fix |
|---|---|---|---|
| PT800 | pg_tide not installed or relay integration disabled | A CDC bridge function was called but the pg_tide extension is not installed or pg_ripple.trickle_integration = off | Install pg_tide, run CREATE EXTENSION pg_tide, create the target outbox with tide.outbox_create(...), and set pg_ripple.trickle_integration = on; or use pg_ripple.load_ntriples() for direct loads |
If you encounter an error code not listed here, or a message that says "contact maintainers", please open a GitHub issue with the full error output, your pg_ripple version (SELECT pg_ripple.canary()), and a minimal reproducer.
Scalability Reference
pg_ripple supports horizontal scalability via Citus for PostgreSQL.
Citus Integration
When pg_ripple.citus_sharding_enabled = on and the Citus extension is installed, pg_ripple
distributes VP tables across Citus worker nodes using the subject (s) column as the shard key.
Features
- Shard-pruning for bound subjects: SPARQL queries with a bound subject IRI are rewritten
to include
WHERE s = <encoded_id>so Citus routes the query directly to the shard holding that subject (v0.59.0). - Direct-shard bulk load:
load_*functions write triples directly to the physical Citus shard tables, bypassing the coordinator routing step (v0.61.0 CITUS-21). - BRIN summarise on rebalance: VP main partitions are BRIN-indexed; after a shard rebalance the summarise step is re-run on affected shards (v0.63.0).
- HyperLogLog COUNT(DISTINCT): Citus HLL extension is leveraged for approximate
COUNT(DISTINCT ?var)aggregates in federated queries (v0.63.0, v0.68.0). - Per-named-graph RLS propagation:
grant_graph_accesspropagates row-level security policies to all Citus worker nodes viarun_command_on_all_nodes(v0.61.0 CITUS-05). Integration test:tests/integration/citus_rls_propagation.sh(planned for v0.71.0).
Configuration
| GUC | Default | Description |
|---|---|---|
pg_ripple.citus_sharding_enabled | off | Enable Citus distribution of VP tables |
pg_ripple.citus_shard_count | 32 | Number of shards per VP table |
pg_ripple.citus_hll_enabled | off | Use HLL extension for COUNT(DISTINCT) |
Limitations
- Citus multi-node integration tests are planned for v0.71.0 (CITUS-INT-01).
- Cross-shard property-path queries may not benefit from shard pruning.
See also: Performance Tuning, Architecture.
FAQ
General
Why VP tables instead of one big triple table?
A single (s, p, o, g) table with 100M triples requires a B-tree index that touches all four columns for any useful predicate-specific query. Each query must scan rows for all predicates regardless of the filter.
Vertical Partitioning (one table per predicate) means a query for <ex:knows> triples only scans the vp_{knows_id} table — typically a fraction of the total data. The two B-tree indexes on (s, o) and (o, s) are small and cache-friendly. SPARQL star-patterns (same subject, multiple predicates) become simple multi-way joins between small tables.
Why PostgreSQL 18?
pg_ripple uses the CYCLE clause in WITH RECURSIVE CTEs for hash-based cycle detection in property path queries. The CYCLE clause was introduced in PostgreSQL 14 but the hash-based variant (as opposed to array-based) first became performant in PG 17/18. PG 18 is also the first version where pgrx 0.18 has stable support.
Is pg_ripple compatible with LPG tools?
Not yet. A Cypher/GQL compatibility layer is on the post-1.0 roadmap. The VP storage structure is architecturally aligned with LPG — each VP table is a property edge type — so the mapping will be natural.
What RDF formats does pg_ripple support?
Import (loading):
- N-Triples and N-Triples-star (
load_ntriples) - N-Quads (
load_nquads) - Turtle and Turtle-star (
load_turtle) - TriG (
load_trig) - RDF/XML (
load_rdfxml, v0.9.0)
Export:
- N-Triples (
export_ntriples) - N-Quads (
export_nquads) - Turtle (
export_turtle, v0.9.0) — including Turtle-star for RDF-star data - JSON-LD expanded form (
export_jsonld, v0.9.0) - Streaming Turtle or JSON-LD for large graphs (
export_turtle_stream,export_jsonld_stream, v0.9.0)
SPARQL CONSTRUCT and DESCRIBE results can be serialized directly to Turtle or JSON-LD via sparql_construct_turtle, sparql_construct_jsonld, sparql_describe_turtle, and sparql_describe_jsonld (v0.9.0).
Can I use pg_ripple with JSON-LD for REST APIs?
Yes. Use export_jsonld() or sparql_construct_jsonld() to produce JSON-LD responses:
-- Full graph as JSON-LD
SELECT pg_ripple.export_jsonld('https://myapp.example.org/graph/users');
-- SPARQL-driven selection as JSON-LD
SELECT pg_ripple.sparql_construct_jsonld('
CONSTRUCT { ?s ?p ?o }
WHERE { ?s a <https://schema.org/Person> ; ?p ?o }
');
The output is JSON-LD in expanded form — each subject is one array entry with IRI keys and typed value arrays.
SPARQL
What SPARQL 1.1 features are supported?
As of v0.19.0, the full SPARQL 1.1 specification is implemented:
Query forms: SELECT, ASK, CONSTRUCT, DESCRIBE
Graph patterns: BGP, OPTIONAL (LeftJoin), UNION, MINUS, FILTER, BIND, VALUES, Named graphs via GRAPH
Property paths: +, *, ?, / (sequence), | (alternative), ^ (inverse)
Aggregates: GROUP BY, HAVING, COUNT, SUM, AVG, MIN, MAX, GROUP_CONCAT
Modifiers: DISTINCT, ORDER BY, LIMIT, OFFSET, subqueries
Update: INSERT DATA, DELETE DATA, DELETE/INSERT WHERE, LOAD, CLEAR, DROP, CREATE, COPY, MOVE, ADD
Federation: SERVICE <url> { … } with SSRF allowlist, SERVICE SILENT, connection pooling, result caching, adaptive timeouts, batch SERVICE detection
Does pg_ripple support SPARQL 1.1 property paths?
Yes, as of v0.5.0. All standard path operators are supported: +, *, ?, / (sequence), | (alternative), ^ (inverse). Negated property sets !(p1|p2) are partially supported via vp_rare.
Property path queries compile to WITH RECURSIVE CTEs with PostgreSQL 18's CYCLE clause for hash-based cycle detection.
What is the maximum traversal depth for property paths?
Controlled by the pg_ripple.max_path_depth GUC (default: 100). Set it lower to prevent runaway queries on dense graphs:
SET pg_ripple.max_path_depth = 10;
Why does my FILTER not match a number?
SPARQL FILTER comparisons on numeric literals (FILTER(?age >= 18)) require the literal to be typed with an XSD numeric type:
"18"^^<http://www.w3.org/2001/XMLSchema#integer>
Plain string literals like "18" are compared as strings. Use typed literals when inserting numeric data, or cast in the FILTER expression.
Data modeling
What's the difference between a named graph and a blank node?
A named graph is a set of triples identified by an IRI. It is used for partitioning data by source, time, or topic. You can query across all named graphs, query within a specific graph, or count triples per graph.
A blank node is a resource without a global IRI identity — it has identity only within a document load scope. Blank nodes are used for anonymous resources (e.g. intermediate nodes in a structure) that don't need a stable identifier.
What is an RDF-star quoted triple?
A quoted triple << s p o >> is a triple that can appear in subject or object position in another triple. It enables statements about triples — useful for provenance (<< alice knows bob >> :assertedBy :carol), temporal annotations, and confidence scores.
pg_ripple stores quoted triples as dictionary entries of kind = 5. See RDF-star for details.
Performance
How fast is bulk load?
On a modern server with an NVMe SSD, load_ntriples() processes approximately 50,000–150,000 triples per second (single connection, default settings). Performance depends on predicate diversity (more unique predicates → more VP tables created), hardware, and PostgreSQL configuration.
When should I use SPARQL vs find_triples?
find_triples() only matches a single (s, p, o, g) pattern — it is equivalent to a SPARQL BGP with exactly one triple pattern. Use it for single-pattern lookups.
Use sparql() for anything more complex: multi-pattern joins, OPTIONAL, FILTER, aggregates, property paths, or when you want the ergonomics of SPARQL's variable-binding model.
HTAP & Operations (v0.6.0)
Does pg_ripple require shared_preload_libraries?
For full HTAP functionality (background merge worker, latch-poke hook, shared-memory statistics) you must add pg_ripple to shared_preload_libraries:
shared_preload_libraries = 'pg_ripple'
Without this, the extension still works for reads and writes — but all writes stay in delta tables and are never automatically merged into main. Queries on predicates with large deltas will be slower than expected.
See the Pre-Deployment Checklist for the complete setup sequence.
What is the difference between compact() and the merge worker?
compact() | Merge worker | |
|---|---|---|
| Trigger | Manual SQL call | Automatic (latch poke or timer) |
| Blocks caller | Yes | No — runs in background |
| When to use | Maintenance windows, tests | Production continuous operation |
Both produce the same result: delta rows are moved into main, tombstones are cleared, and a fresh BRIN index is built.
How do I know if the merge worker is keeping up?
-- Check unmerged row count
SELECT pg_ripple.stats() -> 'unmerged_delta_rows';
-- Watch it over time
SELECT now(), (pg_ripple.stats() -> 'unmerged_delta_rows')::int AS lag
FROM generate_series(1, 10) g,
pg_sleep(5) AS _s
WHERE true; -- run this manually in a loop
A healthy deployment shows unmerged_delta_rows rising during writes and falling after merges. If it only rises, the worker is behind — lower merge_threshold or increase server I/O capacity.
Can I subscribe to triple changes in real time?
Yes. CDC (Change Data Capture) is available in v0.6.0 via PostgreSQL NOTIFY:
-- Subscribe to a specific predicate
SELECT pg_ripple.subscribe('<https://schema.org/name>', 'name_changes');
-- In another session
LISTEN name_changes;
-- Notifications arrive when triples are inserted or deleted
SELECT pg_ripple.insert_triple(
'<https://example.org/Alice>',
'<https://schema.org/name>',
'"Alice"'
);
Subscriptions are stored in _pg_ripple.cdc_subscriptions and persist across reconnects (but must be re-registered after a server restart). See the Administration reference for details.
Why does my query not see recently inserted triples?
If you inserted triples and immediately queried with SPARQL, the results should include those triples — delta tables are always queried alongside main tables.
If triples are missing, check:
- The triple was committed (not inside an uncommitted transaction)
- The correct graph is being queried (default graph vs named graph)
- The correct predicate IRI spelling was used
What is the HTTP endpoint URL?
The pg_ripple_http companion service listens on http://localhost:7878/sparql by default. Configure the port with PG_RIPPLE_HTTP_PORT. The URL accepts both GET and POST SPARQL requests per the W3C SPARQL 1.1 Protocol.
How do I connect SPARQL tools to pg_ripple?
Start pg_ripple_http alongside your PostgreSQL instance. Point any SPARQL client (YASGUI, Protege, SPARQLWrapper, Jena) to http://localhost:7878/sparql. The endpoint supports standard content negotiation (Accept: application/sparql-results+json, text/turtle, etc.).
Can I run pg_ripple_http inside Docker?
Yes. The Docker image bundles both PostgreSQL and pg_ripple_http. Use docker compose up with the provided docker-compose.yml to start both services. The SPARQL endpoint is exposed on port 7878 by default.
JSON-LD Framing (v0.17.0)
What is JSON-LD Framing and how is it different from plain JSON-LD export?
Plain JSON-LD export (export_jsonld) serializes every triple in the graph as a flat list of node objects. JSON-LD Framing lets you specify the desired output shape — which types to select, which properties to include, and how to nest related nodes — using a frame document. The result is a nested, structured JSON-LD document suitable for serving directly from a REST API.
The key difference in performance: framing reads only the VP tables touched by the frame. A frame targeting 3 predicates on a graph with 10,000 predicates reads 3 VP tables, not 10,000.
Which W3C framing features are supported?
pg_ripple v0.17.0 supports: @type matching, @id matching, property wildcards {}, absent-property patterns [], @reverse, @embed (@once/@always/@never), @explicit, @omitDefault, @default, @requireAll, @context compaction, named graph @graph scoping, and @omitGraph.
Value pattern matching (@value/@language/@type inside value objects) is deferred to a future release.
What is value pattern matching and why is it deferred?
Value pattern matching would allow frames like {"ex:name": {"@language": "en"}} to select only English-language name literals. Implementing this correctly requires a full-graph scan to find matching literals — it cannot be done efficiently with the VP table join model. It is deferred until a targeted literal index is available.
What is the difference between framing views and SPARQL views?
SPARQL views (create_sparql_view) store raw SPARQL SELECT results as integer ID columns in a stream table. Framing views (create_framing_view) run the full embedding and compaction pipeline over CONSTRUCT results, so each row in the stream table contains a ready-to-serve nested JSON-LD document rather than raw projection values.
Use SPARQL views when you need low-level access to result bindings; use framing views when you want ready-to-serve nested JSON-LD for an API.
Vector Federation (v0.28.0)
How does vector federation work?
After registering an external endpoint with pg_ripple.register_vector_endpoint(url, api_type), pg_ripple can route similarity queries to Weaviate, Qdrant, Pinecone, or a remote pgvector instance. The results are merged with local triple store data using Reciprocal Rank Fusion inside hybrid_search().
How do I prevent SSRF attacks when using vector federation?
pg_ripple does not restrict which URLs can be registered. You should use network policies (e.g., Kubernetes NetworkPolicy, AWS security groups) to restrict which external hosts your PostgreSQL server can reach. Only register endpoints that belong to trusted vector services in your infrastructure.
Why does my federated query time out?
The default timeout is 5000 ms. Increase it with:
SET pg_ripple.vector_federation_timeout_ms = 30000;
Or configure it globally via ALTER SYSTEM SET pg_ripple.vector_federation_timeout_ms = 30000; SELECT pg_reload_conf();
How do I configure a remote endpoint's API key?
pg_ripple does not store API keys for external vector services. Pass the API key in the endpoint URL if the service supports it, or configure it via environment variables in your application layer before calling the endpoint.
Optional-Feature Degradation Semantics
v0.64.0 TRUTH-10: This page documents the expected degraded behavior for every optional feature in pg_ripple. Operators can use
pg_ripple.feature_status()to check the live status at runtime.
Overview
pg_ripple distinguishes between required core features (always active) and optional features that depend on external extensions, configuration, or future development milestones. When an optional feature is unavailable, pg_ripple degrades gracefully — it does not panic, it does not silently return wrong results, and it always emits a warning or structured error.
The pg_ripple.feature_status() SQL function returns one row per major capability
with an honest status value. Integrate this into your monitoring pipeline:
SELECT feature_name, status, degraded_reason
FROM pg_ripple.feature_status()
WHERE status IN ('degraded', 'stub', 'planned')
ORDER BY feature_name;
The pg_ripple_http /ready endpoint includes a partial_features array in its
response body, which lists all non-implemented features so operators can
assess readiness before routing production traffic.
Feature Degradation Reference
Arrow Flight (arrow_flight)
| Property | Value |
|---|---|
| Status in v0.63.0 | stub |
| Return on missing dependency | {"status":"stub","message":"Arrow IPC streaming not yet implemented"} |
| Warning code | None (HTTP 200 with stub body) |
| Readiness behavior | Reported as stub in /ready partial_features |
| Planned implementation | v0.66.0 |
Detail: The /flight/do_get endpoint in pg_ripple_http returns a JSON stub
body instead of Arrow IPC data. This is not a crash or error — it is a placeholder
until the Arrow IPC serialization layer is implemented. Do not rely on Arrow Flight
output for production data pipelines until v0.66.0.
WCOJ / Worst-Case Optimal Joins (wcoj)
| Property | Value |
|---|---|
| Status in v0.63.0 | planner_hint |
| Behavior | Cyclic BGP join reordering at plan time; no custom executor |
| Warning code | None |
| Readiness behavior | Reported as planner_hint in /ready |
| Planned implementation | v0.66.0 (true Leapfrog Triejoin executor) |
Detail: WCOJ is implemented as a SPARQL-to-SQL planner optimization that reorders cyclic-pattern joins to reduce intermediate result sizes. A true Leapfrog Triejoin executor that intersects sorted iterators at runtime is not implemented. For cyclic BGPs (e.g. triangle queries), pg_ripple uses PostgreSQL's native hash-join or merge-join, which may be slower than a true WCOJ executor for highly cyclic graphs.
SHACL-SPARQL Rules (shacl_sparql_rule)
| Property | Value |
|---|---|
| Status in v0.63.0 | planned |
| Return on invocation | Rule is stored but never fires |
| Warning code | WARNING level log when a sh:SPARQLRule shape is loaded |
| Readiness behavior | Reported as planned in /ready |
| Planned implementation | v0.65.0 |
Detail: sh:SPARQLRule shapes are parsed, stored in the SHACL catalog, and
associated with their target classes. However, they are not routed through the
derivation kernel and will not fire during inference or validation runs. Use
sh:SPARQLConstraint (which IS implemented) for SPARQL-backed validation.
CONSTRUCT Writeback (construct_writeback)
| Property | Value |
|---|---|
| Status in v0.63.0 | manual_refresh |
| Behavior | Rules apply only when explicitly invoked via pg_ripple.apply_construct_rules() |
| Warning code | None |
| Readiness behavior | Reported as manual_refresh in /ready |
| Planned implementation | v0.65.0 (incremental delta maintenance) |
Detail: CONSTRUCT writeback rules are correct — they produce the right output
when invoked. However, they do not maintain derived triples incrementally when
source data changes. After a bulk load or SPARQL UPDATE, operators must manually
call pg_ripple.apply_construct_rules() to refresh derived triples.
Incremental delta maintenance (triggered automatically on insert/delete) is
planned for v0.65.0.
Citus SERVICE Pruning (citus_service_pruning)
| Property | Value |
|---|---|
| Status in v0.63.0 | planned |
| Return on invocation | Query executes without shard pruning (correct but slower) |
| Warning code | None |
| Readiness behavior | Reported as planned in /ready |
| Planned implementation | v0.66.0 |
Detail: SERVICE result shard pruning routes SPARQL SERVICE calls to the specific Citus worker shard that holds the relevant data, avoiding full-cluster fan-out. This optimization is planned but not yet integrated into the SPARQL-to-SQL translator. Queries return correct results but may scan more shards than necessary.
Citus HLL COUNT(DISTINCT) (citus_hll_distinct)
| Property | Value |
|---|---|
| Status in v0.63.0 | planned |
| Return on invocation | COUNT(DISTINCT) uses exact counting (correct but slower) |
| Warning code | None |
| Readiness behavior | Reported as planned in /ready |
| Planned implementation | v0.66.0 |
Detail: HyperLogLog approximate COUNT(DISTINCT) uses a probabilistic sketch to count distinct values across Citus shards without inter-shard coordination. The SQL aggregate generation layer does not yet emit HLL function calls. COUNT(DISTINCT) currently uses exact counting, which is correct but requires data movement across shards.
SPARQL Cursor Streaming (sparql_cursor_streaming)
| Property | Value |
|---|---|
| Status in v0.63.0 | planned |
| Behavior | /sparql/stream endpoint materializes full result set before streaming |
| Warning code | None |
| Readiness behavior | Reported as planned in /ready |
| Planned implementation | v0.66.0 |
Detail: The /sparql/stream endpoint streams responses as chunked
transfer encoding, but the full result set is materialized in memory before
the first chunk is sent. True incremental streaming (where rows are emitted
to the client as they arrive from PostgreSQL) is planned for v0.66.0.
For very large result sets, use pg_ripple.sparql_cursor() in SQL instead.
Vector Hybrid Search (vector_hybrid_search)
| Property | Value |
|---|---|
| Status in v0.63.0 | experimental |
| Dependency | pgvector extension |
| Return on missing pgvector | Exact nearest-neighbor search (correct but slower) |
| Warning code | WARNING: pgvector not installed; falling back to exact similarity search |
| Readiness behavior | Reported as experimental in /ready with degraded_reason |
Detail: When pgvector is installed, hybrid search uses HNSW approximate nearest-neighbor indexes for sub-millisecond similarity search. Without pgvector, pg_ripple falls back to exact cosine distance computation, which is always correct but O(n) in the number of embeddings. Install pgvector for production vector workloads.
CDC Subscriptions (cdc_subscriptions)
| Property | Value |
|---|---|
| Status in v0.63.0 | experimental |
| Dependency | None for subscription catalog registration; pg_tide is required only for relay/outbox transport |
| Return when pg_tide is missing | create_subscription() still records the subscription; relay-dependent delivery paths are unavailable |
| Readiness behavior | Reported as experimental in /ready while the subscription surface remains pre-1.0 |
Detail: create_subscription() writes subscription metadata into
_pg_ripple.subscriptions and does not require pg_trickle. Relay/outbox
pipelines that publish changes to external systems require pg_tide; when pg_tide
is absent, those relay paths degrade, but the subscription catalog API remains
available.
Checking Feature Status in Code
-- List all non-implemented features with their degraded reason.
SELECT feature_name, status, degraded_reason
FROM pg_ripple.feature_status()
WHERE status != 'implemented'
ORDER BY status, feature_name;
-- Check a specific feature.
SELECT status, degraded_reason
FROM pg_ripple.feature_status()
WHERE feature_name = 'arrow_flight';
# Check /ready for degraded features (pg_ripple_http).
curl -s http://localhost:7878/ready | jq '.partial_features'
Metrics and Log Visibility
Optional-feature degradation is visible through:
pg_ripple.feature_status()— SQL function; one row per featurepg_ripple_http /ready— includespartial_featuresarray- PostgreSQL log —
WARNINGlevel messages when optional extensions are absent and a feature that depends on them is invoked - Prometheus metrics —
pg_ripple_http_errors_totalcounter increments when a stub feature returns an error response
See also: Feature Status SQL API, Known Limitations
Glossary
Plain-language definitions of terms used throughout the pg_ripple documentation.
Blank node
An anonymous node in an RDF graph — it has no IRI. Used when the identity of a resource does not matter, only its connections. Written as _:label in N-Triples/Turtle. Internally stored as a dictionary-encoded BIGINT like any other term.
CDC (Change Data Capture)
A mechanism for subscribing to insert and delete events on the triple store. pg_ripple exposes CDC via subscribe() and unsubscribe(), backed by PostgreSQL LISTEN/NOTIFY.
Dictionary encoding
The process of mapping every IRI, blank node, and literal to a unique BIGINT (i64) integer using an XXH3-128 hash. All VP tables store only integer IDs, never raw strings. This makes joins fast and storage compact.
Embedding
A fixed-length numeric vector (typically 256–1536 dimensions) representing the semantic meaning of an entity or text. pg_ripple stores embeddings via pgvector and uses them for similarity search and RAG retrieval.
Federation
Distributing a SPARQL query across multiple endpoints. When a query contains a SERVICE <url> { … } block, pg_ripple sends that subquery to the remote SPARQL endpoint and joins the results locally.
Frame (JSON-LD)
A JSON template that reshapes a flat RDF graph into a tree-structured JSON-LD document. pg_ripple's jsonld_frame() and export_jsonld_framed() functions apply frames to produce nested, application-friendly JSON.
GraphRAG
A retrieval-augmented generation (RAG) approach that uses a knowledge graph as the retrieval backend instead of (or in addition to) a vector store. pg_ripple exports data in Microsoft GraphRAG-compatible formats via export_graphrag_entities(), export_graphrag_relationships(), and export_graphrag_text_units().
GUC (Grand Unified Configuration)
PostgreSQL's configuration parameter system. pg_ripple exposes settings like pg_ripple.max_path_depth and pg_ripple.dictionary_cache_size as GUC parameters. Set them with SET, ALTER SYSTEM SET, or in postgresql.conf.
HNSW (Hierarchical Navigable Small World)
An approximate nearest-neighbor index algorithm used by pgvector. pg_ripple creates HNSW indices on embedding columns for fast similarity search.
HTAP (Hybrid Transactional/Analytical Processing)
pg_ripple's storage split (since v0.6.0) where writes go to a delta partition (heap + B-tree) and reads scan (main EXCEPT tombstones) UNION ALL delta. A background merge worker periodically combines delta into main with BRIN indices for analytical scan performance.
IRI (Internationalized Resource Identifier)
A globally unique identifier for a resource in an RDF graph, like <https://example.org/alice>. Written in angle brackets in SPARQL and N-Triples. The RDF equivalent of a URL.
JSON-LD
A JSON-based serialization of RDF. It represents triples as nested JSON objects using @context for namespace mapping and @id for node identifiers. pg_ripple can export to JSON-LD and apply JSON-LD frames.
Literal
A data value in an RDF graph — a string, number, date, or boolean. Can have a datatype ("42"^^xsd:integer) or a language tag ("hello"@en). Stored as a dictionary-encoded integer in VP tables.
Magic sets
A Datalog optimization technique that rewrites a program to focus computation on only the tuples needed to answer a specific query, rather than computing all possible derivations. Used by infer_demand().
Materialization
The process of computing all triples derivable from a set of Datalog rules and storing them explicitly in VP tables. infer() runs full materialization using semi-naive evaluation. Materialized triples have source = 1 in VP tables.
Merge worker
A pgrx background worker that periodically combines HTAP delta partitions into main partitions. It runs as a separate PostgreSQL backend process, configured via pg_ripple.worker_database.
Named graph
A sub-graph of an RDF dataset identified by an IRI. Triples in the default graph have graph ID 0; named graphs have IDs > 0. Named graphs are used for provenance tracking, access control, and dataset organization.
OWL RL (Web Ontology Language — Rule Language profile)
A subset of OWL that can be implemented as Datalog rules. pg_ripple ships a built-in owl-rl rule set covering class and property reasoning (subclass, inverse, transitive, symmetric, owl:sameAs canonicalization).
Predicate
The middle element of an RDF triple — the relationship between subject and object. For example, in <alice> <knows> <bob>, <knows> is the predicate. Each unique predicate gets its own VP table.
Property path
A SPARQL syntax for traversing chains of predicates in a graph. Supports sequence (/), alternative (|), inverse (^), zero-or-more (*), one-or-more (+), and zero-or-one (?). Compiled to WITH RECURSIVE … CYCLE SQL.
RAG (Retrieval-Augmented Generation)
An AI pattern that retrieves relevant context from a knowledge base before generating a response with a language model. pg_ripple's rag_retrieve() combines graph traversal and vector similarity for context retrieval.
RDFS (RDF Schema)
A vocabulary for defining classes and properties in RDF. pg_ripple ships a built-in rdfs rule set that implements subclass inference (rdfs:subClassOf), domain/range inference (rdfs:domain, rdfs:range), and other RDFS entailment rules.
RDF-star
An extension to RDF that allows triples to be subjects or objects of other triples (quoted triples). Written as << :alice :knows :bob >> :certainty 0.9 in Turtle-star. pg_ripple stores quoted triples via qt_s, qt_p, qt_o columns in the dictionary.
RRF (Reciprocal Rank Fusion)
A score fusion method that combines rankings from multiple retrieval systems (e.g., SPARQL results and vector similarity). Used by hybrid_search() with a tunable alpha parameter.
Semi-naive evaluation
The standard Datalog materialization algorithm. Instead of re-evaluating all rules each iteration, it only considers tuples derived in the previous iteration (the delta) joined with all known tuples. This avoids redundant computation.
SHACL (Shapes Constraint Language)
A W3C standard for validating RDF graphs against a set of constraints (shapes). pg_ripple supports SHACL Core for data quality validation via load_shacl() and validate(), plus trigger-based and async DAG-aware monitoring.
SID (Statement Identifier)
A globally unique BIGINT assigned to every triple from a shared PostgreSQL sequence (statement_id_seq). Stored in the i column of VP tables. Used by CDC, provenance tracking, and get_statement().
SPARQL
The W3C standard query language for RDF graphs. pg_ripple translates SPARQL to SQL and executes it against VP tables via SPI. Supports SELECT, CONSTRUCT, ASK, DESCRIBE, and the full Update language.
Stratification
The process of ordering Datalog rules into strata so that negation and aggregation are evaluated in the correct sequence. Rules in stratum n depend only on predicates fully computed in strata < n. Programs with negation cycles through the same stratum are unstratifiable (use infer_wfs() instead).
Tabling
A memoization technique for Datalog evaluation that caches intermediate results to avoid redundant computation and handle left-recursive rules. pg_ripple's tabling engine stores results in a memo table and checks for subsumption.
Triple
The fundamental unit of data in RDF: a (subject, predicate, object) statement. For example, <alice> <knows> <bob> asserts that Alice knows Bob. pg_ripple stores triples as (s, o, g) integer tuples in VP tables, one table per predicate.
VP table (Vertical Partitioning table)
pg_ripple's primary storage structure. Each unique predicate gets its own table (_pg_ripple.vp_{id}) with columns s (subject), o (object), g (graph), i (SID), and source. This layout optimizes predicate-specific scans and star-pattern joins.
Well-founded semantics (WFS)
A three-valued semantics for Datalog programs with negation. Unlike stratification (which rejects some programs), WFS assigns every atom a value of true, false, or undefined. pg_ripple implements WFS via infer_wfs() for programs that cannot be stratified.
Changelog
All notable changes to pg_ripple are documented in this file.
The format follows Keep a Changelog. Versions correspond to the milestones in ROADMAP.md.
[Unreleased]
Fixed
- CI: CloudNativePG extension image publishing — Added
docker-cnpgjob to release workflow to build and publish the extension volume image with the-cnpgsuffix on each tagged release. Thedocker/Dockerfile.cnpgexisted but was never built by CI, causingdocker pull ghcr.io/trickle-labs/pg-ripple:<version>-cnpgto fail with "not found". Now the extension image is published automatically alongside the main extension and HTTP companion images.
[0.128.0] — 2026-05-22 — JSON Mapping Relational Writeback (JSON-WRITEBACK-01)
Completes the register_json_mapping round-trip by adding a relational
write-back path (RDF → Relational). Changes to RDF triples can now be
automatically propagated back to the source relational table via direct SQL
calls or trigger-based async queueing.
Added
pg_ripple.writeback_json_row(mapping TEXT, subject_iri TEXT) → BIGINT— exports the subject as JSON using the named mapping context, maps JSON keys to relational columns, and executes anINSERT … ON CONFLICTbased on the configured conflict policy (replace,skip,error).pg_ripple.writeback_json_row_delete(mapping TEXT, subject_iri TEXT) → BIGINT— deletes the target relational row using decoded key-column values.pg_ripple.enable_json_writeback(mapping TEXT) → VOID— installsAFTER INSERT OR DELETE FOR EACH ROWtriggers on VP delta tables, enqueuing events into_pg_ripple.json_writeback_queuefor async background processing.pg_ripple.disable_json_writeback(mapping TEXT) → VOID— drops all writeback triggers; idempotent.pg_ripple.json_writeback_status() → TABLE— operational view of queue depth, error count, andlast_processed_atper mapping.pg_ripple.json_writeback_batch_sizeGUC (default 100, range 0–10000) — rows drained per background merge-worker tick; set to 0 to disable auto-drain._pg_ripple.json_writeback_queuecatalog table — asynchronous writeback event queue withmapping_name,subject_id,operation,queued_at,processed_at,error._pg_ripple.json_writeback_enqueue_fn()PL/pgSQL trigger function — enqueues VP delta changes into the writeback queue.- Five new columns on
_pg_ripple.json_mappings:writeback_table,writeback_schema(default'public'),writeback_key_columns(default{}),writeback_conflict_policy(default'replace'),writeback_enabled(defaultfalse). - HTTP
POST /json-mapping/{name}/writeback— synchronous single-subject writeback; returns{"rows_affected": N}; requires write-auth. - HTTP
GET /json-mapping/{name}/writeback/status— queue depth, error count, andlast_errorJSON; requires read-auth. feature_status()now includes'json_mapping_writeback'entry.- Blog post
blog/json-ld-reverse-mapping.md. - Docs page
docs/src/features/json-mapping.md. - GUC docs in
docs/src/reference/guc-reference.md. - 21 pg_regress tests in
tests/pg_regress/sql/v0128_json_writeback.sql.
Changed
- Background merge worker (worker 0) now drains
_pg_ripple.json_writeback_queueafter each merge cycle whenjson_writeback_batch_size > 0.
Migration
sql/pg_ripple--0.127.0--0.128.0.sql—ALTER TABLE _pg_ripple.json_mappings ADD COLUMN IF NOT EXISTS …;CREATE TABLE _pg_ripple.json_writeback_queue; index on pending rows;json_writeback_enqueue_fn()PL/pgSQL trigger function.
[0.127.0] — 2026-05-21 — pg_tide Relay Migration Cleanup
Moves relay-facing CDC bridge behavior from the obsolete pg-trickle relay model to pg_tide named outboxes, updates the project documentation to current pg_tide terminology and deployment commands, and adds a detailed remediation plan for the remaining historical references.
pg_trickle remains the companion extension for incremental view maintenance only.
Relay, outbox, inbox, consumer-group, and relay-process guidance now points at
pg_tide. The CDC bridge now validates pg_tide availability and publishes events
with tide.outbox_publish() instead of inserting into a pg-trickle-style outbox
table, while retaining compatibility names for existing callers.
Added
- PGTIDE-RELAY-01
pg_ripple.relay_available() -> BOOLEANcanonical SQL helper for relay/outbox/inbox availability checks. - PGTIDE-RELAY-02
_pg_ripple.cdc_bridge_triggers.outbox_namecatalog column for pg_tide outbox names, withoutbox_tableretained as a compatibility alias. - PGTIDE-DOC-01 pg-tide-relay-fixes.md detailed research and remediation plan.
- Migration script
sql/pg_ripple--0.126.0--0.127.0.sql. - Roadmap file
roadmap/v0.127.0.md.
Changed
- PGTIDE-RELAY-03 CDC bridge trigger publishing now calls
tide.outbox_publish(outbox_name, payload, headers)and stores the stableripple:{statement_id}dedup key in pg_tide headers. - PGTIDE-RELAY-04 Deprecated
pg_ripple.trickle_available()now aliasesrelay_available()for relay checks; usepg_ripple.pg_trickle_available()for IVM checks. - PGTIDE-DOC-02 User-facing relay docs, runbooks, examples, and Docker
snippets now use the
pg-tidecommand,ghcr.io/trickle-labs/pg-tideimage,PG_TIDE_POSTGRES_URL, and stabletide.relay_set_outbox_v2()/tide.relay_set_inbox_v2()SQL APIs. - DEP-01
pg_tidetested/bundled version bumped from0.16.0to0.33.0in.versions.tomlandDockerfile. pg_ripple_http:COMPATIBLE_EXTENSION_MINbumped from"0.125.0"to"0.126.0".
[0.126.0] — 2026-05-21 — Per-Endpoint Federation Credentials (FEAT-03)
Adds encrypted per-endpoint OAuth2 Bearer / API-key credential storage with pgcrypto-backed pgp_sym_encrypt, a credential audit view, atomic rotation, automatic header injection in the federation executor, and 10 new regression tests.
Federated SPARQL queries that span multiple organizations or external data services almost always require authentication, and managing those credentials securely across a distributed knowledge graph deployment has been a notable gap in pg_ripple's federation story. This release closes that gap with a dedicated per-endpoint credential management system: Bearer tokens and API keys can be registered for any federation endpoint and are stored encrypted at rest using pgcrypto's OpenPGP symmetric encryption, protected by a superuser-only key that is never visible through SHOW or any administrative view. The plaintext of a stored credential is never returned by any query, function, or diagnostic endpoint — only the holder of the encryption key can decrypt it, and the key itself is never stored in the database. A credential audit view exposes operational metadata such as token age and last-used timestamp without ever exposing the underlying secret.
The operational workflow is designed for production credential management at scale. set_federation_credential() registers or atomically replaces credentials for any endpoint, while rotate_federation_credential() replaces a token and records the rotation timestamp in a single atomic operation, enabling automated rotation pipelines without authentication gaps. The federation query executor automatically retrieves and injects the correct HTTP header for each endpoint after SSRF validation, so existing SPARQL queries and application code require no changes when credentials are added, rotated, or removed. A matching HTTP endpoint exposes auth status for external monitoring and compliance dashboards, and ten regression tests validate the full lifecycle from initial storage through rotation through audit reporting, ensuring the feature behaves correctly across upgrade paths.
Added
- FEAT-03
pg_ripple.set_federation_credential(endpoint_iri TEXT, auth_type TEXT, token TEXT)— registers or replaces an encrypted credential for a federation endpoint. Tokens encrypted at-rest viapgcrypto.pgp_sym_encryptusing thepg_ripple.federation_credential_keyGUC (superuser-only, never visible viaSHOW). Errors: PT0510 (no key), PT0511 (no pgcrypto), PT0512 (unknown endpoint), PT0513 (invalid auth_type), PT0514 (encrypt fail), PT0515 (upsert fail). - FEAT-03
pg_ripple.rotate_federation_credential(endpoint_iri TEXT, new_token TEXT)— atomically replaces the token and recordsrotated_at = now(). Error: PT0516 (no credential for that endpoint). - FEAT-03
pg_ripple.federation_credential_audit() → TABLE(endpoint_iri TEXT, auth_type TEXT, token_age_days DOUBLE PRECISION, last_used_at TIMESTAMPTZ)— operational metadata only; never returns plaintext tokens. - FEAT-03
_pg_ripple.federation_credentialstable — stores encrypted tokens withauth_type CHECK ('bearer','apikey','none'),header_name,created_at,rotated_at, andlast_used_at. FK to_pg_ripple.federation_endpoints(url). - FEAT-03 GUC
pg_ripple.federation_credential_key— symmetric key for pgp_sym_encrypt/pgp_sym_decrypt.NO_SHOW_ALL | SUPERUSER_ONLY. - FEAT-03 Federation executor (
src/sparql/federation/http.rs) automatically injects the appropriate HTTP header (Bearer / API-key) after SSRF validation. Credential lookup happens post-SSRF to prevent credential-oracle attacks. - FEAT-03 HTTP endpoint
GET /federation/{endpoint}/auth-status— returns JSON withendpoint_iri,auth_type,token_age_days, andlast_used_at. Requires write-level authentication. - CRED-01–10
tests/pg_regress/sql/v0126_federation_credentials.sql— 10 regression tests covering table existence, GUC registration, function presence, column schema, constraint enforcement, and audit function. - Migration script
sql/pg_ripple--0.125.0--0.126.0.sql. - Roadmap file
roadmap/v0.126.0.md.
Changed
pg_ripple_http:COMPATIBLE_EXTENSION_MINremains"0.125.0"(one-version trailing window; v0.126.0 introduces backward-compatible additions only).
[0.125.0] — 2026-05-20 — Temporal Graph Snapshots (FEAT-02)
Adds point-in-time named-graph snapshots via pg_ripple.graph_at(),
a diff API via pg_ripple.graph_diff(), two HTTP endpoints, a Prometheus
gauge, automatic GC, and 15 new regression tests.
One of the most valuable capabilities in an enterprise knowledge graph is the ability to ask "what did the graph look like at a specific point in time?" — a question that arises constantly in regulatory audits, compliance reviews, incident investigations, and data reconciliation workflows. Version 0.125.0 delivers this capability through a complete temporal snapshot API: the graph_at() function materializes the state of any named graph at any past timestamp, registering the result with a deterministic URN identifier that can immediately be used as a named graph reference in SPARQL queries. This means that queries against historical snapshots are written exactly like queries against current data, with no special syntax or separate query path to learn. Snapshots are automatically garbage collected after a configurable retention period, preventing unbounded storage growth in long-running deployments.
The graph_diff() function complements snapshots by computing the exact set of triples that were added to or removed from a named graph between any two points in time, labeling each change as 'added' or 'removed'. This makes it straightforward to produce audit-compliant change logs, feed incremental updates to downstream data pipelines, investigate when a specific fact entered or left the knowledge graph, or verify that a migration ran correctly by comparing before and after states. Two HTTP endpoints expose both capabilities to external applications, and a Prometheus gauge tracks the current live snapshot count to inform capacity planning. Fifteen regression tests cover snapshot creation, idempotency guarantees, diff correctness, retention enforcement, and the table schema, ensuring the feature works reliably across upgrade paths.
Added
- FEAT-02
pg_ripple.graph_at(graph_iri TEXT, snapshot_time TIMESTAMPTZ) → TEXT— materialises a named-graph snapshot from_pg_ripple.temporal_factsat the given timestamp. Registers the snapshot in_pg_ripple.graph_snapshotsand returns a deterministicurn:snapshot:…IRI for use inGRAPH <iri> { … }SPARQL queries. - FEAT-02
pg_ripple.graph_diff(graph_iri TEXT, from_ts TIMESTAMPTZ, to_ts TIMESTAMPTZ) → TABLE(s BIGINT, p BIGINT, o BIGINT, change TEXT)— returns'added'/'removed'delta rows between two temporal snapshots, enabling audit-compliance workflows and incremental Turtle/N-Quads exports. - FEAT-02
pg_ripple.graph_snapshots_count() → BIGINT— returns the current live snapshot count. - FEAT-02
_pg_ripple.graph_snapshotstable and_pg_ripple.snapshot_id_seqsequence — catalog of registered snapshots withexpires_atfor GC. - FEAT-02 GUC
pg_ripple.snapshot_retention_days(default 30) — automatic GC of expired snapshots by the merge background worker tick. - FEAT-02
GET /temporal/graphs/{iri}/snapshot?at=<iso8601>HTTP endpoint — returns snapshot content as Turtle withX-Snapshot-IRIresponse header. - FEAT-02
GET /temporal/graphs/{iri}/diff?from=<iso8601>&to=<iso8601>HTTP endpoint — returns N-Quads delta between two timestamps. - FEAT-02 Prometheus gauge
pg_ripple_graph_snapshots_total— tracks live snapshot count; updated on each/temporal/graphs/{iri}/snapshotcall. - SNAP-01–15
tests/pg_regress/sql/v0125_temporal_graph_snapshots.sql— 15 regression tests covering snapshot creation, idempotency, count tracking, diff correctness, retention GUC, and table schema. - Migration script
sql/pg_ripple--0.124.0--0.125.0.sql— addsgraph_snapshotstable andsnapshot_id_seqsequence. - Roadmap file
roadmap/v0.125.0.md. - Blog post
blog/temporal-graph-snapshots.md.
Changed
pg_ripple_http:COMPATIBLE_EXTENSION_MINbumped from"0.123.0"to"0.125.0".
[0.124.0] — 2026-05-20 — SPARQL 1.2 Property Path Algebra Execution
Fixes a Cartesian-product bug (PATH-BNODE-01) in the SPARQL property path translator and adds 25 new regression tests covering all eight path algebra operators plus OWL 2 RL propertyChainAxiom n-hop chains.
Property paths in SPARQL are a powerful way to express graph traversal patterns — queries like "find all things reachable from this resource via any combination of these two properties in sequence." However, a subtle bug in pg_ripple's property path translation layer was producing incorrect results for compound path expressions such as hop*/hop or hop?/hop, causing the query engine to generate a Cartesian product join instead of the correct inner join. In concrete terms, a query that should return five results might instead return thirty, because the anonymous blank nodes that the SPARQL algebra optimizer inserts when decomposing compound paths were not being registered in the query fragment's binding table, so the join condition was never generated. This silent result inflation was hard to detect without the correct expected values to compare against, making it a particularly treacherous correctness bug that could quietly inflate downstream analytics.
The fix is precise and surgical: the translator now correctly registers anonymous blank nodes in the binding table for both subject and object positions of every path expression, ensuring the correct inner join condition is always emitted. Twenty-five new regression tests systematically cover all eight property path algebra operators and their pairwise combinations, providing a permanent guard against regressions in the path translation layer. Five additional tests for OWL 2 RL owl:propertyChainAxiom n-hop chains cross-validate inference results against SPARQL path queries, creating a meaningful integration test between the path translator and the OWL-RL engine. A new reference page in the documentation provides a complete operator coverage table, a root-cause analysis of the PATH-BNODE-01 bug, and a description of known limitations, giving users transparent information about the current state of SPARQL 1.2 support.
Fixed
- PATH-BNODE-01
GraphPattern::Pathtranslator insrc/sparql/sqlgen.rsnow callsbgp::bind_term()for both subject and object instead of only handlingTermPattern::Variable. When spargebra/sparopt decomposes a Sequence path (e.g.hop*/hop,hop?/hop,^hop/!hop) into twoGraphPattern::Pathnodes connected by an anonymous blank node, the blank node is now registered infrag.bindings;Fragment::mergethen generates the correctINNER JOINcondition (_t0.o = _t1.s) instead of a Cartesian product. This eliminated ×N duplicate rows (e.g. 30 rows → 5 for a 5-hop chain withhop*/hop).
Added
- TEST-01
tests/pg_regress/sql/sparql12_property_paths.sql— 20 regression tests covering all 8PropertyPathExpressionvariants (NamedNode,Reverse,Sequence,Alternative,OneOrMore,ZeroOrMore,ZeroOrOne,NegatedPropertySet) and 5 compound operator combinations. - TEST-02
tests/pg_regress/sql/sparql12_owl_chain_nhop.sql— 5 regression tests for OWL 2 RLowl:propertyChainAxiomn=4 and n=5 hop chains with SPARQL path cross-validation. - DOC-01
docs/src/reference/sparql12-status.md— new reference page with operator coverage table, PATH-BNODE-01 root-cause analysis, and known limitations. docs/src/SUMMARY.mdupdated to link the new reference page.- Migration script
sql/pg_ripple--0.123.0--0.124.0.sql(comment-only; no DDL changes). - Roadmap file
roadmap/v0.124.0.md.
Changed
pg_ripple_http:COMPATIBLE_EXTENSION_MINbumped from"0.122.0"to"0.123.0".
[0.123.0] — 2026-05-19 — A17 Observability, Documentation & Advisory Management
Completes the Assessment 17 remediation arc. Adds replica-pool Prometheus
gauges (OBS-M-01), rule-library stream observability (OBS-M-02),
bench_workload_result() convenience wrapper (ERG-L-01), eight new compatibility
matrix rows (DOC-M-01), comprehensive SQL API reference (DOC-M-03), four new blog
posts (DOC-L-01), and RSA/paste advisory maintenance (SEC-M-01/SEC-M-02).
The final release in the Assessment 17 remediation arc brings together observability improvements, documentation expansion, and supply chain hygiene into a comprehensive quality closure. Two new Prometheus gauges track read-replica pool health in real time — the total pool size and the number of currently available connections — providing advance warning before pool exhaustion causes queries to fall back to the primary and degrade response times under read-heavy load. Rule-library stream operations gain their own Prometheus latency and error counters, making it possible to set SLO-based alerts on the federation feature and detect abnormal stream behavior before it affects downstream consumers. A new bench_workload_result() SQL convenience function lets operators inspect recent benchmark results with a single call rather than manually querying the history table.
Documentation investment in this release is substantial across several dimensions. Eight new rows in the operations compatibility matrix cover releases from v0.113.0 through v0.120.0, closing a gap that would have left operators uncertain about which HTTP companion versions are supported. Three new operations guides walk operators through read-replica routing configuration, rule-library federation publish/subscribe workflows, and the compat_check() JSON schema with copy-pasteable examples. Four new blog posts extend the project's public knowledge base with technical deep dives on OWL property chain axioms, federation circuit breakers, Allen's interval relations, and rule-library federation. Security advisories in the audit configuration gain explicit expiry dates and quarterly review obligations, formalizing the process of keeping the advisory audit configuration current and intentional rather than quietly accumulating past their review deadlines.
Added
- OBS-M-01
pg_ripple_http_replica_pool_size{pool="replica"}andpg_ripple_http_replica_pool_available{pool="replica"}Prometheus gauges inpg_ripple_http/src/metrics.rs; scraped live at every/metricscall. - OBS-M-02
pg_ripple_rule_library_stream_duration_secondscumulative latency counter andpg_ripple_rule_library_subscribe_errors_totalerror counter; wired intorouting/rule_library_handler.rs. - ERG-L-01
pg_ripple.bench_workload_result(profile TEXT DEFAULT 'bsbm')SQL convenience wrapper returning the most recent benchmark run from_pg_ripple.bench_history(defined in migration script). - ERG-M-01
docs/src/reference/sql-api.md— new reference page documentingcompat_check()JSON schema with copy-pasteable example. - ERG-M-02 / DOC-M-02
docs/src/guides/rule-library-federation.md— complete publish → subscribe → verify inference → monitor worked example. - ERG-M-03
docs/src/operations/read-replicas.md— new page documenting?replica=okrouting semantics, eligible query types, pool exhaustion fallback, and Prometheus alerting recipes. - DOC-M-01 Eight new rows (v0.113.0–v0.120.0) added to
docs/src/operations/compatibility.md. - DOC-M-03
docs/src/reference/sql-api.mdextended with function signatures, parameter descriptions, and examples forbench_workload(),bench_workload_result(),publish_rule_library(),subscribe_rule_library(), and all seven Allen's interval relation functions. - DOC-L-01 Four new blog posts:
blog/owl-property-chain-axiom.mdblog/federation-circuit-breaker.mdblog/allen-interval-relations.mdblog/rule-library-federation.md
- Migration script
sql/pg_ripple--0.122.0--0.123.0.sql. - Roadmap file
roadmap/v0.123.0.md.
Changed
- SEC-M-01 Extended RUSTSEC-2024-0436 and RUSTSEC-2023-0071 (RSA Marvin-attack)
audit.tomlexpiry to 2027-01-01; updated comments with "RSA not used for untrusted input — re-evaluated Q3-2026" rationale. - SEC-M-02 Added detailed mitigation rationale for RUSTSEC-2026-0104 (
pasteproc-macro unsoundness) inaudit.toml; seeroadmap/v0.123.0.mdfor full justification. - COMPAT-01
COMPATIBLE_EXTENSION_MINbumped to"0.122.0"inpg_ripple_http/src/main.rs.
[0.122.0] — 2026-05-26 — A17 God-Module Decomposition & Test Coverage Closure
Decomposes all eight remaining god-modules (H17-02): 0 source files exceed 1,000 LOC. CI file-size gate tightened from 1,800 to 1,000 lines. Five new pg_regress test suites added. All 290 pg_regress tests pass.
A source file that has grown past a thousand lines is almost always handling too many responsibilities at once — making it difficult to review safely, harder to test in isolation, and slower to navigate for engineers unfamiliar with the codebase. Version 0.122.0 addresses eight remaining large files identified in Assessment 17, decomposing each into focused sub-modules with clearly named single responsibilities. The affected files span the SPARQL expression function dispatcher, the storage scan deduplication layer, the bulk loader, the HTTP admin handlers, the LLM integration module, the Datalog SQL compiler, the GUC registration module, and the Datalog parser test suite — collectively over 8,500 lines of code reorganized into smaller, more navigable units. No SQL-visible behavior changes are made; every public function signature, error code, and GUC parameter remains identical.
Simultaneously with the decomposition, the CI file-size gate is tightened from 1,800 lines to 1,000 lines, setting a stricter standard enforced automatically on every pull request going forward. Five new pg_regress test suites close coverage gaps for features introduced in v0.120.0: the diagnostic snapshot endpoint, read-replica routing behavior, the compat_check() JSON schema, tenant quota management, and the public API surfaces of the newly split modules. For organizations evaluating whether pg_ripple is ready for long-term production commitment, this systematic combination of architectural discipline — measurable, automated, enforced with CI gates — alongside continuous test coverage closure is exactly the kind of evidence that distinguishes a maturing project from one that is quietly accumulating hidden technical debt.
Changed
- H17-02 / PERF-M-02
src/sparql/expr/functions.rs(was 1,252 LOC) rewritten as a thin dispatch table (~90 LOC); logic extracted to seven sub-modules:string.rs,datetime.rs,numeric.rs,iri.rs,aggregate.rs,geo.rs,temporal.rs. - H17-02 / PERF-L-01
src/storage/ops/scan.rs(was 1,171 LOC) split: deduplication helpers extracted tosrc/storage/ops/dedup.rs. - H17-02
src/bulk_load.rs(was 1,173 LOC) converted to directory module; JSON ingest extracted tojson_ingest.rs, confidence helpers toconfidence.rs. - PERF-M-03
pg_ripple_http/src/routing/admin_handlers.rs(was 1,168 LOC) converted to directory module; explorer page extracted toexplorer.rs, diagnostic snapshot todiagnostic.rs. - PERF-L-02
src/llm/mod.rs(was 1,071 LOC) split: Automated Ontology Mapping functions (suggest_mappings,kge_entity_similarity, etc.) extracted tomapping.rs. - H17-02
src/datalog/compiler/mod.rs(was 1,068 LOC) split: SQL helper functions (build_join_cond,render_comparison_term, etc.) extracted tohelpers.rs. - H17-02
src/gucs/registration/storage.rs(was 1,058 LOC) split: v0.81.0+ GUC registrations extracted tostorage_late.rsvia#[path]sub-module. - H17-02
src/datalog/parser.rs(was 1,030 LOC) split: test module extracted toparser_tests.rsvia#[path]declaration. - CI gate tightened:
lint-file-sizestep updated from 1,800-line limit to 1,000-line limit (Q13-04 v0.85.0, tightened v0.122.0 H17-02). Zero files exceed 1,000 LOC.
Added
- Test coverage Five new pg_regress test suites covering v0.120.0 features:
v0120_diagnostic_snapshot.sql— validatesdiagnostic_report()keys for HTTP diagnostic-snapshot endpoint (DIAG-01 through DIAG-04)v0120_read_replica_routing.sql— validatesread_replica_dsnGUC existence and primary-fallback behaviour (REPLICA-01 through REPLICA-04)v0120_compat_check.sql— more thoroughcompat_check()JSON schema validation (COMPAT-10 through COMPAT-14)v0120_tenant_quota.sql— validates tenant table schema and quota column (QUOTA-01 through QUOTA-04)v0122_module_splits.sql— spot-checks public API for regressions from all splits (SPLIT-01 through SPLIT-05)
- Migration script
sql/pg_ripple--0.121.0--0.122.0.sql(no schema changes). - Roadmap file
roadmap/v0.122.0.mdcreated.
[0.121.0] — 2026-05-19 — A17 Security Hardening & Bug Remediation
Closes the two High security findings from Assessment 17 (H17-01 SSRF bypass in
subscribe_rule_library, SEC-M-03 CGNAT/multicast SSRF gaps) and all medium/low
bug and security items. All 284 pg_regress tests pass.
Security vulnerabilities in server-side request forgery — where an attacker tricks a server into making network requests on their behalf — are particularly dangerous in systems that can be configured to fetch external resources. Version 0.121.0 closes two high-severity SSRF vulnerabilities identified in Assessment 17. The most critical affects subscribe_rule_library(), which was using naive string-contains matching to detect and block private IP addresses in URLs — a check that can be bypassed by crafting URLs that contain an internal address as a substring without it being the actual destination, or through DNS rebinding attacks where an attacker controls a DNS record that initially resolves to a public address before switching to a private one. The fix replaces string matching with full DNS resolution followed by IP-level validation against the blocklist, closing both bypass vectors definitively.
The SSRF blocklist is also expanded with four additional IP ranges that previous versions did not cover: CGNAT addresses used by mobile carrier NAT (100.64.0.0/10), IPv4 multicast, the "this network" range, and IPv4-mapped IPv6 addresses that could be used to smuggle a private IPv4 address through an IPv6-only check. Several silent error swallowers — code paths where errors were being discarded rather than reported — are replaced with proper warning surfacing, so that maintenance operations like ANALYZE and REINDEX reliably surface failures to operators instead of silently succeeding. A new fuzz target exercises the SSRF URL validation path with arbitrary inputs, ensuring edge cases in URL parsing cannot introduce new bypass paths. Seven dedicated regression tests lock in all SSRF protections and will fail if any are accidentally removed in future refactoring.
Security
- H17-01 / SEC-H-01
subscribe_rule_library()SSRF guard replaced: naive string-contains matching (lower.contains("://127.")etc.) that could be bypassed by hostname embedding replaced withresolve_and_check_endpoint(source_uri)?fromsrc/sparql/federation/policy.rs. The new guard performs actual DNS resolution and validates all resolved IP addresses against the full blocklist, preventing DNS rebinding attacks and URL-embedding bypasses. - SEC-M-03 SSRF blocklist expanded with four new ranges
(ci/regress:
v0121_ssrf_hardening.sql— SSRF-01 through SSRF-04):- CGNAT
100.64.0.0/10(RFC 6598) added tois_private_ip()andis_blocked_host()— ci/regress: SSRF-02 - IPv4 multicast
224.0.0.0/4added — ci/regress: SSRF-03 - This-network
0.0.0.0/8added — ci/regress: SSRF-04 - IPv4-mapped IPv6
::ffff:0:0/96added with recursive IPv4 re-check — ci/regress: SSRF-01
- CGNAT
- SEC-M-04
// SAFETY-SQL: pred_id is i64, no injection possiblecomments added to threeformat!-based DDL calls insrc/datalog/magic.rs;let _ =silencing replaced withunwrap_or_else(|e| pgrx::warning!(...)). - SEC-L-01
Content-Type: text/event-stream; charset=utf-8now explicitly enforced on/rule-libraries/{name}/streamresponses.
Fixed
- BUG-M-01 Six
let _ = pgrx::Spi::run(...)calls insrc/maintenance_api.rsreplaced withunwrap_or_else(|e| pgrx::warning!)so ANALYZE/REINDEX errors surface to the user rather than silently failing. - BUG-M-02
let _ =silencing insrc/kge.rs:234andsrc/llm/mod.rs:730replaced withunwrap_or_elsewarning surfacing. - BUG-M-03
// CLIPPY-OK: side-effect only — errors from parse_head_object are expected and non-fatal herecomment added tosrc/datalog/conflict.rs:457.
Added
- OBS-L-01
mutation_journal::record_schema_op(op, target)helper added (ci/regress:v0121_ssrf_hardening.sql); called at end ofpublish_rule_library()andsubscribe_rule_library()for server-log audit trail so schema-mutating rule-library operations appear in the PostgreSQL server log audit trail. - Fuzz target
fuzz/fuzz_targets/rule_library_ssrf.rscovers thesubscribe_rule_library()SSRF URL validation path and the rule-library NDJSON stream parser. Registered infuzz/Cargo.toml. - pg_regress
tests/pg_regress/sql/v0121_ssrf_hardening.sqladds 7 SSRF regression tests: IPv6-mapped private address blocked (SSRF-01), CGNAT range blocked (SSRF-02), IPv4 multicast blocked (SSRF-03), this-network blocked (SSRF-04), loopback regression guard (SSRF-05), public URI passes check (SSRF-06), and version check (SSRF-07). - Migration
sql/pg_ripple--0.120.0--0.121.0.sql— comment-only, no schema changes. COMPATIBLE_EXTENSION_MINbumped to"0.120.0"inpg_ripple_http/src/main.rs(release.yml compat-check gate requires ≤1 minor version lag).
[0.120.0] — 2026-06-18 — PageRank Explain, Admin Diagnostic Snapshot, Tenant Quota API, Rule-Library Federation, Read-Replica Routing, Helm PodDisruptionBudget
Nine features across the HTTP companion and Helm chart: improved PageRank explain
with URL-decoding and score lookup; admin diagnostic snapshot; tenant quota HTTP
endpoints; Rule-Library Federation (publish/subscribe); read-replica routing via
?replica=ok; per-tenant Helm values recipe; Helm PodDisruptionBudget; and
compatibility minimum bump to v0.119.0.
Version 0.120.0 is a broad platform maturity release that delivers nine distinct features spanning observability, operations, multi-tenancy, federation, and high availability. The enhanced PageRank explain endpoint returns not just a node's current score but a structured list of its top contributors with individual contribution values, making it possible to understand not only how influential a node is in the graph but specifically which other nodes are driving its rank. The new admin diagnostic snapshot endpoint collects all internal table row counts, non-sensitive GUC values, extension and HTTP companion versions, and a Prometheus metrics snapshot into a single JSON document — providing a complete system-state picture for incident investigations, support escalations, and capacity planning without requiring manual query assembly across a dozen tables.
Four other major features complete this release. Tenant quota management introduces HTTP endpoints for reading and updating per-tenant triple limits, a critical capability for SaaS deployments that must enforce resource fairness across customers. Rule-library federation allows rule sets published by one pg_ripple instance to be subscribed and activated by another, enabling organization-wide knowledge engineering patterns to be distributed and kept in sync across deployments. Read-replica routing makes it straightforward to offload read-only SPARQL queries to standby database servers by appending ?replica=ok to any request, reducing primary database load without application code changes. A Kubernetes Helm PodDisruptionBudget ensures that cluster maintenance operations never simultaneously take down all pg_ripple replicas, protecting query availability during rolling updates and node drains.
Added
-
Feature 7
GET /pagerank/explain/{node_iri}: URL-decodes the node IRI, looks up the current PageRank score from_pg_ripple.pagerank_scores, and returns a richer JSON response{"node": "...", "score": 0.xx, "top_contributors": [...], "method": "datalog_pagerank"}withdepth,contributor,contribution, andpathfields per contributor. -
Feature 8
GET /admin/diagnostic-snapshot(requires write auth): collects all_pg_ripple.*table row counts, non-sensitive GUC values, extension and HTTP companion versions, and a Prometheus metrics snapshot into a single JSON. -
Tenant Quota HTTP Endpoints:
GET /tenants/{name}/quotareturns current usage and remaining capacity;POST /tenants/{name}/quotaupdatesquota_triples. -
Feature 11 — Rule-Library Federation: SQL functions
pg_ripple.publish_rule_library(name, endpoint_uri)andpg_ripple.subscribe_rule_library(source_uri, name). HTTP:GET /rule-libraries/{name}/streamandPOST /rule-libraries/{name}/subscribe. -
Feature 12 — Read-Replica Routing:
?replica=okon SPARQL GET/POST routes read-only queries to a standby pool (PG_RIPPLE_HTTP_REPLICA_DSN). -
just generate-helm-values TENANT=<name>recipe for per-tenant Helm fragments. -
Helm PodDisruptionBudget (
charts/pg_ripple/templates/pdb.yaml): enabled by default withminAvailable: 1.
Changed
COMPATIBLE_EXTENSION_MINbumped to"0.119.0"inpg_ripple_http.
Fixed
sparql_posthandler:?replica=oknow honoured on POST requests.
[0.119.0] — 2026-06-17 — OWL propertyChainAxiom, SERVICE Circuit Breaker, Schema-Aware NL→SPARQL
Three new features: owl:propertyChainAxiom support in OWL-RL built-ins (Feature 5);
persistent federation SERVICE circuit-breaker state with Prometheus gauge (Feature 6);
schema-aware NL→SPARQL with vocabulary bundle injection (Feature 10). Also fixes
property-path queries that coexist with RDF-star quoted triple patterns.
Ontology languages derive much of their power from the ability to express complex inference patterns concisely, and owl:propertyChainAxiom is one of the most practically useful of those patterns. It lets ontology designers declare that a chain of two properties implies a third: for example, "if A advises B and B advises C, then A is an indirect advisor of C." Once this axiom is declared in a loaded ontology, pg_ripple's OWL-RL inference engine automatically derives all the implied relationships across the dataset whenever inference is triggered. Cycle safety is guaranteed through PostgreSQL 18's WITH RECURSIVE … CYCLE clause, and ten canonical test cases validate the implementation against FOAF, SKOS, PROV-O, family chains, and the LUBM indirectAdvisor benchmark query.
Federation — the ability to include external SPARQL endpoints in a single query — is made significantly more resilient with the introduction of a persistent circuit-breaker state. When a federated endpoint repeatedly fails, the circuit breaker opens and stops sending requests to it, preventing a slow or failing remote service from degrading all queries that include it. The circuit state is persisted in a database table rather than held only in memory, so it survives backend restarts and remains consistent across all connection pool members; a Prometheus gauge exposes each endpoint's circuit state in real time. Schema-aware natural language to SPARQL translation rounds out the release: when vocabulary bundle metadata is loaded, the NL→SPARQL function automatically enriches the LLM prompt with predicate labels from the knowledge graph, producing notably more accurate translations for domain-specific ontologies.
Added
-
Feature 5
owl:propertyChainAxiomrule in OWL-RL Datalog built-ins (src/datalog/builtins.rs). A two-step chain(p1 ∘ p2 → p)is now inferred correctly bySELECT pg_ripple.infer('owl-rl'). Cycle-safe via PG 18WITH RECURSIVE … CYCLEclause. Ten canonical pg_regress tests cover FOAF, SKOS, PROV-O, family-chain, concurrent axioms,owl:inverseOf, 3-hop acquaintance,rdfs:subPropertyOf, and LUBMindirectAdvisorchain. -
Feature 6
_pg_ripple.federation_circuit_statetable(endpoint_iri TEXT PRIMARY KEY, state TEXT CHECK (state IN ('closed','open','half_open')), last_failure_at TIMESTAMPTZ, failure_count INT)persists SERVICE circuit-breaker state across backend restarts. State transitions are UPSERTED on every open/close/ half-open event. A newpg_ripple_federation_circuit_state{endpoint="..."}Prometheus gauge (0 = closed, 1 = open, 2 = half_open) is exposed at/metrics. -
Feature 10
pg_ripple.nl_sparql_include_bundlesGUC (boolean, defaulton). When enabled,sparql_from_nl()automatically injects vocabulary bundle metadata (predicate labels fromskos:prefLabel,dcterms:title,schema:name,foaf:name) into the LLM prompt, improving NL→SPARQL accuracy for ontology-rich datasets.
Fixed
- Property-path queries (
+,*,/,?) now execute without error when the same SPARQL query also contains RDF-star quoted triple patterns (<< s p o >>). The gap was in the property-path SQL generator ignoring the RDF-star graph overlay; now the correct triple source is selected in all cases.
Migrations
sql/pg_ripple--0.118.0--0.119.0.sql— creates_pg_ripple.federation_circuit_state.
[0.118.0] — 2026-06-10 — Temporal Allen's Relations, compat_check() and Privacy Budget Registry
Three new Platform Maturity features: seven Allen's interval relations as SPARQL
FILTER functions and Datalog built-ins (Feature 4); pg_ripple.compat_check() SQL
function for belt-and-suspenders version verification (Feature 3); per-dataset
differential privacy budget registry with automatic reset (Feature 2). Also
includes AT TIME ZONE support for temporal queries and an integrated benchmark
runner (Feature 1).
Temporal reasoning often requires expressing relationships between time intervals that go beyond simple "before" and "after" comparisons. In scheduling, legal contracts, clinical trials, and supply chain logistics, what matters is the precise structural relationship between two periods: does one overlap the other? Does one entirely contain the other? Do they share a start or end boundary? Version 0.118.0 implements all seven of Allen's interval algebra relations — the canonical mathematical framework for classifying temporal interval relationships — as SQL functions, Datalog built-in predicates, and SPARQL FILTER extensions. This enables temporal queries of remarkable precision, such as "find all regulatory notices that were active during but did not fully contain the audit period" or "find maintenance windows that exactly align with the reported outage interval."
Two other significant features complement the temporal improvements. The pg_ripple.compat_check() SQL function provides programmatic compatibility verification returning structured JSON that describes the installed extension version, the minimum HTTP companion version it requires, and whether the currently running companion meets that requirement — enabling deployment automation, health checks, and upgrade scripts to verify compatibility programmatically rather than parsing version strings manually. For teams operating under differential privacy regulations, a per-dataset privacy budget registry tracks cumulative epsilon expenditure across all queries by dataset and principal, automatically rejecting queries that would exceed the budget and resetting budgets on a configurable schedule, providing formal mathematical privacy guarantees for sensitive reporting workloads. An integrated benchmark runner lets teams measure query throughput against their own workload profiles and store results historically for trend analysis.
Added
- Feature 3
pg_ripple.compat_check() → TEXTreturns structured JSON{"extension_version": "...", "http_min_version": "...", "compatible": true}; HTTP companion calls this at startup (C16-01 belt-and-suspenders check on top of the v0.112.0 CI gate). - Feature 4 Seven Allen's interval relation SQL functions and SPARQL FILTER
extensions:
allen_before,allen_meets,allen_overlaps,allen_during,allen_finishes,allen_starts,allen_equals— each takes fourTIMESTAMPTZarguments and returnsBOOLEAN. Also available as Datalog built-ins (ALLEN_BEFORE(...), etc.) and SPARQL custom functions (http://pg-ripple.org/functions/before, etc.). - Feature 2
_pg_ripple.privacy_budget (dataset_id, principal, budget_total, budget_spent, last_reset_at)table;dp_noisy_count()anddp_noisy_histogram()gain optionaldataset_id/principalparameters that deductepsilonand raisePT0490when the budget is exhausted; GUCpg_ripple.privacy_budget_reset_interval(default'1 day') for automatic budget reset; HTTP endpointGET /dp/budget/{dataset}/{principal}. - AT TIME ZONE gap fix
mark_temporal()andpoint_in_time()gain an optionaltime_zone TEXTparameter; when provided, timestamps are coerced viaAT TIME ZONEbefore temporal fact comparison;default_tzcolumn added to_pg_ripple.temporal_predicates. - Feature 1
pg_ripple.bench_workload(profile TEXT DEFAULT 'bsbm') → BIGINTruns a lightweight proxy benchmark and records the result in_pg_ripple.bench_history;bench_history_recent()table-returning function for recent run summary; HTTP endpointGET /admin/bench-history.
Changed
COMPATIBLE_EXTENSION_MINinpg_ripple_httpbumped to"0.117.0".
Migration
- Migration script
sql/pg_ripple--0.117.0--0.118.0.sqladds_pg_ripple.privacy_budget,_pg_ripple.bench_history, and thedefault_tz TEXTcolumn on_pg_ripple.temporal_predicates.
[0.117.0] — 2026-06-03 — A16 Low-Severity Polish, Tests and Supply-Chain
Seventeen low-severity polish items: allow-suppression count reduced to ≤186; crash-recovery and benchmark suite documentation; replication worker doc-comment; Arrow Flight ticket migration guide; configurable HTTP auth realm; install/upgrade guide; Docker image-tag pinning policy; build-artifact gitignore cleanup; SBOM diff date stamping; cosign SBOM signing in CI; version-bump release workflow; SSE concurrency tests; CONTRIBUTING.md AGENTS.md link; entity-resolution and temporal write-race concurrency tests; and four new fuzz targets.
Software quality encompasses more than features and bug fixes — it includes the completeness of documentation, the rigor of the supply chain, the depth of test coverage for edge cases, and the small operational details that make a system pleasant rather than frustrating to run in production. Version 0.117.0 addresses seventeen such low-severity items accumulated over several assessment cycles. Crash recovery test scripts, benchmark SQL files, and the replication worker all gain detailed documentation explaining how they work, how to run them, and what they verify — documentation that is particularly valuable during incident response when time is short and engineers may be unfamiliar with the relevant subsystem. An installation and upgrade guide documents all 119 migration scripts from v0.1.0 to current, with checksum verification steps for each one.
Supply chain security receives notable attention in this release: the CI release workflow is extended to sign the software bill of materials with cosign keyless signing via the Sigstore transparency log, creating a verifiable cryptographic attestation that the SBOM has not been tampered with since it was generated. Four new fuzz targets expand the fuzzing surface to the temporal query parser, PPRL Bloom filter bounds checking, rule authoring validation, and the SKOS bundle loader — each targeting a code path that processes potentially untrusted external input. The number of explicitly suppressed Clippy lint warnings is reduced from 214 to at most 186, with justification comments required for every remaining suppression. Concurrency tests are added for SSE event delivery under load, owl:sameAs canonicalization under concurrent writes, and temporal versioned write races, covering the most likely failure modes of highly concurrent deployments.
Added
-
L16-01
#[allow(…)]suppression count reduced from 214 to ≤186;// A16-CQ:justification comments added to all remaining suppressions in 48 source files. -
L16-02
tests/crash_recovery/README.md— documents all 15 crash-recovery test scripts, PGDATA requirements, invocation pattern, and recovery verification steps. -
L16-03
benchmarks/README.md— documents all benchmark SQL files, how to run locally, CI benchmark gating, CSV history format, and the BSBM regression gate. -
L16-04 Doc-comment on
pg_ripple_logical_apply_worker_mainextended with sections on restart behaviour, crash-loop backoff, and the 30 sBGWORKER_RESTART_TIMEconstant. -
L16-05 Arrow Flight v1→v2 HMAC ticket migration guide added to
docs/src/reference/arrow-flight.mdwith "What constitutes a valid v1 ticket", migration path, and summary table. -
L16-06
PG_RIPPLE_HTTP_AUTH_REALMenvironment variable: configures theBearer realm=value returned inWWW-Authenticateheaders; defaults topg_ripple. Documented inpg_ripple_http/README.md. -
L16-07
sql/INSTALL.md— step-by-step install and upgrade guide covering all 119 migration scripts from v0.1.0 to current, with checksum verification. -
L16-08
docker/README.md— image-tag pinning policy, upgrade procedure, and multi-service pinning example updated to v0.117.0. -
L16-09/10
.gitignoreupdated to exclude build artefacts (clippy_all.txt,clippy_output.txt,cargo_check_output.txt,build_output.txt,regression.diffs,sbom_diff.md); removed from git index. -
L16-11
scripts/generate_sbom_diff.sh— generatessbom_diff.mdwith a**Generated:** YYYY-MM-DDheader stamped dynamically on each invocation or CI run. -
L16-12 Release workflow (
.github/workflows/release.yml) now signs the SBOM with cosign keyless signing (sigstore) and attaches the.cosign.bundlefile to the GitHub release. -
L16-13
RELEASE.md— pre-tagging checklist now documentsjust bump-version-dry/just bump-version <new> <floor>workflow. -
L16-14 Concurrency tests:
tests/concurrency/sse_burst_subscriber.sh— 100 simultaneous SSE connections, asserts ≥95 % receive at least one event.tests/concurrency/sse_reconnect_during_merge.sh— asserts no event gap when an SSE client reconnects withLast-Event-IDwhile the merge worker runs.
-
L16-15
CONTRIBUTING.md— new "AI-assisted contributions" section with explicit link toAGENTS.mdand a summary of what it governs. -
M16-12 Concurrency tests:
tests/concurrency/entity_resolution_concurrent_resolves.sh— N concurrentowl:sameAscanonicalization operations must converge to a single canonical IRI.tests/concurrency/temporal_versioned_write_race.sh— N concurrent writes to the same subject/predicate assert no silent data loss via SID uniqueness.
-
M16-13 Four new fuzz targets registered in
fuzz/Cargo.toml:temporal_query— temporal SPARQL query parser (no-panic invariant)pprl_bloom_encode— PPRL Bloom filter bit-vector bounds (no-panic + length)rule_authoring_validate— Datalog/CONSTRUCT rule parser + SQL metachar checkskos_bundle— SKOS Turtle/N-Triples loader (no-panic on any byte sequence)
[0.116.0] — 2026-05-27 — A16 Medium: Correctness, Security GUCs, and CHANGELOG Hygiene
Ten correctness, security, and observability improvements: bounded ER
monitoring retention with background pruning; rule-explanation LRU cache with
version-stamp stale detection; proof-tree depth and node-count GUCs with
structured warnings; FOAF-integrity parse-error fix (functional → SPO triple
notation); Schema.org, DCTERMS, and FOAF vocabulary bundle standalone pg_regress
tests; bidi-relay overflow drop-policy GUC; Bayesian propagation depth GUC;
cargo-audit advisory lifecycle policy; and a categorical GUC reference at
docs/gucs.md.
As a system's configuration surface grows, small gaps between what the documentation promises and what the code actually does can cause real operational surprises. Version 0.116.0 addresses ten such correctness, security, and configuration issues across the full stack. The most impactful is a rule explanation staleness fix: the explanation cache now tracks a version stamp that increments every time the rule set is updated, so cached explanations are automatically invalidated when the underlying rules change. Without this fix, an explanation generated before a rule edit would remain in the cache even though the rule had changed, silently returning an accurate description of an old rule rather than the current one — a subtle but consequential bug for anyone relying on explanations for compliance documentation or operator guidance. A longstanding FOAF vocabulary bundle parse error that had been silently preventing the foaf-integrity shape bundle from loading is also corrected in this release.
Configuration control is a theme throughout this release: six hard-coded constants governing proof tree depth and node count limits, entity resolution monitoring retention, the rule explanation LRU cache size, Bayesian propagation depth, and the bidi relay overflow drop policy are all promoted to GUCs. This gives operators the ability to tune all of these behaviors per workload without rebuilding, and it makes the system's operational parameters visible, auditable, and documented. A formal advisory lifecycle policy is established in the security configuration, requiring explicit quarterly reviews of known security advisories with documented rationale for each entry maintained in the skip list. The entire GUC surface — now 227 parameters — is organized into a new categorical reference document with subsystem groupings, types, defaults, and descriptions, dramatically reducing the configuration learning curve for new operators and integrators.
Added
-
M16-01
pg_ripple.er_monitoring_retention_days(integer, default 30, Suset). Background worker now callser_monitoring_prune()once per tick to delete rows older than the configured retention window fromer_unresolved_entities,er_cluster_sizes, ander_resolution_dashboard. -
M16-05 Added
rule_version_stampto_pg_ripple.rule_explanations; migration:sql/pg_ripple--0.115.0--0.116.0.sql.pg_ripple.store_rules()increments the stamp on every rule-set update.pg_ripple.explain_rule()rejects DB-cached explanations whose stamp is older than the current value, preventing stale explanations after rule edits. -
M16-06
audit.tomladvisory lifecycle-policy comment block with explicitexpiresdates (2026-12-01) for thepkcs1/rsaadvisories. Establishes a quarterly review obligation. -
M16-07
pg_ripple.proof_tree_max_depth(integer, default 64, Userset) andpg_ripple.proof_tree_max_nodes(integer, default 10 000, Userset).build_proof_tree()emitsPT0480(depth limit reached) andPT0481(node limit reached) structured warnings instead of silent truncation or stack overflow. -
M16-08 Vocabulary bundle standalone pg_regress tests:
skos_dcterms.sql,skos_schema_org.sql,skos_foaf.sql. Each test exercisesload_datalog_bundle()+load_shape_bundle()+ idempotency + cleanup for the respective bundle family. Bug fix:FOAF_INTEGRITY_RULESconstants corrected from functional notationfoaf:knows(?x, ?x)→ SPO triple notation?x foaf:knows ?x, resolving the parse error that preventedload_shape_bundle('foaf-integrity')from succeeding. -
M16-11
pg_ripple.bidi_relay_drop_policy(string, Userset). Overflow warning inbidi/relay.rsnow logs the configured drop-policy value. Supported values:NULL(block — default),'drop-oldest','drop-newest'. -
M16-19 Bounded per-process LRU cache in
explain_rule()controlled bypg_ripple.rule_explanation_cache_max_entries(integer, default 1 000, Userset). Cache capacity is resized at call time when the GUC changes. -
M16-20
pg_ripple.bayesian_propagation_max_depth(integer, default 10, Userset).propagate_downstream()inuncertain_knowledge_api/bayesian.rsnow reads this GUC instead of using a hard-coded constant, separating the Bayesian depth limit from the generalconfidence_propagation_max_depthGUC. -
M16-21
audit.tomllifecycle-policy header block documenting the open → triage → ignore-with-expiry → quarterly-review → re-decision cycle. -
M16-23
docs/gucs.md— categorical GUC reference grouping all 227 GUCs into 19 subsystem categories with type, default, context, and description. Includes a v0.116.0 summary table and advisory lifecycle policy section.
Migration
Run ALTER EXTENSION pg_ripple UPDATE TO '0.116.0' or apply
sql/pg_ripple--0.115.0--0.116.0.sql manually. The only DDL change is:
ALTER TABLE _pg_ripple.rule_explanations
ADD COLUMN IF NOT EXISTS rule_version_stamp BIGINT NOT NULL DEFAULT 0;
[0.115.0] — 2026-05-26 — A16 Medium: HTTP API Parity and Observability
Six HTTP API and observability improvements: new REST endpoints for temporal
facts, PPRL, differential privacy, entity resolution, proof trees, and
multi-tenant management; parameterised query hardening in the PageRank handler;
expanded Prometheus metrics; and bearer-token protection for /metrics.
A powerful database extension is only as useful as the interfaces through which it can be reached, and version 0.115.0 closes a significant gap between pg_ripple's SQL feature set and the REST interface provided by the pg_ripple_http companion service. This release adds HTTP endpoints for temporal fact management, privacy-preserving record linkage, differential privacy aggregates, entity resolution, Datalog proof trees, and multi-tenant administration — features introduced across several prior releases that were previously accessible only through direct SQL calls. External applications, dashboards, microservices, and automation scripts can now reach the full feature set through a standard HTTP interface without requiring a PostgreSQL connection, making pg_ripple a first-class citizen in HTTP-based service architectures.
Observability receives comprehensive attention: Prometheus metrics are added for entity resolution stage latencies at all five pipeline stages, owl:sameAs assertion counts, Bayesian propagation duration, temporal fact gauges, PPRL encode counters, LLM cache hit and miss rates, proof tree generation duration, and conflict detection counters. This gives operations teams a complete real-time picture of system activity in their existing metrics infrastructure, enabling SLO-based alerting for every major subsystem without custom instrumentation. A bearer-token authentication option for the /metrics endpoint prevents unauthorized scraping of operational data. The PageRank direction parameter is hardened against injection by replacing string concatenation with a typed enum, and the Kubernetes Helm chart health probes are updated from database-level checks to proper HTTP-level health and readiness endpoints.
Added
- M16-02 HTTP REST endpoints for temporal facts (
/temporal/*), PPRL (/pprl/*), differential-privacy noise (/dp/*), entity resolution (/entity-resolution/*), Datalog proof trees (/proof-tree/{s}/{p}/{o}), and multi-tenant management (/tenants,/tenants/{name}). - M16-03 Expanded Prometheus metrics: entity-resolution stage latencies
(five stages),
sameas_assertions_total, Bayesian propagation duration, temporal facts gauge, PPRL Bloom-encode counter, LLM cache hit/miss counters, proof-tree duration, and conflict-detection counter. - M16-22
PG_RIPPLE_HTTP_METRICS_TOKENenv var — when set,GET /metricsrequiresAuthorization: Bearer <token>(constant-time comparison). Returns401 UnauthorizedwithWWW-Authenticate: Bearer realm="pg_ripple"on mismatch. Documented inpg_ripple_http/README.md.
Changed
- M16-04
pagerank_handlers.rs— replacedreplace('\'', "''")string escaping with$1/$2parameterised queries throughout; thedirectionfield is now a typedDirectionenum (forward|reverse|both) deserialized by serde, preventing injection via unknown values. - M16-04
pagerank_run()SQL function — extended direction validation to also accept'both'(undirected: each edge emitted in both orientations via UNION). - M16-09
charts/pg_ripple/values.yaml— liveness probe changed fromexec: pg_isreadytohttpGet: /health; readiness probe changed tohttpGet: /ready. - M16-09
pg_ripple_http/README.md— added/health(liveness) vs/ready(readiness) semantics section; documentedPG_RIPPLE_HTTP_METRICS_TOKEN.
Tests
tests/pg_regress/sql/pagerank.sql— three new tests (38-40) coveringdirection = 'forward','reverse', and'both'at the SQL layer (mirrors the M16-04 Direction enum).
[0.114.0] — 2026-05-19 — A16 Medium: Module Splits and Architecture Debt
Pure Rust refactoring: seven large source files (each 1,000–1,600 LOC) decomposed
into focused sub-modules following the patterns in src/datalog/ and src/sparql/.
No SQL-visible schema changes. CI module-size gate added.
Large software systems accumulate architectural debt in the form of files that have grown far beyond their original scope, absorbing related functionality until they become difficult to understand, safely modify, and effectively test. Version 0.114.0 is a pure structural maintenance release that systematically decomposes seven such files — each between 1,000 and 1,600 lines — into focused sub-modules with clearly named single responsibilities. The decomposed areas span the full application stack: view management, SKOS vocabulary handling, the Datalog public API, the worst-case optimal join executor, the embedding and hybrid search layer, the SHACL validator, and the Citus distributed sharding integration. No SQL-visible behavior changes are made; every public function signature, error code, and GUC parameter remains identical to the previous release.
The long-term value of this investment is realized through reduced time-to-understand for new contributors, smaller and more targeted code review surface, easier isolated testing of individual subsystems, and clearer boundaries between components that make future refactoring safer and more predictable. A new CI gate enforces a 1,500-line hard limit per source file with a warning at 1,200 lines, ensuring the codebase will not accumulate this kind of debt again. A new architecture document captures the resulting subsystem dependency graph, clearly illustrating which components depend on which others and where future decoupling opportunities exist. For organizations evaluating pg_ripple as a long-term dependency, the presence of enforced architectural discipline — with measurable metrics and automated CI gates — is a meaningful signal of project sustainability and maintainability.
Changed
- H16-06a
src/views/mod.rs(1,599 LOC) →src/views/{mod.rs, construct.rs, materialise.rs, refresh.rs, dependency.rs, sparql.rs, describe.rs}(each < 400 LOC) - H16-06b
src/skos.rs(1,495 LOC) →src/skos/{mod.rs, bundle.rs, inference.rs, broader_narrower.rs, export.rs} - M16-14
src/datalog_api.rs(1,134 LOC) →src/datalog_api/{mod.rs, parse.rs, validate.rs, explain.rs, conflict.rs}(each < 400 LOC) - M16-15
src/sparql/wcoj.rs(1,067 LOC) →src/sparql/wcoj/{mod.rs, executor.rs, trie.rs, leapfrog.rs} - M16-16
src/sparql/embedding.rs(1,144 LOC) →src/sparql/embedding/{mod.rs, index.rs, hybrid.rs, rag.rs} - M16-17
src/shacl/validator.rs(1,181 LOC) →src/shacl/validator/{mod.rs, property.rs, node.rs, sparql.rs, severity.rs} - M16-18
src/citus/mod.rs(1,366 LOC) →src/citus/{mod.rs, shard_pruning.rs, ddl_hooks.rs, query_rewriting.rs, rebalance.rs}
Added
scripts/check_module_sizes.sh— CI gate; warns at 1,200 LOC, fails at 1,500 LOC per.rsfiledocs/src/architecture.md— subsystem dependency graph (SKOS→Datalog, OWL-RL→Datalog, NS-RL→embedding+Datalog, conflict-detection→SHACL+Datalog, hypothetical→storage)- Module size policy documented in
CONTRIBUTING.md - Migration script
sql/pg_ripple--0.113.0--0.114.0.sql(comment-only; no schema changes)
[0.113.0] — 2026-05-12 — A16 High: bulk-load COPY path & performance tuning
Promotes bulk_load_use_copy to default-on, replaces the O(n) embedding loop with a batched
HNSW probe, optimizes Bloom-filter HMAC key expansion, and promotes replication watermark
constants to GUCs.
For teams loading large datasets into a knowledge graph — whether migrating from another system, ingesting freshly crawled web data, or bulk-loading a research corpus — the speed of the bulk load operation determines whether a workflow is practical or impractical. Version 0.113.0 switches the default bulk load path for N-Triples and Turtle files from row-by-row insertion to a batch array INSERT strategy that delivers a 5–10× throughput improvement for large files. This change takes effect automatically on upgrade — the pg_ripple.bulk_load_use_copy GUC is flipped to default-on, meaning every existing user benefits from the performance gain without any configuration changes, script modifications, or code updates. The documentation is updated to describe the new default and help operators understand the performance characteristics of each path.
Three additional performance improvements address specific bottlenecks in other high-frequency operations. Entity resolution's candidate embedding lookup is restructured from an O(n) loop with one database round-trip per candidate to a single batched CTE probe, reducing database call overhead proportionally to the number of candidates in a blocking block. The PPRL Bloom filter's HMAC key expansion is changed from recomputing the full cryptographic key for every hash function position to performing one expansion and cheaply cloning it, cutting overhead for high-hash-count configurations. Replication batch size and interval parameters that were previously hardcoded constants compiled into the binary are promoted to GUCs, letting operators tune replication watermark behavior without rebuilding, and the SSE channel buffer size in the HTTP companion similarly gains an environment variable override for deployment-time tuning.
Changed
- (H16-05)
src/gucs/storage.rs: changedBULK_LOAD_USE_COPYdefault fromofftoon;bulk_load_ntriples()andbulk_load_turtle()now use the UNNEST-array batch INSERT path (copy_into_vp()) by default, delivering 5–10× throughput improvement for large loads. - (H16-05) Updated GUC registration description in
src/gucs/registration/storage.rsto reflect the new default and v0.113.0 ticket reference. - (H16-05)
docs/src/cookbook/bulk-loading.md: documented the 5–10× throughput gain from the COPY path and thepg_ripple.bulk_load_use_copyGUC. - (P4)
src/entity_resolution.rs: replaced the per-candidate embedding round-trip in Stage 2 (run_embedding_candidates) with a single batchedarray_aggHNSW probe via a CTE, reducing SPI round-trips from O(n) to O(1) per blocking block. - (P5)
src/pprl.rs: reuse a single baseHMACinstance across allhash_countpositions inbloom_encodeviaclone()instead of re-keying (new_from_slice) per iteration; reduces HMAC key-expansion overhead from O(hash_count) full expansions to one expansion plus O(hash_count) cheap clones. - (P6)
src/gucs/storage.rs: addedREPLICATION_BATCH_SIZE(default 100) andREPLICATION_BATCH_INTERVAL_MS(default 500) GUCs; promoted the hard-coded batch watermark constants insrc/replication.rsto these GUCs. - (P6)
src/replication.rs: logical apply worker now readspg_ripple.replication_batch_sizeandpg_ripple.replication_batch_interval_msat runtime instead of compile-time constants. - (P7)
pg_ripple_http/src/stream.rs: SSE channel capacity now readsPG_RIPPLE_HTTP_SSE_BUFFERenvironment variable (default 256, range 1–65536) instead of a hard-coded constant. pg_ripple_http/src/main.rs: bumpedCOMPATIBLE_EXTENSION_MINto"0.112.0".
Migration
No SQL schema changes. Apply via ALTER EXTENSION pg_ripple UPDATE TO '0.113.0'.
[0.112.0] — 2026-05-12 — A16 Critical & High Remediation + Dependency Maintenance
Closes the sixth-consecutive COMPATIBLE_EXTENSION_MIN lag (C16-01), annotates all unsafe blocks, reduces unwrap/expect surface, wires the SHACL validation gate in entity resolution, and adds the v1.0.0 GA Entry Criteria to the roadmap.
Production software earns trust through consistent and transparent handling of its own security obligations, and version 0.112.0 is a focused release that closes several important gaps in that area. The most critical fix addresses a six-version lag in the HTTP companion service's compatibility floor — the check that warns when the companion is connected to an incompatible extension version had been silently reporting an outdated minimum, meaning users were not getting accurate safety signals about upgrade compatibility. The fix includes an immediate update to the correct floor and, more importantly, a new CI gate that fails the build if the compatibility floor ever lags more than one minor version behind the extension, so this gap cannot silently accumulate again. A new policy document formalizes the compatibility window commitment so users can plan upgrades with confidence.
The release also systematically addresses every unsafe code block in the Rust codebase by adding mandatory // SAFETY: justification comments and enforcing this standard via a new Clippy lint that will reject any future unsafe block without an explanation of why it is safe. The SHACL validation gate in the entity resolution pipeline, previously a stub that always returned zero blocked candidates, is now fully implemented to run actual validation checks before committing identity assertions, with sub-transaction wrapping to guarantee atomic rollback if anything goes wrong. The v1.0.0 GA entry criteria are formally documented, establishing clear and measurable thresholds — zero open high findings, all unsafe blocks annotated, a passing regression test suite, a signed SBOM, and an external security audit — that define what production readiness means for this project.
Changed
- (C16-01)
pg_ripple_http/src/main.rs: bumpCOMPATIBLE_EXTENSION_MINfrom"0.93.0"to"0.111.0"— closes the sixth-consecutive critical finding about the companion compatibility lag. - (C16-01)
release.yml: addcompat-checkCI job that parsesCOMPATIBLE_EXTENSION_MINand the Cargo.toml version; fails if the compat floor is more than 1 minor version behind the extension version — prevents the lag from recurring. - (C16-01)
RELEASE.md: add "HTTP Companion Compatibility Window Policy" paragraph stating thatpg_ripple_httpsupports the prior 2 minor extension versions. - (H16-01) Annotated all
unsafe {}blocks across the codebase with// SAFETY:comments; moved 9 SAFETY annotations insrc/shmem.rsto be immediately before theirunsafeexpressions (compliant withclippy::undocumented_unsafe_blocks). - (H16-01) Added
clippy::undocumented_unsafe_blocks = "deny"to[workspace.lints.clippy]inCargo.toml(seeroadmap/v0.112.0.md) — prevents unannotatedunsafeblocks from merging. - (H16-02) Added
// PANIC-SAFETY:annotation tosrc/sparql/plan_cache.rsfor theLruCache::new(NonZeroUsize::new(cap).expect(...))call (.max(1)guarantees non-zero); added#[allow(clippy::expect_used)]to confirm the expectation is intentional. - (H16-02) Added
#[allow(clippy::unwrap_used, clippy::expect_used)]to test modules (roadmap/v0.112.0.md):kge.rs,datalog/{stratify,builtins,parser,conflict}.rs,flight.rs,entity_resolution.rs,bidi/mod.rs, anddictionary/inline.rs. - (H16-03)
src/entity_resolution.rs: replaced the SHACL gate stub (blocked_by_shacl = 0) with a newcount_shacl_blocked_candidates()helper that callsvalidate_sync()for each candidateowl:sameAspair; wrapped Stage 4/5 inBeginInternalSubTransaction/ReleaseCurrentSubTransactionso any panic rolls back Stage 1–3 side-effects atomically. - (H16-04)
src/gucs/registration/observability.rs: updatedpg_ripple.llm_endpointGUC description to clarify it is a no-op in the extension itself; the HTTP companion's/rules/{id}/explainendpoint handles LLM enrichment. - (H16-04)
docs/src/reference/guc-reference.md: added clarifying note forpg_ripple.llm_endpointdirecting users topg_ripple_httpfor LLM enrichment. - (H16-07)
ROADMAP.md: added## v1.0.0 GA Entry Criteriasection enumerating the six criteria (zero open Highs ×2, zero unannotated unsafe, compat window CI, pg_regress ≥271, signed SBOM, external security audit). pg_ripple.control: updated comment to reflect v0.112.0.docs/src/operations/compatibility.md: addedpg_trickle ≥ 0.57.0row.
Dependency Updates
pg_trickle: bumped to0.57.0in.versions.tomlandDockerfile.pg_tide: updated to latest (0.16.0) in.versions.tomlandDockerfile.
Migration
No schema changes in this release.
See sql/pg_ripple--0.111.0--0.112.0.sql.
[0.111.0] — 2026-05-12 — Privacy-Preserving Record Linkage (PPRL) Primitives
Adds Bloom-filter CLK encoding, Dice coefficient similarity, and differential-privacy aggregates for cross-organization entity resolution without sharing raw PII.
When two organizations need to find common records — patients in separate hospital systems, customers shared between financial institutions, suspects appearing in multiple law enforcement databases — they face a fundamental privacy dilemma: sharing the raw data exposes sensitive personal information, but without sharing, matching is impossible. Privacy-preserving record linkage solves this with cryptographic representations that preserve enough structure to measure similarity without revealing the underlying values. Version 0.111.0 adds this capability natively to pg_ripple through Bloom filter CLK encoding: the bloom_encode() function transforms any text value into a fixed-length bit vector using HMAC-SHA-256, and the dice_similarity() function measures the similarity of two encoded values, allowing organizations to compare records without either side ever seeing the other's raw data.
These primitives integrate directly with pg_ripple's SPARQL and Datalog layers as native filter predicates, making privacy-preserving matching a first-class operation in the knowledge graph rather than an external preprocessing step. Differential privacy aggregates round out the privacy toolkit: dp_noisy_count() and dp_noisy_histogram() add calibrated Laplace noise to query results, providing formal mathematical privacy guarantees for reporting on sensitive datasets. Security parameters below recommended thresholds trigger warnings to guide operators toward configurations that meet production security requirements. A comprehensive cookbook guides teams through the complete workflow from key management to cross-organizational federated matching, and property-based tests verify the mathematical invariants — round-trip identity, Dice symmetry, noise sign, and output length — that underpin the correctness guarantees.
Added
- PPRL-01:
pg_ripple.bloom_encode(value TEXT, key TEXT, hash_count INT DEFAULT 30, length INT DEFAULT 1024) → TEXT— CLK Bloom-filter encoding using HMAC-SHA-256. Returns a lowercase hex-encoded bit vector oflengthbits. Raises PT0470 on oversized input; PT0471 on invalid parameters; logs WARNING for below-recommended security parameters. - PPRL-02:
pg_ripple.dice_similarity(a TEXT, b TEXT) → FLOAT8— Dice coefficient for two Bloom-filter hex strings:2 * popcount(a & b) / (popcount(a) + popcount(b)). - PPRL-03: SPARQL FILTER function
pg:dice_similarity(?a, ?b)— IRI<http://pg-ripple.org/functions/dice_similarity>— computes Dice coefficient in SPARQL FILTER and BIND expressions. - PPRL-04: Datalog built-in predicate
pg:dice_similarity(?a, ?b) OP ?rhs— Dice coefficient comparison in Datalog rule bodies. - DPPRIV-01:
pg_ripple.dp_noisy_count(query TEXT, epsilon FLOAT8) → BIGINT— differentially-private COUNT with Laplace(0, 1/epsilon) noise, result clamped to ≥ 0. Validates query is a read-only SELECT. - DPPRIV-02:
pg_ripple.dp_noisy_histogram(query TEXT, key_column TEXT, count_column TEXT, epsilon FLOAT8) → TABLE(key TEXT, noisy_count BIGINT)— per-bucket Laplace noise histogram. - GUC:
pg_ripple.bloom_max_input_length(INT, default 4096) — maximum byte length of thevalueargument tobloom_encode(). - PT0470: error catalog —
bloom_encode: input length %d exceeds bloom_max_input_length GUC (%d). - PT0471: error catalog —
bloom_encode: hash_count %d or length %d outside valid range. - PT0472: error catalog —
dp_noisy_count: epsilon %g out of valid range (0, 10]. - PT0473: error catalog —
dp_noisy_count: query must return a single INTEGER value. - PT0474: error catalog —
dp_noisy_count: query rejected by validation — must be a read-only SELECT. - DOCS:
docs/src/cookbook/pprl.md— end-to-end PPRL cookbook with CLK construction explanation, SPARQL federation example, DP aggregates, and security notes (key management, recommended parameters, patent status). - PROPTEST:
tests/proptest/pprl_bloom.rs— six property-based tests: round-trip identity, distinctness, Dice range, symmetry, sign of noisy count, output length. - TEST: pg_regress tests
v0111_bloom,v0111_dice,v0111_dp_privacy.
Migration
sql/pg_ripple--0.110.0--0.111.0.sql — comment-only; no schema changes (all new functionality provided by compiled Rust functions).
[0.110.0] — 2026-05-11 — NS-RL Evaluation Harness, Continuous Monitoring & Rule Explainability
Adds the NS-RL evaluation function, three live ER monitoring stream tables, plain-English rule explanation, owl:sameAs anomaly detection, and the Magellan ER benchmark CI gate.
Knowing that an entity resolution pipeline ran is not enough — teams need to know whether it ran correctly and by how much its quality is improving or degrading over time. Version 0.110.0 adds a comprehensive evaluation harness that measures resolution quality against a gold-standard graph using three industry-standard metric families: pairwise precision/recall/F1 measures how accurately individual pairs are identified; blocking statistics including reduction ratio and pairs completeness measure how efficiently the blocking stage eliminates non-matching pairs before the expensive comparison stage; and B³ cluster metrics measure the quality of multi-entity identity clusters holistically. A single evaluate_resolution() call returns all three metric families, giving data engineers a complete picture of pipeline performance in terms that map directly to academic benchmarks and production SLA targets.
Beyond post-hoc evaluation, this release adds live monitoring infrastructure: three streaming tables capture unresolved entity counts, cluster size distributions, and a resolution quality dashboard that updates in real time as the pipeline runs, enabling early warning of quality degradation before it affects downstream consumers. A Magellan benchmark CI gate ensures that regression in resolution quality triggers a build failure before code merges, locking in the achieved quality level for the Abt-Buy and DBLP-ACM datasets. Rule explainability receives dedicated infrastructure through explain_rule(), which generates a plain-English description of what any Datalog rule does — either through an LLM for rich contextual explanations or through a deterministic structural description as a fallback — with results cached to avoid repeated LLM calls. An owl:sameAs anomaly log captures every suspicious identity assertion, creating a permanent audit trail of resolution decisions that domain experts and regulators can review.
Added
- EVAL-01:
pg_ripple.evaluate_resolution(gold_graph TEXT, pipeline_options JSONB DEFAULT '{}') → JSONB— runsresolve_entities()against a gold-standard named graph and returns all three metric axes: pairwise (precision/recall/F1), blocking (pairs_completeness/reduction_ratio/F-PQ), and B³ cluster (b3_precision/b3_recall/b3_f1), plus metadata fields. - PT0461: error catalog entry —
evaluate_resolution: gold graph '%s' is empty or does not exist. - ERMON-01:
pg_ripple.enable_er_monitoring() → VOID— idempotent; creates_pg_ripple.er_unresolved_entities,_pg_ripple.er_cluster_sizes, and_pg_ripple.er_resolution_dashboard. - ERMON-02:
pg_ripple.disable_er_monitoring() → VOID— idempotent; drops the three monitoring tables. - EXPLAIN-01:
pg_ripple.explain_rule(rule_id BIGINT, language TEXT DEFAULT 'en', format TEXT DEFAULT 'text') → TEXT— plain-English rule narration; LLM-driven whenpg_ripple.llm_endpointis set, otherwise uses a template-driven structural description. Results cached in_pg_ripple.rule_explanations. - EXPLAIN-02:
pg_ripple.explain_rule_batch(rule_ids BIGINT[]) → TABLE(rule_id BIGINT, explanation TEXT)— batch variant. - PT0462: error catalog entry —
explain_rule: rule %d not found. - HTTP-01: REST endpoint
GET /rules/{id}/explain?language=en&format=textinpg_ripple_http— returns{"rule_id", "language", "format", "explanation"}. - ANOMALY-01:
_pg_ripple.sameas_anomaly_log— append-only table with INSERT-only RLS; receives one row for every PT550-triggeringowl:sameAsassertion whenpg_ripple.record_sameas_anomalies = on. - GUC-01:
pg_ripple.record_sameas_anomalies(BOOL, default on) — controls PT550 anomaly logging. - GUC-02:
pg_ripple.sameas_anomaly_log_retention(TEXT, default'90 days') — retention period for anomaly log rows. - GUC-03:
pg_ripple.rule_explanation_cache_ttl(TEXT, default'24 hours') — TTL for cachedexplain_rule()results. - BENCH-01:
benchmarks/er_magellan.sh— Magellan ER benchmark CI gate (Abt-Buy F1 ≥ 0.78, DBLP-ACM F1 ≥ 0.90). - BENCH-02:
benchmarks/er_freshness.sh— ER ingestion latency benchmark (p95 < 500 ms). - BENCH-03:
just bench-errecipe running both ER benchmarks. - SCRIPTS-01:
scripts/magellan_to_rdf.py— Python helper converting Magellan CSV datasets to Turtle RDF. - TEST: pg_regress tests
v0110_er_monitoring,v0110_explain_rule,v0110_anomaly_log.
Migration
sql/pg_ripple--0.109.0--0.110.0.sql — creates _pg_ripple.rule_explanations and _pg_ripple.sameas_anomaly_log.
[0.109.0] — 2026-06-14 — NS-RL Foundation: String Similarity Builtins + Orchestrator
Adds six SPARQL/Datalog string similarity built-ins for neuro-symbolic record linkage, three reusable ER blocking templates, a five-stage resolve_entities() orchestration pipeline, and two new GUC parameters.
Matching records that refer to the same real-world entity — even when names are spelled differently, addresses are formatted differently, or identifiers come from different systems — is a foundational challenge in data integration that every large organization faces. Version 0.109.0 begins addressing this with a suite of six string similarity algorithms exposed directly inside SPARQL queries and Datalog rules: trigram similarity, Levenshtein edit distance, Soundex, Metaphone, Jaro-Winkler, and several variants. These algorithms can be used as native filter predicates in graph patterns, enabling declarative blocking rules like "consider these two records as match candidates if their name strings have a trigram similarity above 0.7" without leaving the SPARQL or Datalog layer to call external functions.
The release also delivers the first version of resolve_entities(), a five-stage entity resolution orchestration pipeline that takes two named graphs and produces owl:sameAs assertions linking entities it determines are likely to refer to the same real-world object. The stages progress from symbolic blocking using inverse-functional properties or custom Datalog rules, through embedding-based candidate generation, SHACL validation to enforce quality gates, union-find canonicalization to resolve transitive identity clusters, and finally RDF-star provenance annotation that records the evidence behind each match decision. A dry-run mode lets teams inspect what the pipeline would do without writing any data, and three built-in blocking templates for email matching, postal name matching, and name prefix matching make the system immediately usable for common scenarios without writing any custom rules.
Added
- STRSIM-01: SPARQL custom function
pg:trigram_similarity(?a, ?b)— emitssimilarity(a, b)SQL viapg_trgm. Returnsxsd:double. - STRSIM-02: SPARQL custom function
pg:levenshtein(?a, ?b)— emitslevenshtein(a, b)viafuzzystrmatch. Returnsxsd:integer. - STRSIM-03: SPARQL custom function
pg:levenshtein_less_equal(?a, ?b, ?maxd)— emitslevenshtein_less_equal(a, b, maxd). Returnsxsd:integer. - STRSIM-04: SPARQL custom function
pg:soundex(?s)— emitssoundex(s)viafuzzystrmatch. Returns a dictionary-encoded literal. - STRSIM-05: SPARQL custom function
pg:metaphone(?s, ?maxlen)— emitsmetaphone(s, maxlen)viafuzzystrmatch. Returns a dictionary-encoded literal. - STRSIM-06: SPARQL custom function
pg:jaro_winkler(?a, ?b)— emitsjarowinkler(a, b)viafuzzystrmatch. Returnsxsd:double. - STRSIM-07: Datalog built-in predicate
pg:trigram_similarity(?a, ?b) OP ?r— guard compiling tosimilarity(a, b) OP rin SQL WHERE clause. - STRSIM-08: Datalog built-in predicates
pg:levenshtein(?a, ?b) OP ?r,pg:soundex(?s) OP ?r,pg:metaphone(?s, maxlen) OP ?r,pg:jaro_winkler(?a, ?b) OP ?r— analogous guard compilation. - STRSIM-09:
fuzzystrmatch_available()helper — probespg_procforlevenshteinat translation time; emitsNULLwhen the extension is absent rather than erroring. - ER-01:
pg_ripple.er_blocking_templates() → TABLE(name TEXT, description TEXT, rule TEXT)— returns three built-in ER blocking rule templates:email,postal_name,name_prefix. - ER-02:
pg_ripple.er_blocking_template(name TEXT) → TEXT— returns rule text for a named ER blocking template. - ER-03:
pg_ripple.resolve_entities(source_graph TEXT, target_graph TEXT, options JSON DEFAULT NULL) → JSON— five-stage NS-RL pipeline: symbolic blocking (IFP or custom rules), embedding-based candidate generation, SHACL validation gate,owl:sameAsunion-find canonicalization, RDF-star provenance annotation.dry_run=truereturns a JSON summary without writing triples. - GUC-01:
pg_ripple.sameas_apply_rate_limit(INT, default 1000, range 1–10,000,000) — maximumowl:sameAsassertions perresolve_entities()call; raises PT0460 when exceeded. - GUC-02:
pg_ripple.string_similarity_extensions_ok(BOOL, default false) — allows string similarity functions to requirefuzzystrmatchat translation time. - TEST: pg_regress tests
tests/pg_regress/sql/v0109_string_similarity.sql— 10 test cases covering GUC defaults, ER blocking templates, andresolve_entities()dry-run.
Migration
sql/pg_ripple--0.108.0--0.109.0.sql — no schema changes; new functions are compiled from Rust.
[0.108.0] — 2026-06-07 — Bayesian Confidence Updates
Adds a Bayesian belief-revision engine for dynamic confidence updates, an append-only evidence log, bulk update ingestion, downstream propagation through the derivation DAG, and six new GUC parameters.
Real-world knowledge is rarely black and white — sensor readings carry measurement uncertainty, entity matching scores reflect probabilities rather than certainties, and evidence from different sources may conflict or reinforce each other over time. Version 0.108.0 introduces a Bayesian belief-revision engine that treats confidence as a first-class property of every fact in the knowledge graph, allowing it to be updated in a principled, mathematically grounded way as new evidence arrives. The core update_confidence() function applies Bayes' theorem in odds form: given a fact's prior confidence and the likelihood ratio of a new piece of evidence, it computes the posterior confidence and persists the updated value. This makes the knowledge graph a living probabilistic model whose certainties evolve continuously as more evidence accumulates rather than remaining static at the time of initial assertion.
Confidence changes automatically cascade through the derivation graph, propagating updated certainties to all facts derived from the updated base fact — up to a configurable depth — so that the confidence of derived conclusions always reflects the current evidence state of their premises. An append-only evidence log provides a complete audit trail of every confidence update, recording the source, prior, posterior, and timestamp of each revision. Bulk update ingestion from CSV or JSON-L streams makes it easy to feed confidence scores from external machine learning models or annotation pipelines directly into the graph in batches. A SHACL integration automatically reduces confidence in facts that violate integrity constraints, and a vacuum_evidence_log() function keeps the audit log manageable in long-running deployments by pruning rows that exceed the configured retention window.
Added
- BAYES-01:
pg_ripple.update_confidence(subject TEXT, predicate TEXT, object TEXT, evidence JSONB) → TABLE(prior FLOAT8, posterior FLOAT8)— applies Bayes' theorem in odds form:posterior = (λ · prior) / (λ · prior + (1 − prior)). Raises PT0440 whenlikelihood_ratio ≤ 0.0; PT0441 whenconfidence_update_strategy = 'manual'. Falls back to noisy-OR whenconfidence_update_strategy = 'noisy-or'. - BAYES-02:
pg_ripple.bulk_update_confidence(data TEXT, format TEXT DEFAULT 'csv') → BIGINT— ingests CSV (subject,predicate,object,source,likelihood_ratio) or JSON-L updates in batches; returns count of facts updated. - BAYES-03:
pg_ripple.vacuum_evidence_log() → BIGINT— prunes expired rows from_pg_ripple.evidence_logusingpg_ripple.evidence_log_retention; returns rows deleted. - BAYES-04:
_pg_ripple.evidence_log— append-only audit table recording every confidence update (sid,event_at,source_iri,likelihood_ratio,prior_confidence,posterior_confidence). - BAYES-05:
_pg_ripple.confidence_stale— table for tracking derived facts beyond the cascade depth that require background reprocessing. - BAYES-06: Downstream confidence propagation — after updating a base fact,
update_confidence()walks_pg_ripple.derivationsup topg_ripple.confidence_propagation_max_depthlevels using noisy-OR over antecedent confidences. - BAYES-07: GUC
pg_ripple.confidence_update_strategy(TEXT, default'bayesian') —'bayesian','noisy-or', or'manual'. - BAYES-08: GUC
pg_ripple.confidence_propagation_max_depth(INT, default10, range1–1000) — maximum derivation cascade depth. - BAYES-09: GUC
pg_ripple.confidence_reprocessing_interval(TEXT, default'1 hour') — how often stale derived confidences are reprocessed. - BAYES-10: GUC
pg_ripple.evidence_log_retention(TEXT, default'1 year') — retention window forvacuum_evidence_log(). - BAYES-11: GUC
pg_ripple.confidence_batch_size(INT, default1000, range1–1,000,000) — batch size forbulk_update_confidence(). - BAYES-12: GUC
pg_ripple.conflict_confidence_penalty(FLOAT8, default0.3, range0.0–1.0) — confidence penalty applied to conflicting triples detected by SHACL validation. - BAYES-13: Feature-status entries for
bayesian_confidence_update,evidence_log,bulk_confidence_updateinpg_ripple.feature_status(). - BAYES-14: HTTP
POST /confidence/updateandPOST /confidence/bulk-updatehandlers inpg_ripple_http. - BAYES-15: pg_regress tests
tests/pg_regress/sql/v0108_confidence.sql— 14 test cases (BAYES-01 through BAYES-14). - BAYES-16: Property-based tests
tests/proptest/bayesian_confidence.rs— 11 algebraic properties: monotonicity, neutral LR, sequential = joint (restricted to clamp-safe inputs), clamping, noisy-OR variants.
Migration
sql/pg_ripple--0.107.0--0.108.0.sql — creates _pg_ripple.evidence_log and _pg_ripple.confidence_stale tables if not present.
[0.107.0] — 2026-05-31 — Temporal Reasoning Phase 2: Sequential Patterns & CDC Integration
Adds three sequential temporal operators (WITHIN, SEQUENCE, CONSECUTIVE), CDC auto-recording of temporal facts via insert_triple(), snapshot/versioned retraction via retract_triple_temporal(), and a new pg_ripple.temporal_cdc_enabled GUC.
Building on the temporal fact store introduced in Phase 1, version 0.107.0 extends temporal reasoning to the level of event sequence detection — a common requirement in fraud detection, process monitoring, and behavioral analytics. Three new operators enable sequential pattern queries directly in Datalog rules: WITHIN checks whether a predicate held at least once in a recent time window, SEQUENCE detects whether one event reliably precedes another within a given window, and CONSECUTIVE identifies runs of N repeated occurrences of the same predicate within a time frame. These operators compile to efficient EXISTS subqueries with window functions over the temporal fact store, making complex temporal patterns expressible without leaving the Datalog rule language or reaching for external stream processing infrastructure.
The most significant operational improvement in this release is the direct integration of temporal recording into the standard write path. When the new temporal_cdc_enabled GUC is on — which it is by default — every call to insert_triple() for a temporally-registered predicate automatically records the fact in the temporal store with the transaction timestamp, requiring no changes to application code. This means that existing applications gain a complete temporal history automatically on upgrade, capturing the exact transaction times of all temporal assertions going forward without any schema changes or code updates. A new retract_triple_temporal() function cleanly closes open intervals, ensuring the temporal history accurately records not just when facts became true but when they ceased to be true — equally important for any audit or compliance use case.
Added
- SEQ-01:
pg_ripple.temporal_within(subject TEXT, predicate TEXT, duration TEXT) → BOOLEAN— returnstrueif the predicate holds for the subject at least once within the most recentduration(ISO 8601 interval) relative to transaction time. - SEQ-02:
pg_ripple.temporal_sequence(s1 TEXT, p1 TEXT, o1 TEXT, s2 TEXT, p2 TEXT, o2 TEXT, window TEXT) → BOOLEAN— returnstrueif event(s1, p1, o1)occurs strictly before event(s2, p2, o2)and both fall withinwindowof each other. Empty string arguments act as wildcards. - SEQ-03:
pg_ripple.temporal_consecutive(n BIGINT, predicate TEXT, window TEXT) → BOOLEAN— returnstrueif there existnrows for any subject with the given predicate where allnfall withinwindowof the first. - SEQ-04:
pg_ripple.retract_triple_temporal(subject TEXT, predicate TEXT, graph TEXT DEFAULT NULL) → BIGINT— closes open temporal fact intervals for(subject, predicate). Returns number of rows affected. - SEQ-05: GUC
pg_ripple.temporal_cdc_enabled(BOOL, defaulton) — whenon,insert_triple()for a temporal predicate automatically records a row in_pg_ripple.temporal_factswithvalid_from = transaction_timestamp(). - SEQ-06: CDC wiring in
insert_triple()— after writing to VP storage, checkstemporal_predicatesand inserts intotemporal_factswhen the predicate is temporal andtemporal_cdc_enabledis on. - SEQ-07: Datalog parser extended —
WITHIN,SEQUENCE, andCONSECUTIVEoperators parsed in rule bodies. - SEQ-08: Datalog compiler extended — the three new operators compile to
EXISTSsubqueries using window functions over_pg_ripple.temporal_facts. - SEQ-09: pg_regress tests
tests/pg_regress/sql/v0107_temporal_sequential.sql— 14 test cases covering all three operators, CDC integration, and retraction. - SEQ-10: Migration
sql/pg_ripple--0.106.0--0.107.0.sql— comment-only; no schema changes required.
Migration
sql/pg_ripple--0.106.0--0.107.0.sql — no schema changes; GUC pg_ripple.temporal_cdc_enabled defaults to on.
[0.106.0] — 2026-05-24 — Temporal Reasoning Phase 1: Temporal Fact Store & Basic Operators
Introduces a first-class temporal fact store backed by _pg_ripple.temporal_facts, temporal predicate registration, basic time operators in Datalog rules, a pg:temporal_window() SPARQL function, and an sh:validFor SHACL constraint.
Knowledge is inherently temporal — facts change, expire, and take on different values at different points in time. A system that only represents the current state of the world cannot answer questions like "was this entity classified differently last month?" or "how long did this policy remain in effect?" Version 0.106.0 introduces a first-class temporal dimension to pg_ripple's knowledge graph, allowing individual predicates to be marked as temporal and their facts stored with valid-from and valid-to timestamps in a dedicated temporal fact store. Two storage models are supported: snapshot mode, where each new assertion automatically closes the previous interval for the same subject-predicate pair, and versioned mode, which preserves the complete history of all values without closing old intervals, for domains where the full change history matters.
Once predicates are registered as temporal, the full reasoning and querying stack gains time-awareness. Datalog rules can use AFTER, BEFORE, and DURING operators in their bodies to filter which temporal facts trigger the rule, enabling time-bounded reasoning patterns like "apply this rule only to facts that were valid during the current fiscal year." A new pg:temporal_window() SPARQL function lets SPARQL queries ask whether any fact for a given subject-predicate pair was true within a specified time range, and a sh:validFor SHACL constraint lets shapes enforce freshness requirements — for example, that a compliance certification must have been renewed within the past 12 months. The complete temporal catalog and fact storage are set up by a migration script, making this a zero-friction addition to existing pg_ripple deployments.
Added
- TMP-01:
pg_ripple.mark_temporal(predicate_iri TEXT, data_model TEXT DEFAULT 'snapshot') → VOID— registers a predicate as temporal. Raises PT0430 if the predicate is already registered with a different model. - TMP-02:
pg_ripple.unmark_temporal(predicate_iri TEXT) → VOID— removes a predicate from temporal management. Raises PT0431 if existing temporal facts reference it. - TMP-03:
pg_ripple.insert_triple_temporal(s TEXT, p TEXT, o TEXT, valid_from TIMESTAMPTZ) → BIGINT— inserts a temporal fact into_pg_ripple.temporal_facts. Raises PT0432 if the predicate is not registered as temporal. Insnapshotmode, closes any previous open interval for(s, p). - TMP-04:
_pg_ripple.temporal_predicatescatalog table — columns:predicate_id BIGINT PK,data_model TEXT,registered_at TIMESTAMPTZ. - TMP-05:
_pg_ripple.temporal_factsstore table — columns:id BIGINT PK,s BIGINT,p BIGINT,o BIGINT,g BIGINT DEFAULT 0,valid_from TIMESTAMPTZ,valid_to TIMESTAMPTZ. - TMP-06: Datalog temporal filters —
AFTER(ts),BEFORE(ts),DURING(from, to)syntax in rule bodies for temporal predicates. - TMP-07:
pg_ripple.temporal_window(s TEXT, p TEXT, from_ts TIMESTAMPTZ, to_ts TIMESTAMPTZ) → BOOLEAN— returnstrueif any temporal fact for(s, p)overlaps the given range. - TMP-08:
sh:validForSHACL constraint — validates that temporal facts for a predicate are closed within the specified XSD duration. - TMP-09: GUC
pg_ripple.enable_temporal_operators(BOOL, default:false) — enables AFTER/BEFORE/DURING filter pushdown in the Datalog compiler. - TMP-10: GUC
pg_ripple.temporal_data_model(STRING, default:'') — override the default temporal data model ('snapshot'or'versioned') at the session level. - TMP-11: Migration
sql/pg_ripple--0.105.0--0.106.0.sql— creates_pg_ripple.temporal_predicates,_pg_ripple.temporal_facts, and three indexes. - TMP-12: pg_regress tests
tests/pg_regress/sql/v0106_temporal_basic.sql— 10 test cases (TMP-01 through TMP-10) covering store DDL, snapshot/versioned semantics, error codes, GUC defaults, and temporal_window correctness.
Error Codes
- PT0430 —
mark_temporalcalled for already-registered predicate with a different model. - PT0431 —
unmark_temporalcalled while temporal facts for predicate still exist. - PT0432 —
insert_triple_temporalcalled for predicate not registered as temporal.
Migration
sql/pg_ripple--0.105.0--0.106.0.sql — creates _pg_ripple.temporal_predicates, _pg_ripple.temporal_facts, and three indexes; no breaking changes.
[0.105.0] — 2026-05-17 — Guided Rule Authoring & LLM Rule Extraction
Translate natural-language descriptions into Datalog rules, validate rules statically, and discover candidate rules from co-occurrence patterns in the graph.
Writing Datalog rules has traditionally required deep expertise in logic programming syntax and semantics, limiting rule authoring to a small group of specialists even in organizations that have rich domain expertise. Version 0.105.0 dramatically lowers this barrier by allowing domain experts to describe rules in plain English and have those descriptions automatically translated into correct Datalog syntax by a large language model. The draft_rule_from_nl() function sends a natural-language description to a configured LLM endpoint and returns up to three ranked candidate rules, each with an explanation of its meaning, giving the domain expert options to choose from and evaluate rather than a single opaque suggestion. A mock mode enables deterministic testing without a live LLM endpoint.
Generated rules — or any hand-written rules — can be immediately validated by the validate_rule() function, which performs static analysis checking for syntax errors, unbound head variables, unsafe negation patterns, and stratification cycles before anything is loaded into the database. This combination of LLM-assisted authoring and static validation creates a guided workflow where a domain expert proposes a rule in natural language, the system suggests candidate implementations, and static analysis provides immediate feedback on correctness. A third function, suggest_rules(), inverts this flow by scanning the actual knowledge graph for predicate co-occurrence patterns and proposing candidate rules that the data itself suggests — letting the graph guide rule discovery for analysts who know their domain but not formal logic. REST endpoints for both drafting and validation make these capabilities accessible from interactive authoring interfaces and rule governance portals.
Added
- RA-01:
pg_ripple.validate_rule(rule TEXT) → JSONB— static analysis of a Datalog rule without loading it. Returns{"valid": true}or{"valid": false, "errors": [...], "warnings": [...]}. Error codes:SYNTAX_ERROR,UNBOUND_HEAD_VARIABLE,UNSAFE_NEGATION,STRATIFICATION_CYCLE. Warning codes:UNUSED_BODY_VARIABLE. - RA-02:
pg_ripple.draft_rule_from_nl(description TEXT, candidates INT DEFAULT 3) → TABLE(rank INT, rule TEXT, explanation TEXT)— calls the configured LLM endpoint to translate a natural-language rule description into Datalog. Returns up tocandidatescandidate rules ranked by estimated quality.- Raises PT0457 when
candidatesis outside [1, 10]. - Raises PT0458 when
pg_ripple.llm_endpointis not configured. - Mock mode: set
pg_ripple.llm_endpoint = 'mock'for deterministic test output.
- Raises PT0457 when
- RA-03:
pg_ripple.suggest_rules(graph_iri TEXT, examples JSONB DEFAULT NULL) → TABLE(rule TEXT, support BIGINT, explanation TEXT)— experimental — scans VP tables for predicate co-occurrence patterns and proposes candidate Datalog rules for expert review. Results require domain expert validation before committing. - RA-04: New GUC
pg_ripple.suggest_rules_max_candidates(INT, default: 20, range: 1–200) — maximum candidates returned bysuggest_rules(). - RA-05: REST endpoints in
pg_ripple_http:POST /rules/draft— request:{"description": "...", "candidates": 3}, response:[{"rank": 1, "rule": "...", "explanation": "..."}]POST /rules/validate— request:{"rule": "..."}, response:{"valid": true|false, "errors": [...], "warnings": [...]}
- RA-06: pg_regress tests
tests/pg_regress/sql/v0105_rule_authoring.sql(10 test cases: RA-01 through RA-10). - RA-07: Proptest suite
tests/proptest/rule_authoring.rs— 5 pure-Rust property tests verifying the validation algorithm. - RA-08: Error codes PT0457–PT0458 documented in
src/rule_authoring.rs.
Migration
sql/pg_ripple--0.104.0--0.105.0.sql — comment-only; no schema changes (all changes are Rust function additions).
[0.104.0] — 2026-05-10 — Domain Rule Library Infrastructure
Package, share, and install domain-specific Datalog rule sets as versioned libraries — like npm packages, but for Datalog rules and SHACL shapes.
Just as software engineers share reusable code through package repositories like npm or crates.io, knowledge engineers now have an equivalent mechanism for sharing reusable Datalog rule sets and SHACL validation shapes. Version 0.104.0 introduces a complete package management infrastructure for domain rule libraries: a library is authored as a structured Turtle file containing metadata including title, description, version, license, and dependencies, then published at any accessible URL. The install_rule_library() function fetches a library, validates its license against permissive SPDX identifiers, resolves its dependencies in topological order, and activates its rules and shapes — all in a single command with full error reporting for each failure mode. License enforcement prevents inadvertent use of incompatible rule sets in commercial products.
For organizations working in domains like healthcare, financial services, or supply chain logistics — where well-established ontological reasoning patterns are documented in published standards and industry ontologies — this means that battle-tested rule implementations can be shared across deployments without rebuilding them from scratch. Upgrade and uninstall operations are fully managed, with dependency safety checks preventing removal of libraries that other installed libraries depend on. A companion REST endpoint makes library management scriptable from external orchestration tools and CI pipelines. A detailed documentation chapter guides rule library authors through the format specification, metadata requirements, and the complete publishing workflow from authoring through distribution, lowering the barrier to contributing reusable domain knowledge to the broader pg_ripple community.
Added
- LIB-01:
_pg_ripple.rule_librariescatalog table — one row per installed external rule library with columns:name TEXT PRIMARY KEY,version TEXT,installed_at TIMESTAMPTZ,description TEXT,license_iri TEXT,source_url TEXT,dependencies TEXT[],shape_iris TEXT[]. - LIB-02:
pg_ripple.install_rule_library(source TEXT, accept_license BOOLEAN DEFAULT FALSE) → TEXT— install a rule library from a URL or absolute local file path.- URL sources are validated against the SSRF allowlist before any network request (raises PT0452 if blocked).
- Parses the Turtle file to extract metadata (
dcterms:title,dcterms:description,dcterms:license,owl:versionInfo), Datalog rules (pg:rulesproperty), and SHACL shapes. - Resolves
pg:dependsOndependencies in topological order (raises PT0453 on cycles, PT0454 on fetch failures). - Raises PT0455 when
accept_license = FALSEand the library's license IRI is not a permissive SPDX license (MIT, Apache-2.0, PostgreSQL). - Raises PT0459 when the library name conflicts with a built-in bundle.
- Idempotent: re-installing the same version is a no-op.
- LIB-03:
pg_ripple.upgrade_rule_library(name TEXT) → TEXT— re-fetch fromsource_urland replace rules, shapes, and version; raises PT0456 if a dependent library is installed. - LIB-04:
pg_ripple.uninstall_rule_library(name TEXT) → VOID— removes all rules and shapes installed by this library; raises PT0456 if a dependent library is installed. - LIB-05:
pg_ripple.list_rule_libraries() → TABLE(name, version, installed_at, description, license_iri)— lists all installed libraries. - LIB-06: Error codes PT0452–PT0459 documented in
src/rule_library.rs. - LIB-07:
GET /rule-librariesREST endpoint inpg_ripple_http— returnslist_rule_libraries()result as a JSON array. - LIB-08: Documentation chapter
docs/src/cookbook/rule-libraries.md— format specification, authoring guide, license and disclaimer requirements. - LIB-09: Rule library format: a Turtle file with
pg:RuleLibraryresource, required metadata triples, and optionalpg:rules/ SHACL shapes. - LIB-10: pg_regress test
tests/pg_regress/sql/v0104_rule_libraries.sqlcovering install, idempotency, uninstall, PT0452, PT0455, and Datalog rule activation.
Migration
sql/pg_ripple--0.103.0--0.104.0.sql: creates_pg_ripple.rule_librariescatalog table.
[0.103.0] — 2026-05-10 — Conflict Detection
Find contradictory Datalog rules before they cause problems. Two detection modes: static structural analysis and live runtime scanning of derived facts.
As knowledge graphs grow and multiple teams contribute rules, the risk of logical contradictions quietly entering the rule set increases. One team might write a rule that concludes a product qualifies for a discount under certain conditions, while another writes a rule that concludes it does not, with both rules firing for the same data. Version 0.103.0 addresses this risk with a dedicated conflict detection system that can find such contradictions either statically — by analyzing the structure of the rules themselves before they run — or dynamically, by examining the actual derived facts already in the database. Static analysis catches the most common conflict types: rules with the same head predicate that derive contradictory constant values, and rules that derive triples for predicates that a SHACL shape prohibits.
Runtime detection goes further by querying the actual derivation table to find cases where two different inferred values currently exist for the same subject and predicate, or where inferred facts violate sh:disjoint constraints — surfacing contradictions that only emerge because of specific combinations of data. A configurable block_on_conflict GUC can make the inference engine automatically halt and raise a structured error if any contradictions are found after inference, giving teams an opt-in safety guarantee that inference results are internally consistent. A companion REST endpoint allows external governance and data quality tools to poll for conflict status programmatically, making it straightforward to integrate conflict checking into CI pipelines for rule set changes or into regular data quality monitoring dashboards.
Added
- CONFLICT-01:
pg_ripple.rule_conflicts(ruleset TEXT, mode TEXT DEFAULT 'static') → JSONB— detects conflicting rules in a rule set.- Static mode: structural analysis over the parsed rule AST and SHACL shape catalog (no VP table reads). Detects:
same_head_opposing_values: pairs of rules with the same head predicate and different constant object terms (e.g. one derives?x ex:eligible "true", another derives?x ex:eligible "false").rule_vs_shacl: rules that derive triples for a predicate referenced by ash:not,sh:disjoint, orsh:inSHACL constraint.
- Runtime mode: queries
_pg_ripple.derivationsjoined with_pg_ripple.vp_rareto find already-derived contradictions: same subject + predicate with two different inferred values, orsh:disjointproperty violations among inferred facts. - Returns a JSONB array of conflict objects; empty array means no conflicts.
- Static mode: structural analysis over the parsed rule AST and SHACL shape catalog (no VP table reads). Detects:
- CONFLICT-02:
pg_ripple.rule_conflict_check_on_loadGUC (BOOL, defaultoff) — whenon, static conflict analysis runs automatically atload_rules()time and raises a WARNING for each conflict found (not an error — allows loading of rule sets that have known soft conflicts). - CONFLICT-03:
pg_ripple.block_on_conflictGUC (BOOL, defaultoff) — whenon, theinfer()SQL function callsrule_conflicts(ruleset, 'runtime')after inference completes and raises PT0451 if any conflicts are found. - CONFLICT-04: Error code PT0451:
inference halted: rule conflict detected in ruleset '%s' (set pg_ripple.block_on_conflict = off to continue despite conflicts). - CONFLICT-05:
GET /rule-conflicts/{ruleset}?mode=static|runtimeREST endpoint inpg_ripple_http. - CONFLICT-06:
src/datalog/conflict.rs— new module implementing both detection modes. - CONFLICT-07: pg_regress tests:
tests/pg_regress/sql/v0103_conflicts_static.sql(same-head conflict, rule-vs-SHACL conflict, clean rule set) andtests/pg_regress/sql/v0103_conflicts_runtime.sql(runtime contradiction detection, block_on_conflict check).
Migration
sql/pg_ripple--0.102.0--0.103.0.sql: no schema changes; all new functionality is compiled from Rust.
[0.102.0] — 2026-05-09 — What-if Reasoning (Hypothetical Inference)
Run Datalog inference on speculative graph modifications without persisting changes. All VP tables are left unchanged; isolation is guaranteed via PostgreSQL internal sub-transactions.
The ability to safely experiment with the consequences of proposed changes before committing them is invaluable in any domain where decisions have downstream effects. Version 0.102.0 introduces hypothetical inference — the ability to ask the knowledge graph's reasoning engine "if I were to add or remove these facts, what new conclusions would follow, and which existing conclusions would no longer hold?" The new hypothetical_inference() function accepts a set of proposed assertions and retractions, runs the full inference engine against a speculative version of the graph, and returns two lists: the triples that would be newly derived if the changes were applied, and the triples that were previously derived but would no longer hold. The live database data is never modified at any point during this process.
The isolation guarantee is absolute: all speculative changes are wrapped in PostgreSQL internal sub-transactions that are rolled back after inference completes, meaning no matter how large or complex the proposed change set, the actual database is left exactly as it was before the call. This makes hypothetical inference safe to use in production, from reports, from interactive exploratory sessions, and from multi-step planning workflows where the consequences of each step must be evaluated before proceeding. Use cases range from policy simulation — "if we update this regulation, what classifications change?" — to data quality validation — "if we merge these two customer records, what contradictions does that create?" A configurable limit on the number of hypothetical assertions prevents accidental runaway queries from consuming excessive resources.
Added
- HYPO-01:
pg_ripple.hypothetical_max_assertionsGUC (INT, default10000, min 1, max 1,000,000) — maximum total number of assert + retract triples in a singlehypothetical_inference()call. Exceeding this limit raises error code PT0450. - HYPO-02:
pg_ripple.hypothetical_inference(hypotheses JSONB, rules TEXT DEFAULT 'default') → JSONB— runs the named rule set on a speculative copy of the graph and returns a JSON object with two arrays:"derived": triples that would be newly inferred if the hypotheses were applied."retracted": triples that were previously inferred but would no longer hold.- Input format:
{"assert": [{"s": "…", "p": "…", "o": "…", "g"?: N}, …], "retract": […]}.
- HYPO-03: Subtransaction isolation — all speculative changes are wrapped in
BeginInternalSubTransaction/RollbackAndReleaseCurrentSubTransaction, ensuring VP tables are never modified. - HYPO-04:
POST /hypotheticalHTTP endpoint inpg_ripple_http— accepts the same JSONB format as the SQL function. - HYPO-05:
src/hypothetical.rs— new module implementing snapshot-diff algorithm: captures "before" state of head-predicate VP tables, applies hypothetical changes, re-runs inference from a clean slate, snapshots "after" state, rolls back, and computes the diff. - HYPO-06: 8 pg_regress tests (
tests/pg_regress/sql/v0102_hypothetical.sql) covering GUC default, correct diff, isolation, retraction, side-effect absence, outer-transaction rollback, and PT0450 limit.
Migration
sql/pg_ripple--0.101.0--0.102.0.sql: no schema changes; all overlay tables are session-local and transient.
[0.101.0] — 2026-05-09 — Natural Language Explanation
Natural language explanation of Datalog-derived facts via LLM or deterministic fallback renderer.
Proof trees are valuable for engineers and auditors who can read JSON structures and reason about derivation graphs, but most of the people who need to understand why a system made a decision are not in that category. Version 0.101.0 bridges that gap by translating the technical structure of proof trees into readable, natural-language narratives. The new explain_inference() function retrieves the proof tree for any derived fact and produces a plain-English account of the reasoning chain, either by sending the proof structure to a connected large language model for a rich, context-aware explanation, or by using a clean deterministic fallback renderer that always produces something useful even when no LLM is available. The result can be formatted as plain text or as a structured JSON object containing both the proof tree and the narrative, depending on what the calling application needs.
Explanations are stored in a cache table keyed by fact identifier, output format, and LLM model, so that repeated requests for the same explanation incur no additional LLM cost. A configurable TTL governs how long cached explanations remain valid, and a vacuum_explanation_cache() function removes expired entries on demand. This release also performs an important rename: the explain_inference name is freed for this new natural-language capability, with the older derivation-chain walker available under the new name explain_inference_provenance(). HTTP endpoints expose both capabilities to external applications, meaning customer-facing portals, compliance dashboards, and audit tools can all surface human-readable explanations without requiring any direct SQL access.
Added
- NL-EXPLAIN-01:
_pg_ripple.explanation_cache (sid BIGINT, format TEXT, model TEXT, explanation TEXT, cached_at TIMESTAMPTZ, PRIMARY KEY (sid, format, model))table — caches NL explanations keyed by(fact SID, format, LLM model)to avoid repeated LLM calls. Expires afterpg_ripple.explanation_cache_ttlseconds (default: 3600). - NL-EXPLAIN-02:
pg_ripple.explanation_cache_ttlGUC (INT, default3600) — TTL in seconds for cached explanations. Set to0to disable caching entirely. - NL-EXPLAIN-03:
pg_ripple.explain_inference(subject TEXT, predicate TEXT, object TEXT, format TEXT DEFAULT 'text') → TEXT— returns a plain-English narrative of why a Datalog fact was derived. Retrieves the proof tree viajustify(), then either (a) sends it to the configured LLM endpoint with a domain-appropriate system prompt, or (b) falls back to a deterministic indented-text renderer. ReturnsNULLfor base (non-inferred) facts. Never raises an error — always returns something readable. - NL-EXPLAIN-04:
pg_ripple.explain_inference_jsonb(subject TEXT, predicate TEXT, object TEXT) → JSONB— returns{"proof_tree": <justify() output>, "narrative": "<LLM or fallback explanation>"}for programmatic consumers. - NL-EXPLAIN-05:
pg_ripple.vacuum_explanation_cache() → BIGINT— removes expired rows from_pg_ripple.explanation_cache; returns the count deleted. Call periodically or after bulk inference runs. - NL-EXPLAIN-06: LLM mock mode for testing: when
pg_ripple.llm_endpoint = 'mock',explain_inference()returns a canned narrative containing the rule name — enables full code-path coverage in pg_regress without an external LLM. - NL-EXPLAIN-07:
POST /explainandGET /explainHTTP endpoints inpg_ripple_http— delegates topg_ripple.explain_inference(). - NL-EXPLAIN-08:
src/datalog/nlexplain.rs— new module containing all NL explanation logic: LLM call, mock handler, deterministic fallback renderer, cache read/write.
Changed
- RENAME-01:
pg_ripple.explain_inference(text, text, text, text) RETURNS SETOF record(v0.61.0 derivation-chain walker) renamed topg_ripple.explain_inference_provenance()to free the name for the new NL explanation function. Migration script drops the old function; the renamed variant is available under the new name.
Migration
sql/pg_ripple--0.100.0--0.101.0.sql: drops oldexplain_inference(text, text, text, text)SETOF function; creates_pg_ripple.explanation_cachetable and index.
[0.100.0] — 2026-05-09 — Proof trees & justification infrastructure
Proof trees & justification infrastructure: _pg_ripple.derivations table, pg_ripple.record_derivations GUC, justify() proof-tree function, and vacuum_derivations() cleanup. No schema changes required beyond the new derivations table and supporting indexes added via migration script.
One of the most powerful guarantees a data system can offer is not just the ability to answer questions, but the ability to explain how it arrived at those answers. Version 0.100.0 lays the foundation for this kind of deep accountability by introducing proof trees — a persistent record of every reasoning step taken by the inference engine. Each time the system derives a new fact by applying a Datalog rule, it now records exactly which source facts were used as premises, which rule was applied, and how deep the chain of reasoning ran. This history is stored in a dedicated _pg_ripple.derivations table and can be queried at any time through a new justify() function that walks the derivation chain backward from any derived fact to its ultimate source evidence.
For organizations that must explain and defend their automated decisions — whether for regulatory compliance, legal review, or internal governance — this infrastructure transforms pg_ripple from a powerful reasoning engine into a fully transparent and auditable knowledge system. A configurable on/off switch means the performance overhead of recording derivations is only incurred when it is actually needed, keeping production workloads lean while allowing auditing modes to be activated on demand. Automatic cleanup of orphaned derivation records, cycle protection in the proof tree walker, and a configurable depth limit ensure the system remains robust even for very deep reasoning chains with complex interdependencies. This release also corrects a subtle version comparison bug that affected tests using minor version numbers of 100 or greater, ensuring the test suite remains reliable as the project crosses into triple-digit versioning territory.
Added
- PROOF-TREE-01:
_pg_ripple.derivationstable recording(id BIGINT IDENTITY, derived_sid BIGINT, rule_name TEXT, rule_set TEXT, antecedent_sids BIGINT[], created_at TIMESTAMPTZ)for every Datalog-derived fact when recording is enabled. Unique constraint on(derived_sid, rule_name)prevents duplicate rows. - PROOF-TREE-02:
pg_ripple.record_derivationsGUC (defaultoff) gates derivation recording overhead. Set toonbefore callinginfer_with_stats()to capture provenance. - PROOF-TREE-03: Derivation recording wired into the semi-naive inference engine (
run_inference_seminaive). After each fixpoint run, antecedent SIDs are captured via delta-table joins and stored in_pg_ripple.derivations. Delta-table approach correctly identifies newly-derived triples (avoids false positives from pre-existing base facts). - PROOF-TREE-04:
pg_ripple.justify(subject TEXT, predicate TEXT, object TEXT) → JSONBSQL function returning the full backward-chaining proof tree. Returns a JSONB object with"type"("inferred"or"base"),"sid","triple"(subject/predicate/object as human-readable strings), and"derivations"array. Returns NULL for triples not in the store. - PROOF-TREE-05: Recursive derivation-graph walker with cycle protection (visited-SID set prevents infinite loops) and depth limit (MAX_DEPTH = 64 levels).
- PROOF-TREE-06: Batch dictionary decode for human-readable IRI labels in proof output — all SIDs in the proof tree are decoded in a single SQL batch query.
- PROOF-TREE-07:
pg_ripple.vacuum_derivations() → BIGINTfunction to remove orphan derivation rows (wherederived_sidno longer exists in any VP table). Returns the count of rows removed. - PROOF-TREE-08: Derivations survive DRed retraction cleanly:
run_dred_on_deleteremoves derivation rows for deleted SIDs; subsequentvacuum_derivations()cleans residual orphans. - SEMVER-FIX-01: Version comparison tests in pg_regress test suite updated to use integer-based semantic version comparison (
split_part(version, '.', N)::int) to correctly handle minor versions ≥ 100 (lexicographic comparison breaks for0.100.0vs0.99.x).
Migration
- Schema changes: New
_pg_ripple.derivationstable with GIN index onantecedent_sidsand B-tree index onderived_sid. - Migration script:
sql/pg_ripple--0.99.2--0.100.0.sql.
[0.99.2] — 2026-05-08 — pg_trickle 0.49.1 patch; new repository
Patch release: bumps pg_trickle to 0.49.1 to pick up upstream bug fixes, and establishes the new grove/pg-ripple GitHub repository. No schema changes. Evidence: docker-compose.yml, sql/pg_ripple--0.99.1--0.99.2.sql.
Changed
- DOCKER-01:
docker-compose.ymlimage tag and DockerfilePG_TRICKLE_VERSIONbumped from0.49.0to0.49.1to pick up upstream bug fixes released in pg_trickle 0.49.1. - TRICKLE-PROBE-01:
.versions.tomlpg_trickleversion updated to0.49.1;PG_TRICKLE_TESTED_VERSIONcompile-time constant updated automatically viabuild.rs. - REPO-01: Repository relocated to
grove/pg-ripple(new GitHub organization). All CI workflows, artifact URLs, and documentation links updated accordingly.
Migration
- No schema changes.
- Migration script:
sql/pg_ripple--0.99.1--0.99.2.sql(comment-only).
[0.99.1] — 2026-05-07 — pg_trickle & pg_tide version probe fix; view decode=true IVM fix; IMMEDIATE mode
Patch release: aligns the pg_trickle compatibility probe with the deployed version, fixes decode=true on SPARQL/Datalog views to preserve BIGINT stream table columns for pg_trickle IVM correctness, and adds immediate mode support to all view creation functions. No schema changes.
Fixed
- TRICKLE-PROBE-01:
PG_TRICKLE_TESTED_VERSIONconstant insrc/lib.rscorrected from"0.3.0"to"0.49.0". The stale constant caused pg_ripple to emit a WARNING about pg_trickle being "newer than tested" on every startup when deployed with the Dockerfile-bundled pg_trickle, and in some configurations causedcreate_datalog_view_from_rule_setto fail with a columnsmismatch in the compiled VP table query. - DOCKER-01: Dockerfile
PG_TRICKLE_VERSIONbumped from0.48.0to0.49.0to pick up upstream bug fixes. - DOCKER-02: Dockerfile
PG_TIDE_VERSIONbumped from0.15.0to0.16.0. - VIEW-DECODE-01 (issue #81):
create_sparql_viewandcreate_datalog_viewwithdecode = trueno longer wrap the stream table query with dictionary decode subqueries. The stream table always storesBIGINTdictionary IDs; whendecode = true, a separate companion VIEW{name}_decodedis created that performs dictionary lookups at read time (e.g.pg_ripple.people_names_decoded).drop_sparql_viewanddrop_datalog_viewnow also drop the_decodedcompanion view. This mirrors the existing behaviour ofcreate_construct_viewandcreate_framing_view. - IDEMPOTENT-01 (issue #83):
load_rules/load_rules_builtincalled a second time with the same rule-set name no longer duplicates rules in_pg_ripple.rules.store_rulesnow deletes all existing rows for the rule set before re-inserting, so repeated calls leave exactly one copy of each rule. - IDEMPOTENT-02 (issue #83):
create_datalog_view_from_rulesandcreate_datalog_view_from_rule_setare now idempotent. Apgtrickle.drop_stream_tablecall (errors silently ignored) is issued before eachpgtrickle.create_stream_table, replacing the existing view cleanly on repeated invocations. This fixes dbt re-runs and CI pipelines that re-apply the same SQL against an existing database. - IDEMPOTENT-03: The same drop-before-create guard was applied to all remaining view creation functions:
create_sparql_view,create_framing_view,create_construct_view,create_describe_view,create_ask_view,create_extvp,enable_live_statistics, andenable_shacl_monitors. All view/monitor creation functions are now fully idempotent.
Added
- VIEW-IMMEDIATE-01 (issue #82):
immediateboolean parameter added tocreate_sparql_view,create_datalog_view,create_datalog_view_from_rule_set,create_framing_view,create_construct_view,create_describe_view, andcreate_ask_view. Whenimmediate := true, the underlyingpgtrickle.create_stream_table()call includesrefresh_mode => 'IMMEDIATE', enabling constraint-style in-transaction refresh. Defaults tofalse; all existing call sites are unaffected. - BUILD-INFO-01 (issue #77):
pg_ripple.build_info()SQL function returns a JSONB object with compile-time metadata:version,profile("debug"or"release"),built(RFC-3339 timestamp), andgit_sha(short SHA). Useful for diagnostics:SELECT pg_ripple.build_info();.
Migration
- No schema changes.
- Migration script:
sql/pg_ripple--0.99.0--0.99.1.sql(comment-only).
[0.99.0] — 2026-05-06 — DCTERMS, Schema.org & FOAF Vocabulary Bundles
Implements v0.99.0 roadmap: native Datalog rule sets and SHACL integrity bundles for Dublin Core Terms, Schema.org, and FOAF — completing the "Big 5" vocabulary suite together with SKOS (v0.98.0). Evidence: src/datalog/builtins.rs, src/skos.rs, tests/pg_regress/sql/v099_features.sql, docs/src/cookbook/common-vocabularies.md, sql/pg_ripple--0.98.0--0.99.0.sql.
Real-world data rarely follows just one vocabulary — organizations routinely combine Dublin Core for document metadata, Schema.org for search engine compatibility, and FOAF for social and professional networks in the same knowledge graph. Version 0.99.0 completes pg_ripple's "Big 5" vocabulary suite by adding native Datalog rule sets and SHACL integrity bundles for all three of these ubiquitous standard vocabularies, alongside cross-vocabulary bridges that automatically connect them. When Schema.org's schema:author property is used, the system can now automatically infer the FOAF foaf:maker relationship; when Dublin Core's dcterms:creator is present, it is bridged to foaf:maker as well. Fifteen Schema.org rules, eleven Dublin Core rules, and eight FOAF rules activate a rich web of entailments with a single function call per vocabulary.
For teams building knowledge graphs that must interoperate with the broader semantic web ecosystem, this release is a significant quality-of-life improvement. Instead of manually writing dozens of inference rules to handle the standard relationships defined in these well-known vocabularies, teams can activate a complete, W3C-aligned rule set instantly. Integrity bundles automatically catch common modeling mistakes — self-referential relationships, circular hierarchies in Dublin Core's hasPart/isPartOf structure, inconsistent date ranges, and knows-chain cycles in FOAF. SQL helper functions like pg_ripple.schema_type_ancestors() and pg_ripple.foaf_persons() turn complex SPARQL traversal queries into simple, readable function calls that domain experts can use directly.
Added
- DCTERMS-01:
load_datalog_bundle('dcterms')— 11 Datalog rules: fivedc11:compatibility aliases (dc11:creator→dcterms:creator, etc.), four structural inverse pairs (hasPart/isPartOf,hasVersion/isVersionOf,replaces/isReplacedBy+ reversehasVersion), DC-SKOS-01 bridge (resources whosedcterms:subjectis in a SKOS scheme receive askos:Concepttype assertion). - DCTERMS-02:
load_shape_bundle('dcterms-integrity')— 8 integrity validators: self-referential creator/contributor/relation checks, cycle detection inhasPart/isPartOfhierarchies, date range consistency. - SCHEMA-01:
load_datalog_bundle('schema')— 15 Datalog rules: four inverse pairs (subjectOf/about,hasPart/isPartOf,workExample/exampleOfWork,member/memberOf), eight type-hierarchy shortcuts connectingLocalBusiness,Organization,Person,Product,Event,CreativeWork,Place,Actiontoschema:Thing, three cross-vocabulary bridges (SCHEMA-FOAF-01, SCHEMA-DC-01, SCHEMA-DCAT-01). - SCHEMA-02:
load_shape_bundle('schema-integrity')— 6 integrity validators: required-name self-reference, self-contained price range cycles, schema-type self-reference constraints. - FOAF-01:
load_datalog_bundle('foaf')— 8 Datalog rules:foaf:knowssymmetry,foaf:Person/foaf:Organization/foaf:Group→foaf:Agenttype subsumption,foaf:account/foaf:accountForinverse,foaf:made/foaf:makerinverse, DC-FOAF-01 bridge (dcterms:creator→foaf:maker). - FOAF-02:
load_shape_bundle('foaf-integrity')— 5 integrity validators:foaf:knowsself-reference, knows-chain cycle detection, account-of self-reference. - CROSS-01: Cross-vocabulary Datalog bridges: DC-FOAF-01 (
dcterms:creator→foaf:maker), SCHEMA-FOAF-01 (schema:author→foaf:maker), SCHEMA-DC-01 (schema:name→dcterms:title), DC-SKOS-01. - HELPER-01:
pg_ripple.schema_type_ancestors(iri TEXT)— returns all Schema.org type ancestors visible in the current graph for the given IRI. - HELPER-02:
pg_ripple.foaf_persons()— returns allfoaf:PersonIRIs and theirfoaf:namelabels from the current graph. - GUC-01:
pg_ripple.rule_graph_scopedefault updated to'all'(was'default'); ontology-level reasoning across all named graphs is now the default behavior. - DOCS-01: Cookbook chapter
docs/src/cookbook/common-vocabularies.mdwith runnable examples for all three bundles. - DOCS-02: Blog post
blog/dcterms-schema-foaf-bundles.md. - TEST-01:
tests/pg_regress/sql/v099_features.sql— 45+ new tests covering all bundle loading, prefix registration, rule counts, cross-vocab bridges, SQL helpers, and integrity bundles.
Changed
pg_ripple.rule_graph_scopeGUC description updated to reflect the new'all'default.
Migration
- No schema changes; all new functionality is compiled from Rust.
- Migration script:
sql/pg_ripple--0.98.0--0.99.0.sql(comment-only).
[0.98.0] — 2026-05-06 — SKOS Support, Named Bundle API & Graph Intelligence
Implements v0.98.0 roadmap: full SKOS/SKOS-XL entailment stack, formal named bundle API, contradiction explanation, federation trust scoring, and graph coverage metrics. Evidence: src/skos.rs, tests/pg_regress/sql/v098_features.sql, docs/src/cookbook/skos-thesaurus.md, sql/pg_ripple--0.97.0--0.98.0.sql.
Taxonomies, thesauri, and controlled vocabularies are the backbone of enterprise information management — from library catalogs and medical terminology systems to product hierarchies and regulatory classification frameworks. Version 0.98.0 delivers a complete, W3C-compliant SKOS inference engine covering all 28 SKOS entailment rules plus the SKOS-XL label extension, enabling pg_ripple to automatically derive transitive broader/narrower concept relationships, symmetric associations, and correct label hierarchies without any custom rule writing. Five SQL helper functions — skos_ancestors(), skos_descendants(), skos_label(), skos_related(), and skos_siblings() — turn complex SPARQL traversal queries into simple function calls that data librarians and ontology engineers can use directly.
The release also introduces a formal named bundle API that treats rule sets and SHACL shapes as first-class, versioned artifacts with dependency management. Instead of loading individual rules and shapes manually, teams can call load_datalog_bundle() once and have all dependencies resolved automatically in the correct order. New contradiction explanation capabilities let users ask why two statements in a knowledge graph are logically inconsistent — surfacing the minimal set of conflicting facts in plain terms useful for debugging, data quality audits, and explaining validation failures to domain experts. Federation trust scoring and graph coverage metrics give teams quantitative measures of how complete and reliable their knowledge graph is across different topic areas, turning abstract data quality concerns into actionable numbers.
Added
- SKOS-01:
"skos"built-in Datalog rule set (28 rules, S7–S45) implementing all W3C SKOS entailments:skos:broaderTransitive/skos:narrowerTransitivetransitive closures,skos:narrower/skos:broaderinverse inference,skos:relatedsymmetry, concept-scheme rules, label/documentation sub-properties, mapping property propagation, and collection sub-class assertion. - SKOS-02:
"skosxl"built-in rule set (3 rules, S55–S57) projectingskosxl:Labelinstances to plainskos:prefLabel/skos:altLabel/skos:hiddenLabeltriples. - SKOS-03:
"skos-transitive"built-in rule set (7-rule subset for riverbank compatibility): transitive closures + symmetry + exactMatch transitivity. - SKOS-04:
register_standard_prefixes()extended to includeskos:andskosxl:prefixes (auto-registered when loading any SKOS rule set). - SKOS-05: Five SQL helper functions:
pg_ripple.skos_ancestors(iri, scheme),skos_descendants(iri, scheme),skos_label(iri, lang),skos_related(iri),skos_siblings(iri). - SKOS-06:
"skos-integrity"shape bundle (10 validators, W3C S9/S13/S14/S27/S37/S46 + ISO 25964-1): loaded viapg_ripple.load_shape_bundle('skos-integrity'). - SKOS-07:
pg_ripple.validate_skos()— integrity report wrapper returning(violation_id, subject, message). - RB-01: Named bundle API:
pg_ripple.load_datalog_bundle(name, named_graph),load_shape_bundle(name)with implicit dependency resolution,pg_ripple.active_datalog_bundlescatalog view. - RB-01:
_pg_ripple.datalog_bundlescatalog table andpg_ripple.active_datalog_bundlesview (schema change in migration script). - RB-02:
pg_ripple.explain_contradiction(subject_iri, named_graph, max_depth, mode)— greedy/exact minimal-hitting-set contradiction explainer. - RB-02:
pg_ripple.explain_contradiction_json(...)— JSONB variant. - RB-03:
pg_ripple.federation_endpointstable (name, endpoint_url, auth_token, min_confidence, timeout_ms, created_at) for the federation trust layer (schema change). - RB-03: GUC
pg_ripple.allow_unregistered_service_endpoints(bool, defaultoff): when off,SERVICEclauses against unregistered endpoints raise PT-SSRF-01. - RB-04:
pg_ripple.coverage_map(named_graphs, topic_predicate, top_k)— per-topic coverage SRF returning triple count, source count, mean/min confidence, contradiction count, and time range. - RB-04:
pg_ripple.refresh_coverage_map(target_graph, named_graphs)— writespgc:CoverageMaptriples intotarget_graph; schedulable via pg_trickle. - SKOS-08: Cookbook chapter
docs/src/cookbook/skos-thesaurus.md. - SKOS-09: Blog post
blog/skos-knowledge-organization.md. - Tests: 5 new pg_regress test files:
skos.sql,bundle_api.sql,explain_contradiction.sql,federation_trust.sql,coverage_map.sql, andv098_features.sql.
Changed
pg_ripple.control:default_versionupdated to'0.98.0'.
Migration
sql/pg_ripple--0.97.0--0.98.0.sql: creates_pg_ripple.datalog_bundles,pg_ripple.federation_endpoints, andpg_ripple.active_datalog_bundlesview.
[0.97.0] — 2026-05-06 — Assessment 15 Low-Severity Polish & Supply-Chain
Implements v0.97.0 roadmap: closes all 13 Low-severity findings from PLAN_OVERALL_ASSESSMENT_15.
A healthy production system requires more than correct functionality — it demands confidence in the supply chain, rigorous test coverage for edge cases, and documentation that accurately reflects real system behavior. Version 0.97.0 addresses all thirteen low-severity findings from the fifteenth assessment cycle with improvements spread across code quality, testing, documentation, and supply chain transparency. All unsafe Rust code blocks now carry mandatory justification comments explaining why each use is safe, a practice later formalized as an automatically enforced lint. Conformance suite badges for the Jena SPARQL test suite and OWL 2 RL conformance suite are added to the README and automatically updated by CI, giving users at-a-glance visibility into standards compliance status without needing to run the test suites themselves.
A particularly important improvement fixes the migration chain test, which previously relied on a manually maintained checkpoint constant that could silently fall out of sync as new migrations were added. The test now automatically discovers the most recent migration script, eliminating a class of false-passing tests that could miss coverage of recent migrations. New example SQL files covering Arrow Flight bulk export, PageRank analysis, and bidirectional CDC relay setup are verified in CI, ensuring they actually work rather than just appearing to. A cycle-safety regression test for owl:sameAs confirms the system handles symmetric, triangular, and self-referential identity assertions gracefully without entering infinite loops, closing a class of potential infinite-loop vulnerabilities in identity-heavy datasets.
Added
- L15-02: Three new example SQL files:
examples/arrow_flight_export.sql,examples/pagerank_analysis.sql, andexamples/bidi_relay_setup.sqldemonstrating bulk export, PageRank computation, and bidirectional CDC relay setup. - L15-03:
examples/test_all.sh --livewired in CI: pgrx PG18 instance started, extension installed, examples run in transaction-rollback safety mode. - L15-04:
#![warn(clippy::missing_safety_doc)]and#![warn(clippy::undocumented_unsafe_blocks)]added tosrc/lib.rs. All unsafe blocks now have// SAFETY:comments preceding theunsafekeyword (14 blocks fixed). - L15-05: Q15-xx justification convention:
// Q15-01:comments added tosrc/(128 sites; seeroadmap/v0.97.0.md). Searchable viagrep -rn 'Q15-' src/. - L15-06:
gen_random_uuid()availability check added tosql/pg_ripple--0.96.0--0.97.0.sql(DO block GEN-UUID-01) andsrc/schema/rls.rs(extension_sql!block). Emits aWARNINGif the function is unavailable (always passes on PostgreSQL 18). - L15-08: RDF-star
<<>>position support matrix added todocs/src/reference/sparql-compliance.mdcovering 18 positions across all SPARQL query/update forms. - L15-09:
cargo doc --no-depswithRUSTDOCFLAGS="-D missing_docs"added to.github/workflows/ci.yml(testjob), enforcing zero missing-documentation warnings. - L15-10:
HIGHEST_CHECKPOINTintests/test_migration_chain.shis now auto-computed fromls sql/pg_ripple--*--*.sql | sort -V | tail -1, eliminating the hand-maintained constant that caused MIGCHAIN-SYNC failures. - L15-11: New "Sequence Exhaustion (
statement_id_seq)" section added todocs/src/operations/scaling.mddocumenting exhaustion rates, monitoring query, and recovery procedure. - L15-12:
tests/pg_regress/sql/owl_sameas_cycle.sqlregression test added, asserting graceful handling of symmetric, triangular, and self-referentialowl:sameAscycles (no infinite loop; store stable after inference). - L15-14: Jena (≥95% pass) and OWL 2 RL (≥95% pass) conformance-suite badges
added to
README.md. CIjena-suiteandowl2rl-suitejobs updateresults/jena-badge.json/results/owl2rl-badge.jsonon push tomain. Newdocs/src/reference/jena-results.mddocuments the Jena suite coverage. - M15-16:
serde_cborsupply-chain audit documented: it is a transitive dep frompgrx 0.18.0, not used directly. Documented inCargo.tomlcomment and tracked indeny.toml.
Fixed
- L15-01:
CHANGELOG.mdentry for v0.90.0 had2026-05-XXas a date placeholder; corrected to2026-04-30.
[0.96.0] — 2026-05-06 — Assessment 15 Medium: Performance, Code Quality, Test Coverage
Implements v0.96.0 roadmap (A15 medium quality pillars): HTAP tombstone-skip optimisation, star-join collapse GUC, federation connect-timeout GUC, sub-splits of five large source files and the HTTP routing module, zero missing-docs warnings, concurrent-load PageRank benchmark, SHACL column-order regression test, four new Prometheus metrics, Datalog cyclic-group parallel regression test, and Arrow Flight EXPLAIN-only path.
Version 0.96.0 delivers a focused set of performance and code quality improvements from the Assessment 15 medium-severity findings. The most impactful performance improvement is a tombstone-skip optimization in the HTAP storage layer: when a VP table has no deleted rows, the query path now omits the LEFT JOIN against the tombstone table entirely, eliminating join overhead that previously existed even when there were no deletions to account for. A new star-join collapse optimization detects when multiple triple patterns share the same subject variable and combines them into a more efficient query structure, reducing redundant table access for the common star-shaped query patterns that dominate RDF workloads. A configurable federation connect-timeout GUC gives operators fine-grained control over how long federation requests wait before failing.
Code quality receives substantial attention with the decomposition of five large source files into focused sub-modules and the HTTP routing module for Datalog handlers being split in two, making the handler code easier to navigate and test in isolation. A new Arrow Flight EXPLAIN-only row estimation path replaces a COUNT(*) pre-check that could produce inconsistent row counts on active HTAP tables. Four new Prometheus metrics cover merge cycle duration, Datalog stratum evaluation time, SHACL validation queue depth, and CDC replication slot lag — giving operations teams comprehensive visibility into the most resource-intensive internal subsystems. A parallel Datalog regression test and a concurrent PageRank plus HTAP merge benchmark ensure performance characteristics remain stable under the simultaneous read/write/analyze workloads common in production.
Added
-
M15-05:
src/storage/merge.rs—htap_view_sql()andrebuild_htap_view()generate a tombstone-skip (no LEFT JOIN) form of the HTAP view whentombstone_count = 0and switch to the LEFT JOIN form on first tombstone.sql/pg_ripple--0.95.0--0.96.0.sql— addstombstone_count BIGINT NOT NULL DEFAULT 0to_pg_ripple.predicates. -
M15-06:
src/gucs/sparql.rs—STAR_JOIN_COLLAPSEbool GUC (pg_ripple.star_join_collapse, defaulttrue).src/sparql/optimizer.rs—detect_star_groups()function groups triple patterns by subject variable and sorts by ascending selectivity cost. -
M15-11:
src/gucs/federation.rs—FEDERATION_CONNECT_TIMEOUT_SECSint GUC (pg_ripple.federation_connect_timeout_secs, default10, range 1–3600).src/sparql/federation/circuit.rs—get_agent()applies.timeout_connect()before the overall request timeout. -
M15-13: Sub-split five large source files into sub-modules to keep each
mod.rsunder 800 lines:src/sparql/expr/→functions.rs(translate_function_value, is_numeric_function),cast.rs(xsd_cast_datatype, xsd_cast_sql)src/sparql/execute/→construct.rs,describe.rs,update.rs,explain.rssrc/export/→csv.rs(GraphRAG Parquet export, 939 lines)src/storage/ops/→scan.rs(query, graph management, SID API, dedup)src/datalog/compiler/→sql.rs(recursive, on-demand CTE, semi-naive delta, aggregate rules),shacl_rules.rs(constraint check compiler)
-
M15-14: Sub-split
pg_ripple_http/src/routing/datalog_handlers.rs(1232 → 442 lines):routing/datalog_inference.rs— inference HTTP handlers (infer, infer_with_stats, infer_agg, infer_wfs, infer_demand, infer_lattice, query_goal, check_constraints)routing/datalog_admin.rs— admin HTTP handlers (cache_stats, tabling_stats, list_lattices, create_lattice, list_views, create_view, drop_view)
-
M15-15: Verified zero
missing documentationwarnings under#![warn(missing_docs)]. -
M15-17:
benchmarks/pagerank_with_writes.sh— concurrent-load benchmark combining 4 pgbench writer clients, 1 SPARQL reader, and 1 PageRank computation for a configurable duration (default 60 s). Appends results tobenchmarks/pagerank_throughput_history.csv. -
M15-18:
tests/pg_regress/sql/probabilistic.sql— Test 4b: asserts thatshacl_report_scoredreturns columnsfocus_node,shape_iri,result_severity,result_severity_score,messageviapg_get_function_result. -
M15-19:
pg_ripple_http/src/metrics.rs— four new Prometheus counters/gauges:pg_ripple_merge_cycle_duration_seconds,pg_ripple_datalog_stratum_duration_seconds,pg_ripple_shacl_validation_queue_depth,pg_ripple_cdc_replication_slot_lag_bytes. -
M15-21:
tests/pg_regress/sql/datalog_cyclic_parallel.sql— regression test verifying that mutually-recursive Datalog rules execute correctly underpg_ripple.datalog_parallel_workers = 2(cyclic-group pre-check, P13-06). -
M15-22:
pg_ripple_http/src/arrow_encode.rs— Arrow Flight row-count guard now uses EXPLAIN-only path; removed COUNT(*) fallback that could produce inconsistent row counts on hot HTAP tables.
[0.95.0] — 2026-05-05 — Assessment 15 Medium: Correctness, Security, Storage
Implements v0.95.0 roadmap: replace unreachable!() panics, DNS rebinding fix for federation, sql_drop event trigger for replication-slot cleanup, SSE error redaction, dictionary VACUUM threshold GUC, property-path+OPTIONAL+ vp_rare regression tests, NaN/Inf confidence rejection, schema_generation plan cache key, and ADD/COPY/MOVE through full SPARQL update pipeline.
Correctness and security in a database extension require constant vigilance about how user-supplied data flows through the system and where assumptions can be violated by unusual inputs. Version 0.95.0 addresses eight medium-severity correctness and security findings, starting with a complete fix for DNS rebinding attacks in the federation layer. The new resolve_and_check_endpoint() function resolves hostnames to IP addresses once, validates every resolved IP against the SSRF blocklist, and uses the IP-based URL for the actual connection — preventing an attacker from exploiting the window between hostname validation and connection establishment to redirect a request to a private address.
Several other important correctness improvements ship alongside the security fixes. A new event trigger automatically cleans up CDC replication slots when the extension is dropped, preventing orphaned slots from consuming PostgreSQL WAL resources indefinitely. SPARQL ADD, COPY, and MOVE operations now correctly flow through the mutation journal and SPARQL audit log, matching the post-processing applied to all other SPARQL Update operations — ensuring that CONSTRUCT writeback rules fire for these operations just as they do for INSERT and DELETE. Explicit NaN and infinity checks in the confidence loader produce clear, actionable error messages rather than cryptic numeric range failures. A schema generation sequence lets the plan cache detect when VP tables have been promoted and automatically evict stale plans, preventing "table not found" errors after rare-predicate promotion.
Added
- M15-01:
src/pagerank/export.rs,src/pagerank/centrality.rs— replaced bareunreachable!()match arms withpgrx::error!()so unexpected input produces a clean PostgreSQL error rather than a server crash.scripts/check_no_unreachable_in_production.sh— new CI lint script..github/workflows/ci.yml—No unreachable!() in production source lintstep. - M15-02:
src/sparql/federation/policy.rs— newResolvedEndpointstruct andresolve_and_check_endpoint()function that resolves hostnames once, validates every resolved IP against the SSRF blocklist, and returns the IP-based connect URL.src/sparql/federation/http.rs— bothexecute_remote()andexecute_remote_partial()now use the resolved endpoint to prevent DNS TOCTOU / rebinding attacks. - M15-03:
src/schema/rls.rs—_pg_ripple.cleanup_on_drop()event trigger function +_pg_ripple_cleanup_on_dropevent trigger onsql_dropthat drops CDC replication slots whenDROP EXTENSION pg_rippleis executed. - M15-04:
pg_ripple_http/src/stream.rs— SSE initialisation error paths (pool unavailable, non-SELECT query) now useredacted_error()fromcommon.rsinstead of inline JSON responses with raw error details. - M15-07:
src/gucs/storage.rs—DICT_VACUUM_THRESHOLDGUC (pg_ripple.dict_vacuum_threshold, default: 10000, Userset).src/dictionary/mod.rs—maybe_vacuum_dictionary()helper runsVACUUM ANALYZE _pg_ripple.dictionarywhen encode count exceeds threshold.src/bulk_load.rs— callsmaybe_vacuum_dictionary()after large loads.src/schema/rls.rs—autovacuum_vacuum_scale_factor = 0.01andautovacuum_analyze_scale_factor = 0.005reloptions on_pg_ripple.dictionary. - M15-08:
tests/pg_regress/sql/sparql_optional_path_in_graph_rare.sql— new regression test covering OPTIONAL + property path (+ * | /) + vp_rare predicates insideGRAPH <g> {}. - M15-09:
src/bulk_load.rs— explicitis_nan()andis_infinite()checks inload_triples_with_confidence()before the range check, with clear PT0301 error messages naming the specific problem (NaN or +/-Infinity). - M15-10:
src/storage/mod.rs—bump_schema_generation()andcurrent_schema_generation()helpers read/write_pg_ripple.schema_generation_seq.src/sparql/plan_cache.rs—schema_gen=Ncomponent added to plan cache key.src/storage/promote.rs—promote_predicate_impl()callsbump_schema_generation()after promotion. - M15-12:
src/sparql/execute/mod.rs—try_execute_add_copy_move()early return now includes mutation journal flush and SPARQL audit log entry, matching the post-processing applied to all other SPARQL Update operations. - SQL:
sql/pg_ripple--0.94.0--0.95.0.sql— schema_generation_seq, dictionary autovacuum reloptions, cleanup_on_drop event trigger.
BREAKING
None.
[0.94.0] — 2026-05-05 — Assessment 15 High Remediation
Implements v0.94.0 roadmap: security hardening (SECURITY DEFINER search_path injection fix), bump-version recipe improvements, bounded bidi relay channel with Prometheus counter, and shared copy_into_vp() helper for COPY-style bulk loads.
When the same system function is used in security-sensitive contexts, even a small gap in its defenses can create an exploitable vulnerability. Version 0.94.0 addresses the highest-priority finding from Assessment 15: a potential search-path injection vulnerability in a SECURITY DEFINER trigger function that could allow a privileged attacker to substitute their own functions for standard PostgreSQL library calls. The fix pins the search path to known-safe schemas inside the function definition, and a new CI lint script verifies that every SECURITY DEFINER function in the codebase has an explicit, pinned search path — turning a one-time fix into a permanent, automatically enforced protection that prevents the issue from recurring in future code.
This release also introduces a bounded bidi relay channel with an operator-configurable inflight limit and automatic back-pressure, preventing the relay from consuming unlimited memory when the downstream consumer falls behind. A Prometheus counter tracks dropped events when the inflight limit is reached, giving operations teams visibility into backpressure events before they affect data reliability. A new copy_into_vp() helper function implements an efficient batch insertion path using UNNEST-array semantics, laying the groundwork for the 5–10× bulk load performance improvement delivered in v0.113.0. The bump-version justfile recipe is enhanced to accept an explicit compatibility minimum argument, making coordinated version management less error-prone for teams maintaining the extension alongside the HTTP companion.
Added
- H15-01:
justfile—bump-version NEW_VERSION [COMPAT_MIN]accepts an optionalCOMPAT_MINargument to setCOMPATIBLE_EXTENSION_MINindependently from the release version;check-version-syncrecipe updated to allowCOMPAT_MIN ≤ EXT_VER(notCOMPAT_MIN == EXT_VER), matching the CI gate.COMPATIBLE_EXTENSION_MINbumped to"0.93.0"for this release. - H15-02:
src/schema/triggers.rs—_pg_ripple.ddl_guard_vp_tables()gainsSET search_path = pg_catalog, _pg_ripple, publicto prevent search-path injection.scripts/check_security_definer_search_path.sh— new CI script verifying all SECURITY DEFINER functions insrc/have a pinnedSET search_pathclause. Wired into.github/workflows/ci.ymlasSECURITY DEFINER search_path lint. - H15-03:
src/gucs/storage.rs—BIDI_RELAY_MAX_INFLIGHTGUC (pg_ripple.bidi_relay_max_inflight, default: 1000, Suset).src/stats.rs—BIDI_RELAY_INFLIGHTandBIDI_RELAY_DROPPED_TOTALstaticAtomicI64counters;relay_inflight_acquire()/relay_inflight_release()helpers. Both counters exposed instreaming_metrics().src/bidi/relay.rs:ingest_jsonld_impl()— gated onrelay_inflight_acquire(); emits WARNING and returns 0 when inflight limit is reached. - L15-13:
pg_ripple_http/src/metrics.rs—bidi_relay_dropped_totalfield;update_bidi_relay_dropped_total()/bidi_relay_dropped_total()methods.pg_ripple_http/src/routing/admin_handlers.rs—pg_ripple_bidi_relay_dropped_totalPrometheus counter in/metricsendpoint. - H15-05:
src/storage/ops/mod.rs—copy_into_vp()helper using UNNEST-array based batch insertion;pub(crate)for use by bulk loader, R2RML, and CDC paths.src/gucs/storage.rs—BULK_LOAD_USE_COPYGUC (pg_ripple.bulk_load_use_copy, default: off, Userset).batch_insert_encoded()dispatches tocopy_into_vp()when the GUC is on. - SQL:
sql/pg_ripple--0.93.0--0.94.0.sql— recreates_pg_ripple.ddl_guard_vp_tables()withSET search_pathand records schema version. - Roadmap:
roadmap/v0.94.0.md+roadmap/v0.94.0-full.mdcreated.
Changed
pg_ripple.controlcommentupdated tov0.94.0.
[0.93.0] — 2026-05-04 — pg_tide Integration & Documentation Modernisation
Implements v0.93.0 roadmap: integrates pg_tide as the recommended relay transport layer and modernises all documentation to reflect the new pg-trickle v0.46.0 architecture (relay, outbox, inbox extracted to pg_tide).
The CDC relay ecosystem around pg_ripple underwent a significant architectural change when pg_trickle version 0.46.0 extracted the relay, outbox, and inbox components into a separate pg_tide extension. Version 0.93.0 updates pg_ripple to work correctly with this new architecture, adding runtime detection of pg_tide, clear error messages that guide users toward installing it when it is absent, and comprehensive updates to all documentation, blog posts, and example code that referenced the old relay API. The Docker image is updated to bundle both pg_trickle 0.46.0 and pg_tide 0.4.0 together, so deployments using the official image get the correct combination out of the box.
For existing users who relied on relay functionality, this release provides a clear migration path and backward-compatible behavior: a new pg_ripple.pg_tide_available() function lets applications detect at runtime whether pg_tide is installed and adapt their behavior accordingly. All documentation — including the bidirectional relay operations guide, the semantic hub blog post, and the integration plans — is updated with correct API references, migration notes that document the exact pg_trickle to pg_tide call-site changes, and version compatibility tables. A new compatibility matrix section clearly documents which versions of pg_trickle and pg_tide work with which versions of pg_ripple, giving operators the information they need to plan upgrades with confidence and without trial and error.
Added
- TIDE-1:
src/lib.rs—has_pg_tide()runtime detection helper;_PG_initINFO message about pg_tide relay support;pg_ripple.pg_tide_available()SQL function (viasrc/views_api.rs) for client-side detection. - TIDE-3:
src/views/mod.rs—PGTIDE_HINTconstant for relay error paths:"pg_tide extension is not installed; install pg_tide ≥0.1.0 from https://github.com/trickle-labs/pg-tide". - SQL:
sql/pg_ripple--0.92.0--0.93.0.sql— comment-only migration script documenting all TIDE-1 through TIDE-DOCKER-01 changes. No schema changes.
Changed
- TIDE-2:
src/bidi/mod.rs— BIDI-OUTBOX-01 and BIDI-INBOX-01 doc comments updated to reference pg_tide API (tide.outbox_create,tide.outbox_publish,tide.inbox_create,tide.inbox_status). - TIDE-4:
docs/src/operations/pg-trickle-relay.md— full rewrite to pg-tide API:- 20+ API call sites updated (pgtrickle.* → tide.*)
- Prerequisites updated: pg_tide ≥0.4.0 + pg_trickle ≥0.46.0 required
- New outbox publish trigger pattern (
tide.outbox_publish) - Architecture diagram updated to reference pg_tide
pg-tide-relaybinary replacespgtrickle-relayin docker-compose example- Related pages extended with pg_tide repository link
- TIDE-5:
blog/semantic-hub-trickle-relay.md— renamed integration to pg-tide; hub-and-spoke examples updated totide.*API; clarified pg_trickle is IVM-only since v0.46.0. - TIDE-6:
plans/pg_trickle_relay_integration.md— prominent backward-compat migration note added documenting full API migration path from pg_trickle ≤ 0.45.0 to pg_tide ≥ 0.1.0. - TIDE-7:
roadmap/v0.52.0.md,roadmap/v0.77.0-full.md— inline footnotes noting relay examples require pg_trickle < 0.46.0 or pg_tide ≥ 0.1.0; updatedtide.relay_set_outbox()example in v0.77.0-full.md. - TIDE-8:
docs/src/operations/compatibility.md— new "pg_tide / pg_trickle Extension Compatibility" section with version compatibility tables; pg_ripple_http 0.93.x row added to the main compatibility table. - TIDE-DOCKER-01:
Dockerfile—PG_TRICKLE_VERSIONbumped to0.46.0;PG_TIDE_VERSIONbumped to0.4.0; corrupted COPY sections fixed; image description label updated; header comment updated. - Version bumps:
Cargo.tomlandpg_ripple_http/Cargo.tomlbumped to 0.93.0;pg_ripple.controlupdated todefault_version = '0.93.0';COMPATIBLE_EXTENSION_MINbumped to"0.92.0"inpg_ripple_http/src/main.rs.
[0.92.0] — 2026-05-03 — Assessment 14 Low-Severity Polish & Hardening
Implements v0.92.0 roadmap: closes all 39 Low-severity findings from PLAN_OVERALL_ASSESSMENT_14. This is the final Assessment 14 remediation release before v1.0.0 production hardening.
Version 0.92.0 closes all thirty-nine low-severity findings from Assessment 14, touching nearly every part of the system. PageRank receives improvements to its documentation — a new "Tuning Damping for Your Graph" section gives operators guidance tailored to sparse social, citation, knowledge, and temporal graph types — and its row-level security policies are hardened with the same graph isolation policies applied to the confidence side table. The owl:sameAs handling documentation is clarified to confirm that entity clusters are merged before PageRank computation, preventing double-counting an important correctness guarantee for entity-rich datasets where multiple IRIs refer to the same real-world object.
Operations receive several meaningful improvements. The diagnostic report function is extended with four new PageRank-related entries covering confidence row counts, last PageRank computation time, queue depth, and centrality metrics, giving operators a more complete system health picture in a single call. The HTTP companion gains a configurable graceful shutdown timeout, allowing operators to tune how long the service waits for in-flight requests to complete before exiting — critical in production environments where SLA requirements prohibit abrupt connection drops. CDC payload length is now checked against PostgreSQL's 8,000-byte pg_notify limit before sending, raising a warning rather than silently truncating notifications. The PageRank find_duplicates() function's volatility is correctly classified as STABLE rather than VOLATILE, enabling the query planner to make better optimization decisions for queries that call it repeatedly.
Added
- CB-07:
src/pagerank/ivm.rs—pagerank_lower()/pagerank_upper()doc comments include PR-STALE-BOUNDS-01 formula:bound = score ± (alpha^k * delta_per_iter). - CB-08:
docs/src/features/pagerank.md— "Tuning Damping for Your Graph" subsection with guidance for sparse social, citation, knowledge, and temporal graph types. - CB-09:
tests/pg_regress/sql/sparql_federation.sql— TLS handshake failure scenario assertingSERVICE SILENTswallows connection errors and returns empty result set. - CB-10:
docs/src/reference/sparql-compliance.md—symmetricalias contract documented as permanent (guaranteed stable for the 1.x API line; alias andscbdremain semantically identical). - SEC-07:
sql/pg_ripple--0.91.0--0.92.0.sql—ALTER TABLE _pg_ripple.pagerank_dirty_edges ENABLE ROW LEVEL SECURITY+CREATE POLICY pagerank_dirty_edges_graph_isolationmirroring the_pg_ripple.confidenceRLS pattern. - SEC-09:
.github/workflows/cargo-audit.yml—--deny unmaintainedadded tocargo auditinvocation; existingaudit.tomlignores have valid expiry dates. - PERF-07:
src/gucs/pagerank.rs—PAGERANK_PARTITIONdefault changed fromfalsetotrue; description updated to document auto-tune behaviour (min(num_cpus, named_graph_count)). - PERF-08:
src/uncertain_knowledge_api/mod.rs—_fuzzy_match_guard()and_token_set_ratio_guard()annotatedstable, parallel_safe(was: default VOLATILE). - CON-04:
tests/pg_regress/sql/datalog_parallel.sql— regression test for cyclic parallel Datalog stratification pre-check; asserts no crash for non-cyclic RDFS rule set. - CON-05:
tests/concurrency/confidence_subxact_rollback.sql— noisy-OR aggregation rollback test:BEGIN; SAVEPOINT s1; infer(); ROLLBACK TO s1; COMMITasserts confidence table unchanged. - TEST-06:
benchmarks/pagerank_throughput_history.csv— PageRank throughput history for Karate Club (1M-edge) benchmark; wired toperformance_trend.ymlartifact upload. - CQ-06:
src/uncertain_knowledge_api/mod.rs,src/pagerank/mod.rs— all#[allow(dead_code)]attributes in v0.87/v0.88 additions carry// Q14-08: <reason>. - STD-04:
docs/src/features/pagerank.md— portability note:pg:pagerank()etc. are pg_ripple-specific extension functions; not portable to other SPARQL endpoints. - STD-05:
docs/src/features/uncertain-knowledge.md—sh:severityWeightextension note: pg_ripple-specific, community submission to W3C SHACL CG under consideration. - OBS-04:
src/sparql/execute/mod.rs—algebra_optimized(en_US) accepted as alias foralgebra_optimised(en_GB) inexplain_sparql()format parameter. - OBS-05:
src/maintenance_api.rs—diagnostic_report()extended with four v0.87/v0.88 catalog rows:confidence_row_count,pagerank_last_computed,pagerank_queue_depth,centrality_metrics. - HTTP-05:
pg_ripple_http/src/main.rs—PG_RIPPLE_HTTP_SHUTDOWN_TIMEOUT_SECSenv var (INT, default 30) configures graceful-shutdown drain timeout. - HTTP-06:
pg_ripple_http/src/routing/middleware.rs— documentation note confirmingtower_governor0.8 withaxumfeature automatically includesRetry-Afterheader in 429 responses. - DL-04:
src/datalog/magic.rs—run_infer_goal()doc comment explicitly documents magic-sets pre-condition (bound predicate requirement) and fallback behaviour. - DL-05:
docs/src/features/pagerank.md—owl:sameAshandling note: entity clusters are merged before PageRank to avoid double-counting. - IVM-03:
docs/src/reference/ivm.md— cross-module dependency scheduling note: rules writing to_pg_ripple.confidenceare not in the CWB topological sort;register_ivm_dependency()API reserved for future use. - CDC-03:
src/cdc.rs—notify_named_subscriptions()now checks payload length against 8000-bytepg_notifylimit; raises PT5001 WARNING instead of silently truncating. - CDC-04:
tests/concurrency/sse_slow_subscriber.sh— SSE backpressure load test asserting server remains responsive under slow subscriber. - DOC-03:
blog/pagerank.md+blog/uncertain-knowledge.md— stub blog posts for v0.88.0 and v0.87.0 features. - DOC-04:
examples/test_all.sh— static validation script for all.sqlexamples; wired as CI step in.github/workflows/ci.yml. - BUILD-04:
build.rs—SOURCE_DATE_EPOCHsupport verified and documented (already implemented in BUILD-TIME-FIELD-01, v0.83.0); documented inCONTRIBUTING.md. - BUILD-05:
CONTRIBUTING.md—src/uncertain_knowledge_api/andsrc/pagerank/module structure documented; magic comment conventions (// SAFETY:,// CLIPPY-OK:,// Q13-05:,// Q14-08:) documented. - SEC-06:
RELEASE.md— RSA advisory calendar entry: RUSTSEC-2024-0436 and RUSTSEC-2023-0071 expire 2026-12-01; re-audit required before v1.0.0. - WC-01–WC-05: Post-v1.0.0 aspirational tracking documented in
roadmap/v0.92.0-full.md.
Changed
- SEC-08:
src/pagerank_api.rs—pagerank_find_duplicates()volatility changed from VOLATILE to STABLE (DB-state dependent, not time/random). - PERF-07:
pg_ripple.pagerank_partitionGUC default changed fromfalsetotrue.
[0.91.0] — 2026-05-03 — Assessment 14 Medium: Observability, API, Standards, Build & Documentation
**Implements v0.91.0 roadmap: completes the second half of PLAN_OVERALL_ASSESSMENT_14 Medium remediations. Adds PageRank IVM Prometheus gauges, SHACL score log retention GUC
- vacuum function, PostgreSQL jsonlog documentation, HTTP middleware extraction, Arrow
Flight EXPLAIN row estimation, SPARQL
pg:prefix auto-declaration,explain_pagerank_json(), PT0301–PT0423 error code docs, RDF-star compliance matrix, compatibility table rows through v0.91.x, ProbLog citation, IVM boundary architecture doc, named-argument pagerank_run examples, SSE regression test, BUILD lint-version-sync CI job, dedicated migration-chain workflow, bidi relay throughput benchmark wiring, and two new GUCs for CDC watermark control.**
Version 0.91.0 delivers the second half of the Assessment 14 medium-severity remediation with twenty-seven improvements spanning observability, API ergonomics, standards compliance, build quality, and documentation. Three new Prometheus gauges expose PageRank IVM queue telemetry — queue depth, maximum delta, and oldest enqueue age — enabling SLO-based alerting when PageRank becomes stale and helping teams understand the lag between graph changes and updated rank scores. Arrow Flight row count estimation is improved by using EXPLAIN-style plan row estimates rather than COUNT(*), which could produce inconsistent counts on active HTAP tables. A dedicated migration chain CI workflow runs automatically whenever migration SQL files are modified, ensuring the upgrade path is always tested when it changes.
An important ergonomics improvement makes the pg: namespace prefix automatically available in all SPARQL queries without a manual prefix declaration, eliminating a friction point that required users to add PREFIX pg: <http://pg-ripple.org/fn/> before using any pg_ripple extension function. A new explain_pagerank_json() function returns a structured explanation tree showing exactly why a node has its current rank and which nodes contributed to it — the PageRank equivalent of a query execution plan. A complete error code documentation table covers all PT0301–PT0423 codes, giving developers immediate context when they encounter any error the system can produce. Two new GUCs provide fine-grained control over CDC watermark batch processing, allowing operators to tune throughput and latency independently.
Added
- OBS-01:
pg_ripple_http/src/metrics.rs— Three new Prometheus gauges for PageRank IVM queue telemetry:pg_ripple_pagerank_queue_depth,pg_ripple_pagerank_queue_max_delta,pg_ripple_pagerank_queue_oldest_enqueue_seconds. Exposed in/metricsendpoint viapg_ripple_http/src/routing/admin_handlers.rs. - OBS-02:
src/gucs/observability.rs+src/gucs/registration/observability.rs—pg_ripple.shacl_score_log_retention_daysGUC (INT, default 30, range 0–3650, Suset).src/uncertain_knowledge_api/mod.rs—vacuum_shacl_score_log()pg_externfunction purges rows older than the retention window. - OBS-03:
docs/src/reference/observability.md— PostgreSQL Structured Logging section explainingpgrx::log!→log_destination=jsonlogmapping; no double-serialisation risk. - HTTP-02:
tests/integration/sse_stream.sh— SSE stream regression test for/sparql/streamendpoint (validatesContent-Type: text/event-streamanddata:event emission). - HTTP-03:
pg_ripple_http/src/routing/middleware.rs(new file) — Extractedapply_rate_limit()andbuild_cors_layer()middleware helpers.pg_ripple_http/src/routing/mod.rs—pub mod middleware;module declaration. - HTTP-04:
pg_ripple_http/src/arrow_encode.rs— Replaced COUNT() row-count pre-check withEXPLAIN (FORMAT JSON, ANALYZE FALSE)plan row estimation.extract_plan_rows_from_explain()helper with COUNT() fallback. - API-04:
src/sparql/parse.rs—PG_FN_NAMESPACEconstant +inject_pg_prefix_if_needed()auto-declaresPREFIX pg: <http://pg-ripple.org/fn/>when a query usespg:without an explicit prefix. Wired intosparql()andsparql_ask()insrc/sparql/mod.rs.docs/src/reference/sparql.md— SPARQL Extension Function IRI Namespace section with function reference table and federation note. - API-05:
src/pagerank_api.rs—explain_pagerank_json(node_iri, top_k)pg_externfunction returning JSONB explanation tree. - API-06:
docs/src/reference/error-codes.md— PT0301–PT0307 (Uncertain Knowledge) and PT0401–PT0423 (PageRank) error code tables. - STD-01:
plans/sparql12_tracking.md— Updated version to v0.91.0; last_reviewed to 2026-05-03. - STD-02:
docs/src/reference/sparql-compliance.md— RDF 1.2 / SPARQL-star Compliance Matrix section. - STD-03:
docs/src/features/uncertain-knowledge.md— ProbLog citation (De Raedt, Kimmig & Toivonen 2007) with extension-vs-standard note. - DOC-01:
docs/src/operations/compatibility.md— Compatibility table rows for v0.87.x through v0.91.x. - DOC-02:
docs/src/features/pagerank.md— Named-argumentpagerank_run()examples;explain_pagerank_json()function documentation. - IVM-01:
docs/src/reference/ivm.md(new file) — IVM boundary architecture document for CWB-IVM vs. PageRank-IVM; monitoring commands; GUC reference. Added todocs/src/SUMMARY.md. - IVM-02:
tests/pg_regress/sql/construct_rules.sql+tests/pg_regress/expected/construct_rules.out— CWB confidence propagation regression test verifyingsource=1marking for inferred triples. - BUILD-01:
.github/workflows/ci.yml—lint-version-syncjob checksCargo.toml==pg_ripple.control==pg_ripple_http/Cargo.tomland validatesCOMPATIBLE_EXTENSION_MIN≤ extension version. - BUILD-02:
.github/workflows/migration-chain.yml(new file) — Dedicated migration chain workflow triggered on SQL/control file changes. - CDC-02:
.github/workflows/performance_trend.yml—benchmarks/bidi_relay_throughput.sqlwired asbench_bidi_relaybenchmark step. - New GUCs (v0.91.0):
pg_ripple.shacl_score_log_retention_days(INT, default 30): days to retain SHACL score log entries.pg_ripple.cdc_watermark_batch_size(INT, default 100): number of CDC events per watermark flush batch.pg_ripple.cdc_watermark_flush_interval_ms(INT, default 50): milliseconds between watermark flush cycles.
Changed
pg_ripple_httpversion bumped to0.91.0(in sync with extension).COMPATIBLE_EXTENSION_MINupdated to"0.90.0"inpg_ripple_http/src/main.rs.
Dependency Triage (DEP-01, DEP-02)
- ureq 3.x (DEP-01): ureq remains at 2.x in v0.91.0. The 3.x API is a breaking change requiring significant refactoring of the federation HTTP client. Triage decision: defer to post-v1.0.0 hardening cycle. Tracking: https://github.com/algesten/ureq/blob/main/CHANGELOG.md.
- arrow / parquet (DEP-02):
cargo update --dry-runshows compatible minor bumps available (arrow 55.x → 55.y). These are patch-compatible and will be picked up by Dependabot/Renovate in the normal dependency update cycle. No blocking issue identified.
[0.90.0] — 2026-04-30 — Assessment 14 Medium Remediation
Implements v0.90.0 roadmap: closes 24 Medium-severity findings from PLAN_OVERALL_ASSESSMENT_14. Adds PageRank WCOJ integration, convergence norm GUC, IVM full-recompute threshold, Count-Min Sketch GUCs, temp-threshold GUC, advisory lock for concurrent runs, ANALYZE after confidence bulk load, SPARQL MINUS blank-node regression test, seven pre-emptive module splits, pagerank/uncertain_knowledge_api module directories, datalog_handlers routing migration, clippy::unwrap_used lint gate, convergence norm documentation, IVM error bounds documentation, cyclic convergence guarantee documentation, and new test coverage (proptest oracle, fuzz confidence loader, concurrency scripts, scale benchmark).
As pg_ripple's feature surface expanded with the probabilistic Datalog and PageRank engines in versions 0.87 and 0.88, a number of medium-severity findings accumulated around their operational characteristics — convergence behavior, concurrency safety, and the configuration surface needed to tune them for different workloads. Version 0.90.0 addresses all twenty-four of these findings. PageRank gains integration with the worst-case optimal join executor for graphs exceeding ten million edges, making large-scale graph analysis practical without exponential intermediate result sets. An advisory lock prevents concurrent pagerank_run() calls on the same topic from interfering with each other, and an IVM full-recompute threshold automatically triggers a complete recalculation when incremental updates have accumulated enough staleness to compromise accuracy.
This release marks a major structural investment in code organization: seven large source files approaching the 1,800-line CI gate are proactively decomposed into focused sub-modules before they become maintenance problems. The src/pagerank.rs and src/uncertain_knowledge_api.rs files become proper directory modules with logical sub-components, and the Datalog HTTP handlers are extracted to the routing module where they belong architecturally. A clippy::unwrap_used CI gate prevents new panicking unwrap calls from entering the codebase going forward. Five property-based tests verify the algebraic identity laws of the noisy-OR confidence operator — commutativity, associativity, monotonicity, and absorbing elements — and a fuzz target exercises the confidence loader with adversarial float inputs including NaN, infinities, and denormal numbers that can cause incorrect results in probabilistic computations.
Added
- CB-02:
docs/src/features/pagerank.md— Convergence Norm section documenting L1/L2/Linf norm selection viapg_ripple.pagerank_convergence_normGUC; NetworkX vs. igraph behaviour comparison. - CB-04:
docs/src/features/pagerank.md— Incremental Refresh Error Bounds section with formal $\alpha^K$ bound and automatic full-recompute threshold documentation. - CB-05:
tests/pg_regress/sql/sparql_minus_blank_scope.sql+ expected output — regression test for SPARQL MINUS blank-node scoping (per SPARQL 1.1 §18.6). - PERF-01:
src/pagerank/executor.rs— WCOJ threshold check atpagerank_run()entry;pg_ripple.pagerank_wcoj_thresholdGUC (INT, default 10, units: millions of edges). - PERF-02:
docs/src/features/pagerank.md— Count-Min Sketch parameter documentation with error bound formula. - PERF-03:
Cargo.tomlworkspace lints —clippy::unwrap_used = "warn"andclippy::expect_used = "warn"GUC-style gates added; CI blocks on new violations (ci/regress:unwrap_cap). - PERF-06:
src/bulk_load.rs—ANALYZE _pg_ripple.confidenceafterload_triples_with_confidence()completion. - CON-01:
tests/concurrency/pagerank_during_merge.sh— deadlock test: 8 concurrent writers + HTAP merge +pagerank_run(). - CON-02:
benchmarks/probabilistic_overhead.sqlextended with hot-row confidence contention benchmark (noisy-OR ON CONFLICT on narrow key range). - CON-03:
src/pagerank/executor.rs—pg_advisory_xact_lock(hashtext($1))per topic atpagerank_run()entry. - TEST-02:
tests/proptest/pagerank_oracle.rs— pure-Rust PageRank oracle proptest (5 invariants: sum, positivity, fixed-point, damping monotonicity, sink handling). - TEST-03:
fuzz/fuzz_targets/confidence_loader.rs— cargo-fuzz target for adversarial confidence float inputs (NaN, ±∞, negative, >1.0, denormals). - TEST-04:
benchmarks/pagerank_scale.sh— scale benchmark gate (1M/10M edges with wall-time assertions). - TEST-05:
tests/concurrency/confidence_subxact_rollback.sql— SAVEPOINT/ROLLBACK confidence table consistency test. - DL-02:
docs/src/features/uncertain-knowledge.md— Convergence Guarantees for Cyclic Probabilistic Rules section with Knaster–Tarski fixed-point theorem citation. - CQ-02 / DL-03: Pre-emptive module splits for 7 files approaching the 1,800-line CI gate:
src/sparql/execute.rs→execute/{exec_core,construct,describe,update,explain}.rssrc/sparql/expr.rs→expr/{functions,filters,aggregates,cast}.rssrc/datalog/compiler.rs→compiler/{sql,prob,shacl_rules,builtins}.rssrc/storage/ops.rs→ops/{insert,delete,scan,merge}.rssrc/export.rs→export/{turtle,jsonld,ntriples,csv,common}.rssrc/citus.rs→citus/{sharding,rls,aggregate,federation}.rssrc/views.rs→views/{construct,describe,sparql}.rs
- CQ-03:
src/pagerank.rs→src/pagerank/directory split (executor, ivm, sketch, centrality, export, explain, mod). - CQ-04:
src/uncertain_knowledge_api.rs→src/uncertain_knowledge_api/directory split (mod + confidence_table, fuzzy, prov, shacl stubs). - CQ-05:
pg_ripple_http/src/datalog.rs→pg_ripple_http/src/routing/datalog_handlers.rswith backward-compat re-export shim. - New GUCs (v0.90.0):
pg_ripple.pagerank_convergence_norm(TEXT, default'l1'): convergence norm selection.pg_ripple.pagerank_full_recompute_threshold(FLOAT8, default0.01): IVM stale fraction triggering full recompute.pg_ripple.pagerank_wcoj_threshold(INT, default10): WCOJ path threshold in millions of edges.pg_ripple.pagerank_sketch_width(INT, default2000): Count-Min Sketch columns.pg_ripple.pagerank_sketch_depth(INT, default5): Count-Min Sketch depth.pg_ripple.pagerank_temp_threshold(INT, default0= auto): streaming temp-table threshold.
Already implemented (verified in codebase)
- CB-06:
pg_ripple.export_pagerank()raises PT0417 for unknown format (silent CSV default was fixed;src/pagerank/export.rs). - PERF-05:
src/sparql/embedding.rsfast-path gate onpg_ripple.pgvector_enabledGUC. - DL-01: Probabilistic weight parser validates NaN/negative/> 1.0 and raises PT0301.
Migration notes
No SQL schema changes. The migration script sql/pg_ripple--0.89.0--0.90.0.sql is a
comment-only file listing the new GUCs and behaviour changes.
[0.89.0] — 2026-05-03 — Assessment 14 Critical & High Remediation
Implements v0.89.0 roadmap: deletes stale backup file, extends migration chain test, bumps HTTP compat min, adds bump-version dry-run, adds confidence noisy-OR proptest, GUC name audit with deprecated aliases for v0.87/v0.88 GUCs, default rate limit 100 req/s, fuzzy input length guard (SEC-02), pagerank seed array guard (SEC-03), IRI escaping in export_pagerank (SEC-04), and actionable pg_trgm diagnostic in fuzzy SPARQL (CB-03).
Security findings, even when they appear minor in isolation, require immediate and thorough remediation to prevent them from combining into larger vulnerabilities. Version 0.89.0 addresses the critical and high findings from Assessment 14, starting with four targeted security improvements. A default rate limit of 100 requests per second is now enforced in the HTTP companion out of the box — previously the rate limiter was disabled, leaving deployments with no protection against denial-of-service; a maximum input length guard prevents oversized strings from reaching the fuzzy matching functions; a seed array size limit caps the number of PageRank seed nodes that can be specified in a single call; and the PageRank export function is hardened against injection by switching from string interpolation to parameterized SQL with percent-encoded IRI output.
The release also corrects a naming convention gap: several GUC parameters introduced in v0.87.0 and v0.88.0 violated pg_ripple's established noun_verb_unit snake_case naming convention. Canonical names are introduced for all four non-conforming GUCs, with the deprecated names kept functional until the v1.0.0 removal to avoid breaking existing configurations. The migration chain test is extended with checkpoint assertions for v0.84.0 through v0.88.0, verifying that the complete upgrade path works correctly end-to-end across five releases. A suite of seven algebraic property tests for the noisy-OR confidence operator validates commutativity, associativity, monotonicity, idempotence, and output range invariants that the probabilistic reasoning engine depends on for mathematically correct confidence propagation.
Added
- DEAD-FILE-01 (CQ-01): Deleted
src/gucs/registration.rs.bak; added.bak,.orig,.swppatterns to.gitignore; newlint-no-backup-filesCI job. - TEST-01: Extended migration chain test with checkpoints for v0.84–v0.88 (DDL assertions for new tables in each release); added MIGCHAIN-SYNC structural version-sync assertion. Evidence:
sql/pg_ripple--0.88.0--0.89.0.sqlmigration script; GUC and DDL checkpoint assertions in migration chain test. - HTTP-COMPAT-01:
COMPATIBLE_EXTENSION_MINbumped from0.87.0to0.88.0inpg_ripple_http/src/main.rs. - ROAD-02:
justfilebump-versionextended with CHANGELOG stub creation; newbump-version-drydry-run recipe. - CB-01:
tests/proptest/confidence_algebra.rs— 7 algebraic-identity proptests for noisy-OR operator (commutativity, associativity, monotonicity, idempotence, identity, absorbing element, output range). - SEC-01:
pg_ripple_httpdefault rate limit changed from0(disabled) to100req/s; operators withPG_RIPPLE_HTTP_RATE_LIMIT=0are unaffected. - API-01 (GUC canonical names): canonical aliases for v0.87/v0.88 GUCs that violated
pg_ripple.noun_verb_unitconvention; deprecated names remain registered until v1.0.0 removal:pg_ripple.katz_alpha→pg_ripple.pagerank_katz_alpha(canonical)pg_ripple.federation_minimum_confidence→pg_ripple.pagerank_federation_confidence_minpg_ripple.default_fuzzy_threshold→pg_ripple.fuzzy_match_threshold
- SEC-02:
pg_ripple.fuzzy_max_input_lengthGUC (INT, default 4096, range 1–65536);pg:fuzzy_match()andpg:token_set_ratio()raise PT0308 when either argument exceeds the limit. - SEC-03:
pg_ripple.pagerank_max_seedsGUC (INT, default 1024, range 1–1048576);pagerank_run(..., seed_iris TEXT[])raises PT0411 when the array exceeds the limit. - SEC-04 + CB-03:
export_pagerank()now uses parameterized SQL for thetopicparameter (no more direct interpolation); node IRI output is percent-encoded per RFC 3987.pg:fuzzy_match()/pg:token_set_ratio()now route throughpg_ripple._fuzzy_match_guard()/pg_ripple._token_set_ratio_guard()guard functions that raise actionable PT0302 (pg_trgm missing) and PT0308 (input too long).
Changed
pg_ripple_httpdefault rate limit default changed from 0 to 100 req/s (SEC-01). SetPG_RIPPLE_HTTP_RATE_LIMIT=0to restore the old disabled behavior.
Migration notes
No SQL schema changes. The migration script sql/pg_ripple--0.88.0--0.89.0.sql is a comment-only file.
[0.88.0] — 2026-05-XX — Datalog-Native PageRank & Graph Analytics
Implements v0.88.0 roadmap: iterative PageRank engine via Datalog^agg + subsumptive tabling, topic-sensitive and personalized PageRank, IVM dirty-edge queue (K-hop incremental refresh), confidence-weighted edges, four centrality measures (betweenness, closeness, degree, Katz), score-explanation trees, standard-format export (CSV/Turtle/N-Triples/JSON-LD), probabilistic score bounds, SHACL-aware ranking, federation blend mode, centrality-guided entity deduplication, HTTP companion PageRank/centrality REST API, pg_regress test suite, and benchmarks.
PageRank is one of the most famous algorithms in computer science — the formula originally used by Google to rank web pages and now widely applied to knowledge graphs, citation networks, social media, and supply chains to identify the most influential or important nodes. Version 0.88.0 implements a full, Datalog-native PageRank engine inside pg_ripple that goes far beyond a simple implementation: it supports topic-sensitive and personalized PageRank, incremental refresh when the graph changes, confidence-weighted edges, and four centrality measures (betweenness, closeness, degree, and Katz centrality). Results include probabilistic score bounds derived from the confidence side table, giving every rank score a lower and upper bound that reflects the uncertainty in the underlying data, and a SHACL-aware ranking mode can exclude nodes that fail shape validation constraints.
The practical applications are wide-ranging. Business intelligence teams can identify the most influential products in a supply chain graph or the most connected entities in a customer relationship graph. Research teams can surface the most-cited papers in a literature knowledge base or the most authoritative sources in a fact-checking graph. The pg:pagerank() SPARQL extension function makes it possible to filter and sort SPARQL query results by centrality without leaving the query language. A centrality-guided entity deduplication function uses PageRank scores to identify the canonical representative when resolving owl:sameAs identity clusters. Score explanation trees show exactly why a node has its current rank and which neighbors contributed to it most, and standard-format exports in CSV, Turtle, N-Triples, and JSON-LD make it easy to consume results in downstream analytics and visualization tools.
Added
- PR-DATALOG-01:
src/pagerank.rs— Datalog-native iterative PageRank viaWITH RECURSIVESQL; subsumptive tabling for convergence-aware early termination;_pg_ripple.pagerank_scorespersistence table. - PR-ITER-01: Power-iteration loop; L1-norm convergence test; per-iteration delta tracking.
- PR-DAMPING-01: Configurable damping factor (
pg_ripple.pagerank_damping, default 0.85); teleportation redistributes to dangling nodes. - PR-BLANK-01:
pg_ripple.pagerank_include_blank_nodesGUC; blank nodes excluded by default. - PR-PERSONAL-01: Personalization vector via
seed_iris+biasparameters; uniform bias when no seeds. - PR-SPARQL-FN-01:
pg:pagerank()andpg:pagerank(?node, ?topic)SPARQL extension functions. - PR-TOPN-01:
pg:topN_approx()sketch-based approximate top-N;top_kparameter onpagerank_run(). - PR-SQL-FN-01:
pg_ripple.pagerank_run(damping, max_iterations, convergence_delta, direction, topic, ...)SQL set-returning function. - PR-VIEW-01:
_pg_ripple.pagerank_scores (node, topic, score, score_lower, score_upper, computed_at, iterations, converged, stale, stale_since)table; BRIN index oncomputed_at. - PR-MAGIC-01: Magic-sets transformation for goal-directed partial-graph evaluation (bound node shortcut).
- PR-TRICKLE-01:
_pg_ripple.pagerank_dirty_edgesIVM queue; K-hop incremental refresh;pg_ripple.pagerank_incrementalGUC;pg_ripple.vacuum_pagerank_dirty(). - PR-TRICKLE-CONF-01: Confidence-attenuated K-hop propagation;
pg_ripple.pagerank_trickle_confidence_attenuationGUC. - PR-CONF-01: Confidence-weighted edges via
_pg_ripple.confidencejoin;pg_ripple.pagerank_confidence_weightedGUC. - PR-PROB-DATALOG-01: Probabilistic PageRank score bounds via
@weightDatalog rules;score_lower/score_uppercolumns;pg_ripple.pagerank_probabilisticGUC. - PR-TOPIC-01: Topic-sensitive multi-run via
topicparameter andpg_ripple.pagerank_run_topics(topics text[]). - PR-WEIGHT-01: Edge-weight predicate (
edge_weight_predicateparam);pg_ripple.pagerank_confidence_defaultGUC. - PR-REVERSE-01:
directionparameter:'forward'/'reverse'/'undirected'. - PR-EXPLAIN-SCORE-01:
pg_ripple.explain_pagerank(node_iri, top_k)returns depth/contributor/contribution/path tree. - PR-STALE-BOUNDS-01:
stale/stale_sincecolumns;pg_ripple.is_stale()helper;pg_ripple.pagerank_lower()/pg_ripple.pagerank_upper(). - PR-IVM-METRICS-01:
pg_ripple.pagerank_queue_stats()returning(queued_edges, max_delta, oldest_enqueue, estimated_drain_seconds). - PR-SKETCH-01:
pg_ripple.pagerank_selective_thresholdGUC for selective per-node recomputation gating. - PR-PARTITION-01:
pg_ripple.pagerank_partitionGUC; per-named-graph parallel evaluation. - PR-SELECTIVE-01: Selective recomputation of high-centrality nodes only.
- PR-TEMPORAL-01:
decay_rate+temporal_predicateparameters for temporal authority decay. - PR-SHACL-01:
pg_ripple.pagerank_shacl_thresholdGUC;shacl_score()threshold gate;sh:importance/sh:excludeFromRankingawareness. - PR-EXPORT-01:
pg_ripple.export_pagerank(format, top_k, topic)— CSV, Turtle, N-Triples, JSON-LD. - PR-FED-01:
pg_ripple.pagerank_federation_blendGUC; federation blend mode. - PR-FED-CONF-01: Confidence-gated federation edges.
- PR-CENTRALITY-01:
pg_ripple.centrality_run(metric)for betweenness, closeness, degree, Katz;_pg_ripple.centrality_scorestable. - PR-TRUST-EIGEN-01: Source-trust-weighted eigenvector centrality.
- PR-ENTITY-RESOLUTION-01:
pg_ripple.pagerank_find_duplicates(metric, centrality_threshold, fuzzy_threshold)— centrality-guided entity deduplication. - PR-KATZ-TEMPORAL-01: Temporal authority via Katz centrality;
pg_ripple.katz_alphaGUC. - PR-HTTP-01: 10 new HTTP endpoints in
pg_ripple_http(/pagerank/*,/centrality/*);pagerank_handlers.rs. - PR-CI-01:
tests/pg_regress/sql/pagerank.sqlpg_regress test suite (30 tests). - PR-BENCH-01:
benchmarks/pagerank.sql— 10 pgbench scenarios for scale-free graph. - PR-DOCS-01:
docs/src/features/pagerank.md. - PR-EXPLAIN-01:
explain_pagerank()score-explanation tree (tree traversal viaWITH RECURSIVE). - PR-ERR-01: Error constants PT0401–PT0410, PT0411–PT0419, PT0420–PT0423 for PageRank error catalog (ranges: PT040x, PT041x, PT042x).
- PR-MIGRATE-01:
sql/pg_ripple--0.87.0--0.88.0.sqlmigration script; 3 new tables + BRIN index + RLS policies. - 22 new GUC parameters in
src/gucs/pagerank.rs. - 8 new
feature_statusrows (pagerank_datalog,pagerank_incremental,pagerank_confidence_weighted,pagerank_centrality,pagerank_explain,pagerank_export,pagerank_entity_resolution,pagerank_http_api). pg_ripple_httpversion bumped to 0.88.0.
[0.87.0] — 2026-05-XX — Uncertain Knowledge Engine
Implements v0.87.0 roadmap: probabilistic Datalog with @weight annotations, confidence
side table (_pg_ripple.confidence), fuzzy SPARQL extension functions (pg:confidence(),
pg:fuzzy_match(), pg:token_set_ratio(), pg:confPath()), soft SHACL quality scoring
(pg_ripple.shacl_score(), pg_ripple.shacl_report_scored()), confidence-aware bulk load
(pg_ripple.load_triples_with_confidence()), PROV-O confidence propagation, RDF-star Turtle
export with confidence annotations, HTTP companion endpoints (/confidence/*), and garbage
collection (pg_ripple.vacuum_confidence()).
Real-world knowledge graphs rarely deal with certainties — information extracted from text has varying reliability, facts from sensors carry measurement error, and rules that combine multiple pieces of evidence should produce conclusions with appropriately reduced confidence. Version 0.87.0 introduces a complete uncertain knowledge engine for pg_ripple: a confidence side table stores a numerical confidence score for any triple in the graph, probabilistic Datalog rules with @weight annotations propagate uncertainty through inference using noisy-OR semantics, and SPARQL queries can filter by confidence threshold or traverse high-confidence paths using the new pg:confPath() property path operator. The result is a knowledge graph that represents not just what it knows, but how confident it is in each thing it knows.
Soft SHACL quality scoring extends the validation framework into the probabilistic domain: instead of simply passing or failing, shapes can now return weighted quality scores, and a score log tracks how data quality evolves over time — allowing teams to set alerts when quality drops below a threshold. Bulk loading of triples with associated confidence values makes it straightforward to feed scores from external machine learning models, document extractors, or human annotation pipelines directly into the graph. A PROV-O integration propagates confidence through provenance chains, so the reliability of a conclusion can be traced back through the complete derivation to the reliability of its source facts. Eight HTTP endpoints expose the full confidence API to external applications, making probabilistic knowledge graph queries accessible from any programming language.
Added
- PROB-DATALOG-01:
@weight(F)annotation on Datalog rules; noisy-OR confidence propagation via_pg_ripple.confidenceside table. - CONF-TABLE-01:
_pg_ripple.confidence (statement_id, confidence, model, asserted_at)side table;confidence_stmt_idxindex; optionaldict_trgm_idxGIN index whenpg_trgmis installed. - FUZZY-SPARQL-01:
pg:confidence(?s,?p,?o),pg:fuzzy_match(a,b),pg:token_set_ratio(a,b)SPARQL extension functions;pg:confPath(pred, threshold)property path operator. - SOFT-SHACL-01:
pg_ripple.shacl_score(graph_iri),pg_ripple.shacl_report_scored(graph_iri),pg_ripple.log_shacl_score(graph_iri)functions;sh:severityWeightsupport;_pg_ripple.shacl_score_logtable. - LOAD-CONF-01:
pg_ripple.load_triples_with_confidence(data, confidence, format, graph_uri)bulk loader. - CONF-EXPORT-01:
pg_ripple.export_turtle_with_confidence(graph)with RDF-star confidence annotations;pg_ripple.export_confidenceGUC. - PROV-CONF-01:
pg_ripple.prov_confidenceGUC for PROV-Opg:sourceTrustconfidence propagation. - CONF-CWB-01:
pg_ripple.cwb_confidence_propagationGUC; CWB confidence propagation inrun_full_recompute. - CONF-GC-01: Orphaned confidence row cleanup in
delete_triple_by_ids,run_dred_retraction, and HTAPmerge_all. - CONF-HTTP-01: HTTP endpoints
POST /confidence/load,GET /confidence/shacl-score,GET /confidence/shacl-report,POST /confidence/vacuum. - CONF-EXPLAIN-01:
explain_datalog()now includes a"confidence"node with per-rule weights. - CONF-CYCLIC-01:
prob_datalog_cyclic,prob_datalog_max_iterations,prob_datalog_convergence_delta,prob_datalog_cyclic_strictGUCs. - CONF-ERR-01: Error variants PT0301–PT0307 in
UncertainKnowledgeErrorenum. - CONF-RLS-01: Row-level security policies on
_pg_ripple.confidenceand_pg_ripple.shacl_score_log. - CONF-DOCS-01:
docs/src/features/uncertain-knowledge.md; 9 new GUC entries indocs/src/operations/configuration.md. - CONF-PERF-01:
benchmarks/probabilistic_overhead.sqlandbenchmarks/confidence_join_scale.sql. - CONF-SBOM-01:
postgresql-contribadded to Dockerfile runtime layer;audit.tomlpg_trgm note;sbom.jsonregenerated for v0.87.0. - 5 new feature status rows (probabilistic_datalog, fuzzy_sparql, confidence_side_table, soft_shacl_scoring, prov_confidence).
pg_ripple_httpversion bumped to 0.87.0;COMPATIBLE_EXTENSION_MINupdated to 0.87.0.tests/pg_regress/sql/probabilistic.sqlregression test.
[0.86.0] — 2026-05-02 — Assessment 13 Tests, API Polish, Observability, Supply Chain & Standards
Implements v0.86.0 roadmap: closes the remaining 30+ Low-priority and backlog findings from
Assessment 13. All 82 A13 findings are now resolved. Key additions: SSE streaming cursor
(HTTP-02), axum graceful shutdown (O13-05), structured JSON log output (O13-04),
new Prometheus metrics (O13-02, S13-03), Arrow Flight 413 guard before materialisation
(S13-08), CONSTRUCT/SHACL-SPARQL fuzz targets (T13-03), conformance trend CSV artifact
(T13-04), describe_form GUC (SC13-04), unreachable! → pgrx::error! conversions
(Q13-07/CC13-05), POSTGRES_PASSWORD_FILE docker-compose pattern (S13-07), audit.toml
expiry dates (DS13-02/S13-04), Renovate rust-toolchain update config (DS13-04),
error-codes registry (A13-03), deprecated-gucs docs (A13-04), GeoSPARQL function
inventory (SC13-03), compatibility matrix v0.80–v0.86 rows (D13-01), blog post version
index (D13-04), and CDC slot cleanup crash-recovery test (T13-07).
Version 0.86.0 closes the final thirty-plus low-priority and backlog findings from Assessment 13, marking the complete resolution of all 82 findings in that assessment cycle. Among the most operationally significant additions is a Server-Sent Events streaming cursor that allows clients to receive SPARQL query results as a continuous stream rather than waiting for the entire result set to materialize — a key capability for dashboards and monitoring applications that display live graph data. Axum graceful shutdown ensures the HTTP companion completes in-flight requests before exiting on SIGTERM, protecting queries in progress during rolling restarts. Structured JSON log output from the HTTP companion makes it straightforward to feed logs into centralized aggregation platforms like Elasticsearch or Loki without custom parsing.
Security and supply chain hardening receive careful attention throughout. The Arrow Flight endpoint now runs a row count pre-check before materializing results and returns HTTP 413 with a generic message when the export limit is exceeded — importantly, the actual row count is only logged server-side to avoid leaking internal state in error responses visible to clients. The Docker Compose configuration is updated to use the POSTGRES_PASSWORD_FILE Docker secrets pattern, replacing plaintext password environment variables with secrets file injection. Four security advisories in audit.toml gain explicit expiry dates, turning an implicit "we know about this" list into a tracked obligation with documented review deadlines. A complete error code registry and a deprecated-GUCs reference document close two documentation gaps that had been making troubleshooting harder than necessary.
Tests (T13-02 – T13-07)
- T13-03 — added
fuzz/fuzz_targets/construct_rule.rsandfuzz/fuzz_targets/shacl_sparql.rs; registered infuzz/Cargo.toml; wired into weekly fuzz CI job. - T13-04 — added CI artifact
tests/conformance/history.csvtracking per-version pass rates across all five conformance suites; addeddocs/src/reference/conformance-trends.mdpage. - T13-05 —
#[pg_extern]coverage gap re-audited; gap confirmed closed by v0.85.0 REG-TESTS-01. - T13-06 —
scripts/bench_check_regression.py --fail-on-regression 10confirmed in benchmark workflow; gate active. - T13-07 — added
tests/crash_recovery/cdc_slot_cleanup_during_kill.sh; creates a slot, simulates SIGKILL mid-cleanup, asserts slot is reclaimed on restart.
API Polish (A13-01 – A13-06)
- A13-01 —
json_ld_loadalias doc comment updated to note-- removal scheduled for v1.0.0; deprecation warning already present since v0.83.0. - A13-03 — created
docs/src/reference/error-codes.mdlisting every PT code with meaning and source file. - A13-04 — created
docs/src/reference/deprecated-gucs.mdlisting deprecated GUCs with replacement names and removal versions. - A13-06 — SPARQL parse errors consistently return
PT400error code across HTTP companion and extension.
Documentation (D13-01 – D13-05)
- D13-01 —
docs/src/operations/compatibility.mdupdated with v0.80–v0.86 rows. - D13-04 —
blog/README.mdupdated with a "Posts by Version" index. - D13-05 —
plans/probabilistic-features.mdlinked from ROADMAP.md v0.87.0 section header.
Supply Chain (DS13-02 – DS13-04)
- DS13-01 (triage) — Dependency upgrade triage decisions documented:
ureqstays at 2.x: ureq 3.x has breaking API changes (AgentBuilderremoved, allsend_*call sites affected across federation code); upgrade deferred to v0.87.0+ after API migration.parquetstays at 58.x /arrowstays at 55.x: arrow 56.x not yet available on crates.io as of 2026-05-02; will upgrade when available.tokio-streamis now justified by the SSE streaming implementation inpg_ripple_http/src/stream.rs(HTTP-02); previously it was a potential removal candidate.
- DS13-02/S13-04 —
audit.tomlexpiry dates added to all four RUSTSEC ignores; structured ignore objects replace plain strings. - DS13-04 —
renovate.jsonupdated withmatchFileNames: ["rust-toolchain.toml"]rule for automatic toolchain update proposals (manual merge required).
Observability (O13-02 – O13-05)
- O13-02 — added Prometheus metrics:
pg_ripple_federation_endpoint_requests_total,pg_ripple_federation_endpoint_duration_seconds,pg_ripple_dictionary_cache_hit_ratio,pg_ripple_merge_worker_delta_rows_pending. - O13-04 —
pg_ripple_httprespectsRUST_LOG_FORMAT=jsonenv var to switchtracing-subscriberto JSON layer for structured log output. - O13-05 — added
axum::serve(...).with_graceful_shutdown(shutdown_signal())for 30-second SIGTERM drain window.
Security (S13-03, S13-06, S13-07 – S13-10)
- S13-03 — added
pg_ripple_http_cors_permissive_requests_totalPrometheus counter; incremented whenPG_RIPPLE_HTTP_CORS_ORIGINS=*is active; documented indocs/src/operations/security.md. - S13-07 —
docker-compose.ymlupdated to usePOSTGRES_PASSWORD_FILEDocker secrets pattern; secrets directory gitignored. - S13-08 — Arrow Flight endpoint runs a
COUNT(*)pre-check before materialising results; returns HTTP 413 with a generic message (no row count) ifARROW_MAX_EXPORT_ROWSexceeded; actual count logged server-side only. - S13-09 —
pg_ripple_http/README.mdtop-level note warns operators to network-isolate the metrics endpoint. - S13-10 —
docs/src/operations/security.mddocuments supported auth schemes (Bearer only; Basic not accepted).
Standards Conformance (SC13-03, SC13-04)
- SC13-03 — created
docs/src/reference/geosparql-functions.mdwith status table for all ~30 GeoSPARQL 1.1 functions. - SC13-04 — added
pg_ripple.describe_formGUC (values:cbd,scbd,symmetric;symmetricis an alias forscbd); supersedespg_ripple.describe_strategywhen set.
HTTP Companion (HTTP-02, DS13-05)
- HTTP-02 —
pg_ripple_http/src/stream.rsimplemented with SSE streaming SELECT cursor (stream_sparql_select()); justifiestokio-streamdependency. - DS13-05 —
tokio-streamdependency is now fully justified by the SSE implementation usingReceiverStream; the previous "remove if no streaming" triage decision is closed.
Code Quality (Q13-07)
- Q13-07/CC13-05 — all 9
unreachable!calls in production code converted topgrx::error!("internal: <description> — please report")at:src/datalog/explain.rs:115,src/sparql/federation/circuit.rs:157,src/views.rs:839,869,src/construct_rules/mod.rs:239,257,src/construct_rules/delta.rs:111,138,src/replication.rs:78.
[0.85.0] — 2026-07-17 — Assessment 13 Medium Findings
Implements v0.85.0 roadmap: all 22 medium-priority findings from Assessment 13
(correctness, performance, code quality, and concurrency). Key additions:
batch_decode respects strict_dictionary GUC, schema.rs and federation.rs
module splits, CI 1,800-line lint gate, describe_cbd depth GUC, per-predicate
merge fence lock, encode_batch single-CTE API, dictionary hot-cache Prometheus
counters, and VP-promotion crash-recovery regression test.
Data correctness in a knowledge graph system depends on precise handling of edge cases that can seem minor until they cause subtle bugs in production. Version 0.85.0 addresses all twenty-two medium-severity findings from Assessment 13. The dictionary's batch_decode() function is corrected to properly handle negative IDs — which represent inline-encoded integers — that were being incorrectly treated as missing values and triggering unnecessary warnings. A new strict_dictionary GUC mode makes missing dictionary IDs raise a proper PostgreSQL error rather than silently returning empty strings, giving teams a choice between strict correctness guarantees and graceful degradation for legacy compatibility. Typed literals are now correctly routed through the typed literal encoder in the Datalog magic sets compiler, fixing incorrect encoding of values like "42"^^xsd:integer that would silently produce wrong inference results.
Performance improvements are grounded in measured optimization. A new encode_batch() internal API reduces the number of database round-trips when encoding multiple terms simultaneously from O(n) to O(1) by using a single CTE INSERT for all cache-miss terms — providing a meaningful throughput improvement for bulk operations. Per-predicate merge fence locks replace the previous global lock, eliminating contention between concurrent merge workers operating on different predicates and allowing multiple predicates to merge simultaneously. A VP-promotion crash-recovery regression test verifies that an interrupted promotion leaves the system in a recoverable state, providing confidence that the self-healing recovery function works correctly before it is needed in production. A CI file size gate enforces the 1,800-line per-file limit going forward, preventing architectural debt from silently accumulating.
Correctness
- C13-02 —
batch_decodenow raises a PostgreSQL error (PT512) when a dictionary ID is missing andpg_ripple.strict_dictionary = on. Previously returned a silent empty string. Graceful-degradationWARNINGpath retained forstrict_dictionary = off. - C13-03 — Blank-node-in-quoted-triple limitation documented in
docs/src/reference/sparql-compliance.md. Regression test added intests/pg_regress/sql/v085_features.sql. - C13-04 —
execute_dropandexecute_clearinsrc/sparql/execute.rsannotated with doc comments documenting the mutation journal flush obligation. - C13-05 — Plan cache key for
INFERENCE_MODEnow trimmed and lowercased before hashing, preventing spurious cache misses from capitalisation or padding differences. - C13-06 —
GRAPH ?gdefault-graph exclusion behaviour documented indocs/src/reference/sparql-compliance.md. Regression test verifies?gbinds only named graphs (SPARQL 1.1 §8.3). - C13-07 —
batch_decodewarning guard tightened fromid <= 0toid == 0. Negative IDs (inline-encoded integers) are now correctly passed through. - C13-08 —
encode_tokeninsrc/datalog/magic.rsnow detects typed literals (^^<suffix) and routes toencode_typed_literal()instead of plain string encoding. - C13-09 —
parse_nt_tripleinsrc/lib.rsnow rejects IRIs longer than 4 KiB (emits aWARNINGand returnsNone) and requires the IRI to end with>. - C13-10 —
xsd:dateTimesub-millisecond precision truncation documented indocs/src/reference/sparql-compliance.md. Regression test added. - C13-11 —
describe_cbdrecursion depth capped by new GUCpg_ripple.describe_max_depth(default 16, range 1–256). Prevents runaway recursion on cyclic or deep graphs.
Performance
- P13-02 — New
encode_batch(terms: &[(&str, i16)]) → Vec<i64>internal API insrc/dictionary/mod.rs. Uses a single CTE INSERT for all cache-miss terms. Exposed viapg_ripple.batch_encode_terms(TEXT[], SMALLINT[]) → BIGINT[]. - P13-03 — Merge-worker heartbeat log already throttled to once per 60 seconds (delivered in v0.83.0); confirmed as done. See
src/merge_worker.rsthrottle guard androadmap/v0.83.0.md. - P13-04 —
execute_select()insrc/sparql/execute.rsbatches allSET LOCALcalls into a single SPI round-trip. - P13-05 — Datalog inference in
src/datalog/seminaive.rsstreams rule SQL in batches of 100, reducing peak SPI call count for large rule sets. - P13-06 —
partition_into_parallel_groups()insrc/datalog/parallel.rspre-checks for directed cycles before union-find SCC evaluation; logs a warning on cycle detection. - P13-07 —
PathCtx.counterfield made private;next_alias()mutation method andvalue()accessor added tosrc/sparql/property_path.rs. - P13-08 —
dictionary_hot_cache_hits_totalanddictionary_hot_cache_misses_totalPrometheus counters added. Exposed in-database viapg_ripple.dictionary_cache_stats()and in the HTTP/metricsPrometheus endpoint. The legacy shared-memory cache statistics function (previously also nameddictionary_cache_stats) is now exposed aspg_ripple.shmem_cache_stats()to avoid a naming conflict; it continues to return the same four-column table (hits, misses, evictions, hit_rate) as introduced in v0.47.0.
Code Quality
- Q13-02 —
src/schema.rs(1,939 lines) split intosrc/schema/{tables,views,triggers,rls}.rs. - Q13-03 —
src/sparql/federation.rs(1,693 lines) split intosrc/sparql/federation/{circuit,policy,http,decode}.rs. - Q13-04 — CI lint gate (
lint-file-sizejob in.github/workflows/ci.yml): anysrc/**/*.rsfile exceeding 1,800 lines fails the build unless it contains an// @allow-large-file: <reason>annotation. - Q13-05 — All
#[allow(dead_code)]markers audited. Each now carries a// Q13-05comment explaining the justification (BGW indirection, public API surface, etc.).
Concurrency
- CC13-01 — New VP-promotion crash-recovery regression test
tests/crash_recovery/promote_sigkill.sh. SIGKILLs a backend during rare-predicate promotion and assertsrecover_interrupted_promotions()returns a consistent state. - CC13-02 — Merge fence advisory lock namespaced per-predicate (
predicate_id + 0x5052_5000). Eliminates global lock contention between concurrent merge workers on different predicates.
[0.84.0] — 2026-07-16 — Assessment 13 Critical/High & Operational Remediation
Implements v0.84.0 roadmap: 13 items addressing all Critical and High findings from Assessment 13. Key additions: HTTP companion version sync (6-version drift closed), PG_RIPPLE_HTTP_STRICT_COMPAT env var, docker-compose image tag CI gate, SECURITY DEFINER inline annotations, migration-chain v0.80–v0.83 test coverage, gucs/registration.rs 6-domain split, nested OPTIONAL+EXISTS regression test, /health/ready deep-check endpoint, plan-cache double-parse elimination, and justfile automation recipes.
Version 0.84.0 closes all thirteen critical and high findings from Assessment 13, with the most significant being a six-version lag in the HTTP companion's compatibility tracking that had been recurring across multiple assessment cycles. The companion's COMPATIBLE_EXTENSION_MIN is raised to the current value, and a new PG_RIPPLE_HTTP_STRICT_COMPAT environment variable allows operators to configure the companion to exit with an error rather than just log a warning when connected to an incompatible extension version — providing a hard guarantee for production environments where silent incompatibility could cause subtle failures. A new /health/ready deep-check endpoint performs a real PostgreSQL round-trip to verify both database connectivity and extension installation, giving orchestration platforms accurate readiness information.
Code quality improvements address two security-sensitive areas. The src/gucs/registration.rs file, at over 2,000 lines, is decomposed into six domain-specific submodules covering SPARQL, storage, federation, Datalog, security, and observability GUCs — making the GUC catalog far easier to navigate, audit, and extend. A plan-cache double-parse bug is fixed by threading the canonical SPARQL form through the parse and cache layers so each query string is parsed only once rather than twice on cache misses, improving performance for uncached queries. The justfile gains four automation recipes for version bumping, SBOM regeneration, OpenAPI spec generation, and version sync verification — eliminating manual multi-file updates that were a frequent source of version string inconsistencies across the codebase.
HTTP Companion (pg_ripple_http)
- HTTP-01 / MF-B —
pg_ripple_httpbumped to0.84.0.COMPATIBLE_EXTENSION_MINraised from"0.79.0"to"0.84.0"inpg_ripple_http/src/main.rs. - S13-05 — New
PG_RIPPLE_HTTP_STRICT_COMPAT=1environment variable. When set, an extension-version mismatch (belowCOMPATIBLE_EXTENSION_MIN) causes the service to exit with code 1 instead of only logging a warning. Default: off (backward-compatible). - O13-01 — New
/health/readyHTTP endpoint performs a real PostgreSQL round-trip (SELECT 1 FROM pg_extension WHERE extname='pg_ripple') with a hard 2-second deadline. Returns200 {"status":"ok"}or503 {"status":"unavailable","reason":"..."}./healthremains a fast liveness probe;/readyremains the deep feature-status probe.
Security
- S13-01 — Both
SECURITY DEFINERoccurrences (src/schema.rs:996andsql/pg_ripple--0.55.0--0.56.0.sql:60) annotated with-- SECURITY-JUSTIFY:inline comments explaining the privilege requirement.scripts/check_no_security_definer.shupdated to require the marker on any SECURITY DEFINER line. - S13-02 —
scripts/check_no_string_format_in_sql.shconfirmed as a required CI step in.github/workflows/ci.yml(SQL-injection gate).
Build & Tooling
- BUILD-01 —
docker-compose.ymlimage tags updated from0.54.0to0.84.0. Newlint-docker-compose-versionCI job asserts the image tag matchesCargo.tomlversion on every PR. - BUILD-02 —
justfilegains four new automation recipes:bump-version NEW_VERSION— atomically updates Cargo.toml (root + pg_ripple_http), pg_ripple.control, COMPATIBLE_EXTENSION_MIN, docker-compose tag, creates migration script stubregen-sbom— regeneratessbom.jsonviacargo cyclonedxregen-openapi— fetches the live OpenAPI spec from the running HTTP servicecheck-version-sync— asserts all version strings are consistent
- BUILD-03 — Migration-chain test confirmed as a required CI step.
Testing
- T13-01 —
tests/test_migration_chain.shextended with checkpoint assertions for v0.80.0–v0.83.0 (21 migration scripts total). Checks:predicates.triple_countcolumn (v0.80),_pg_ripple.cdc_lsn_watermarktable (v0.81), merge-worker and federation stats tables (v0.82), core table column integrity (v0.83). - C13-01 — New pg_regress test
tests/pg_regress/sql/sparql_optional_exists.sqlcovering nestedOPTIONAL { ... FILTER(EXISTS { ... }) }andFILTER NOT EXISTSsemantics.
Performance
- P13-01 —
src/sparql/plan_cache.rs: newget_canonical(canonical: &str)andput_canonical(canonical: &str, entry)functions accept thespargebra::QueryDisplay form.src/sparql/plan.rsupdated to parse once and pass the canonical form through, eliminating the double-parse on every cache-miss path.
Code Quality
- Q13-01 —
src/gucs/registration.rs(2,032 lines) split into 6 per-domain submodules undersrc/gucs/registration/:sparql.rs,storage.rs,federation.rs,datalog.rs,security.rs,observability.rs. Public re-exports frommod.rsunchanged; callers unaffected.
Process
- PROMPT-01 —
plans/overall_assesment_prompt.mdtemplate created. Anchors automated assessments to the latest tagged release, preventing prompt-vs-reality gaps like the one identified in Assessment 13. - V084-01 —
ROADMAP.mdscope decision recorded: uncertain knowledge engine (probabilistic Datalog, fuzzy SPARQL, soft SHACL) moved to v0.87.0; v0.84.0–v0.86.0 reserved for Assessment 13 remediation.
[0.83.0] — 2026-07-09 — Assessment 12 Test Coverage, API Polish & Code Quality
Implements v0.83.0 roadmap: 25 items across test coverage, API polish, code quality, and security hardening. Key additions: N-Triples/N-Quads/TriG fuzz targets, proptest reference-implementation comparison (oxigraph), CDC LISTEN/NOTIFY barrier integration test, bidi module split, blank node export validation, load_jsonld alias, datalog cost-model GUCs, merge worker exponential backoff, RFC 3339 build timestamp in /health, JSON 401 error envelope, WWW-Authenticate header, and CHANGELOG/GUC naming conventions.
A knowledge graph system's correctness guarantees are only as strong as its ability to detect regressions, and version 0.83.0 significantly expands that ability. Three new fuzz targets exercise the N-Triples, N-Quads, and TriG format parsers with arbitrary byte sequences, hardening the parsers against malformed input that could cause unexpected behavior. A new proptest suite compares pg_ripple's N-Triples parsing results against oxigraph as an independent reference implementation, catching any case where the two implementations disagree on what constitutes valid syntax. A CDC LISTEN/NOTIFY integration test demonstrates the correct event-driven pattern for waiting on CDC events without using sleep(), which is prone to timing-related flakiness in CI.
Two API improvements in this release have lasting impact. The load_jsonld() function is introduced as the new canonical name for JSON-LD loading, with the old json_ld_load() emitting a deprecation notice and scheduled for removal at v1.0.0, giving teams a clear migration path with a long runway. The src/bidi.rs module, at over 2,500 lines, is decomposed into five focused sub-modules — protocol, relay, subscribe, and sync — the largest single-file split in the project's history. The HTTP companion gains proper 401 JSON error responses with WWW-Authenticate headers consistent with RFC 7235, matching the JSON error envelope used throughout the REST API. Merge worker exponential backoff replaces a flat retry interval, making the worker recover more gracefully from transient errors without flooding logs during extended incidents.
Test Coverage
- FUZZ-BULK-01 — Three new fuzz targets:
ntriples_load,nquads_load,trig_loadinfuzz/fuzz_targets/. Registered infuzz/Cargo.tomland CI fuzz workflow. - FUZZ-UPDATE-01 — SPARQL Update fuzz target (
fuzz/fuzz_targets/sparql_update.rs) confirmed present from v0.79.0; corpus seeded fromtests/sparql/UPDATE files. - PROPTEST-02 — New proptest suite
tests/proptest/ntriples_oxigraph.rscompares rio_turtle triple count against oxigraph as a reference implementation for randomly generated N-Triples documents.oxigraphadded as a dev-dependency. ci/test: tests/proptest/ntriples_oxigraph.rs - CDC-ASYNC-01 — New integration test
tests/integration/cdc_notify_barrier.shdemonstrates LISTEN/NOTIFY barrier pattern (nosleep()) for CDC subscription validation. - KFAIL-DOC-01 — Every entry in
tests/w3c/known_failures.txtandtests/conformance/known_failures.txtnow has a# Reason:and# Issue:comment explaining the failure. - REG-TESTS-01 — Regression tests added in
tests/pg_regress/sql/v083_features.sqlfor 13 previously untested pg_extern functions:export_ntriples,export_nquads,load_jsonld,bidi_wire_version,refresh_stats_cache,bidi_health, and GUC default assertions. - ERRPATH-01 — Eight error-path regression tests added in
tests/pg_regress/sql/error_paths.sql: dictionary overflow guard, HTAP merge during DROP, SubXact abort, federation timeout, Arrow export row limit, SPARQL depth limit, tenant-name validation, CDC slot exhaustion. - DATALOG-MAXITER-TEST-01 — Regression test
tests/pg_regress/sql/datalog_maxiter.sqlexercises the seminaive max-iteration guard (10,000 iterations) and asserts termination.
API
- API-RENAME-01 — New SQL function
pg_ripple.load_jsonld(url TEXT, graph_uri TEXT DEFAULT NULL)added as preferred alias.json_ld_load()emits aNOTICEdeprecation warning; removal scheduled for v1.0.0. - API-GRAPH-COL-01 —
pg_ripple.find_triples()RETURNS TABLE confirmed to includeg BIGINT(named-graph column); no schema changes required.
Code Quality
- MOD-BIDI-01 —
src/bidi.rs(2,516 lines) split into five focused modules:src/bidi/mod.rs,src/bidi/protocol.rs,src/bidi/relay.rs,src/bidi/subscribe.rs,src/bidi/sync.rs. Public API re-exported frommod.rswith no signature changes. - GUC-NAME-01 — GUC naming convention (
pg_ripple.noun_verb_unitsnake_case) documented inCONTRIBUTING.md. Deprecation notices added for 4 non-conforming GUCs. - CHANGELOG-BREAK-01 —
**BREAKING:**tag convention adopted inCHANGELOG.mdfor incompatible API/GUC changes. Back-annotated in affected v0.73.0–v0.79.0 entries. - CHANGELOG-FMT-01 — CI lint job
lint-changelogadded to.github/workflows/ci.yml; validates## [vX.Y.Z]heading format and**BREAKING:**tag usage. - DEPAUDIT-01 —
serde_cbor(unmaintained) confirmed absent fromCargo.tomlsince v0.64.0 when the Arrow IPC path was migrated toparquet. No replacement needed. - RENOVATE-01 —
renovate.jsonadded: groups pgrx/rdf-parsing deps, pins pgrx to exact versions, auto-merges patch updates for utility crates on a weekly schedule. - P-05-EVAL —
plans/p05_shared_dict_eval.md: shared-memory dictionary LRU cache evaluated and closed as "not worth it" for v0.83.0 (modelled ≤10% throughput gain vs. significant complexity). Per-backend LRU retained; revisit criteria documented.
Performance
- DL-COST-GUC-01 — New GUCs
pg_ripple.datalog_cost_bound_s_divisor(default 100) andpg_ripple.datalog_cost_bound_so_divisor(default 10) replace hardcoded selectivity divisors insrc/datalog/compiler.rscost-based rule reordering. - MERGE-BACKOFF-01 — Merge worker now uses exponential backoff (1 s × 2ⁿ) capped at
pg_ripple.merge_max_backoff_secs(default 60) instead of flatmerge_interval_secswait on every error.
Security / pg_ripple_http
- BUILD-TIME-FIELD-01 —
/healthJSON responsebuild_timefield now contains an RFC 3339 build timestamp (fromSOURCE_DATE_EPOCHenv var or current build time), replacing the Cargo version string. - HTTP-401-WWW-AUTH-01 —
check_auth()inpg_ripple_http/src/common.rsnow emitsWWW-Authenticate: Bearer realm="pg_ripple"on all 401 responses (RFC 7235 §4.1). - AUTH-RESP-FMT-01 —
check_auth()failure response changed from plain-text"unauthorized"to JSON{"error": "PT401", "message": "unauthorized"}, consistent with all other error envelopes. - METRICS-AUTH-DOC-01 —
# SECURITY: intentionally publiccomment added at/metricsand/metrics/extensionroute registration inpg_ripple_http; operations guide updated. docs/src/operations/monitoring.md - EXPORT-BNODE-VALID-01 —
src/export.rsvalidates blank node labels against the N-Triples BNodeLabel production before emitting;_prefixed and empty labels are rejected.
[0.82.0] — 2026-06-03 — Assessment 12 Performance & Observability
Implements v0.82.0 roadmap: 30 performance, observability, and security
hardening items from Assessment 12. Key additions: configurable plan-cache
capacity GUC, ANY($1::bigint[]) batch decode, two-phase merge with tunable
lock timeout, merge worker heartbeat, enriched sparql_explain() with algebra
tree, structured Prometheus labels, sparql_normalise() function, federation
response Content-Length pre-check, and SPARQL depth DoS protection.
Performance
- CACHE-CAP-01 —
pg_ripple.plan_cache_capacityGUC (default 1024, range 64–65536) replaces hardcoded constant inplan_cache.rs. - DECODE-BIND-01 —
batch_decode()migrated fromIN (id1, id2, …)toWHERE id = ANY($1::bigint[])bind parameter, preventing plan proliferation. - MERGE-PRED-01 — Merge worker caches predicate IDs with 60-second TTL; SIGHUP invalidates the cache. Eliminates repeated
_pg_ripple.predicatesscans per merge cycle. - MERGE-LOCK-GUC-01 — Hardcoded
lock_timeout = '5s'replaced bypg_ripple.merge_lock_timeout_msGUC (default 5000, range 100–60000 ms). - PROPPATH-UNBOUNDED-01 —
pg_ripple.all_nodes_predicate_limitGUC (default 500) caps wildcard property-path UNION ALL branches to prevent parser stack overflow on large schemas. - VACUUM-DICT-BATCH-01 —
vacuum_dictionary()now batches UNION ALL construction into groups ofpg_ripple.vacuum_dict_batch_sizepredicates (default 200). - GUC-BOUNDS-01 — Explicit min/max validators added to
vp_promotion_threshold(min 100),dictionary_cache_size(min 1024, max 1 GiB), and newpg_ripple.merge_batch_sizeGUC (min 100, max 100,000,000).
Observability
- EXPLAIN-ALG-01 —
sparql_explain()now includes a-- SPARQL Algebra --section showing the parsed algebra tree (viaspargebra::Display). - MERGE-HBEAT-01 — Merge background worker emits a LOG-level heartbeat every
pg_ripple.merge_heartbeat_interval_secondsseconds (default 60) and writes to the new_pg_ripple.merge_worker_statustable. - STATS-DOC-01 —
pg_ripple.stats_scan_limitGUC (default 1000) caps the number of VP tables scanned pergraph_stats()call; documented in administration reference. - PGSS-NORM-01 — New
pg_ripple.sparql_normalise(TEXT) RETURNS TEXTfunction replaces string/IRI/numeric literals with$S/$I/$Nplaceholders forpg_stat_statementsgrouping. - STATS-CACHE-01 — New
_pg_ripple.predicate_stats_cachetable andpg_ripple.refresh_stats_cache()function materialise per-predicate triple counts; background refresh everypg_ripple.stats_refresh_interval_secondsseconds. - FED-COST-01 — New
_pg_ripple.federation_statstable accumulates call latency (P50/P95 approximation), error counts, and row estimates per federation endpoint; updated after every HTTP call. - ADMIN-LOCK-01 — Lock levels documented for
vacuum(),reindex(), andvacuum_dictionary()in the SQL reference.
Security
- TENANT-NAME-01 — Tenant name validation regex tightened to
^[A-Za-z0-9_]{1,63}$; uppercase letters now allowed; max 63 characters enforced. - ROLE-UNICODE-01 —
quote_ident_safe()now falls back to SPISELECT quote_ident($1)for role names containing non-ASCII characters. - SHMEM-SAFE-01 — Shared-memory size arithmetic uses
checked_mul().expect()to detect overflow early (misconfigured GUC rather than silent wraparound). - RUSTSEC-01 —
audit.tomlupdated:RUSTSEC-2023-0071(RSA PKCS#1 timing) added as an exemption with justification comment; review date updated to v0.82.0. Cargo-audit CI gate (.github/workflows/ci.yml) passes. - SPARQL-COMPLEX-01 —
pg_ripple.sparql_max_algebra_depthGUC (default 256) already enforced; confirmed and documented. - LISTEN-LEN-01 —
/subscribe/{subscription_id}endpoint inpg_ripple_httpnow returns HTTP 400 for subscription IDs longer than 63 characters. - FED-BODY-STREAM-01 / FED-SIZE-01 — All five
response.into_string()call sites infederation.rsnow check theContent-Lengthheader before allocating the body buffer. - REDACT-01 — Remaining raw error exposure in
rag_handler.rsreplaced withredacted_error(); confirmed uniform coverage across all 82 handler error paths.
Rust / Extension
- DATALOG-SILENT-01 — 29
let _ = Spi::run_with_args()calls inwfs.rsandseminaive.rsreplaced with.unwrap_or_else(|e| pgrx::log!("...: {e}")). - DECODE-WARN-01 —
batch_decode()now emits aWARNINGfor any ID present in query results but absent from the dictionary. - EMBED-MODEL-01 — All embedding paths confirmed to read
pg_ripple.embedding_modelGUC. - FED-COUNTER-ORDER-01 —
FED_CALL_COUNTincremented only after the endpoint policy check passes. - EXPORT-JSONLD-OOM-01 —
export_jsonld()emits aWARNINGwhen buffering more than 1,000,000 triples; recommends the streaming cursor variant.
pg_ripple_http companion
- ARROW-LIMIT-01 — Arrow Flight export enforces
ARROW_MAX_EXPORT_ROWSenv var (default 10,000,000); HTTP 400 returned when the limit is exceeded. - METRICS-LABELS-01 — Prometheus
/metricsendpoint now includesquery_type(SELECT/ASK/CONSTRUCT/DESCRIBE/UPDATE) andresult_size_bucket(empty/small/medium/large) label dimensions.
[0.81.0] — 2026-05-14 — Correctness & Stability Hardening
Implements v0.81.0 roadmap: 34 correctness, stability, and security
hardening items. No breaking schema changes; one new internal table
(_pg_ripple.cdc_lsn_watermark) and one new public function
(pg_ripple.recover_stuck_promotions()).
Version 0.81.0 is a deep correctness and stability release addressing thirty-four issues spanning the full system stack. Several correctness bugs that had been present since early versions are fixed: the HTAP merge was using non-deterministic SID selection due to a missing ORDER BY before DISTINCT ON, DRed retraction was performing only a single seed pass instead of a full semi-naive fixpoint (causing incomplete re-derivation after retraction), and blank-node variable names are now prefixed with a query-scoped hex nonce to prevent aliasing across subqueries. The OPTIONAL-to-INNER-JOIN optimization is extended from single-predicate patterns to multi-predicate basic graph patterns, improving query performance for a structural pattern that appears frequently in real-world SPARQL queries.
Infrastructure reliability receives equal attention. A new recover_stuck_promotions() function detects and repairs VP promotions that were abandoned mid-flight without a server restart, making the promotion mechanism self-healing without operator intervention. A CDC slot cleanup background worker automatically drops orphaned replication slots that have been idle beyond a configurable threshold, preventing slot accumulation from blocking WAL cleanup and causing disk space exhaustion in production. Dictionary race conditions are addressed: a hash collision now raises a clean PostgreSQL error instead of panicking, and sub-transaction aborts correctly invalidate the LRU caches to prevent stale entries from causing incorrect decode results for subsequent queries. The plan cache key is extended to include nine additional GUC parameters, ensuring that changing any relevant session setting mid-session correctly evicts stale plans rather than returning results computed under different settings.
Correctness
- MERGE-SID-01 —
ORDER BY i ASCadded beforeDISTINCT ONin HTAP merge CTE template (tests/pg_regress/sql/htap_merge.sql), fixing non-deterministic SID selection during merge. - DRED-FIXPOINT-01 — DRed re-derive phase now runs a full semi-naïve fixpoint instead of a single seed pass, correcting incomplete re-derivation after retraction.
- DL-AGG-01 — Guard added in
stratify()to reject aggregation functions in recursive Datalog rule heads (tests/pg_regress/sql/datalog_agg.sql), with a descriptive error (PT511). - DL-PAR-01 — Intra-stratum cycle detection added to the parallel group partition step (tests/pg_regress/sql/datalog_parallel.sql), preventing non-terminating stratum evaluation.
- DL-PAR-02 — Parallel Datalog SCC scheduling now uses topological order (Kahn's BFS) instead of stratum order, ensuring producers run before consumers.
- OPT-INNER-01 — OPTIONAL→INNER JOIN optimisation extended to multi-predicate BGPs (previously only applied to single-predicate BGPs).
- BN-SCOPE-01 — Blank-node variable names are now prefixed with a query-scoped hex nonce to prevent aliasing across subqueries.
- RETRACT-PARAM-01 — Flat-VP
DELETEinsrc/construct_rules/retract.rsparameterised with$1–$4bind variables (previously used string interpolation). - SCHEDULER-ERR-01 — Topological sort in
construct_rules/scheduler.rsnow propagates errors viaResultinstead of callingpgrx::error!(), giving callers cleaner error handling. - DICT-RACE-01 —
encode_inner()now raises a PostgreSQL error (PT501) on 0-row RETURNING (hash collision or concurrent dict truncation) instead of panicking. - DICT-SUBXACT-01 — A
SubXactCallbackregistered in_PG_initnow invalidates both the decode and encode LRU caches on subtransaction abort.
Security
- RAG-SQL-INJECT-02 —
rag_retrieve()inpg_ripple_httpmigrated fromformat!()with manual quote-escaping to fully parameterised tokio-postgres$1–$5query parameters. - FED-URL-01 — Federation endpoint URLs normalised to lowercase scheme+host before allowlist comparison, preventing case-bypass attacks.
- FILTER-STRICT-01 — New
pg_ripple.strict_sparql_filtersGUC; when enabled, unknown SPARQL built-in function names raise error PT422 instead of evaluating toUNDEF.
Stability
- SHACL-TXN-01 — SHACL shape-store write wrapped in a savepoint so a constraint failure rolls back only the failing shape rather than the entire transaction.
- FED-TRUNC-01 — Federation JSON results exceeding
federation_result_max_bytesnow emit a WARNING and partially materialise instead of raising a fatal error. - FED-CACHE-01 — Federation query cache key normalised to canonical SPARQL
form via
spargebra::Display, preventing spurious cache misses. - MERGE-FENCE-01 — HTAP merge advisory lock acquisition moved to just before the rename-swap phase (Phase 2), reducing the ExclusiveLock hold time from minutes to milliseconds.
- PROMO-LOCK-01 — Per-predicate
pg_advisory_xact_lock(pred_id)confirmed as the exclusive coordination mechanism for VP promotion (no table-level lock). - PROMO-ATOMIC-01 —
predicatescatalog status update is part of the atomic CTE that inserts the new VP table, eliminating the TOCTOU window. - PROMO-STUCK-01 — New
pg_ripple.recover_stuck_promotions()SQL function detects and re-runs VP promotions abandoned mid-flight (without a server restart). - CDC-SLOT-01 — New background worker (
pg_ripple_cdc_slot_cleanup_main) drops orphaned CDC replication slots idle longer thanpg_ripple.cdc_slot_idle_timeout_seconds. - CDC-LSN-01 — New
_pg_ripple.cdc_lsn_watermark(slot_name, last_lsn)table tracks CDC replication progress; updated after each batch commit. - DICT-STRICT-01 — New
pg_ripple.strict_dictionaryGUC; when enabled,decode()raises a PostgreSQL error for unrecognised IDs. - PLAN-CACHE-GUC-02 — Plan-cache key extended to include
NORMALIZE_IRIS,WCOJ_ENABLED,WCOJ_MIN_TABLES,TOPN_PUSHDOWN,SPARQL_MAX_ROWS,SPARQL_OVERFLOW_ACTION,FEDERATION_TIMEOUT,PGVECTOR_ENABLED, andINFERENCE_MODE. Changing any of these GUCs mid-session now invalidates the per-backend plan cache. - PRELOAD-WARN-01 —
_PG_initemits a WARNING when the extension is loaded viaCREATE EXTENSIONwithoutshared_preload_libraries, preventing silent misconfiguration. - PGFINI-01 —
_PG_finiadded (roadmap/v0.81.0.md) to unregister SubXact callback, ExecutorEnd hook, and transaction callback when the extension library is unloaded. - REPL-UNWRAP-01 — All
.unwrap()calls insrc/replication.rsreplaced withunwrap_or_else(...)orpgrx::error!()to avoid Rust panics on SPI errors. - FEATURE-STATUS-BIDI-01 — 12 missing rows for BIDI (v0.77.0) and BIDIOPS
(v0.78.0) features added to
feature_status().
[0.80.0] — 2026-05-07 — Assessment 12 Critical/High Remediation
Implements v0.80.0 roadmap: addresses all 13 critical and high findings from Security Assessment 12. No new SQL schema changes; all fixes are in the Rust implementation and companion HTTP service.
Security in depth means that every layer of the system independently validates and protects against threats, without depending on other layers to catch what it misses. Version 0.80.0 closes all thirteen critical and high security findings from Assessment 12. Five SQL injection vulnerabilities in the views catalog insertion functions are fixed by migrating from format!() string interpolation with manual quote escaping to fully parameterized Spi::run_with_args() calls with typed bind parameters — a change that makes injection impossible by construction rather than by careful escaping. A plan cache cross-user leakage vulnerability is closed by including the current PostgreSQL role OID in the plan cache key, preventing cached query plans from being shared across users with different privilege levels.
The SPARQL Update mutation journal is wired to flush at the end of every update statement, ensuring that CONSTRUCT writeback rules fire correctly for every mutation rather than only when the dictionary API is used. IPv6 Unique Local addresses are added to the SSRF blocklist, closing a gap that could have allowed federation requests to reach internal network services via IPv6 even when the IPv4 blocklist was complete. The /explorer admin interface now requires authentication, preventing unauthenticated access to the interactive graph explorer in deployments where the HTTP companion is reachable from less-trusted networks. All HTTP error responses from the companion are standardized to return JSON with structured error codes rather than inconsistent text or empty bodies, making error handling in client code more predictable and diagnostic.
Security fixes
-
FLUSH-02-01 —
sparql_update()andexecute_delete_insert()now callmutation_journal::flush()at the end of every SPARQL UPDATE statement, ensuring CONSTRUCT writeback rules fire correctly for the primary mutation path. -
CACHE-RLS-01 — Plan cache key now includes the current PostgreSQL role OID and
pg_ripple.inference_modeGUC value to prevent cross-user plan leakage via shared plan cache entries. -
SQL-INJ-01 — All five catalog INSERT statements in
src/views.rs(create_sparql_view,create_datalog_view,create_datalog_view_from_rule_set,create_framing_view,create_construct_view) migrated fromSpi::run(&format!())with manual quote-escaping toSpi::run_with_args()with typed$1, $2, …parameters. -
SQL-INJ-02 —
model_tagfilter insrc/sparql/embedding.rsreplaced fromAND e.model = '{}'string interpolation to parameterisedAND e.model = $1. -
SSRF-RFC1918-01 —
is_blocked_host()insrc/sparql/federation.rsnow also blocks IPv6 Unique Local addresses (fc00::/7, i.e.fc/fdprefix hosts). -
EXPLORER-AUTH-01 —
GET /explorerinpg_ripple_httpnow requires authentication viacheck_auth(). Unauthenticated clients receive HTTP 401.
Improvements
-
HTTP-ERR-01 — All 4xx/5xx HTTP responses from
pg_ripple_httpnow returnapplication/jsonwith{"error":"PTxxx","message":"..."}bodies. NewErrorResponsestruct andjson_error()helper added topg_ripple_http/src/common.rs. -
COMPAT-MIN-01 —
COMPATIBLE_EXTENSION_MINinpg_ripple_http/src/main.rsupdated from"0.75.0"to"0.79.0".pg_ripple_httpnow at v0.77.0. -
COMPAT-MATRIX-01 — Compatibility matrix in
docs/src/operations/compatibility.mdupdated with rows forpg_ripple_httpv0.73.x, v0.74.x, v0.75.x, and v0.76.x. -
PROPPATH-CYCLE-01 — Module comment in
src/sparql/property_path.rsupdated to document thatCYCLE s, o SETis required (and already in use) to prevent infinite recursion in recursive property-path CTEs. -
JOURNAL-R2RML-01 — Confirmed and documented that R2RML and CDC write paths route through
bulk_load::load_ntriples()which already callsmutation_journal::flush().
Infrastructure
-
MIGCHAIN-01 —
tests/test_migration_chain.shextended with checkpoint assertions at v0.65.0, v0.70.0, v0.75.0, v0.79.0 and a script-count verification for all 18 migration scripts from v0.62.0 to v0.79.0. -
SBOM-04 —
sbom.jsonregenerated at v0.80.0. CI SBOM version gate added to.github/workflows/ci.ymlto fail the build ifsbom.jsonversion does not matchCargo.tomlversion.
[0.79.0] — 2026-04-30 — Query Engine Completeness
Implements v0.79.0 roadmap: closes the last two known query-engine limitations
(WCOJ-LFTI-01 and SHACL-SPARQL-01). All feature_status() rows now show
implemented. The "Known limitations" table has been removed from README.md.
Two capabilities had been marked as planned but unimplemented in pg_ripple's feature status since the beginning of the project, and version 0.79.0 delivers both. The first is a true Leapfrog Triejoin executor for cyclic join patterns — graph patterns involving triangles, cliques, and other cyclic structures where standard nested-loop joins produce exponentially large intermediate results. The LFTI executor loads VP table data into sorted in-memory structures and evaluates multi-way joins using the Leapfrog algorithm, achieving worst-case optimal join complexity and making previously impractical graph pattern queries feasible on large graphs without requiring any hint-based query tuning.
The second is full sh:SPARQLRule support in the SHACL engine. SHACL rules expressed as SPARQL CONSTRUCT or SELECT queries can now be parsed, validated, and executed natively, with their results materialized into the target graph. sh:order is respected for execution ordering, and fixpoint iteration ensures that newly materialized triples can trigger further rules until no new conclusions are drawn. With both features delivered, all rows in pg_ripple.feature_status() show implemented status, and the "Known limitations" section is removed from the README — replaced by a note pointing users to feature_status() for the authoritative, machine-readable status surface that stays current automatically.
What's new
-
WCOJ-LFTI-01 — True Leapfrog Triejoin executor for cyclic BGP joins. Implements
TrieIterator/SortedIterator,leapfrog_intersect,EdgeData, andexecute_leapfrog_triejoininsrc/sparql/wcoj.rs. For cyclic patterns (triangles, cliques, social-network paths), the LFTI executor loads VP table edge data into sorted in-memory structures and evaluates n-way joins using the Leapfrog algorithm (Veldhuizen 2012), achieving the worst-case optimal complexity guarantee. The SQL planner-hint path remains as a fallback. New GUC:pg_ripple.wcoj_min_cardinality(INT, default 0).feature_status()rowwcojupdated fromplanner_hinttoimplemented. -
SHACL-SPARQL-01 — Full
sh:SPARQLRulesupport.bridge_shacl_rules()now parsessh:construct/sh:selectbodies from SHACL shapes, prepends prefix declarations, validates the CONSTRUCT query, and executes it via the existing SPARQL CONSTRUCT engine (sparql_construct_rows()). Results are materialised into the target graph via the standard VP insert path.sh:orderis respected for execution ordering. Fixpoint iteration (up toshacl_rule_max_iterations) ensures newly materialised triples can trigger further rules. The PT481 WARNING is now emitted at most once per session (de-dup). New GUCs:pg_ripple.shacl_rule_max_iterations(INT, default 100) andpg_ripple.shacl_rule_cwb(BOOL, default false).feature_status()rowshacl_sparql_ruleupdated fromplannedtoimplemented. -
README-LIMITS-01 — Removed the "Known limitations" section from README.md. Replaced with a note directing users to
pg_ripple.feature_status()for the machine-readable status surface.
New GUCs
| GUC | Type | Default | Description |
|---|---|---|---|
pg_ripple.wcoj_min_cardinality | INT | 0 | Minimum VP table edge count before LFTI executor is used; 0 = always use LFTI for cyclic patterns |
pg_ripple.shacl_rule_max_iterations | INT | 100 | Maximum fixpoint iterations for sh:SPARQLRule evaluation |
pg_ripple.shacl_rule_cwb | BOOL | false | When on, sh:SPARQLRule rules are registered as CWB rules |
Migration
No schema changes. Run ALTER EXTENSION pg_ripple UPDATE TO '0.79.0' to upgrade.
Tests
tests/pg_regress/sql/v079_wcoj.sql— LFTI GUC, triangle query, 4-cliquetests/pg_regress/sql/v079_shacl_sparql_rule.sql—sh:SPARQLRuleGUCs, materialisation,sh:ordertests/pg_regress/sql/v079_features.sql—feature_status()completeness check
[0.78.0] — 2026-05-22 — Bidirectional Integration Operations
Implements v0.78.0 roadmap: all BIDIOPS- deliverables closing the operational gaps identified in the v0.77.0 review. Data semantics are unchanged; this release adds the management plane that production deployments need.*
Introducing bidirectional data integration between a knowledge graph and external systems is only the first step — operating it reliably in production requires a full management plane. Version 0.78.0 delivers the operational infrastructure that makes the v0.77.0 integration primitives production-ready: write-side outbox depth limits with three overflow policies (pause, drop oldest, drop newest), a dead-letter table for events that exhausted their retry policy, and a reconciliation toolkit for resolving divergent states between the knowledge graph and its integration partners. Fine-grained per-subscription bearer tokens with named scopes let administrators grant exactly the permissions each integration partner needs and revoke them independently.
Data governance receives first-class support through two significant new features. Frame-level redaction allows specific predicates containing PII or sensitive information to be marked for redaction, so their values are replaced with a structured redaction marker in the outbound event stream while an unredacted variant remains available for internal compliance pipelines. A complete event audit log records every mutating action with token hash, remote address, and session user, creating a tamper-evident trail of all integration activity. Schema evolution policies govern how changes to frame definitions, IRI templates, or exclude-graph lists are handled when subscriptions are modified, preventing incompatible schema changes from propagating silently to downstream systems. A draft vendor-neutral "RDF Bidirectional Integration Profile" specification captures the design decisions in a reusable, community-shareable format.
What's new
-
BIDIOPS-QUEUE-01 — Write-side outbox depth limits and dead-letter table. Three overflow policies (
pause,drop_oldest,drop_newest);dead_letter_afterinterval policy;_pg_ripple.event_dead_letterscatalog table. New SQL API:list_dead_letters(),requeue_dead_letter(),drop_dead_letter(). -
BIDIOPS-PAUSE-01 —
bidi_status()exposes pg-trickle pause state.bidi_health()reportspausedwhen any subscription is paused. Pause/resume is delegated topg_trickle.pause_subscription/pg_trickle.resume_subscription. -
BIDIOPS-EVOLVE-01 — Schema-evolution policies for frame, IRI template, and exclude-graphs changes. New SQL API:
alter_subscription()withframe_change_policy,iri_change_policy,exclude_change_policyparameters. All changes recorded in_pg_ripple.subscription_schema_changes. -
BIDIOPS-AUTH-01 — Per-subscription bearer tokens with fine-grained scopes (
linkback,divergence,abandon,outbox_read,dead_letter_admin). New SQL API:register_subscription_token(),revoke_subscription_token(),list_subscription_tokens(). SHA-256 token hashing viasha2crate. Admin tokens stored separately in_pg_ripple.admin_tokens. -
BIDIOPS-REDACT-01 — Frame-level
"@redact": truefor PII / secret-bearing predicates.apply_frame_redaction()renders{"@redacted": true}in place of redacted predicate values. Unredacted outbox variant supported for compliance pipelines. Documented in the bidi runbook. -
BIDIOPS-AUDIT-01 —
_pg_ripple.event_auditrecords every side-band mutating call and admin action with token hash, remote address, and session user. New SQL API:purge_event_audit().pg_ripple.audit_retentionGUC (default: 90 days). -
BIDIOPS-PROPTEST-01 — Six convergence properties tested via
proptest(1,000 cases each): determinism, order-independence (latest_wins), no-loss, source_priority, linkback round-trip, convergence under retries. Added totests/proptest_suite.rs. -
BIDIOPS-CHAOS-01 — Fault injection smoke tests in
tests/stress/bidi_chaos.sh: abandon_linkback idempotency, audit purge safety, reconciliation round-trip, bidi_health status validity, token register/revoke. -
BIDIOPS-RECON-01 — Reconciliation toolkit:
_pg_ripple.reconciliation_queuetable;reconciliation_enqueue(),reconciliation_next(),reconciliation_resolve()SQL API; four resolution actions:accept_external,force_internal,merge_via_owl_sameAs,dead_letter. -
BIDIOPS-DASH-01 — Consolidated operations surface:
bidi_status()(16 columns) andbidi_health()(3 columns) monitoring views. -
BIDIOPS-MIG-01 — Migration script
sql/pg_ripple--0.77.0--0.78.0.sqlwith all DDL additions.pg_ripple.controlupdated todefault_version = '0.78.0'. -
BIDIOPS-PERF-01 — Benchmark suite
benchmarks/bidiops_throughput.sqlcovering queue depth estimation, audit insert throughput, scope-check latency, and frame redaction render cost. -
BIDIOPS-DOC-01 — Operations runbook (
docs/src/operations/bidi-runbook.md) and production-readiness checklist (docs/src/operations/bidi-production-checklist.md) covering all day-two operations: queue drain, token rotation, redaction, schema evolution, reconciliation, and chaos-test interpretation. -
BIDI-SPEC-01 — Draft vendor-neutral RDF Bidirectional Integration Profile v1 (
docs/spec/rdf-bidi-integration-v1.md) with 16 sections covering all 8 motivating problems and candidate conformance levels.
[0.77.0] — 2026-05-15 — Bidirectional Integration Primitives
Implements v0.77.0 roadmap: all BIDI- deliverables for bidirectional integration between pg_ripple and external systems via named-graph attribution, declarative conflict policies, upsert/diff ingest modes, symmetric delete, linkback rendezvous, CAS events, pg-trickle outbox/inbox transport, per-graph observability, and a frozen JSON wire format.*
Modern enterprise data architectures rarely have a single authoritative source — the same information may exist in a CRM, an ERP, a data warehouse, and a knowledge graph simultaneously, with each system maintaining its own version. Version 0.77.0 introduces a comprehensive set of primitives for bidirectional synchronization between pg_ripple and external systems. Source attribution, declarative conflict resolution policies, upsert and diff ingest modes, symmetric delete, linkback rendezvous for target-assigned IDs, compare-and-swap event verification, and pg-trickle outbox/inbox transport are all delivered in a single release, providing the complete foundation for production-grade bidirectional integration.
Four conflict resolution strategies give teams precise control over how divergent values are handled: source priority (a ranked list of authoritative sources), latest-wins (highest timestamp prevails), reject-on-conflict (raise an error if values disagree), and union (all values coexist). The diff ingest mode derives per-triple change timestamps from payload-level fields and stores them as RDF-star annotations, enabling time-aware queries and audit of exactly what changed and when. A frozen JSON wire format with a version discriminator ensures that event consumers remain compatible across pg_ripple upgrades without custom parsing changes. Per-graph observability through graph_stats() gives operations teams a live view of triple counts, last write times, conflict rejection rates, and active subscription counts for each named graph.
What's new
-
BIDI-ATTR-01 — Source attribution API consistency pass.
register_json_mappinggainsdefault_graph_iri,timestamp_path,timestamp_predicate,iri_template, andiri_match_patternparameters.ingest_jsonandingest_jsonldgainmodeandsource_timestampparameters. Whengraph_iriis omitted, the mapping'sdefault_graph_iriis used automatically. -
BIDI-CONFLICT-01 —
pg_ripple.register_conflict_policy(predicate, strategy, config)with strategies:source_priority(priority-ordered graph list with null fall-through),latest_wins(highest per-triple timestamp wins; falls back to VPicolumn with NOTICE),reject_on_conflict(raises an error on divergent values),union(all values coexist).drop_conflict_policyandrecompute_conflict_winnersfor lifecycle management. Non-authoritative_pg_ripple.conflict_winnerscache with register-time backfill and drop-time cleanup. -
BIDI-NORMALIZE-01 — Optional
normalizeexpression in conflict policy config. Expressions validated against a whitelist (STR, LCASE, UCASE, ROUND, SUBSTR, casts). Forbidden constructs (SELECT, WHERE, SERVICE, aggregate functions) raise an error at registration time. -
BIDI-UPSERT-01 —
ingest_json(..., mode => 'upsert')deletes existing values forsh:maxCount 1predicates (from the registered shape) before inserting, enabling idempotent updates for functional properties. -
BIDI-DIFF-01 —
ingest_json(..., mode => 'diff')derives per-triple change timestamps from a payload-levellastModifiedfield (configurable viatimestamp_path). Timestamps are stored as RDF-star annotations usingprov:generatedAtTime. Only predicates whose values actually changed are written. -
BIDI-DELETE-01 —
pg_ripple.delete_by_subject(mapping, subject_iri, graph_iri)deletes all triples for a subject.delete_mapped_predicates(mapping, subject_iri, graph_iri)deletes only the predicates declared in the mapping's context. Both respect the mapping'sdefault_graph_iriwhengraph_iriis omitted. -
BIDI-LOOP-01 —
exclude_graphs TEXT[]andpropagation_depth SMALLINTcolumns added to_pg_ripple.subscriptionsfor loop-safe subscription configuration. -
BIDI-CAS-01 —
pg_ripple.assert_cas(event, actual)verifies that thebaseobject in an outbound event matches the current state in the target system. No-ops when base is empty or whenafteralready matches actual (idempotent delivery). -
BIDI-LINKBACK-01 —
_pg_ripple.pending_linkbacksand_pg_ripple.subscription_buffertables for target-assigned ID rendezvous.record_linkback(event_id, target_id, target_iri)expands bare IDs through the target graph'siri_template, writesowl:sameAs, flushes buffered events, and deletes the pending row atomically.abandon_linkback(event_id)drops buffered events with a NOTICE and records the miss in_pg_ripple.iri_rewrite_misses. -
BIDI-OUTBOX-01 —
outbox_table,outbox_distribution_column,outbox_format, andoutbox_mergecolumns added to_pg_ripple.subscriptionsfor pg-trickle outbox configuration. -
BIDI-INBOX-01 —
pg_ripple.install_bidi_inbox(inbox_table)creates a schema, inbox table, dispatch PL/pgSQL function, andAFTER INSERTtrigger that routeslinkbackandabandonevents to the appropriate SQL helpers. -
BIDI-WIRE-01 — Frozen flat JSON event shape with top-level
version: "1.0"discriminator.pg_ripple.bidi_wire_version()returns"1.0". JSON Schema published atdocs/src/operations/event-schema-v1.json. -
BIDI-OBS-01 —
pg_ripple.graph_stats(graph_iri)returns per-graph triple count, last-write timestamp, conflict rejection count, and active subscription count._pg_ripple.graph_metricstable stores the persistent counters. -
BIDI-MIG-01 —
sql/pg_ripple--0.76.0--0.77.0.sqlmigration script creates all new catalog tables and schema extensions. Schema blocks added tosrc/schema.rsfor fresh installs. -
BIDI-PERF-01 —
benchmarks/bidi_relay_throughput.sqlpgbench script for measuring conflict-policied ingest throughput. -
BIDI-DOC-01 —
docs/src/operations/pg-trickle-relay.mdupdated with a bidirectional CRM ⇄ ERP walkthrough documenting mesh, federated, and named-graph patterns.
Schema changes
New tables: _pg_ripple.conflict_policies, _pg_ripple.conflict_winners,
_pg_ripple.iri_rewrite_misses, _pg_ripple.graph_metrics,
_pg_ripple.pending_linkbacks, _pg_ripple.subscription_buffer.
Altered tables: _pg_ripple.json_mappings (5 new columns),
_pg_ripple.subscriptions (12 new columns).
[0.76.0] — 2026-04-30 — Assessment 11 Low-Severity Findings and Production Polish
Implements v0.76.0 roadmap: toolchain version pin, RLS policy hash widening to 128-bit, Arrow dep minor-version pin, benchmark baseline refresh, 24 new regression tests (227 total), /metrics auth documentation, xact PRE_COMMIT SPI citation, log-hook defense-in-depth audit, clippy re-verification, and cross-verification of LLM/KGE feature status and CI integration.
Production deployments require not just correct functionality but reproducible builds, well-documented security tradeoffs, and a regression test suite wide enough to catch subtle breakage. Version 0.76.0 addresses all low-severity findings from Assessment 11 with twenty-four new regression tests — raising the total to 227 — covering SPARQL BIND, HAVING, NOT EXISTS, LANG filters, string and numeric functions, VALUES, CONSTRUCT with blanks, named graph copy, and many more patterns that real queries use every day. The Rust toolchain is pinned to a specific version in rust-toolchain.toml, making builds fully reproducible across CI runner updates and developer machines.
The RLS policy name generation function is upgraded from XXH3-64 to XXH3-128 hashing, reducing the birthday-paradox collision probability from roughly 50% at four billion named graphs to essentially zero — a change that matters for large multi-tenant deployments managing thousands of named graphs. Benchmark baselines are refreshed to reflect the HTAP merge optimizations delivered across the previous twenty versions, providing accurate performance regression detection for ongoing development. A defense-in-depth audit of all log call sites confirms that no HMAC keys, connection strings, bearer tokens, or other credentials appear in any error or warning message, closing a class of potential credential leakage via log aggregation pipelines. The /metrics Prometheus endpoint authentication is fully documented, giving operators clear guidance on securing the observability endpoint.
What's new
-
TOOLCHAIN-PIN-01 —
rust-toolchain.tomlnow pinschannel = "1.87.0"instead ofchannel = "stable". Builds are now fully reproducible across CI runner updates. Renovate can be configured withpackage-ecosystem: rust/files: ["rust-toolchain.toml"]to open automated PRs when new stable releases are available. -
RLS-HASH-01 — RLS policy name generation in
src/security_api.rsupgraded from XXH3-64 to XXH3-128. Policy name suffixes are now 32 hex characters instead of 16, reducing the birthday-paradox collision probability from ~50% at 4 billion graphs to essentially zero (~2×10⁻²⁰). Migration script rebuilds all existing policies from the_pg_ripple.graph_accesscatalog using the new naming scheme. -
ARROW-PIN-01 —
pg_ripple_http/Cargo.tomlnow pinsarrow = "55.1"(minor-version pinned) instead of just"55". This prevents surprise breakage from minor-version updates that introduce API changes in practice. -
BENCH-REFRESH-01 —
benchmarks/merge_throughput_baselines.jsonrefreshed from v0.53.0 to v0.76.0 baselines. The new measurements reflect HTAP merge optimisations introduced across v0.54.0–v0.75.0 (multi-worker pipeline, BRIN summarise, delta compaction). p50 throughput increased by ~7–15% across all worker counts. -
TEST-GROWTH-01 — 24 new pg_regress tests added, bringing the total from 203 to 227 (target ≥220). New tests cover:
sparql_bind_clause,sparql_having_filter,sparql_not_exists,sparql_lang_filter,sparql_string_functions,sparql_numeric_functions,sparql_values_clause,sparql_construct_blank,named_graph_copy,datalog_builtin_functions,sparql_order_limit,owl_rl_sameas,shacl_maxcount,rdf_star_nested,sparql_union_branches,sparql_subquery,sparql_insert_delete,datalog_rule_chain,sparql_path_alternation,sparql_ask_queries,dictionary_properties,sparql_optional_multi,admin_api_v076, andv076_features. -
METRICS-AUTH-DOC-01 — The
/metricsand/metrics/extensionendpoints inpg_ripple_httpare documented as unauthenticated by design indocs/src/operations/monitoring.md. The new section includes operator guidance for restricting access at network level when the service is exposed on a public interface. -
XACT-SPI-DOC-01 — The comment in
src/lib.rsexplaining whyflush()is not called fromXACT_EVENT_PRE_COMMITnow includes a citation to the PostgreSQL 18 source (src/backend/access/transam/xact.c) with an explanation of the exact memory context and lock constraints that make SPI unsafe at that callback stage. -
LOG-HOOK-01 — Defense-in-depth audit of all
pgrx::error!(),pgrx::warning!(),tracing::error!(), andtracing::warn!()call sites. No raw HMAC keys, connection strings, bearer tokens, or other credentials are logged in any error path. Findings documented indocs/src/operations/security.md. NoRegisterEmitLogHookis required. -
CLIPPY-VERIFY-01 —
cargo clippy --all-targets --features pg18 -- -D warningsre-verified to produce zero warnings. The CI gate in.github/workflows/ci.ymlis confirmed to enforce--deny warnings. -
LLM-KGE-STATUS-01 — Cross-verified that
src/llm/(llm_sparql_repair,nl_to_sparql) andsrc/kge.rs(kge_embeddings) are present infeature_status()withimplementedstatus and correct evidence paths (v0.73.0 FEATURE-STATUS-02 delivered). -
CI-INTEGRATION-VERIFY-01 — Cross-verified that Citus integration (
citus-integrationjob) and Arrow export integration (arrow-integrationjob) are wired to CI workflows (v0.75.0 CI-INTEGRATION-01/02 delivered).
[0.75.0] — 2026-04-30 — Assessment 11 Medium Finding Remediation
Implements v0.75.0 roadmap: unwrap audit, RLS error surfacing and documentation, Citus and Arrow CI integration tests, roadmap status validation, property-path/vp_rare regression tests, URL host parser fuzz target, fuzz duration increase, HTTP companion production docs, and mutation_journal feature_status entry.
Security and robustness improvements often require verifying not just that a feature works correctly in isolation, but that it integrates correctly with the full system under realistic conditions. Version 0.75.0 addresses all medium-severity findings from Assessment 11, with a focus on verifying that protections that should be in place actually are. CI integration tests for both Citus distributed tables and Arrow Flight bulk export are added, ensuring these features are tested end-to-end in the CI pipeline rather than relying only on unit tests. A roadmap status validation job verifies that the current version is correctly marked as Released in the roadmap after each deployment, catching forgotten status updates before they mislead users browsing the roadmap.
RLS error surfacing is improved: failures in applying row-level security policies were previously silently discarded; they now surface as WARNING messages that operators can detect in PostgreSQL logs and act on before they cause data access issues. A URL host parser fuzz target exercises the Citus shard-pruning host extraction function with arbitrary input, expanding the fuzz surface to cover network-layer parsing code that handles externally supplied hostnames. Property path regression tests are added for three specific combinations that had not previously been tested: property paths inside OPTIONAL clauses, inside GRAPH clauses, and on vp_rare predicates. The HTTP companion production documentation is completed with a compatibility warning explaining exactly why bypassing the version check in production is unsafe, giving operators the context they need to make an informed decision.
What's new
-
UNWRAP-AUDIT-01 — Audited all
.unwrap()calls in production code outside#[cfg(test)]blocks.pg_ripple_httpjson_response()helpers incommon.rsanddatalog.rsupdated to use.expect("infallible: hardcoded valid HTTP headers")for clearer panic messages. All other productionunwrap()calls are either in test modules or already annotated with#[allow(clippy::unwrap_used)]+// SAFETY:comments. ci/regress: cargo clippy --features pg18. -
CI-INTEGRATION-01 —
citus-integrationCI job added (.github/workflows/ci.yml): runs allcitus_*.sqlpg_regress tests in a dedicated job after main test/regress jobs pass. Tests verify graceful-degradation behavior when Citus is not installed. ci/test:.github/workflows/ci.ymlcitus-integrationjob. -
CI-INTEGRATION-02 —
arrow-integrationCI job added (.github/workflows/ci.yml): exercisesexport_arrow_flight()against a populated database, verifies the returned ticket is non-empty BYTEA, and confirmsarrow_flight_exportisimplementedinfeature_status(). ci/test:.github/workflows/ci.ymlarrow-integrationjob. -
ROADMAP-VALIDATE-01 —
scripts/check_roadmap_status.pyadded (see ROADMAP.md): validates that ROADMAP.md marks the current Cargo.toml version asReleased ✅. Newvalidate-roadmap-statusCI job runs post-release to catch forgotten status updates. ci/test:.github/workflows/ci.ymlvalidate-roadmap-statusjob. -
RLS-ERROR-01 —
apply_rls_to_vp_table()andapply_rls_policy_to_all_dedicated_tables()now surfaceALTER TABLE ENABLE ROW LEVEL SECURITYandCREATE POLICYerrors asWARNINGmessages instead of silently discarding them vialet _ = .... Operators can now detect RLS failures in PostgreSQL logs. ci/regress: v075_features.sql. -
ROLE-DOC-01 —
is_safe_role_name()documentation updated to explicitly state that non-ASCII Unicode role names are rejected with a guidance note on the limitation and why it exists (SQL-injection-safe allowlist). docs/src/operations/security.md. -
RLS-AUDIT-01 —
apply_rls_policy_to_all_dedicated_tables()fully audited: role quoting viaquote_ident_safe()confirmed correct; function doc comment added describing the security invariants. ci/regress: v075_features.sql. -
PROPPATH-TEST-01 —
tests/pg_regress/sql/v075_features.sqladds property-path regression tests for: property-path (+) insideOPTIONAL, property-path insideGRAPHclause, and property-path directly invp_rarepredicates (confirming no promotion is required). ci/regress: v075_features.sql. -
FUZZ-URL-01 —
fuzz/fuzz_targets/url_host_parser.rsadded (fuzz.yml): fuzzesextract_url_host()fromsrc/citus.rsfor panics and assertion violations. Target added tofuzz/Cargo.tomlandfuzz.ymlmatrix. ci/test:.github/workflows/fuzz.ymlurl_host_parsertarget. -
COMPAT-DOC-01 —
docs/src/operations/compatibility.mdupdated with a production warning forPG_RIPPLE_HTTP_SKIP_COMPAT_CHECK=1, clarifying it is only for testing/development and must not be set in production environments. docs/src/operations/compatibility.md. -
FUZZ-DURATION-01 — Nightly fuzz duration increased from 60s to 120s per target (default for
workflow_dispatchunchanged at 3600s). ci/test: fuzz.yml. -
FEATURE-STATUS-JOURNAL-01 —
mutation_journalrow added tofeature_status()withimplementedstatus. Documents all wired call sites (bulk_load, dict_api executor-end hook, Datalog seminaive, SPARQL Update) and the per-statement flush semantics. ci/regress: v075_features.sql. -
HTTP-VERSION-01 —
pg_ripple_httpversion bumped to 0.75.0;COMPATIBLE_EXTENSION_MINupdated to"0.74.0". pg_ripple_http/Cargo.toml.
Migration
- Migration:
sql/pg_ripple--0.74.0--0.75.0.sql.
[0.74.0] — 2026-05-09 — Assessment 11 Critical/High Remediation
Implements v0.74.0 roadmap: evidence truthfulness for all 12 missing reference docs, mutation journal wired through Datalog inference and executor-end hook, VP promotion plan-cache invalidation, interrupted-promotion recovery, and comprehensive CI validation.
Trustworthy documentation and correctly wired system internals are the foundation that makes everything else in a knowledge graph system reliable. Version 0.74.0 addresses the highest-priority findings from Assessment 11, starting with a documentation truthfulness audit that revealed twelve reference documentation pages cited by the feature status API simply did not exist. All twelve pages were created — covering SPARQL, Datalog, SHACL, storage, CONSTRUCT rules, federation, CDC, GraphRAG, observability, query optimization, vector search, and development guides — giving every feature status citation a live, informative destination. A CI job now verifies that all evidence paths cited by feature_status() resolve to real files on disk, making it impossible for future features to be marked as delivered with missing documentation.
Three important internal correctness fixes ship alongside the documentation improvements. The mutation journal is now correctly wired through the Datalog inference engine, ensuring that CONSTRUCT writeback rules fire automatically after Datalog-derived triples are inserted — a gap that had required manual refresh_construct_rule() calls after inference runs. VP promotion now resets the query plan cache after completing a predicate promotion, preventing queries compiled against the vp_rare table from running stale plans after the predicate gets its own dedicated VP table. A background recovery mechanism calls recover_interrupted_promotions() at merge worker startup to repair any promotions interrupted by a server crash, making the promotion process fully self-healing.
What's new
-
EVIDENCE-01 — Created 12 missing
docs/src/reference/pages cited byfeature_status():sparql.md,datalog.md,shacl.md,storage.md,construct-rules.md,federation.md,cdc.md,graphrag.md,observability.md,query-optimization.md,vector-search.md,development.md. SUMMARY.md updated with all new entries. -
GATE-05 — Fixed
validate-feature-statusCI job: replaced subshell-bypass pattern withmissing=$(...)variable capture so missing evidence paths cause a real non-zero exit. -
GATE-06 — Added
validate-feature-status-populatedCI job (.github/workflows/ci.yml): installs extension, inserts sample triples, then validates thatfeature_status()returns nodegradedrows on a populated DB. -
JOURNAL-DATALOG-01 — Wired Datalog inference through the mutation journal (CF-D + HF-C fixes):
run_inference_seminaive()records affected graph IDs from_dl_delta_*tables after VP-rare insertion and callsmutation_journal::flush().run_inference()similarly flushes after any triples are derived. -
SBOM-03 — SBOM regenerated to v0.74.0 (
sbom.json). Addedjust check-sbom-versiontarget to the justfile and wired it intojust assess-releaseas the first check. -
HTTP-VERSION-01 —
pg_ripple_httpversion bumped to 0.74.0;COMPATIBLE_EXTENSION_MINupdated to "0.73.0". -
DOC-JOURNAL-01 — Updated
mutation_journalmodule andflush()doc comments to accurately list all wired call sites (bulk_load, dict_api, Datalog seminaive, executor-end hook); removed false claim that SPARQL Update was wired. -
PROMO-RECOVER-01 — Background merge worker (worker 0) now calls
recover_interrupted_promotions()at startup inside a catch-unwind block. A newvp_promotion_recoveryrow (statusimplemented) is added tofeature_status(). -
CACHE-INVALIDATE-01 —
promote_predicate()callscrate::sparql::plan_cache_reset()after completing a VP promotion, so stale query plans that hard-codedvp_rareare evicted. -
TEST-04 — Added
tests/pg_regress/sql/v070_features.sqlregression test covering construct_writeback status, evidence-path coverage, vp_promotion_recovery, and plan_cache_reset. -
FLUSH-DEFER-01 — Executor-end hook (
register_executor_end_hook) callsmutation_journal::flush()at the start of each hook invocation, providing per-statement CWB rule firing even when dict_api is not used.
Schema changes
None — all changes are in the Rust implementation only.
- Migration:
sql/pg_ripple--0.73.0--0.74.0.sql.
[0.73.0] — 2026-05-05 — SPARQL 1.2 Tracking, Live Subscriptions, and JSON Mapping Registry
Implements v0.73.0 roadmap: SPARQL 1.2 compatibility tracking, SPARQL live subscription API via SSE, named bidirectional JSON↔RDF mapping registry, multi-graph JSON-LD ingest, CONTRIBUTING.md, Helm chart sidecar image config, and feature-status taxonomy.
Modern data applications don't just query data once — they subscribe to changes and react in real time. Version 0.73.0 introduces SPARQL live subscriptions: clients can register a named subscription with any SPARQL SELECT or CONSTRUCT query, and pg_ripple will automatically re-execute that query and deliver updated results via Server-Sent Events every time the relevant graph is written to. This makes it straightforward to build dashboards, monitoring applications, and notification pipelines that stay current without polling. Each subscription is lightweight, and pg_ripple sends a compact "changed" notification rather than recomputing and transmitting the full result set when payloads would exceed 8 KB.
The release also introduces a formal bidirectional JSON⇔RDF mapping registry that makes it much easier to exchange data between REST APIs and the knowledge graph. Teams can register named mappings that specify a JSON-LD @context and an optional SHACL shape for validation, then use ingest_json() and export_json_node() to convert in either direction without writing custom conversion code. Multi-graph JSON-LD documents can now be ingested in a single call, with each node automatically routed to the correct named graph. A SPARQL 1.2 tracking document records compatibility status for every new SPARQL 1.2 feature, and a full CONTRIBUTING.md gives new contributors a clear path from first clone to merged PR.
What's new
-
SUB-01 — SPARQL live subscription API:
subscribe_sparql(id, query, graph_iri)registers a subscription in_pg_ripple.sparql_subscriptions;unsubscribe_sparql(id)removes it;list_sparql_subscriptions()enumerates active subscriptions. After each graph write,notify_affected_subscriptions()re-executes the query and firespg_notify('pg_ripple_subscription_<id>', <json>). Payloads >8 KB send{"changed":true}instead. Thepg_ripple_httpcompanion now exposesGET /subscribe/{id}as a Server-Sent Events stream. Regression test:tests/pg_regress/sql/v073_features.sql. -
JSON-MAPPING-01 — Named bidirectional JSON↔RDF mapping registry:
register_json_mapping(name, context_jsonb, shape_iri)stores a JSON-LD@contextin_pg_ripple.json_mappings. Inconsistencies with the optional SHACL shape are recorded as warnings in_pg_ripple.json_mapping_warnings.ingest_json(mapping, document)andexport_json_node(mapping, iri)use the stored context for bidirectional conversion. -
JSONLD-INGEST-02 — Multi-graph JSON-LD ingest:
json_ld_load(document jsonb, default_graph text) → bigintwalks@grapharrays or single-node JSON-LD documents and loads each node into the triple store, returning the total number of triples inserted. -
SPARQL12-01 — SPARQL 1.2 compatibility tracking document at
plans/sparql12_tracking.mdlisting all SPARQL 1.2 features and their current status in pg_ripple. -
CONTRIB-01 —
CONTRIBUTING.mdadded with branch naming conventions, commit format, pre-commit checklist, migration discipline, and PR checklist. -
TAXONOMY-01 — Feature status taxonomy documentation at
docs/src/reference/feature-status-taxonomy.mdwith promotion criteria for each status tier. -
HELM-01 — Helm chart
charts/pg_ripple/values.yamlupdated to include a separatehttp.imagesection for the pg_ripple_http sidecar;statefulset.yamluseshttp.image.tagto pin the sidecar version independently. -
FEATURE-STATUS-02 —
feature_status()now includes entries forllm_sparql_repair,kge_embeddings,sparql_nl_to_sparql,sparql_12,sparql_subscription,json_ld_multi_ingest, andjson_mapping. -
R2RML-DOC-01 —
plans/r2rml-virtual.mddocuments the planned virtual R2RML layer and its scope relative toregister_json_mapping. -
CONTROL-01 —
pg_ripple.controlcommentupdated to reflect v0.73.0 capabilities.
Schema changes
- New table
_pg_ripple.sparql_subscriptions(subscription_id TEXT PK, query TEXT, graph_iri TEXT, created_at TIMESTAMPTZ). - New table
_pg_ripple.json_mappings(mapping_name TEXT PK, context JSONB, shape_iri TEXT, created_at TIMESTAMPTZ). - New table
_pg_ripple.json_mapping_warnings(id BIGSERIAL PK, mapping_name TEXT, kind TEXT, detail TEXT, created_at TIMESTAMPTZ). - Migration:
sql/pg_ripple--0.72.0--0.73.0.sql.
[0.72.0] — 2026-05-01 — Architecture and Protocol Hardening
Implements v0.72.0 roadmap: sub-transaction safety, JSON-LD fixes, Flight nonce replay protection, observability, module splitting.
A database system that silently discards data during a transaction rollback is fundamentally unreliable, and version 0.72.0 closes this gap. Sub-transaction savepoint support is added via a PostgreSQL sub-transaction callback, ensuring that CONSTRUCT writeback rule entries accumulated within a savepoint are correctly cleaned up when the savepoint is rolled back. Three JSON-LD encoding bugs are fixed: object-form @context term definitions that were being silently dropped, integer values exceeding i64::MAX that were causing panics instead of correct string encoding, and fractional JSON numbers like 1.5 that were being misclassified as integers. An IRI key validation check prevents malformed IRIs from ever entering the triple store.
Security receives meaningful hardening with the introduction of Arrow Flight nonce replay protection: the HTTP companion now maintains a time-bounded nonce cache and rejects any Arrow Flight ticket whose nonce has been used before, preventing replay attacks against the bulk export endpoint. A new /metrics/extension endpoint exposes extension-level Prometheus metrics directly from the PostgreSQL side, and a new export_jsonld_node() SQL function returns the complete JSON-LD representation of any subject IRI. Five large source files are decomposed into focused modules across the SPARQL, HTTP routing, storage, and GUC subsystems, reducing average file size and making each sub-component easier to review and test in isolation.
What's new
-
XACT-01 — Sub-transaction savepoint/rollback support:
RegisterSubXactCallbackregistered in_PG_init; CWB writer entries are now cleaned up onROLLBACK TO SAVEPOINT. Regression test:tests/pg_regress/sql/cwb_savepoint_rollback.sql. -
BUG-JSONLD-CONTEXT-01 — Object-form JSON-LD
@contextentries (term definitions with@id/@type) are now correctly preserved inbulk_load.rsinstead of being silently dropped. -
RT-FIX-04B —
i64overflow in JSON number →xsd:integerno longer panics; values exceedingi64::MAXare preserved as thexsd:integerstring form. -
RT-FIX-06 —
is_f64()checked beforeis_i64()injson_value_to_nt_termso fractional JSON numbers (e.g.1.5) are not misclassified as integers. -
RT-FIX-07 — IRI key validation (
validate_iri_key_or_error) added before triple insert to prevent malformed IRIs from entering the triple store. (ci/regress: json_roundtrip_fixes.sql) -
FLIGHT-NONCE-01 — Arrow Flight nonce replay protection:
AppStategains anonce_cache: DashMap<String, Instant>with 5-minute TTL. Replayed nonces return401 Unauthorized. -
OBS-02 —
/metrics/extensionroute added topg_ripple_http, emitting Prometheus-format extension-level metrics (triple count, active graphs, GUC settings). -
JSONLD-NODE-01 —
export_jsonld_node(iri TEXT) → jsonbSQL function added, returning the JSON-LD representation of all triples for a given subject IRI. Regression test:tests/pg_regress/sql/export_jsonld_node.sql. -
PROPTEST-01 — Property-based tests for
ConstructTemplate/apply_construct_templateadded intests/proptest/construct_template.rsusing theproptest 1crate. Self-contained (no pgrx dependency). (see ROADMAP.md v0.72.0) -
MOD-01 — Source files exceeding 500 lines split into focused sub-modules:
src/gucs/registration.rs— all GUC registrations extracted from_PG_initsrc/lib_tests.rs— pgrx integration tests extracted fromsrc/lib.rssrc/storage/dictionary_io.rs— RDF-term I/O helperssrc/storage/vp_rare_io.rs— VP-table I/O helperssrc/storage/ops.rs— storage operations (insert/delete/query/graph management)pg_ripple_http/src/routing/sparql_handlers.rs— SPARQL GET/POST/stream handlerspg_ripple_http/src/routing/rag_handler.rs— RAG endpoint handlerpg_ripple_http/src/routing/admin_handlers.rs— admin/observability/explorer handlers
Schema changes
None.
[0.71.0] — 2026-04-29 — Arrow Flight Validation, Citus Integration Tests, and Compatibility Hardening
Implements v0.71.0 roadmap: closes the High-severity Assessment 10 gaps requiring runtime infrastructure.
Arrow Flight is the high-performance columnar data transport protocol that makes bulk exports of triple store data possible at enterprise scale. Version 0.71.0 upgrades the Arrow Flight implementation from a basic batch response to true streaming: the HTTP companion now produces chunked Transfer-Encoding responses, allowing clients to begin processing Arrow record batches before the full export completes. This means that a large export no longer requires the client to wait for all data to be serialized before any of it can be processed — a meaningful improvement for analytical pipelines that feed RDF data into columnar data stores or stream it to downstream consumers.
Version compatibility checking is introduced as a first-class feature: the HTTP companion now queries the installed extension version at startup and warns operators when the extension is below the minimum compatible version. A new compatibility matrix document clearly shows which HTTP companion versions work with which extension versions. A multi-node Citus integration test verifies that row-level security policies are correctly propagated to VP shard tables on distributed deployments — a critical correctness guarantee for multi-tenant environments. Approximate distinct count documentation clarifies exactly when HyperLogLog is used and what error bounds to expect, and a new Citus SERVICE pruning GUC enables a planned 10× speedup for bound-subject federated queries.
What's new
-
FLIGHT-STREAM-01 —
pg_ripple_http/flight/do_getnow usesaxum::body::Body::from_streamwith 64 KiB chunks, producingTransfer-Encoding: chunkedHTTP responses. The IPC buffer is streamed lazily so clients can begin decoding Arrow record batches before the full export completes. Integration testtests/http_integration/arrow_export_large.shvalidates streaming behavior and RSS bounds.docs/src/reference/arrow-flight.mdupdated with memory-bound documentation. -
CITUS-INT-01 —
tests/integration/citus_rls_propagation.shcreated. The multi-node integration test starts a Citus cluster viadocker-compose, enables sharding, inserts triples in both allowed and restricted named graphs, promotes a predicate past the threshold, and asserts that non-superuser RLS restricts cross-graph access. Thefeature_status()citation forcitus_rls_propagationnow resolves to an existing file. -
COMPAT-01 —
pg_ripple_httpnow performs a version compatibility check at startup: it queriesextversionfrompg_extensionand warns if the installed extension is belowCOMPATIBLE_EXTENSION_MIN = "0.70.0".docs/src/operations/compatibility.mdadded with the full version compatibility matrix and upgrade procedure. -
HLL-DOC-01 —
docs/src/reference/approximate-aggregates.mdcreated, documenting when HLL is used (approx_distinct=on+hllextension), error bounds at default precision (log2m=14, ~0.81% standard error for ≥ 10,000 distinct values), and fallback to exactCOUNT(DISTINCT). pg_regress testhll_accuracy.sqlvalidates GUC toggle and COUNT(DISTINCT) correctness. -
CITUS-BENCH-01 —
docs/src/reference/citus-service-pruning.mdcreated, documenting thecitus_service_pruningGUC and expected 10× speedup for bound-subject SERVICE queries. pg_regress testcitus_service_pruning.sqlvalidates GUC plumbing and confirmsfeature_status()showsexperimental.
Schema changes
None.
[0.70.0] — 2026-04-29 — Assessment 10 Critical Remediation
Implements v0.70.0 roadmap: closes four Critical and seven High/Medium findings from Overall Assessment 10.
Four critical and seven high and medium findings from the tenth security and correctness assessment are closed in version 0.70.0. The most important correctness improvement ensures that CONSTRUCT writeback rules fire automatically after every write operation, not just those that use the dictionary API. Bulk-load functions now wire into the mutation journal and flush it after each batch, and SPARQL Update flushes are deferred to the pre-commit event hook so that writeback fires once per statement regardless of how many individual triples were modified. Together these changes mean that raw-to-canonical data transformation pipelines built on CONSTRUCT rules work correctly without any manual refresh calls.
Documentation truthfulness receives careful attention: twelve documentation citations pointing to nonexistent files are fixed, stub pages are created for Arrow Flight and scalability references, and the README is updated to accurately reflect the system's current capabilities rather than a version from months earlier. Role name validation in the RLS grant functions prevents SQL injection through malformed role names, and grant_graph_access() now uses parameterized DDL throughout. A version check script is added to the justfile's release assessment tool, making it impossible to publish a release with a stale README version number. A migration chain test extended to cover v0.67.0 through v0.69.0 verifies that the complete upgrade path works end-to-end.
What's new
-
BULK-01 — Bulk-load functions (
load_ntriples,load_turtle,load_nquads,load_trig,load_rdfxml, and their graph-aware variants) now wire into the mutation journal and callflush()after all batches. CONSTRUCT writeback rules fire automatically after everyload_*call without requiringrefresh_construct_rule. -
FLUSH-01 — SPARQL Update and single-triple API calls no longer flush the CWB pipeline once per individual triple. Journal flush is deferred to
XACT_EVENT_PRE_COMMITvia the existingxact_callback_c, so CONSTRUCT writeback fires once per statement boundary regardless of how many triples the statement inserts or deletes. -
GATE-03 —
feature_status()evidence paths cleaned up: stub pages created atdocs/src/reference/scalability.mdanddocs/src/reference/arrow-flight.md; the non-existenttests/integration/citus_rls_propagation.shreference replaced with the newsecurity_rls_role_injection.sqltest evidence path. Thevalidate-feature-statusCI job now fails hard when any cited evidence file is missing. -
SHACL-DOC-01 —
docs/src/features/shacl-sparql-rules.mdrewritten:sh:SPARQLRuleis clearly documented as not supported (emits PT481 WARNING and skips).sh:TripleRuleandsh:SPARQLConstraintremain fully supported. -
README-01/02 —
README.mdupdated from v0.67.0 to v0.69.0 in "What works today" and "Known limitations" sections.scripts/check_readme_version.shadded and wired intojust assess-release. -
RLS-SQL-01 —
grant_graph_access()andapply_rls_to_vp_table()now validate role names against[A-Za-z_][A-Za-z0-9_$]*(PT711 error on mismatch) and usequote_ident_safe()in DDL. SQL injection test added:tests/pg_regress/sql/security_rls_role_injection.sql. -
SBOM-02 —
sbom.jsonregenerated for v0.70.0. Release CIrelease.ymlconfirmed to include a blocking SBOM-version-match step. -
GATE-04 — Legacy
scripts/check_roadmap_evidence.shandscripts/check_api_drift.shdeleted.justfileassess-releasetarget now calls.pyversions exclusively. Verified by.github/workflows/ci.yml(Validate feature status job). -
TEST-01 —
tests/pg_regress/sql/v067_features.sqladded (mutation journal smoke test, Arrow Flight GUC check, feature_status evidence path regression guard). -
TEST-02 —
tests/pg_regress/sql/v069_features.sqladded (module restructuring API stability regression guard,construct_pipeline_status()check,feature_status()coverage check). -
TEST-03 —
tests/pg_regress/sql/recover_promotions.sqladded (fullrecover_interrupted_promotions()regression test including simulated-interruption scenario). -
DOC-01 —
roadmap/v0.67.0.mdstatus already confirmed as Released ✅ (no change needed). -
CWB test extension —
cwb_write_path_equivalence.sqlextended with a Path 5 bulk-load arm (load_ntriples_into_graph) that asserts derived triples appear immediately after a bulk load.
Schema changes
None.
Exit criteria
All 192+ pg_regress tests pass; validate-feature-status CI job exits non-zero when evidence file missing; bulk-load CWB arm in cwb_write_path_equivalence.sql passes.
[0.69.0] — 2026-05-06 — Module Architecture Restructuring
Implements v0.69.0 roadmap: splits four large source modules along single-responsibility boundaries with zero behavioral changes.
A 2,252-line file is not a module — it is a wall of code that no reviewer can navigate safely, no test can exercise cleanly, and no new contributor can approach without weeks of orientation. Version 0.69.0 makes a focused investment in code organization, decomposing four large modules into properly bounded sub-components with clear single responsibilities. The SPARQL engine is split across parse, plan, decode, and execute modules. The HTTP companion main entry point shrinks from 2,252 lines to 250 lines by extracting routing, database bridge, Arrow Flight, and streaming components. The CONSTRUCT writeback rules module is restructured into catalog, scheduler, delta, and retraction components, each with a clear and auditable purpose.
The storage module's public mutation API is narrowed so that insertion and deletion functions are only accessible within the crate, preventing internal implementation details from leaking into callers that should be using the higher-level journal-aware APIs. VP promotion helpers are extracted to their own module with proper documentation. All 186 regression tests continue to pass without change — this is purely a code quality release with no behavioral changes. The result is a codebase that is meaningfully easier to review, easier to test in isolation, and easier for new contributors to navigate without needing a guide through the full architecture before making their first contribution.
What's new
-
ARCH-01 —
src/sparql/mod.rssplit (already delivered in prior sessions):parse.rs,plan.rs,decode.rs,execute.rsextracted;mod.rsis now a 157-line facade with re-exports and the three public SQL-entry-point functions. -
ARCH-02 —
pg_ripple_http/src/main.rssplit: Handler functions, content-type constants, and response formatters extracted torouting.rs; SPARQL execution helpers (execute_select/ask/construct/describe) tospi_bridge.rs; Arrow IPC Flight endpoint toarrow_encode.rs; streaming placeholder tostream.rs.main.rsis now 250 lines (startup code +main()only). -
ARCH-03 —
src/construct_rules.rssplit into a module directory:catalog.rs(ensure_catalog),scheduler.rs(collect_source_graphs + compute_rule_order topological sort),delta.rs(compile_construct_to_inserts + run_full_recompute),retract.rs(retract_exclusive_triples),mod.rs(public API + write hooks). -
ARCH-04 —
src/storage/mod.rsnarrowed public API:insert_triple_by_ids,delete_triple_by_ids, andbatch_insert_encodednarrowed topub(crate)with journal-caller doc comments. VP promotion helpers (promote_predicate,promote_rare_predicates,recover_interrupted_promotions,vp_promotion_threshold,create_extended_statistics) extracted tostorage/promote.rs. -
ARCH-05 — All 186 pg_regress tests pass; no SQL-visible changes.
Schema changes
None. This is a pure Rust module restructuring.
Files changed
- src/sparql/parse.rs — query complexity checks + ARQ aggregate preprocessing (new)
- src/sparql/plan.rs — SPARQL algebra → SQL plan cache (new)
- src/sparql/decode.rs — batch dictionary decode (new)
- src/sparql/execute.rs — SPI execution, CONSTRUCT/DESCRIBE/UPDATE, explain (new)
- src/sparql/mod.rs — thin facade: re-exports + 3 public SQL entry points
- pg_ripple_http/src/routing.rs — all HTTP handlers, response formatters, build_router (new)
- pg_ripple_http/src/spi_bridge.rs — execute_sparql_with_traceparent + execute_select/ask/construct/describe (new)
- pg_ripple_http/src/arrow_encode.rs — Arrow Flight bulk-export (new)
- pg_ripple_http/src/stream.rs — SSE/streaming placeholder (new)
- pg_ripple_http/src/main.rs — startup code only (250 lines, was 2252)
- src/construct_rules/mod.rs — public API + on_graph_write/delete hooks (new directory)
- src/construct_rules/catalog.rs — ensure_catalog (new)
- src/construct_rules/scheduler.rs — topological sort (new)
- src/construct_rules/delta.rs — compile + recompute (new)
- src/construct_rules/retract.rs — retract_exclusive_triples (new)
- src/storage/promote.rs — VP promotion helpers (new)
- src/storage/mod.rs — narrowed mutation API, pub(crate) for mutation functions
[0.68.0] — 2026-04-29 — Distributed Scalability, Streaming Completion, and Fuzz Hardening
Implements v0.68.0 roadmap: portal-based CONSTRUCT cursor streaming, Citus HLL COUNT(DISTINCT), Citus SERVICE shard pruning, nonblocking VP promotion with crash recovery, and scheduled nightly fuzz CI.
Three important platform capabilities reach production quality in version 0.68.0. CONSTRUCT query results can now be streamed through a lazy portal-based cursor that materializes only one page at a time, bounding memory use to a configurable batch size regardless of result set size. This makes it practical to run large CONSTRUCT queries — generating Turtle or JSON-LD exports of entire named graph subsets — without risking memory exhaustion. Citus deployments gain two new optimizations: HyperLogLog approximate COUNT(DISTINCT) for SPARQL aggregates on distributed tables, and SERVICE shard pruning that routes federated queries to the single Citus worker that owns the relevant data range.
VP promotion — the process by which a predicate that has accumulated enough triples moves from the shared vp_rare table to its own dedicated VP table — is made non-blocking and crash-safe in this release. Promotion now tracks its progress in a status column so that a recovery function can detect and retry any promotion interrupted by a server crash, eliminating a class of inconsistent half-promoted states that previously required manual intervention. A comprehensive fuzz CI workflow runs all twelve fuzz targets nightly for 60 seconds each, with corpus and crash artifacts uploaded on every run, providing continuous automated discovery of parser and codec vulnerabilities across the SPARQL, Turtle, Datalog, SHACL, JSON-LD, federation, and geospatial parsing paths.
What's new
-
Portal-based CONSTRUCT cursor streaming (STREAM-01):
sparql_cursor_turtle()andsparql_cursor_jsonld()now stream CONSTRUCT results using a lazyConstructCursorIter— a portal-based iterator that applies the CONSTRUCT template per page and serializes each page as a Turtle/JSON-LD chunk. Memory use is bounded topg_ripple.export_batch_sizerows per page. New helpersprepare_construct()andapply_construct_template()insrc/sparql/mod.rspre-encode constant IRIs/literals to i64 once at query-plan time. -
Citus HLL approximate COUNT(DISTINCT) (CITUS-HLL-01): When
pg_ripple.approx_distinct=onand thehllPostgreSQL extension is installed,COUNT(DISTINCT ?x)SPARQL aggregates are translated tohll_cardinality(hll_add_agg(hll_hash_bigint(x)))::bigintfor scalable approximate counts on distributed VP tables. Falls back to exactCOUNT(DISTINCT)whenhllis absent orapprox_distinct=off. New GUCpg_ripple.approx_distinct(BOOL, defaultoff). -
Citus SERVICE shard pruning (CITUS-SVC-01): When
pg_ripple.citus_service_pruning=on, the SERVICE translator callscitus_service_shard_annotation()which detects Citus worker endpoints viais_citus_worker_endpoint()and wires shard-constraint SQL annotations for pruning. New GUCpg_ripple.citus_service_pruning(BOOL, defaultoff). Full multi-node infrastructure required for end-to-end testing. -
Nonblocking VP promotion with crash recovery (PROMO-01): VP promotion now tracks progress via a
promotion_status TEXTcolumn in_pg_ripple.predicates(values:'promoting'during copy,'promoted'when complete). New SQL functionpg_ripple.recover_interrupted_promotions()scans for'promoting'entries and retries interrupted promotions — call it after an unclean server shutdown. New GUCpg_ripple.vp_promotion_batch_size(INT, 1–1000000, default 10000). -
Scheduled nightly fuzz CI (FUZZ-01):
.github/workflows/fuzz.ymlruns all 12 fuzz targets (sparql_parser, turtle_parser, rdfxml_parser, dictionary_hash, federation_result, datalog_parser, shacl_parser, jsonld_framer, http_request, r2rml_mapping, geosparql_wkt, llm_prompt_builder) nightly for 60 s each. Manualworkflow_dispatchsupports extended runs. Corpus and crash artifacts are uploaded on each run.
Schema changes
_pg_ripple.predicatestable: addedpromotion_status TEXTcolumn (NULL = legacy/no promotion started,'promoting'= copy in progress,'promoted'= fully promoted). Added to initial schema CREATE TABLE and to migration scriptsql/pg_ripple--0.67.0--0.68.0.sql.
GUCs added
| GUC | Type | Default | Level | Purpose |
|---|---|---|---|---|
pg_ripple.approx_distinct | BOOL | off | USERSET | Route COUNT(DISTINCT) through Citus HLL when available |
pg_ripple.citus_service_pruning | BOOL | off | USERSET | Enable Citus worker shard annotations for SERVICE |
pg_ripple.vp_promotion_batch_size | INT | 10000 | USERSET | Batch size for nonblocking VP promotion copy phase |
SQL functions added
| Function | Returns | Description |
|---|---|---|
pg_ripple.recover_interrupted_promotions() | bigint | Scan and retry interrupted VP promotions after crash |
Files changed
- src/sparql/cursor.rs — new
ConstructCursorIterstruct +ConstructFormatenum;sparql_cursor_turtleandsparql_cursor_jsonldnow returnimpl Iterator - src/sparql/mod.rs —
TemplateSlot,ConstructTemplate,prepare_construct(),apply_construct_template() - src/sparql_api.rs — updated SETOF wrappers for new iterator API
- src/sparql/translate/group.rs — HLL aggregate translation +
citus_hll_available() - src/gucs/storage.rs —
APPROX_DISTINCT,CITUS_SERVICE_PRUNING,VP_PROMOTION_BATCH_SIZEGUC statics - src/citus.rs —
is_citus_worker_endpoint(),citus_service_shard_annotation(),extract_url_host() - src/sparql/translate/graph.rs — wire
citus_service_shard_annotation()in SERVICE translator - src/storage/mod.rs —
promote_predicate()with status tracking;recover_interrupted_promotions() - src/dict_api.rs —
recover_interrupted_promotions()pg_extern - src/lib.rs — three new GUC registrations
- src/schema.rs —
promotion_status TEXTin predicates CREATE TABLE;v068_schema_version_stamp - src/feature_status.rs — updated status for 6 deliverables + new
continuous_fuzzingentry - sql/pg_ripple--0.67.0--0.68.0.sql — migration script (ADD COLUMN, schema_version stamp)
- .github/workflows/fuzz.yml — new scheduled fuzz workflow
- tests/pg_regress/sql/v068_features.sql — new regress test (186 total, 0 failures)
- tests/pg_regress/expected/v068_features.out — bootstrapped expected output
[0.67.0] — 2026-05-06 — Production Hardening and Assessment 9 Remediation
Implements v0.67.0 roadmap: Arrow Flight security hardening, mutation journal for CONSTRUCT writeback, Row Level Security propagation to all VP tables, panic→error conversion, Python gate tooling, benchmark correctness fixes, and scheduled performance trend CI.
Production readiness requires not just functional correctness but a comprehensive set of operational and security protections that catch problems before they become incidents. Version 0.67.0 addresses eight findings from Assessment 9, starting with two Arrow Flight security hardening items: unsigned tickets are now rejected by default (requiring an explicit opt-in GUC for development use only), and the tombstone exclusion query prevents deleted triples from appearing in bulk exports. A transaction-local mutation journal unifies CONSTRUCT writeback across all write paths — insert_triple, SPARQL INSERT DATA, load_ntriples, and load_turtle — so that derived triples are always updated regardless of which entry point was used to make a change.
The release also delivers significant infrastructure improvements for reliability and observability. A validate-feature-status CI job verifies after every build that all features claimed as implemented have live evidence files on disk — making it structurally impossible to ship a release with misleading status claims. Row-level security policies are now applied to VP delta and main tables when created and when predicates are promoted, closing a gap where RLS protection was only applied to the original table at creation time. A scheduled weekly performance trend CI workflow catches regressions automatically by comparing new benchmark results against a four-week rolling average. Python-based replacements for legacy shell gate scripts provide more reliable parsing and require a --version argument to prevent stale invocations from passing silently.
What's new
-
Arrow Flight unsigned-ticket hardening (FLIGHT-SEC-01): Unsigned Arrow Flight tickets are now rejected by default. New GUC
pg_ripple.arrow_unsigned_tickets_allowed(BOOL, defaultoff) must be explicitly set to allow unsigned tickets. CorrespondingARROW_UNSIGNED_TICKETS_ALLOWEDenv var forpg_ripple_http. Ticket rejections are tracked instreaming_metrics(). Evidence:pg_ripple.feature_status(),pg_ripple.streaming_metrics(). -
Arrow Flight tombstone-exclusion and batch streaming (FLIGHT-SEC-02):
POST /flight/do_getnow uses tombstone-exclusion query (main EXCEPT tombstones UNION ALL delta) to prevent serving deleted triples. Export is streamed in configurable batches via new GUCpg_ripple.arrow_batch_size(INT, 1–100000, default 1000). Response headerx-arrow-batchesreports batch count.arrow_batches_sentcounter added tostreaming_metrics(). Evidence:pg_ripple.feature_status(). -
Transaction-local mutation journal (MJOURNAL-01/02/03): A Rust
thread_local!mutation journal (src/storage/mutation_journal.rs) unifies CONSTRUCT writeback across all write paths:insert_triple, SPARQL INSERT DATA,load_ntriples, andload_turtle. Fast-path skips journal accumulation when no CONSTRUCT rules are defined. Evidence:tests/pg_regress/sql/cwb_write_path_equivalence.sql. -
Row Level Security on VP delta/main tables (RLS-01/02):
enable_graph_rls()andgrant_graph_access()now apply RLS policies to dedicated VP_deltaand_maintables at creation, promotion, and grant/revoke time. Evidence:tests/pg_regress/sql/rls_promotion.sql. -
Panic → pgrx::error conversion (PANIC-01):
construct_rules.rstopological-sortpanic!()replaced withpgrx::error!()to ensure clean PostgreSQL error reporting under load. Evidence:src/construct_rules.rs. -
Python gate tooling (GATE-01):
scripts/check_api_drift.shandscripts/check_roadmap_evidence.shreplaced with portable Python 3 equivalents (scripts/check_api_drift.py,scripts/check_roadmap_evidence.py) that require--version X.Y.Zto prevent stale invocations. Evidence:scripts/check_api_drift.py,scripts/check_roadmap_evidence.py. -
validate-feature-statusCI job (GATE-02): New CI job added to.github/workflows/ci.ymlthat runs aftertestandregress, callsfeature_status(), verifies evidence paths exist on disk, and runs both Python gate scripts. Evidence:.github/workflows/ci.yml. -
Documentation truth (GATE-03):
README.md"What works today" updated from v0.63.0 to v0.67.0, pgrx version reference corrected to 0.18, v0.64.0roadmap/v0.64.0.mdstatus corrected toReleased ✅. -
SBOM version verification (SBOM-01): Release workflow now verifies that the regenerated
sbom.jsoncomponent version matchesCargo.tomlbefore uploading to the GitHub release. Evidence:.github/workflows/release.yml. -
Benchmark correctness (BENCH-01):
.github/workflows/benchmark.ymlno longer usesbashto execute SQL files. Merge throughput and vector index benchmarks now usepgbench -fandpsql -frespectively.|| truesuppressors removed;continue-on-error: falseadded. Benchmark failures now fail the CI run. -
Scheduled performance trend CI (BENCH-02): New weekly workflow
.github/workflows/performance_trend.ymlruns insert throughput, merge throughput, and hybrid search benchmarks, appends results tobenchmarks/*_history.csv, and fails if any metric drops more than 10% below the 4-week rolling average.
GUCs added
| GUC | Type | Default | Level | Purpose |
|---|---|---|---|---|
pg_ripple.arrow_unsigned_tickets_allowed | BOOL | off | SIGHUP | Allow unsigned Arrow Flight tickets (dev-only) |
pg_ripple.arrow_batch_size | INT | 1000 | USERSET | Arrow IPC export batch size per record batch |
Dependencies added
None — all v0.67.0 changes are implemented using already-present dependencies.
Implements the v0.66.0 roadmap: true paged SPARQL cursors via PostgreSQL portal API, HMAC-SHA256 signed Arrow Flight v2 tickets, real Arrow IPC streaming in pg_ripple_http, WCOJ explain metadata, streaming observability metrics, and Citus BRIN summarise SQL API.
Version 0.66.0 delivers three capabilities that make pg_ripple competitive with dedicated graph analytics platforms. True SPARQL cursor streaming uses the PostgreSQL portal API to provide memory-bounded paging — peak memory use is proportional to page size rather than total result size, making it possible to stream arbitrarily large result sets without risk of memory exhaustion. Real Arrow IPC streaming in the HTTP companion validates HMAC-SHA256-signed tickets, connects directly to the PostgreSQL extension, and delivers triple store data as a binary columnar stream that Arrow-native analytics tools can consume at full memory bandwidth. WCOJ explain metadata in the query plan output shows operators whether the worst-case optimal join executor was activated for a given query and why it made that decision.
The observability improvements are equally significant: a new streaming_metrics() function exposes live atomic counters for every streaming activity in the system — cursor pages opened, rows streamed, Arrow batches sent, ticket rejections, and Citus BRIN summarize completions — giving operations teams real-time visibility into the data export subsystem. The Citus BRIN summarize SQL API enables operations teams to keep BRIN index statistics current across all promoted VP main-partition tables with a single function call, an important maintenance operation for keeping the HTAP merge storage path efficient on Citus distributed deployments.
What's new
-
True SPARQL cursor streaming (STREAM-01):
sparql_cursor()now uses the PostgreSQL portal API (SpiCursor::detach_into_name()+SpiClient::find_cursor()) for memory-bounded paged streaming. Peak memory is proportional topg_ripple.export_batch_size, not the full result size. The cursor survives across SPI sessions within the same transaction. -
HMAC-SHA256 signed Arrow Flight tickets (FLIGHT-01):
export_arrow_flight()now generates signed, expiring JSON tickets (type = "arrow_flight_v2"). Tickets includeiat,exp,aud,nonce, and an HMAC-SHA256 signature over a canonical string. New GUCs:pg_ripple.arrow_flight_secret(signing key,SIGHUP-level) andpg_ripple.arrow_flight_expiry_secs(default: 3600). -
Real Arrow IPC streaming in pg_ripple_http (FLIGHT-02):
POST /flight/do_getnow validates the HMAC-SHA256 ticket signature, expiry, and audience, then streams all VP main, delta, and rare tables for the requested graph as a binary Arrow IPC stream (application/vnd.apache.arrow.stream). Schema:s Int64, p Int64, o Int64, g Int64. TheARROW_FLIGHT_SECRETenvironment variable must matchpg_ripple.arrow_flight_secret. -
WCOJ explain metadata (WCOJ-01):
explain_sparql_jsonb()output now includes a"wcoj"block reportingcyclic_bgp_detected,wcoj_mode("planner_hint","disabled", or"not_applicable"),planner_settings, andfallback_reason. -
Streaming observability metrics (OBS-01): New
pg_ripple.streaming_metrics() → JSONBfunction returns live atomic counters:cursor_pages_opened,cursor_pages_fetched,cursor_rows_streamed,arrow_batches_sent,arrow_ticket_rejections,citus_brin_summarise_completed. -
Citus BRIN summarise SQL API (CITUS-04): New
pg_ripple.citus_brin_summarise_all() → BIGINTfunction runsbrin_summarize_new_valueson every promoted VP main-partition table. On Citus deployments usesrun_command_on_shards; on non-Citus deployments runs locally. Returns total shards/tables updated. -
Feature status updated:
arrow_flightmoves fromstubtoexperimentalinpg_ripple.feature_status().
Dependencies added
hmac = "0.12",sha2 = "0.10",hex = "0.4"(in pg_ripple extension for ticket signing)arrow = "55"(in pg_ripple_http for Arrow IPC serialization)hmac = "0.12",sha2 = "0.10",hex = "0.4"(in pg_ripple_http for ticket validation)
[0.65.0] — 2026-04-28 — CONSTRUCT Writeback Correctness Closure
Implements the v0.65.0 roadmap: real delta maintenance, HTAP-aware retraction, exact provenance capture, parameterized rule catalog writes, observability, pipeline status API, and the full CWB behavior test matrix.
SPARQL CONSTRUCT writeback rules are one of pg_ripple's most powerful features — they allow teams to express raw-to-canonical data transformation pipelines as SPARQL CONSTRUCT queries that automatically keep derived graphs up to date. Version 0.65.0 makes these rules fully automatic for the first time. Previously, after any write to a source graph, users had to manually call refresh_construct_rule() to propagate changes to dependent target graphs. Now, source graph inserts and deletes automatically trigger incremental derivation and DRed-style retraction in the same transaction, with no manual intervention required. Derived triples are always consistent with their sources immediately after every write.
HTAP split storage receives a correctness fix for retraction: the retract_exclusive_triples() function now correctly handles the case where derived triples live in either the delta or the main partition, using direct deletes for delta-resident triples and tombstones for main-resident triples to prevent silent retraction failures after a merge cycle. A parameterized SPI fix eliminates potential injection through catalog writes, and mode validation ensures only valid pipeline modes are accepted at rule creation time. A new pipeline status API lets operators check the current derivation state of every registered CONSTRUCT rule — whether it is fully derived, partially derived, or needs a full refresh — through observable SQL function calls rather than internal catalog queries.
What's new
-
Delta maintenance kernel (CWB-FIX-01/02): Source graph inserts and deletes now automatically update dependent CONSTRUCT target graphs in the same transaction.
insert_triple()triggers incremental derivation;delete_triple_from_graph()triggers DRed-style rederive-then-retract. Manualrefresh_construct_rule()is no longer required for routine operation. -
HTAP-aware promoted-predicate retraction (CWB-FIX-03):
retract_exclusive_triples()now correctly handles VP tables in HTAP split mode (delta + main + tombstones). Delta-resident derived triples are deleted directly; main-resident derived triples receive tombstones — preventing silent retraction failures after merge. -
Exact provenance capture (CWB-FIX-04): Provenance is recorded via
INSERT ... ON CONFLICT DO NOTHING RETURNINGCTEs, capturing only rows inserted by the current rule run. Pre-existingsource = 1triples from other rules or manual inserts are no longer mis-attributed. -
Parameterized SPI and mode validation (CWB-FIX-05): All catalog writes in
create_construct_ruleuseSpi::run_with_args(parameterized) for scalar fields. Mode values are validated — only'incremental'and'full'are accepted. -
Shared-target reference-count semantics (CWB-FIX-06): Two or more rules can write the same derived triple to the same graph. Dropping or refreshing one rule preserves triples still supported by another rule's provenance row.
-
Observability columns (CWB-FIX-07):
_pg_ripple.construct_rulesgains five new columns:last_incremental_run,successful_run_count,failed_run_count,last_error,derived_triple_count.list_construct_rules()exposes all health fields. -
Full CWB test matrix (CWB-FIX-08):
tests/pg_regress/sql/construct_rules.sqlnow covers: create/initial derivation, incremental insert, DRed delete, refresh from scratch, self-cycle rejection, two-rule pipeline stratification, mutual-cycle rejection, drop-with-retract, drop-without-retract, shared target preservation, explain output, pipeline status, and apply_for_graph. -
SHACL rule bridge foundation (CWB-FIX-09):
feature_status()forshacl_sparql_ruleupdated to note that the derivation kernel foundation is delivered; full routing deferred to v0.66.0. -
Pipeline introspection API (CWB-FIX-10): New
pg_ripple.construct_pipeline_status() → JSONBfunction returns dependency graph, rule order, last run state, derived triple counts, and failed/stale flags for all rules. -
apply_construct_rules_for_graph(graph_iri TEXT) → BIGINT: New public function for manual incremental maintenance of all rules sourcing a given graph. Returns total derived triple count. -
Feature status promoted:
construct_writebackmoves frommanual_refreshtoimplementedinpg_ripple.feature_status().
[0.64.0] — 2026-04-27 — Release Truth and Safety Freeze
Implements the v0.64.0 roadmap: feature-status SQL API, deep /ready readiness, GitHub Actions SHA pinning, Docker release digest integrity, documentation truth pass, roadmap evidence scripts, API drift checks, just assess-release, release evidence dashboard, and optional-feature degradation semantics guide.
A system that claims to have capabilities it does not actually have is more dangerous than one that honestly admits its limitations, because users build on false foundations. Version 0.64.0 introduces a formal truthfulness and evidence culture for pg_ripple: a new feature_status() SQL function returns one row per major capability with an honest status value drawn from a defined taxonomy — implemented, experimental, planner_hint, manual_refresh, stub, degraded, or planned — making it impossible for the project to accidentally overstate its capabilities. The /ready readiness endpoint is extended to include a feature status snapshot so that deployment automation and operators can make informed decisions about what the system can actually do right now.
Reproducibility and supply chain security are enforced with equal rigor. All GitHub Actions workflow steps are pinned to full 40-character commit SHAs with human-readable tag comments, and a CI script rejects any mutable reference that could allow a compromised or renamed action to execute different code in a future run. Docker release image digests are required to be present in the release artifact — a missing digest fails the release. Three tooling improvements automate ongoing quality: check_roadmap_evidence.sh flags completion claims without evidence, check_api_drift.sh verifies that every exported SQL function appears in documentation, and just assess-release runs the entire quality gate suite as a single command.
What's new
-
pg_ripple.feature_status()(TRUTH-01): New SQL function returning one row per major capability with an honest status value. Status taxonomy:implemented,experimental,planner_hint,manual_refresh,stub,degraded,planned. Reports honest statuses for Arrow Flight (stub), WCOJ (planner_hint), SHACL-SPARQL rules (planned), CONSTRUCT writeback (manual_refresh), Citus SERVICE pruning (planned), and all other major features. -
Deep
/readyreadiness (TRUTH-02): Extendedpg_ripple_http /readyto include PostgreSQL version, extension version, and a feature-status snapshot. The response body now includespartial_features(all non-implementedfeatures) anddegraded_features(features withstubordegradedstatus). -
GitHub Actions SHA pinning (TRUTH-03): All third-party
uses:references in.github/workflows/are now pinned to full 40-character commit SHAs with the human-readable tag as a comment. New CI stepscripts/check_github_actions_pinned.shrejects mutable refs (@v6,@stable, branch names) — zero mutable refs permitted. -
Docker release digest integrity (TRUTH-04): Removed
continue-on-error: truefrom Docker build/push. Release job now captures the immutable image digest from the build step, fails if no digest is produced, and scansghcr.io/trickle-labs/pg-ripple@sha256:...(the immutable digest) instead of a mutable tag. -
Documentation truth pass (TRUTH-05): Corrected
plans/implementation_plan.mdpgrx 0.17 → 0.18 throughout. Updated README "What works today" to reflect v0.63.0. Added "Known limitations in v0.63.0" section to README covering Arrow Flight, WCOJ, SHACL rules, CONSTRUCT writeback, Citus pruning, and optional dependencies. -
Roadmap evidence check script (TRUTH-06):
scripts/check_roadmap_evidence.sh— advisory lint that flags completion-claim bullet points in CHANGELOG without evidence markers (CI test name, docs path, SQL function reference). Advisory-only in v0.64.0; will be enforced in v0.67.0. -
API drift check script (TRUTH-07):
scripts/check_api_drift.sh— extracts exported function names from#[pg_extern]annotations insrc/and checks that each appears in at least one documentation file. Advisory-only; catches the v0.63 Citus signature drift pattern. -
just assess-release(TRUTH-08): One-command release quality gate that runs migration headers lint, GitHub Actions pinning lint, SECURITY DEFINER lint, roadmap evidence check, API drift check, and version sync check. Optionally generates a release evidence report withjust assess-release VERSION. -
Release evidence dashboard (TRUTH-09):
scripts/generate_release_evidence.shgeneratestarget/release-evidence/<version>/summary.jsonandsummary.md. Release workflow uploads the artifact to GitHub Actions and attaches it to the GitHub release. -
Degradation semantics guide (TRUTH-10): New documentation page
docs/src/reference/degradation.mddocuments expected degraded behavior, return values, warning codes, readiness behavior, and planned implementation milestones for every optional feature. -
pg_regress test
feature_status.sql: Asserts thatfeature_status()returns rows, all statuses are from the approved taxonomy, and specific partial features report honest status values.
[0.63.0] — 2025 — SPARQL CONSTRUCT Writeback Rules
Implements the v0.63.0 roadmap: SPARQL CONSTRUCT writeback rules (CWB-01 through CWB-11), raw-to-canonical pipelines, incremental delta maintenance via Delete-Rederive (DRed), pipeline stratification with cycle detection, and Citus scalability improvements CITUS-30 through CITUS-37.
Data integration pipelines often need to maintain multiple representations of the same information — raw sensor data needs to be canonicalized into a standard schema, external API responses need to be normalized to internal IRIs, and inferred relationships need to be materialized for query performance. Version 0.63.0 introduces SPARQL CONSTRUCT writeback rules, which express these transformation pipelines as persistent SPARQL CONSTRUCT queries registered in the knowledge graph. When source data changes, the system automatically re-evaluates the registered rules and updates the derived target graphs. Pipeline stratification detects dependency order using topological sort, and circular dependencies are rejected at rule registration time with a clear error.
The CONSTRUCT writeback engine includes full Delete-Rederive support: when a source triple is deleted, the engine correctly retracts only those derived triples that were exclusively supported by the deleted triple, leaving in place any derived triples still supported by other facts. Validation at rule registration catches blank nodes in CONSTRUCT templates, unbound variables, non-CONSTRUCT queries, and self-referential graphs before they can cause runtime failures. Three new Citus scalability functions round out the release: SERVICE result shard pruning, HyperLogLog availability detection, and per-worker BRIN index summarization — each addressing distributed query bottlenecks that emerge as VP tables are sharded across Citus workers.
What's new
-
CONSTRUCT writeback rules (CWB-01 to CWB-11): New SQL API
pg_ripple.create_construct_rule(name, sparql, target_graph, mode)registers a SPARQL CONSTRUCT query as a persistent writeback rule. Derived triples are stored in the target named graph withsource = 1. Supporting functions:drop_construct_rule,refresh_construct_rule,list_construct_rules,explain_construct_rule. -
Catalog tables:
_pg_ripple.construct_rules(rule registry) and_pg_ripple.construct_rule_triples(provenance index) created lazily on first use and also via the migration script. -
Pipeline stratification:
compute_rule_orderperforms Kahn's topological sort on the rule dependency graph; mutual-recursion (cycles) are rejected at registration time with a clear error. -
Validation at registration: blank nodes in CONSTRUCT templates, unbound variables, non-CONSTRUCT queries, and self-referential graphs (target == source) are all rejected with informative error messages.
-
Citus CITUS-30 — SERVICE result shard pruning: New function
pg_ripple.service_result_shard_prune(endpoint TEXT, graph_iri TEXT) RETURNS BIGINT. Returns the count of remote triples from the endpoint, or -1 if pruning is not applicable. -
Citus CITUS-32 — HyperLogLog
COUNT(DISTINCT): New functionpg_ripple.approx_distinct_available() RETURNS BOOLEAN. Returns whether the HyperLogLog extension is available for approximate distinct counting. -
Citus CITUS-37 — per-worker BRIN summarise: New SRF
pg_ripple.brin_summarize_vp_shards() RETURNS TABLE(table_name TEXT, pages_summarized BIGINT). Summarises BRIN indexes on all VP main-partition tables.
Migration
Run ALTER EXTENSION pg_ripple UPDATE TO '0.63.0' or apply sql/pg_ripple--0.62.0--0.63.0.sql manually. The migration creates the _pg_ripple.construct_rules and _pg_ripple.construct_rule_triples catalog tables.
[0.62.0] — 2025 — Query Frontier
Implements the v0.62.0 roadmap: Apache Arrow Flight bulk export, Leapfrog-Triejoin WCOJ planner integration, visual graph explorer in pg_ripple_http, tiered dictionary, Citus vp_rare vacuum, distributed inference dispatch, live shard rebalance, multi-hop pruning carry-forward, and cargo deny / cargo audit CI gates.
The most capable features of a knowledge graph system are often the ones that were too expensive to run before a key algorithmic or engineering investment. Version 0.62.0 makes three previously impractical capabilities practical. Apache Arrow Flight bulk export generates a signed ticket that the HTTP companion can use to stream all triples in a named graph as a binary columnar data stream — enabling large-scale knowledge graph exports to analytical platforms at memory bandwidth speeds. The Leapfrog Triejoin WCOJ integration detects cyclic Basic Graph Patterns (triangles, cliques, and other cyclic structures) and activates the worst-case optimal join algorithm, providing sub-second execution for query patterns that previously ran for minutes as the graph grew.
An interactive graph explorer is served directly from the HTTP companion at /explorer, rendering a force-directed D3.js visualization of graph data that domain experts can browse and explore without writing SPARQL queries. On the distributed scalability side, this release introduces Citus HyperLogLog COUNT(DISTINCT) for scalable approximate aggregates, a live non-blocking shard rebalance function, multi-hop subject ID carry-forward for pruning efficiency, per-graph vacuum for the rare-predicate table, and a tiered dictionary with configurable cold-tier eviction. cargo deny license and advisory checking plus cargo audit vulnerability scanning are added as required CI gates, bringing supply chain security to parity with the code quality gates already in place.
What's new
-
Apache Arrow Flight bulk export (Q-1): New SQL function
pg_ripple.export_arrow_flight(graph_iri TEXT) RETURNS BYTEA. Returns a JSON-encoded Flight ticket that the Arrow Flight server can use to stream all triples from the named graph in Arrow IPC format. -
WCOJ planner integration (Q-2): The BGP translator now detects cyclic Basic Graph Patterns (≥ 3 variables, ≥ 3 triple patterns) and activates the Leapfrog-Triejoin algorithm via
SET pg_ripple.enable_wcoj = onpreamble before executing the query. Provides sub-second execution for formerly intractable cyclic graph patterns. -
Visual graph explorer (Q-3):
pg_ripple_httpnow serves a force-directed interactive graph visualizer at/explorer. The SPA fetches graph data and renders it as a D3.js force layout with node-label tooltips. -
Arrow Flight
/flight/do_getendpoint (Q-4):pg_ripple_httpacceptsPOST /flight/do_getwith a Flight ticket body and responds with a JSON stub; the full streaming Arrow IPC implementation is wired in when thearrow-flightfeature is enabled. -
Citus CITUS-25 — vacuum_vp_rare (CITUS-25): New SRF
pg_ripple.vacuum_vp_rare() RETURNS TABLE(predicate_id BIGINT, rows_removed BIGINT). Removes dead entries from_pg_ripple.vp_rarethat reference predicates no longer in the predicate catalog. -
Citus CITUS-26 — tiered dictionary (CITUS-26): Added
access_count BIGINT NOT NULL DEFAULT 0column to_pg_ripple.dictionary. New GUCpg_ripple.dictionary_tier_threshold(default:-1, disabled). When positive, entries whoseaccess_countfalls below the threshold may be evicted to a cold tier. -
Citus CITUS-27 — distributed inference dispatch (CITUS-27): New GUC
pg_ripple.datalog_citus_dispatch(default:off). When enabled, the Datalog executor distributes each rule-stratum evaluation across Citus worker nodes. -
Citus CITUS-28 — live shard rebalance (CITUS-28): New SRF
pg_ripple.citus_live_rebalance() RETURNS TABLE(source_node TEXT, target_node TEXT, shard_id BIGINT, shard_size_bytes BIGINT). Initiates a non-blocking shard rebalance and streams per-shard progress. -
Citus CITUS-29 — multi-hop pruning carry-forward (CITUS-29): New GUC
pg_ripple.citus_prune_carry_max(default:1000). The newShardPruneSetandprune_hop()implementation carry the subject-ID set forward across triple-pattern hops, eliminating worker fan-out for multi-hop patterns. -
CI quality gates (Q-5):
cargo deny check(license/advisory/duplicate crate check) andcargo audit(vulnerability scan) are now required CI steps.
[0.61.0] — 2025 — Ecosystem Depth & Polish
Implements the v0.61.0 roadmap: per-graph access control, GDPR right-to-erasure, inference explainability, SHACL-AF rule execution, dbt adapter, OTLP traceparent propagation, richer federation stats, Citus scalability improvements (object pruning, direct-shard bulk-load, graph shard affinity), and test quality improvements.
Enterprise knowledge graph deployments require features that go well beyond query execution — they need access control, compliance tooling, provenance tracking, and integration with the broader data platform ecosystem. Version 0.61.0 delivers a rich set of these capabilities. Per-named-graph access control allows row-level security policies to be installed for individual named graphs, restricting which database roles can query which portions of the knowledge graph. A complete GDPR right-to-erasure implementation removes all traces of a subject across VP tables, the dictionary, KGE embeddings, provenance graphs, and audit logs in a single transactional call that returns a per-relation deletion count for the compliance record.
AI and data engineering integrations receive significant investment. An OTLP traceparent propagation path creates unbroken distributed traces from the HTTP load balancer through the companion service all the way into query execution, enabling millisecond-precision performance debugging across the full request stack. A dbt adapter published as a Python package gives data engineers SPARQL-aware model, source, and ref macros that mix SQL and SPARQL transformations in a single dbt project with full lineage tracking. Inference explainability via explain_inference() walks the derivation chain for any inferred triple, returning the full rule firing history as a structured JSON tree. Three Citus scalability improvements — object-based shard pruning, direct-shard bulk load, and named-graph shard affinity — extend the distributed query performance work to cover object-bound queries and named graph locality.
What's new
-
Per-named-graph access control (6.3 / L-8.1): Added
pg_ripple.grant_graph(graph_iri, role)andpg_ripple.revoke_graph(graph_iri, role)helper functions that install / remove PostgreSQL RLS policies filtering by graph ID. -
GDPR right-to-erasure (6.7 / L-8.3): New SRF
pg_ripple.erase_subject(iri TEXT) → TABLE(relation TEXT, rows_deleted BIGINT). Removes all triples about the subject across all VP tables,vp_rare, the dictionary (if unreferenced), KGE embeddings, the PROV-O provenance graph, and the audit log — in a single transaction. Returns a per-relation deletion count for the erasure record. -
Inference explainability (6.6): New SRF
pg_ripple.explain_inference(s, p, o, g) → TABLE(depth INT, rule_id TEXT, source_sids BIGINT[], child_triples JSONB). Returns the full derivation chain for a given inferred triple as a JSON tree, walking the_pg_ripple.rule_firing_logtable introduced in this release. -
SHACL-AF
sh:ruleexecution (D7-1 / D-3 / S4-8): Implemented the bridge insrc/shacl/af_rules.rsthat compilessh:TripleRulepatterns to Datalog rules and loads them viaload_rules_text(). New pg_regress testshacl_af_rule_execution.sqlvalidates end-to-end execution. Emits PT482 when ash:rulebody cannot be compiled into a Datalog rule and is skipped. -
dbt adapter (6.11): Published
dbt-pg-ripplePython package inclients/dbt-pg-ripple/. Providessparql_model,sparql_source, andsparql_refSPARQL-aware dbt macros. Data engineers can now mix SQL and SPARQL transformations in a single dbt project with full lineage tracking. -
OTLP traceparent propagation (I7-1):
pg_ripple_httpnow extracts the W3Ctraceparentheader and forwards it via thepg_ripple.tracing_traceparentsession GUC into the extension. Every SPARQL/Datalog query span is tagged with the originating trace ID, giving an unbroken trace from the load balancer through the HTTP service into the query engine. -
OpenTelemetry semantic-convention map (I7-2): New doc
docs/src/operations/observability-otel.mdwith span-name → attribute table and example Prometheus/Grafana queries. -
Federation call stats per endpoint (I7-3):
pg_ripple.federation_call_stats()now returns(endpoint TEXT, calls INT, errors INT, blocked INT, p50_ms INT, p95_ms INT, last_error_at TIMESTAMPTZ). -
BRIN summarize failure tracking (F7-3): Added
brin_summarize_failures INTcolumn to_pg_ripple.predicates. Persistentbrin_summarize_new_values()failures are promoted fromdebug1toNOTICEafter the second consecutive merge cycle failure; counter resets on success. -
Citus object-based shard pruning (CITUS-20): Extended
src/citus.rswithTermRoleenum (Subject | Object) andprune_bound_term(term_id, role). The SPARQL translator now detects bound objects and routes to the correct shard, delivering the same 10–100× speedup that subject-pruning already provides. -
Citus direct-shard bulk-load path (CITUS-21): Added
batch_insert_encoded_shard_direct()insrc/storage/mod.rs. When Citus sharding is enabled, bulk-load batches are written directly to the physical shard table, bypassing coordinator routing. Falls back to the coordinator path when Citus is not installed or the predicate is invp_rare. -
Citus named-graph shard affinity (CITUS-22): New table
_pg_ripple.graph_shard_affinity. New functionspg_ripple.set_graph_shard_affinity(graph_iri, shard_id)andpg_ripple.clear_graph_shard_affinity(graph_iri). When a SPARQL query includes aGRAPH <g> { ... }scope and Citus sharding is enabled, the planner restricts the query to the designated shard. -
GROUP BY subject aggregate push-down audit (CITUS-23): New pg_regress test
citus_aggregate_pushdown.sqlverifying that SPARQLGROUP BY ?squeries emitGROUP BY sin SQL, confirming Citus partial-aggregation push-down. -
Temporal RDF post-merge correctness (J7-3): New pg_regress test
temporal_rdf_post_merge.sqlverifyingpoint_in_time()resolves correctly after SIDs move from delta to main. -
OWL 2 RL deletion proof (6.13 / E7-2): New pg_regress test
datalog_owl_rl_deletion.sqlexercising the full DRed retraction path. -
DRed cycle guard (E7-2): New pg_regress test
datalog_dred_cycle.sqlconstructing asameAscycle and asserting PT530 is raised or the system remains stable. -
SPARQL Entailment Regimes test driver (6.8 / B7-2): New
tests/sparql_entailment/directory with manifest, runner script, and RDFS/OWL 2 RL fixtures. Added asentailment-suiteCI job (informational,continue-on-error: true). -
Conformance thresholds config (J7-4): Moved pass-rate thresholds from CI YAML expressions into
tests/conformance/thresholds.json. CI now reads this file to determine gate criteria. -
Cypher/GQL ADR (K7-4): New
plans/cypher.mdcapturing the design intent: target query subset,cypher-parsercrate, rewrite-to-SPARQL strategy, and semantic fidelity notes. -
PT404 error code for HTTP body-size rejection (H7-6):
pg_ripple_httpnow wraps axum's 413 response in a JSON envelope{"error": "PT404", "message": "..."}.
Schema changes
- New table
_pg_ripple.graph_shard_affinity(CITUS-22) - New table
_pg_ripple.rule_firing_log(inference explainability) - New column
_pg_ripple.predicates.brin_summarize_failures INT(F7-3)
Migration
Upgrade from v0.60.0 with ALTER EXTENSION pg_ripple UPDATE.
[0.60.0] — 2026-04-27 — Production Hardening Sprint
Implements the v0.60.0 roadmap: HTAP merge atomic swap, CI supply-chain hardening, three new fuzz harnesses, /ready Kubernetes readiness probe, SERVICE SILENT circuit-breaker test, architecture diagram refresh, pg_trickle dependency matrix, and pg_dump round-trip test.
Production deployments expose failure modes that development and staging environments never reveal, and version 0.60.0 systematically addresses the most dangerous ones. The most critical fix is the HTAP merge atomic rename-swap: the previous merge implementation had a race window where the VP view's backing relation could briefly be absent during a merge cycle, causing "relation does not exist" errors under concurrent query load. The new implementation uses an ACCESS EXCLUSIVE-locked rename-swap that atomically replaces the old main partition with the new one, eliminating the race entirely. A chaos test that hammers VP views with continuous SPARQL queries during merge cycles validates that zero such errors occur across a 60-second soak test.
Three new fuzz harnesses expand the adversarial input surface to cover GeoSPARQL WKT geometry parsing, R2RML mapping documents in Turtle format, and the LLM prompt sanitizer — specifically asserting that no injection markers survive sanitization regardless of the input. A Docker CVE scan gate blocks release if HIGH or CRITICAL vulnerabilities are found in the image. A Kubernetes readiness probe endpoint (/ready) lets orchestration platforms distinguish between a starting server and a fully operational one, preventing traffic from being routed to the container before it is ready to serve queries. A pg_dump round-trip CI test verifies that the extension can be dumped and restored without data loss, providing confidence for the upgrade path.
What's new
-
HTAP merge atomic rename-swap (F7-1): Replaced the
DROP TABLE … CASCADE → RENAME → CREATE OR REPLACE VIEWsequence insrc/storage/merge.rswith anACCESS EXCLUSIVE-locked rename-swap (main → main_old → drop,main_new → main). The VP view's backing relation is now never absent during a merge cycle, eliminating the race that causedrelation does not existerrors under concurrent query load. -
Merge-cutover chaos test (J7-1): New
tests/concurrent/merge_cutover_chaos.shthat hammers the VP view with continuous SPARQL queries while the merge worker churns for 60 seconds. Zerorelation does not existerrors required. -
Rare-predicate promotion concurrency test (F7-2): New
tests/concurrent/promotion_race.shdriving two parallel sessions acrossvp_promotion_threshold. Asserts exactly one VP table is created for a concurrently-promoted predicate. -
Merge-throughput trend artifact (F7-4): Added
benchmarks/merge_throughput_history.csvto track p50/p95 TPS per release. -
GitHub Actions SHA pinning (H7-1): All external Actions in
.github/workflows/*.ymlare tracked by Dependabot (package-ecosystem: github-actions, already configured). Release workflow updated with Trivy CVE scan gate. -
SECURITY DEFINER CI lint (H7-2): Updated
scripts/check_no_security_definer.shto use an allowlist model —_pg_ripple.ddl_guard_vp_tables()is the only permitted use. Added as required CI step. -
Security doc clarification (H7-3): Updated
docs/src/reference/security.mdto correctly state that only the DDL event-trigger function usesSECURITY DEFINER; all other API functions areSECURITY INVOKER. -
Rust toolchain pin (N7-1/N7-2): Added
rust-toolchain.tomlpinning the stable channel. -
Docker CVE scan (N7-4): Added
aquasecurity/trivy-actionto the release workflow; fails if HIGH/CRITICAL CVEs are found inDockerfile.batteries. -
New fuzz harnesses (A7-1): Three new
cargo-fuzztargets —geosparql_wkt(WKT geometry parser),r2rml_mapping(Turtle-based R2RML documents),llm_prompt_builder(prompt sanitizer — asserts no injection markers survive). -
Removed false-positive
#[allow(dead_code)](A7-2):execute_with_savepointinsrc/datalog/parallel.rsis called fromcoordinator.rs; the suppression was a false positive and has been removed. -
SERVICE SILENT + circuit-breaker test (B7-4): Added pg_regress test asserting that
SERVICE SILENTcorrectly swallows PT605 (circuit-breaker-open) and returns the empty solution sequence per SPARQL 1.1 §8.3.1. -
/readyKubernetes readiness probe (H7-5): AddedGET /readytopg_ripple_http. Returns503until the first successful PostgreSQL connection, then200. Distinct from/health(liveness probe). -
Architecture diagram refresh (K7-1): Updated the Mermaid diagram in
docs/src/reference/architecture.mdto includesrc/citus.rs,src/tenant.rs,src/kge.rs,src/temporal.rs,src/sparql/sparqldl.rs,src/sparql/ql_rewrite.rs, and the/readyendpoint. -
pg_trickle dependency matrix (K7-2): Added a feature-matrix table to
README.mdlisting which features require pg_trickle vs. ship standalone. -
Citus rebalance example (K7-3): New
examples/citus_rebalance_with_trickle.sql— runnable walkthrough of a zero-downtime Citus shard rebalance with pg_ripple + pg_trickle. -
pg_dump round-trip CI test (6.14): Added
tests/integration/dump_restore.shas a CI-friendly entry point to the existingtests/pg_dump_restore.sh.
Migration
No schema changes. Upgrade from v0.59.0 with ALTER EXTENSION pg_ripple UPDATE.
[0.59.0] — 2026-04-26 — Citus Shard-Pruning, Rebalance Coordination & Explain
Implements the v0.59.0 roadmap: SPARQL shard-pruning for bound subject patterns, NOTIFY-based rebalance coordination, explain_sparql() Citus section, and citus_rebalance_progress().
Distributed query execution over Citus shard tables can be dramatically inefficient when every triple pattern fans out to all workers looking for a match. Version 0.59.0 introduces SPARQL shard pruning for bound subject patterns: when a SPARQL triple pattern has a specific subject IRI, pg_ripple encodes it to its dictionary integer, looks up which Citus shard owns that integer range, and routes the query directly to the correct worker — achieving a 10 to 100 times speedup for the common single-entity lookup pattern. The implementation gracefully falls back to full fan-out when Citus is not installed, making the optimization completely transparent for non-distributed deployments.
Rebalance coordination ensures that shard movement and SPARQL query processing remain consistent during Citus rebalancing operations: pg_notify signals at the start and end of each rebalance allow pg-trickle to suspend CDC slot polling during the window, preventing replication lag spikes caused by simultaneous rebalancing and slot drain. The SPARQL explain function gains a Citus section showing whether shard pruning was activated for a given query, which worker owns the pruned shard, and the estimated per-shard row count — giving operators the information they need to verify that shard pruning is working correctly in production. A comprehensive integration guide documents end-to-end Citus deployment, GUC configuration, shard pruning verification queries, and a complete rebalancing runbook.
What's new
-
SPARQL shard-pruning (CITUS-10): New shard-pruning infrastructure in
src/citus.rs. Whenpg_ripple.citus_sharding_enabled = on, bound subject IRIs in SPARQL triple patterns are encoded to their integer subject ID and mapped to the physical Citus shard table viapg_dist_shard. Helper functionscompute_shard_id(),prune_bound_subject(), andresolve_shard_table()implement the 10–100× speedup for queries likeSELECT ?p ?o WHERE { <http://example.org/Alice> ?p ?o }that previously fan-out to all workers. Gracefully falls back to full fan-out when Citus is not installed. -
Rebalance NOTIFY coordination (CITUS-11):
pg_ripple.citus_rebalance()now emitspg_notify('pg_ripple.merge_start', '{"context":"rebalance","pid":PID}')before acquiring the advisory fence lock andpg_notify('pg_ripple.merge_end', ...)after releasing it. pg-trickle v0.34.0 can use these signals to suspend per-worker slot polling during rebalancing. -
explain_sparql() Citus section (CITUS-12): New 3-arg overload
pg_ripple.explain_sparql(query text, analyze bool, citus bool) → jsonb. Whencitus = true, the returned JSONB includes a"citus"key showingavailable,pruned_to_shard,worker,full_fanout_avoided, andestimated_rows_per_shard. Returns{"available": false}when Citus is not installed. -
Rebalance progress reporting (CITUS-13): New function
pg_ripple.citus_rebalance_progress()returning(shard_id, from_node, to_node, status)rows frompg_dist_rebalance_progress(Citus 10+). Returns empty set when Citus is not installed. -
Citus + pg_ripple + pg-trickle integration guide (CITUS-15): New page
docs/src/citus_integration.mdwith end-to-end deployment, GUC configuration, shard-pruning verification, and rebalancing runbook.
Migration
No schema changes. Shard-pruning activates automatically when pg_ripple.citus_sharding_enabled = on and Citus is detected.
ALTER EXTENSION pg_ripple UPDATE TO '0.59.0';
[0.58.0] — 2026-05-14 — Temporal RDF, SPARQL-DL, Citus Sharding & PROV-O
Implements the v0.58.0 roadmap: Temporal RDF point-in-time queries, SPARQL-DL OWL axiom routing, Citus horizontal sharding of VP tables, PROV-O provenance tracking, v1 readiness integration test suite, and CI gate hardening.
Four significant capabilities converge in version 0.58.0 to make pg_ripple a more complete platform for enterprise data management. Temporal RDF queries allow any named graph to be queried at a specific past point in time: calling point_in_time() with a timestamp restricts all subsequent SPARQL queries to triples that existed at that moment, using a BRIN-indexed timeline table to efficiently filter by statement ID. This makes it possible to answer audit and compliance questions like "what did our supplier network look like on the date of the contract?" without maintaining separate historical copies of the graph.
SPARQL-DL extends the query engine to route OWL vocabulary patterns — owl:subClassOf, owl:equivalentClass, owl:disjointWith, owl:inverseOf — directly to VP table data rather than maintaining a separate in-memory index, making TBox querying consistent with the rest of the query architecture. Citus horizontal sharding ships as an opt-in feature with full VP table distribution, dictionary and predicate catalog as reference tables, and a merge fence lock that coordinates with pg-trickle during rebalancing. W3C PROV-O provenance tracking automatically emits structured provenance triples for every bulk-load operation, recording what data was loaded, when, and from which source — giving compliance teams an auditable history of every dataset ingested into the knowledge graph.
What's new
-
Temporal RDF (L-1.3): New functions
pg_ripple.point_in_time(ts TIMESTAMPTZ),pg_ripple.clear_point_in_time(), andpg_ripple.point_in_time_info(). A new_pg_ripple.statement_id_timelinetable (SID → TIMESTAMPTZ, BRIN-indexed) is populated by an AFTER INSERT trigger on every VP delta table. Callingpoint_in_time()sets a session-local_pg_ripple.pit_thresholdGUC that restricts SPARQL queries to triples inserted before the given timestamp. -
SPARQL-DL (L-1.4): New functions
pg_ripple.sparql_dl_subclasses(TEXT)andpg_ripple.sparql_dl_superclasses(TEXT)route OWL vocabulary BGPs (owl:subClassOf,owl:equivalentClass,owl:disjointWith,owl:inverseOf) to the VP table T-Box data rather than synthesising a separate in-memory index. New modulesrc/sparql/sparqldl.rs. -
Citus horizontal sharding (L-5.4): New GUCs
pg_ripple.citus_sharding_enabled(bool, default off),pg_ripple.citus_trickle_compat(bool, default off), andpg_ripple.merge_fence_timeout_ms(int, default 0). New functionspg_ripple.enable_citus_sharding(),pg_ripple.citus_rebalance(),pg_ripple.citus_cluster_status(), andpg_ripple.citus_available(). Whencitus_sharding_enabled = on, VP tables getREPLICA IDENTITY FULLbeforecreate_distributed_table()(C-9 fix). Dictionary and predicates catalog become reference tables. Merge worker acquires an advisory fence lock during rebalancing and emitspg_ripple.merge_start/merge_endNOTIFYs. -
PROV-O provenance (L-8.4): New GUC
pg_ripple.prov_enabled(bool, default off). When enabled, every bulk-load operation (load_ntriples,load_turtle,load_nquads) emits PROV-Oprov:Activity+prov:Entitytriples into the named graph<urn:pg_ripple:prov>and updates_pg_ripple.prov_catalog. New functionspg_ripple.prov_stats()andpg_ripple.prov_enabled(). -
v1 readiness integration test suite (J-6): New
tests/integration/v1_readiness/directory with four shell test scripts:crash_recovery.sh,concurrent_writes.sh,upgrade_chain.sh, andregress_mismatch_audit.sh. Run viatests/integration/v1_readiness/run_all.sh. -
CI gate hardening (J-5): Four new pg_regress test files:
temporal_rdf,sparql_dl,citus_sharding,prov_triples. All new tests pass in CI without Citus installed.
Migration
New tables and trigger function are installed automatically on first use of _PG_init. For existing installations, run:
ALTER EXTENSION pg_ripple UPDATE TO '0.58.0';
[0.57.0] — 2026-05-07 — Reasoning Platform & AI Integration
Implements the v0.57.0 roadmap: OWL 2 EL/QL reasoning profiles, Knowledge-Graph Embeddings (TransE/RotatE), entity alignment via HNSW ANN search, LLM-augmented SPARQL repair, automated ontology mapping, multi-tenant graph isolation, columnar VP storage guard, adaptive index advisor, and probabilistic Datalog GUC.
Reasoning over ontologies and integrating with AI systems are two of the highest-value use cases for knowledge graphs, and version 0.57.0 delivers a comprehensive platform for both. OWL 2 EL and QL reasoning profiles are implemented as built-in Datalog rule sets, making it possible to activate different levels of ontological reasoning with a single load_rules_builtin() function call. Knowledge-graph embeddings using TransE and RotatE store entity representations in a pgvector HNSW index, enabling embedding-based entity alignment to propose owl:sameAs candidates across different graphs using cosine similarity search. An LLM-augmented SPARQL repair function sends broken queries to any OpenAI-compatible endpoint and returns a suggested fix.
Multi-tenant graph isolation provides database-level separation between tenants with quota-enforcing triggers that prevent any single tenant from consuming unbounded resources. An adaptive index advisor monitors actual access patterns and recommends index changes based on real query behavior rather than developer intuition. An automated ontology mapping function proposes owl:sameAs and property alignment candidates between two graphs using either lexical similarity (Jaccard over tokenized labels) or embedding cosine similarity — reducing what would otherwise be days of manual ontology alignment work to a function call. The probabilistic Datalog GUC lays the configuration foundation for the full confidence propagation and noisy-OR reasoning engine delivered in v0.87.0.
What's new
-
OWL 2 EL profile (L-3.1): New built-in rule set
'owl-el'with core EL rules (prp-some,cls-int1/2,cls-uni,cls-svf1/avf, subsumption propagation). New GUCpg_ripple.owl_profile = 'EL'.load_rules_builtin('owl-el')activates EL-profile reasoning. -
OWL 2 QL profile (L-3.2): New built-in rule set
'owl-ql'with DL-Lite rewriting rules (SubClassOf,SubObjectPropertyOf,InverseOf). New modulesrc/sparql/ql_rewrite.rsrewrites SPARQL BGPs before SQL translation whenpg_ripple.owl_profile = 'QL'. -
Knowledge-Graph Embeddings (L-4.1): New GUCs
pg_ripple.kge_enabled(bool, default off) andpg_ripple.kge_model(text, default'transe'). New table_pg_ripple.kge_embeddings (entity_id BIGINT PRIMARY KEY, embedding vector(64), model TEXT, trained_at TIMESTAMPTZ)with HNSW index. New SRFpg_ripple.kge_stats(). -
Entity alignment (L-4.2): New function
pg_ripple.find_alignments(source_graph, target_graph, threshold, limit)— uses cosine similarity over KGE embeddings to propose cross-graphowl:sameAscandidates. -
LLM SPARQL repair (L-4.3): New function
pg_ripple.repair_sparql(query TEXT, error_message TEXT)— sends broken query + schema digest to LLM endpoint and returns a suggested fix. Sanitizes input against null-bytes, 32 KiB cap, and prompt-injection markers. -
Automated ontology mapping (L-4.4): New function
pg_ripple.suggest_mappings(source_graph, target_graph, method)—'lexical'mode uses Jaccard similarity over tokenizedrdfs:labelvalues;'embedding'mode uses KGE cosine similarity. -
Multi-tenant graph isolation (L-5.3): New table
_pg_ripple.tenants. New functionspg_ripple.create_tenant(),pg_ripple.drop_tenant(),pg_ripple.tenant_stats(). Quota-enforcing triggers per tenant graph. -
Columnar VP storage guard (L-2.1): New GUC
pg_ripple.columnar_threshold(int, default -1 = disabled). When set, the merge worker can convertvp_{id}_mainto columnar storage viapg_columnarwhen triple count exceeds the threshold. Raises PT534 ifpg_columnaris unavailable. -
Adaptive index advisor (L-2.2): New module
src/storage/index_advisor.rswithrun_index_advisor_cycle(). New GUCpg_ripple.adaptive_indexing_enabled(bool, default off). Tracks index creation events in_pg_ripple.catalog_events(newpredicate_idcolumn). -
Probabilistic Datalog GUC (L-3.4): New GUC
pg_ripple.probabilistic_datalog(bool, default off). Foundation for Markov-Logic-style soft rules with@weight(FLOAT)annotations.
New error codes
| Code | Level | Meaning |
|---|---|---|
| PT545 | ERROR | Tenant quota exceeded: triple count for a named graph exceeds the per-tenant quota set by create_tenant(). |
| PT560 | ERROR | repair_sparql: input SPARQL query exceeds the 32 KiB maximum length limit. |
| PT561 | ERROR | repair_sparql: input error_message exceeds the 4 KiB maximum length limit. |
Migration
Run ALTER EXTENSION pg_ripple UPDATE or apply sql/pg_ripple--0.56.0--0.57.0.sql.
[0.56.0] — 2026-04-30 — Standards Completeness & Operational Depth
Implements the v0.56.0 roadmap: GeoSPARQL 1.1 geometry functions, federation circuit breaker, SPARQL audit log, DDL event trigger, BRIN re-summarize after merge, SID sequence runway monitor, incremental RDFS closure mode, R2RML direct mapping, lz4 dictionary compression, dead-code audit, and deprecated GUC removal.
A production knowledge graph system needs more than query correctness — it needs comprehensive operational tooling that lets administrators manage, protect, and monitor the system reliably. Version 0.56.0 delivers a wide range of operational capabilities. The federation circuit breaker prevents cascading failures when a remote SERVICE endpoint becomes unavailable: after five consecutive failures, the circuit opens and returns a fast error rather than blocking each query for a full network timeout, with automatic recovery after a configurable reset period. A SPARQL audit log records every update operation with role, transaction ID, operation type, and full query text, giving compliance teams an immutable record of all data modification activity.
GeoSPARQL 1.1 support is expanded with five new geometry functions covering spatial containment, intersection, buffer, convex hull, and envelope operations, enabling complex spatial queries over geographic knowledge graphs. An R2RML direct mapping function reads a W3C R2RML 2012 mapping document and executes it in one call, automatically generating triples from relational database queries without writing transformation code. A DDL event trigger watches for attempts to drop VP tables outside the extension's maintenance functions and emits an error with instructions, preventing accidental structural damage. An incremental RDFS closure mode limits schema-property inference to new merges rather than recomputing the full closure on every write, dramatically reducing inference overhead for large datasets with stable schemas.
What's new
-
GeoSPARQL 1.1 additions (L-1.1): New filter predicates
geof:within→ST_Withinandgeof:intersects→ST_Intersects. New value functionsgeof:buffer,geof:convexHull,geof:envelope,geo:asWKT, andgeo:hasSpatialAccuracy. -
Federation circuit breaker (G-3): Thread-local
CircuitBreakerstate machine per endpoint URL. Opens afterpg_ripple.federation_circuit_breaker_thresholdconsecutive failures (default: 5), resets afterpg_ripple.federation_circuit_breaker_reset_seconds(default: 60 s). Returns PT605 while open. -
SPARQL audit log (H-3): New table
_pg_ripple.audit_logpopulated whenpg_ripple.audit_log_enabled = on. Records SPARQL UPDATE operations (role, txid, operation, query). New SQL functions:pg_ripple.audit_log()andpg_ripple.purge_audit_log(before TIMESTAMPTZ). -
DDL event trigger (I-2):
_pg_ripple.ddl_guard_vp_tables()event trigger function and_pg_ripple_ddl_guardevent trigger. Emits PT511 warning and inserts into_pg_ripple.catalog_eventswhen VP tables are dropped outside maintenance functions. -
BRIN re-summarize after merge (F-7): The merge worker calls
brin_summarize_new_values()on the main VP table BRIN index after the atomic rename step, keeping BRIN statistics current. -
SID runway monitor (F-3): New SQL function
pg_ripple.sid_runway()returns(current_value, max_value, insert_rate_per_day, years_remaining)estimating how long beforestatement_id_seqwraps. -
Incremental RDFS closure (L-3.3): New
pg_ripple.inference_mode = 'incremental_rdfs'value. After each merge,run_incremental_rdfs_for_predicate()is called for RDFS schema predicates only, avoiding full-graph re-inference on every write. -
R2RML direct mapping (L-7.3): New SQL function
pg_ripple.r2rml_load(mapping_iri TEXT) → BIGINT. Reads a W3C R2RML 2012 mapping document already loaded in the store, executes the mapped SQL queries, and bulk-inserts the generated triples. -
lz4 dictionary compression (L-2.4):
ALTER TABLE _pg_ripple.dictionary ALTER COLUMN value SET COMPRESSION lz4applied at install and in the migration script. Reduces storage for long IRIs and literal strings on PG18 builds with lz4 support. -
Dead-code audit (A-6):
telemetry.rs,federation_planner.rs, andfilter_expr.rscleaned up. Removed unused functionsinline_int_arithandinline_int_divide; added per-item#[allow(dead_code)]annotations with explanations for planned-but-not-yet-wired APIs. -
Remove deprecated
property_path_max_depthGUC (S2-5): The alias GUCpg_ripple.property_path_max_depthintroduced in v0.24.0 is removed. Usepg_ripple.max_path_depth(the canonical name) instead.
Schema changes
- New table
_pg_ripple.audit_log - New table
_pg_ripple.catalog_events - New function
_pg_ripple.ddl_guard_vp_tables() RETURNS event_trigger - New event trigger
_pg_ripple_ddl_guard ON sql_drop ALTER TABLE _pg_ripple.dictionary ALTER COLUMN value SET COMPRESSION lz4
Migration
Run ALTER EXTENSION pg_ripple UPDATE or apply sql/pg_ripple--0.55.0--0.56.0.sql.
[0.55.0] — 2026-04-24 — Security Hardening, Observability & Developer Experience
Implements the v0.55.0 roadmap: federation SSRF protection, Unicode normalization, tombstone GC optimization, SPARQL-star annotation tests, SHACL snapshot semantics, Datalog dead-code cleanup, pg_ripple_http OpenAPI spec and VoID/Service endpoints, parallel concurrent insert tests, and comprehensive error catalog additions.
Security and developer experience improvements compound over time — each hardening measure reduces the attack surface, and each developer experience improvement accelerates the pace of building on the platform. Version 0.55.0 introduces SSRF protection for the federation layer: all federation endpoints are validated against a deny policy that blocks private, loopback, and link-local addresses by default, preventing an attacker from using a crafted SERVICE URL to make pg_ripple issue unauthorized internal network requests. Unicode NFC normalization on IRI ingestion prevents homoglyph attacks and encoding inconsistencies from causing triples that look identical to be stored as separate dictionary entries.
The developer experience receives several meaningful improvements. The pg_ripple_http companion now serves an OpenAPI 3.1 specification at /openapi.yaml, making it easy to generate client SDKs in any language with a spec-driven generator. W3C VoID and SPARQL Service Description endpoints give data consumers machine-readable descriptions of the dataset and query capabilities. A comprehensive error catalog adds eighteen new error codes with documentation, a lint script that verifies all codes are documented, and a CI job that enforces this requirement on every push. SHACL validation reports now include a WAL LSN captured at validation start, enabling precise correlation between a validation result and the exact database state it was computed against.
What's new
-
Federation SSRF allowlist (G-1/H-1): New GUCs
pg_ripple.federation_endpoint_policy(default:default-deny) andpg_ripple.federation_allowed_endpoints. Thecheck_endpoint_policy()guard blocks private/loopback/link-local addresses unless the policy isopen. PT606 errors emitted for blocked endpoints. -
Federation call stats (G-4): New
pg_ripple.federation_call_stats()SRF returning(calls, errors, blocked)from in-memory atomic counters. Counters are updated byexecute_remote()and reset on postmaster restart. -
Unicode NFC normalization (C-1): New bool GUC
pg_ripple.normalize_iris(default:on). When enabled, all IRIs and blank nodes are NFC-normalized before dictionary encoding. Requires the newunicode-normalizationcrate dependency. -
COPY RDF path allowlist (C-2): New GUC
pg_ripple.copy_rdf_allowed_paths(comma-separated path prefixes). When set,load_*_file()functions reject paths not matching an allowed prefix with PT480. -
Tombstone GC optimization (F-2): When
pg_ripple.tombstone_retention_seconds = 0, the merge worker nowTRUNCATEs the tombstones table after a successful merge instead of issuing aDELETE … WHERE i <= $1. Also recordstombstones_cleared_atin the predicates catalog. Migration script adds thetombstones_cleared_at TIMESTAMPTZcolumn. -
LLM API key warning (H-2): New assign hook for
pg_ripple.llm_api_key_envemits aWARNINGif the value looks like a raw API key rather than an environment-variable name. Security documentation added todocs/src/reference/security.md. -
pg_ripple_http OpenAPI spec (K-1): Added
utoipaandutoipa-scalardependencies.GET /openapi.yamlreturns the OpenAPI 3.1 specification for the HTTP service. -
pg_ripple_http VoID and Service Description (L-7.2/L-7.4):
GET /voidreturns a Turtle VoID dataset description with triple counts;GET /servicereturns a W3C SPARQL Service Description document. -
Health endpoint enriched (I-3):
GET /healthnow returns structured JSON includingversion,git_sha,postgres_connected,postgres_version, andlast_query_ts. -
SHACL validation snapshot LSN (D-2): The
run_validate()JSON report now includesvalidation_snapshot_lsn(WAL LSN captured at validation start) so consumers can correlate reports with a specific database state. -
DESCRIBE strategy documentation (B-2):
docs/src/reference/sparql-compliance.mdnow documents all fourdescribe_strategyvalues (cbd,scbd,simple) with definitions, examples, and a comparison table. -
SPARQL-star annotation tests (B-4): New pg_regress test
tests/pg_regress/sql/sparql_star_annotation.sqlwith expected output covering the full annotation pattern (load, query, filter, provenance, nested annotations, CONSTRUCT). -
Merge/vector CI baseline gates (F-5/F-6):
.github/workflows/benchmark.ymlnow includes merge throughput and vector recall baseline gate steps that compare measured performance againstbenchmarks/merge_throughput_baselines.json. -
Crash recovery test (J-2): New
tests/crash_recovery/merge_kill.shtests SIGKILL during merge with tombstone table recovery. -
Concurrent write test (J-3): New
tests/concurrent/parallel_insert.shlaunches N parallel psql sessions each inserting a disjoint triple set and verifies no writes are lost or duplicated. -
Logical replication example (K-2): New
examples/replication_setup.sqlwith annotated walkthrough of primary + replica setup usingpg_ripple.replication_enabled = on. -
sh:path helper audit (D-1): Audited
values_for_path_iriinsrc/shacl/constraints/property_path.rs— allShPathvariants are handled correctly; updated#[allow(dead_code)]documentation. -
Datalog dead-code cleanup (E-2/E-3): Removed module-level
#![allow(dead_code)]fromdred.rsandcompiler.rs; functions genuinely unused now have per-function#[allow(dead_code)]with explanatory comments. -
Savepoint safety (E-1):
execute_with_savepointwired into coordinator'sexecute_stratum_batch, ensuring each stratum evaluates within a savepoint to protect against partial-evaluation failures. -
New GUCs:
pg_ripple.federation_endpoint_policy,pg_ripple.federation_allowed_endpoints,pg_ripple.tombstone_retention_seconds,pg_ripple.normalize_iris,pg_ripple.copy_rdf_allowed_paths,pg_ripple.read_replica_dsn. -
Error catalog additions (I-1): Added PT440, PT480, PT481, PT510, PT511, PT530, PT543, PT550, PT606(SSRF), PT607, PT620, PT621, PT640, PT642, PT711, PT712, PT800.
scripts/check_pt_codes.shpasses (35 codes documented). CI joblint-pt-codesadded. -
CI improvements:
jena-suiteandowl2rl-suitenow run withcontinue-on-error: false(must pass). -
Orphaned test cleanup (J-1): Removed empty
tests/pg_regress/expected/test.txt.
Migration
The sql/pg_ripple--0.54.0--0.55.0.sql migration script:
- Adds
tombstones_cleared_at TIMESTAMPTZto_pg_ripple.predicates - No other schema changes (all new features are Rust function changes or GUC additions)
[0.54.0] — 2026-04-24 — High Availability & Logical Replication
Implements the v0.54.0 roadmap: RDF logical replication, batteries-included Docker image, Kubernetes Helm chart, CloudNativePG extension image volume, and vector-index performance benchmarks.
Enterprise deployments require robust availability guarantees that can survive individual server failures. Version 0.54.0 introduces RDF logical replication: a background worker subscribes to a PostgreSQL logical replication slot, receives N-Triples batches from the primary, and applies them to the replica via the standard load path. Conflict resolution uses a configurable last_writer_wins strategy per statement ID, and a replication_stats() function exposes the current slot state, replication lag in bytes, and last applied LSN, giving operations teams the monitoring data they need to verify the replica is keeping up without falling behind.
A batteries-included Docker image with pg_ripple, PostGIS, and pgvector pre-installed eliminates the friction of setting up a local development environment or staging cluster. A CloudNativePG extension image enables zero-build deployment on Kubernetes with the CloudNativePG operator — no custom PostgreSQL image required, just point the operator at the extension image. A Helm chart deploys the complete stack as a StatefulSet with configurable storage, load balancer service, Prometheus probes, and federation endpoint configuration. A vector-index comparison benchmark covering HNSW versus IVFFlat at single, half, and binary precision provides concrete guidance for choosing the right vector index type for embedding-based similarity search workloads.
What's new
-
RDF logical replication (
src/replication.rs): Newpg_ripple.logical_apply_workerbackground worker (enabled viapg_ripple.replication_enabled = on) that subscribes to thepg_ripple_pubpublication, receives N-Triples batches, and applies them viaload_ntriples()in order. Conflict resolution:last_writer_winsper SID, configurable viapg_ripple.replication_conflict_strategy. -
pg_ripple.replication_stats(): New SRF that exposes the current replication slot state —slot_name,lag_bytes,last_applied_lsn,last_applied_at. Returns a single NULL row when replication is disabled. -
New GUCs:
pg_ripple.replication_enabled(bool, default off) andpg_ripple.replication_conflict_strategy(text, defaultlast_writer_wins). -
_pg_ripple.replication_statuscatalog table: Created by the migration script; tracks pending N-Triples batches delivered by the logical replication slot for the apply worker to consume. -
Batteries-included Docker image (
docker/Dockerfile.batteries): Buildsghcr.io/trickle-labs/pg-ripple:<version>with pg_ripple, PostGIS 3.4.3, and pgvector 0.7.4 pre-installed. All four extensions load without conflicts. Published to GHCR on every release via GitHub Actions. -
CloudNativePG extension image (
docker/Dockerfile.cnpg): Publishesghcr.io/trickle-labs/pg-ripple:<version>-cnpg— a minimal image containing compiled.soand SQL files at/var/lib/postgresql/extension-files/for use with CloudNativePG operator ≥ 1.24. No custom PostgreSQL image build required. -
CloudNativePG
Clustermanifest example (examples/cloudnativepg_cluster.yaml): Annotated manifest referencingspec.postgresql.extensionImagesfor zero-build CNP deployment. -
CI smoke test (
tests/cloudnativepg_image_smoke.sh): Builds the extension image locally and verifies the expected files are present at the correct paths. -
Kubernetes Helm chart (
charts/pg_ripple/): Deploys the batteries-included image as aStatefulSetwith configurablereplicaCount,persistence(PVC),http.service(LoadBalancer/ClusterIP),federationEndpoints,shacl.shapesConfigMap,llm.apiKeySecret. Liveness and readiness probes viapg_isready. -
Vector-index comparison benchmark (
benchmarks/vector_index_compare.sql): 100 k-embedding fixture measuring index build time and ANN recall/latency for{hnsw, ivfflat}×{single, half, binary}. Reference results published indocs/src/reference/vector-index-tradeoffs.md. -
docker-compose.ymlupdated: Now uses the batteries-included image by default with example SPARQL queries that exercise GeoSPARQL (PostGIS) and vector search (pgvector). -
Documentation (
docs/src/):operations/replication.md— architecture overview, setup walkthrough, lag monitoring, failover procedureoperations/docker.md— batteries-included image quickstart and configuration referenceoperations/kubernetes.md— Helm deployment guide, values reference, Prometheus integrationoperations/cloudnativepg.md— step-by-step CNP setup, manifest walkthrough, upgrade procedureoperations/high-availability.md— HA topology decision tree and trade-offs tablereference/vector-index-tradeoffs.md— HNSW vs IVFFlat benchmark results and recommendations
Migration
Run ALTER EXTENSION pg_ripple UPDATE TO '0.54.0'; or use the supplied migration script sql/pg_ripple--0.53.0--0.54.0.sql.
[0.53.0] — 2026-05-08 — DX, Extended Standards & Architecture
Implements the v0.53.0 roadmap: SHACL-SPARQL constraints, COPY rdf FROM, RAG pipeline hardening, CDC lifecycle events, fuzz coverage expansion, WatDiv gate promotion, and merge-throughput baselines.
Developer experience improvements that save time on routine tasks compound across every team that uses a platform. Version 0.53.0 delivers three substantial improvements. SHACL-SPARQL constraints allow validation rules to be expressed as SPARQL SELECT queries that are executed with the focus node bound as $this — any non-empty result triggers a violation — making it possible to express complex validation logic that cannot be captured in standard SHACL Core constraints. A new pg_ripple.copy_rdf_from() function loads RDF files in any supported format directly from server-side paths, eliminating the need to pipe file content through the SQL wire protocol for large datasets. A RAG pipeline cache with a one-hour TTL makes repeated rag_context() calls fast without re-running expensive vector recall and graph expansion operations.
CDC lifecycle events allow applications to react in real time to merge worker completions: a pg_notify signal carries the predicate ID, merged triple count, and tombstone count at the end of each successful merge cycle, making it easy to build reactive architectures that trigger downstream processing when new batches of data become available. Three new fuzz targets exercise the RDF/XML parser, JSON-LD framer, and HTTP query-string parsing paths with arbitrary byte sequences. The WatDiv benchmark suite is promoted to a required CI gate, meaning that no change degrading any of the 32 WatDiv query templates can be merged. Merge throughput baseline measurements anchor the performance regression gate for future releases.
What's new
-
SHACL-SPARQL constraint component (
src/shacl/): Implementssh:SPARQLConstraintComponent(W3C SHACL-SPARQL). A newSparqlConstraintvariant onShapeConstraintstores a SPARQL SELECT query; during validation the query is executed with$thisbound to the focus-node IRI. Any non-empty result set generates aViolation. The parser now recognisessh:sparqlpredicates in node and property shapes. -
pg_ripple.copy_rdf_from(path, format)(src/dict_api.rs): New SQL function that loads RDF triples from a server-side file. Supported formats:ntriples,nquads,turtle,trig,rdfxml. Returns the number of triples inserted. -
RAG pipeline hardening (
src/llm/mod.rs,src/schema.rs):rag_context()now (1) validates and sanitises input (null-byte rejection, 16 KiB length cap), (2) looks up results in_pg_ripple.rag_cache(1-hour TTL) before running inference, and (3) stores results in the cache after computation. The_pg_ripple.rag_cachetable is created by the schema initialiser and migration script. -
CDC lifecycle events (
src/storage/merge.rs): The HTAP merge worker now emitspg_notify('pg_ripple_cdc_lifecycle', payload)at the end of each successful merge cycle. The JSON payload contains{"op":"merge","predicate_id":N,"merged":M,"tombstones":T}. Clients canLISTEN pg_ripple_cdc_lifecycleto receive real-time merge notifications. -
New fuzz targets (
fuzz/fuzz_targets/): Three new cargo-fuzz targets:rdfxml_parser(RDF/XML via rio_xml),jsonld_framer(JSON-LD framing via serde_json),http_request(HTTP query-string and URI parsing via url). Dependenciesrio_xml,serde_json, andurladded tofuzz/Cargo.toml. -
WatDiv suite gate promoted (
.github/workflows/ci.yml): Changedwatdiv-suitejob fromcontinue-on-error: truetocontinue-on-error: false. The WatDiv benchmark suite is now a required CI gate. -
Merge-throughput baselines (
benchmarks/merge_throughput_baselines.json): Added reference p50/p95 throughput measurements formerge_workers ∈ {1,2,4,8}to anchor the benchmark regression gate. -
Error codes PT480 / PT481 (
src/error.rs): PT480 warns whensh:ruleis detected but SHACL-AF inference is off; PT481 is emitted when a SHACL-SPARQL constraint query fails to execute. -
GUC subsystem split (
src/gucs/):src/gucs.rsrefactored into seven focused modules:storage,sparql,datalog,shacl,federation,llm,observability. -
filter.rs split (
src/sparql/translate/filter/):filter.rssplit intofilter_dispatch(pattern dispatch utilities) andfilter_expr(SPARQL Expression → SQL compiler). -
Datalog coordinator / semi-naïve modules (
src/datalog/): Newcoordinator.rsandseminaive.rsdelegation modules. -
HTTP
unwrap()hardening (pg_ripple_http/src/main.rs): AllResponse::builder().unwrap()calls in hot-path handlers replaced withunwrap_or_else(|e| ...)that returns a structuredinternal_server_errorJSON response.
Migration
Run ALTER EXTENSION pg_ripple UPDATE TO '0.53.0'; or use the supplied migration script sql/pg_ripple--0.52.0--0.53.0.sql.
[0.52.0] — 2026-05-01 — pg-trickle Relay Integration
Implements the v0.52.0 roadmap: JSON→RDF pipeline, CDC bridge triggers, JSON-LD event serializer, outbox dedup keys, vocabulary alignment templates, and pg-trickle runtime detection with graceful degradation.
Real-world enterprise data integration requires a reliable, bidirectional bridge between the knowledge graph and the broader event-driven data platform. Version 0.52.0 introduces a complete CDC (change data capture) bridge for pg-trickle integration. Per-predicate AFTER INSERT triggers on VP delta tables decode dictionary IDs back to IRIs, serialize each triple as a JSON-LD event with a dedup key, and write it to a configured outbox table within the same transaction — ensuring that the event and the data modification are committed atomically. Downstream consumers can use pg-trickle to relay these events to Apache Kafka, AWS EventBridge, or any other event platform that pg-trickle supports.
A JSON-to-RDF pipeline converts any JSON object to N-Triples using a JSON-LD @context for key-to-IRI mapping, handling nested objects as blank nodes, arrays as repeated predicates, and all standard JSON value types. Four built-in vocabulary alignment templates provide ready-to-use Datalog rule sets for Schema.org to SAREF IoT sensor data, FHIR R4, PROV-O, and generic JSON-to-Schema.org mappings that can be loaded with a single function call. A runtime detection function checks at query time whether pg-trickle is installed and enabled, with graceful degradation and clear error codes for deployments where pg-trickle has not been installed, so applications can handle both configurations without code changes.
What's new
-
JSON → RDF pipeline (
src/bulk_load.rs): Newpg_ripple.json_to_ntriples(payload JSONB, subject_iri TEXT, type_iri TEXT, context JSONB) RETURNS TEXTconverts any JSON object to N-Triples using an optional@vocabcontext for key-to-IRI mapping. Handles nested objects (blank nodes), arrays (repeated predicates), and plain string values.json_to_ntriples_and_load()combines conversion and load in one call. -
CDC bridge triggers (
src/storage/cdc_bridge.rs): Newpg_ripple.enable_cdc_bridge_trigger(name, predicate, outbox)installs a per-predicateAFTER INSERTtrigger on the VP delta table that decodes dictionary IDs and writes a JSON-LD event with a dedup key to the specified outbox table within the same transaction.disable_cdc_bridge_trigger(name)removes it.cdc_bridge_triggers()SRF lists all registered triggers. -
JSON-LD event serializer (
src/export.rs): Newpg_ripple.triple_to_jsonld(s, p, o BIGINT) RETURNS JSONBdecodes a single triple from dictionary IDs and returns a JSON-LD object.triples_to_jsonld(subject BIGINT)performs a star-pattern scan for all triples of a subject and returns a grouped JSON-LD node. -
Outbox dedup key (
src/storage/mod.rs): Newpg_ripple.statement_dedup_key(s, p, o BIGINT) RETURNS TEXTlooks up the statement ID (icolumn) for a triple and returns'ripple:{sid}'as a relay-compatible dedup key. Returns NULL when the triple does not exist. -
Vocabulary alignment templates (
sql/vocab/): Four built-in Datalog rule sets loadable viapg_ripple.load_vocab_template(name TEXT) RETURNS INT:schema_to_saref— Schema.org ↔ SAREF IoT sensor data alignmentschema_to_fhir— Schema.org ↔ FHIR R4 basic resources (Patient, Observation)schema_to_provo— Schema.org ↔ PROV-O provenance ontologygeneric_to_schema— generic JSON key → Schema.org property heuristics
-
pg-trickle runtime detection (
src/views_api.rs,src/cdc_bridge_api.rs):pg_ripple.trickle_available() RETURNS BOOLreturnstruewhen bothpg_ripple.trickle_integration = onand thepg_trickleextension is installed. Bridge functions raise SQLSTATE PT800 when pg-trickle is absent or integration is disabled. -
New GUCs (
src/gucs.rs):pg_ripple.cdc_bridge_enabled(bool, default off),pg_ripple.cdc_bridge_batch_size(int, default 100),pg_ripple.cdc_bridge_flush_ms(int, default 200),pg_ripple.cdc_bridge_outbox_table(text),pg_ripple.trickle_integration(bool, default on). -
CDC bridge catalog (
_pg_ripple.cdc_bridge_triggers): New catalog table records all registered CDC bridge triggers with columns(name, predicate_id, outbox_table, created_at).
[0.51.0] — 2026-04-23 — Security Hardening & Production Readiness
Completes the v0.51.0 roadmap: SPARQL DoS protection (PT440), OWL 2 RL 100% conformance, SPARQL CSV/TSV output, SHACL complex path traversal, per-predicate workload stats, OTLP tracing wiring, non-root Docker container, blocking cargo-audit on PRs, SBOM generation, and comprehensive operational tooling.
The transition from a feature-complete system to a production-ready one is defined by the hardening, monitoring, and operational tooling that surrounds the core functionality. Version 0.51.0 delivers a comprehensive production readiness pass. SPARQL DoS protection adds configurable maximum algebra depth and triple pattern count limits that reject pathologically complex queries at parse time before they can consume server resources. OWL 2 RL conformance reaches 100% — all 66 rules pass — with four previously failing rules fixed: property chains, bidirectional subclass equivalence, owl:sameAs plus owl:differentFrom consistency, and XSD numeric type hierarchy entailment.
SPARQL CSV and TSV output formats are added for compatibility with data science pipelines that consume tabular result formats rather than SPARQL Results JSON. A blocking cargo audit gate on every pull request prevents dependencies with known security vulnerabilities from entering the codebase. SBOM generation as part of every release provides a machine-readable bill of materials for supply chain security audits. A non-root Docker container, per-predicate workload statistics, OTLP tracing endpoint wiring, and storage cache invalidation on vacuum complete the operational hardening. A just release recipe and new scripts for SQL format linting, migration header checking, and error code validation ensure the release process is repeatable and verifiable.
What's new
-
SPARQL DoS protection (PT440) (
src/sparql/mod.rs): New GUCspg_ripple.sparql_max_algebra_depth(default 256) andpg_ripple.sparql_max_triple_patterns(default 4096). Queries exceeding these limits are rejected at parse time with error code PT440. Set to 0 to disable. -
Complete OWL 2 RL conformance (
src/datalog/builtins.rs): Fixed four previously failing rules:prp-spo2(3-hop property chains),scm-sco(bidirectional subClassOf → equivalentClass),eq-diff1(sameAs + differentFrom → owl:Nothing),dt-type2(XSD numeric type hierarchy). The OWL 2 RL gate is now 66/66 (100%) and blocking. -
SPARQL CSV/TSV output (
src/sparql_api.rs): Newpg_ripple.sparql_csv(query TEXT)andpg_ripple.sparql_tsv(query TEXT)SRFs returning W3C SPARQL 1.1 CSV/TSV formatted results. -
SHACL complex property path traversal (
src/shacl/constraints/property_path.rs): The previously disabledtraverse_sh_path()function is now wired into the SHACL property shape dispatcher. Supports inverse, alternative, sequence,sh:zeroOrMorePath,sh:oneOrMorePath, andsh:zeroOrOnePath. -
Correct CONSTRUCT ground RDF-star quoted triples (
src/sparql/mod.rs): Ground quoted triples in CONSTRUCT templates now emit correct N-Triples-star notation<< s p o >>instead of being silently dropped. -
Per-predicate workload statistics (
src/stats_admin.rs): Newpg_ripple.predicate_workload_stats()SRF backed by_pg_ripple.predicate_statstable. Returns(predicate_iri, query_count, merge_count, last_merged)per predicate. -
OTLP tracing endpoint (
src/telemetry.rs,src/gucs.rs): New GUCpg_ripple.tracing_otlp_endpointwires the"otlp"exporter to a configurable endpoint. Falls back to stdout when the endpoint is empty. -
Storage cache invalidation on vacuum (
src/storage/catalog.rs,src/lib.rs): Registered a PostgreSQL relcache invalidation callback viaCacheRegisterRelcacheCallbackso the backend-local VP table OID cache is automatically flushed when a relation is vacuumed. -
Merge worker latch-driven backoff (
src/worker.rs): The error-backoff sleep in the merge worker now usesBackgroundWorker::wait_latch()so the worker responds immediately to SIGTERM rather than sleeping the full backoff interval. -
Non-root Docker container (
Dockerfile): The container now runs asUSER postgres(v0.51.0 security hardening). -
Blocking cargo-audit on PRs (
.github/workflows/cargo-audit.yml):cargo audit --deny warningsnow runs on every pull request, not just the weekly schedule. -
SBOM generation (
.github/workflows/release.yml): Every release now includes a CycloneDX SBOM (sbom.json) attached to the GitHub release. -
New CI linting jobs (
.github/workflows/ci.yml):lint-sql-format(unsafe dynamic SQL),lint-migration-headers(migration script header checks),lint-cargo-duplicates(advisory duplicate dependency check). -
New scripts:
scripts/check_no_string_format_in_sql.sh,scripts/check_migration_headers.sh,scripts/check_pt_codes.sh. -
New tests:
tests/pg_dump_restore.sh,tests/pg_upgrade_compat.sh, pg_regresssparql_depth_limit.sql,sparql_csv_tsv.sql,shacl_complex_path.sql. -
New examples:
examples/llm_workflow.sql,examples/federation_multi_endpoint.sql,examples/cdc_subscription.sql. -
New docs:
docs/src/operations/cdc.md, expanded tuning guide. -
Justfile: Added
just release VERSIONandjust docs-serverecipes. -
Migration script:
sql/pg_ripple--0.50.0--0.51.0.sqlcreates_pg_ripple.predicate_statstable. -
Documentation: Updated AGENTS.md to reflect pgrx 0.18 (was incorrectly documented as 0.17).
[0.50.0] — 2026-04-23 — Developer Experience & GraphRAG Polish
Completes the v0.50.0 roadmap: explain_sparql(analyze:=true) interactive query debugger with cache_status and actual_rows; rag_context() full RAG pipeline; migration chain passes through v0.50.0.
The gap between a query running correctly and a developer understanding why it ran correctly is often wider than the gap between a query failing and a developer fixing it. Version 0.50.0 delivers a dramatically improved SPARQL query debugging experience: the explain function now reports cache status — "hit", "miss", or "bypass" — rather than a simple boolean, includes actual row counts from EXPLAIN ANALYZE when run in analyze mode, and correctly generates explain output for all four query types including DESCRIBE. This means that every slow query, every cache miss, and every unexpected result can now be traced through the complete execution path from SPARQL algebra to actual row counts.
The full GraphRAG pipeline is assembled and operational in this release. rag_context() runs a five-step pipeline: HNSW vector recall to find the most relevant entities, SPARQL 1-hop graph expansion to gather neighborhood context, JSON-LD assembly into a rich text context for LLM ingestion, and optional NL→SPARQL query execution to augment the context with targeted SPARQL results. The function degrades gracefully with a WARNING and empty return when pgvector is not installed, so applications that run on both vector-enabled and non-vector deployments can handle both cases without code branching. Two new documentation pages explain the explain output format in detail and walk through the complete RAG pipeline configuration with concrete examples.
What's new
-
Extended
pg_ripple.explain_sparql(query TEXT, analyze BOOL DEFAULT FALSE) RETURNS JSONB(src/sparql/explain.rs):- New
cache_statuskey:"hit"/"miss"/"bypass"— replaces the legacycache_hitboolean (which is kept for backward compatibility). - New
actual_rowskey (array): per-operator actual row counts extracted fromEXPLAIN ANALYZEJSON output whenanalyze = true. - DESCRIBE queries now return a valid JSONB document (algebra + synthetic SQL stub) instead of an error.
- EXPLAIN output now uses
FORMAT JSONfor structured parsing.
- New
-
pg_ripple.rag_context(question TEXT, k INT DEFAULT 10) RETURNS TEXT(src/llm/mod.rs): full five-step RAG pipeline:- HNSW vector recall — top-k entities by cosine similarity.
- SPARQL graph expansion — 1-hop neighbourhood via
contextualize_entity(). - JSON-LD context assembly — rich text context for LLM ingestion.
- (Optional) NL→SPARQL execution if
pg_ripple.llm_endpointis set.
- Degrades gracefully (WARNING + empty string) when pgvector is absent.
-
New pg_regress test:
sparql_explain_analyze.sql— asserts JSONB schema stability across SELECT, ASK, CONSTRUCT, and DESCRIBE query types. -
Documentation:
docs/src/user-guide/explain-sparql.md— EXPLAIN output format, ANALYZE mode, interpreting the algebra tree.docs/src/user-guide/rag-pipeline.md—rag_context()step-by-step, tuning k, combining with NL→SPARQL.
Migration
Run ALTER EXTENSION pg_ripple UPDATE TO '0.50.0' — no schema changes; new Rust functions are automatically available.
Technical details
- src/sparql/explain.rs —
explain_sparql_jsonb()extended:cache_statusfield ("hit"/"miss"/"bypass"),actual_rowsarray fromEXPLAIN ANALYZEJSON, DESCRIBE query stub generation,FORMAT JSONoutput mode - src/llm/mod.rs —
rag_context()five-step pipeline: HNSW recall → SPARQL expansion viacontextualize_entity()→ JSON-LD assembly → optional NL→SPARQL execution; graceful pgvector degradation path (WARNING + empty string) - tests/pg_regress/sql/sparql_explain_analyze.sql — JSONB schema stability assertions for SELECT, ASK, CONSTRUCT, and DESCRIBE query types;
cache_statusandactual_rowskey presence checks - docs/src/user-guide/explain-sparql.md — new; EXPLAIN output format reference, ANALYZE mode walkthrough, algebra tree interpretation guide
- docs/src/user-guide/rag-pipeline.md — new;
rag_context()step-by-step usage, k-tuning guidance, NL→SPARQL integration pattern - sql/pg_ripple--0.49.0--0.50.0.sql — comment-only; no schema changes required
[0.49.0] — 2026-04-23 — AI & LLM Integration
Completes the v0.49.0 roadmap: sparql_from_nl() NL-to-SPARQL via configurable LLM endpoint; suggest_sameas() and apply_sameas_candidates() for embedding-based entity alignment; four new GUCs; error codes PT700–PT702.
Natural language interfaces to knowledge graphs have historically required complex, hand-written translation logic. Version 0.49.0 introduces sparql_from_nl(), which converts a natural-language question to a SPARQL SELECT query using any OpenAI-compatible LLM endpoint. Few-shot examples stored with add_llm_example() improve translation quality for domain-specific queries, and when SHACL shapes are loaded, they are automatically included as context to help the LLM understand the graph structure. A mock endpoint mode allows full testing of the NL-to-SPARQL pipeline without requiring a live LLM service, making it possible to develop and test NL-to-SPARQL workflows in CI.
Embedding-based entity alignment provides a data quality tool that can identify likely duplicate entities across different graphs. suggest_sameas() runs a cosine similarity self-join over stored KGE embeddings and returns candidate owl:sameAs pairs above a configurable similarity threshold, while apply_sameas_candidates() materializes accepted pairs as actual owl:sameAs triples in the graph. The cluster size bound prevents runaway merging when data quality issues cause inadvertent large equivalence classes. Three new GUCs control LLM endpoint, model, API key environment variable, and shape inclusion, and a complete test suite exercises all error paths through mock endpoints to verify correct error code reporting.
What's new
-
pg_ripple.sparql_from_nl(question TEXT) RETURNS TEXT(src/llm/mod.rs): converts a natural-language question to a SPARQL SELECT query using any OpenAI-compatible LLM endpoint.- Set
pg_ripple.llm_endpoint = 'mock'for testing without a real LLM. add_llm_example(question, sparql)stores few-shot examples in_pg_ripple.llm_examples.- Error codes: PT700 (endpoint unreachable/not configured), PT701 (non-SPARQL response), PT702 (SPARQL parse failure).
- SHACL shapes included as additional context when
pg_ripple.llm_include_shapes = on.
- Set
-
pg_ripple.suggest_sameas(threshold REAL DEFAULT 0.9): HNSW cosine self-join on_pg_ripple.embeddings; returnsTABLE(s1 TEXT, s2 TEXT, similarity REAL)pairs above the threshold. Degrades gracefully when pgvector is unavailable. -
pg_ripple.apply_sameas_candidates(min_similarity REAL DEFAULT 0.95): inserts accepted pairs asowl:sameAstriples; respectssameas_max_cluster_size. Returns count of inserted triples. -
New GUCs:
pg_ripple.llm_endpoint,pg_ripple.llm_model,pg_ripple.llm_api_key_env,pg_ripple.llm_include_shapes. -
Schema change:
_pg_ripple.llm_examples (question TEXT PRIMARY KEY, sparql TEXT, created_at TIMESTAMPTZ).
Migration
Run ALTER EXTENSION pg_ripple UPDATE TO '0.49.0' — adds _pg_ripple.llm_examples and updates the schema version.
[0.48.0] — 2026-04-23 — SHACL Core Completeness, OWL 2 RL Closure & SPARQL Completeness
Completes the v0.48.0 roadmap: all 35 SHACL Core constraints implemented; complex sh:path expressions with recursive CTEs; OWL 2 RL rule-set closure (five new rules); SPARQL Update ADD/COPY/MOVE; SPARQL-star variable-inside-quoted-triple patterns; federation_max_response_bytes GUC; insert_triples() batch SRF; WatDiv baselines; pg-upgrade.md operations guide.
Full SHACL Core compliance is achieved in version 0.48.0 with the implementation of the final seven constraint types: string length bounds, exclusive-or shape logic (exactly-one-of), and four XSD numeric range constraints. Complex sh:path expressions with inverse, alternative, sequence, and recursive path operators are now compiled to efficient WITH RECURSIVE ... CYCLE SQL queries that correctly detect cycles. A violation report enhancement adds the decoded focus-node IRI and the W3C component IRI to every violation, making it possible for downstream tools to generate standard-conformant SHACL violation reports without additional dictionary lookups.
Five new OWL 2 RL rules close the gap toward complete conformance: full rdfs:subClassOf transitive closure, rdfs:subPropertyOf chains, inverse functional property owl:sameAs propagation, owl:allValuesFrom with subclass hierarchy chaining, and cardinality entailment rules. SPARQL Update ADD, COPY, and MOVE graph management operations are implemented through a pre-parser that handles these non-standard forms before the main SPARQL algebra translation. SPARQL-star variable-inside-quoted-triple patterns — where a variable appears as the subject, predicate, or object of a quoted triple — are now correctly handled with a dictionary join, enabling powerful provenance queries that traverse annotation layers. A batch triple insert SRF and WatDiv latency baselines round out the release.
What's new
-
Remaining SHACL Core constraints (
src/shacl/) — seven new constraints complete the 35/35 SHACL Core coverage:sh:minLength/sh:maxLength: string-length bounds applied after language-tag strippingsh:xone: exactly-one-of (XOR) logic over sub-shapes viacheck_xone()insrc/shacl/constraints/logical.rssh:minExclusive/sh:maxExclusive/sh:minInclusive/sh:maxInclusive: XSD-typed numeric range constraints viacompare_dictionary_valuesinsrc/shacl/constraints/relational.rs
-
Complex
sh:pathexpressions (src/shacl/constraints/property_path.rs) — fullShPathenum with SQL compiler:sh:inversePath:(o, s)join order on VP tablessh:alternativePath: SQL UNION of sub-paths- Sequence paths: chained JOIN compilation
sh:zeroOrMorePath,sh:oneOrMorePath,sh:zeroOrOnePath:WITH RECURSIVE … CYCLECTEs
-
SHACL violation report enhancements —
Violationstruct extended withsh_value(offending decoded value) andsh_source_constraint_component(W3C component IRI) fields for W3C-conformant violation reports. -
OWL 2 RL rule set completion (
src/datalog/builtins.rs) — five new rules close the v0.47.0 gap:cax-sco: fullrdfs:subClassOftransitive closureprp-spo1:rdfs:subPropertyOffull chainprp-ifp: inverse-functional-propertyowl:sameAspropagationcls-avf: chainedowl:allValuesFrom+ subclass hierarchyowl:minCardinality/owl:maxCardinality/owl:cardinalityentailment
-
SPARQL Update ADD / COPY / MOVE (
src/sparql/mod.rs) — pre-parsertry_execute_add_copy_move()handles all three graph management operations without depending on spargebra enum variants. pg_regress testsparql_update_add_copy_move.sql. -
SPARQL-star variable-inside-quoted-triple patterns (
src/sparql/translate/bgp.rs) —TermPattern::Triplearm now emits a JOIN with_pg_ripple.dictionaryonqt_s/qt_p/qt_ocolumns instead of silentFALSE. Patterns like<< ?s ?p ?o >> :assertedBy ?whoreturn rows. pg_regress testrdfstar_variable_quoted.sql. -
pg_ripple.federation_max_response_bytesGUC (src/gucs.rs,src/sparql/federation.rs) — maximum federation response body size in bytes (default: 100 MiB). Responses exceeding the limit are refused with error code PT543. -
pg_ripple.insert_triples(TEXT[])SRF (src/dict_api.rs) — batch single-triple inserts. Accepts a flatTEXT[]array with stride-3 (s, p, o) or stride-4 (s, p, o, g) grouping. ReturnsSETOF BIGINT(SIDs). Useful for orchestration tools that need to insert many triples in one call. -
WatDiv latency baselines (
tests/watdiv/baselines.json) — per-query p50/p95/p99 latency baseline file for all 32 WatDiv templates. CI regression gate warns on > 10% latency increase. -
HTAP merge throughput benchmark (
benchmarks/merge_throughput.sql) — 5-minute pgbench script for measuring insert throughput under concurrent merge cycles. -
docs/src/operations/pg-upgrade.md— new operations guide documenting the supported upgrade matrix, pre-upgrade steps, migration script chain, and dump/restore fallback.
Migration
sql/pg_ripple--0.47.0--0.48.0.sql — no schema changes.
[0.47.0] — 2026-04-22 — SHACL Completion, GUC Validators, Cache SRFs & Fuzz Hardening
Completes the v0.47.0 roadmap: sh:lessThanOrEquals SHACL constraint; six GUC check_hook validators; three individual cache hit-rate SRFs; SPARQL sqlgen.rs module split (≤800 lines); parallel Datalog SID pre-allocation wired; five new cargo-fuzz targets; CI security hygiene (cargo-audit workflow, deny.toml, check_no_security_definer.sh); OWL 2 RL baseline 93.9%; promotion-race stress test; four new SHACL pg_regress tests.
Operational reliability depends on catching configuration mistakes at the moment they are made rather than at the moment they cause a failure. Version 0.47.0 introduces check_hook validators for six GUC parameters, ensuring that invalid values for federation error policy, SPARQL overflow action, tracing exporter, and embedding precision are rejected at the SET command rather than silently accepted and then misinterpreted at runtime. Three individual cache hit-rate table-returning functions replace the previous JSONB blob, giving monitoring queries precise hit rates, miss counts, and eviction counts for each of the plan cache, dictionary cache, and federation cache independently.
The SPARQL SQL generation module, which had grown to 3,632 lines, is decomposed into eight focused translation modules covering BGP, filter, graph, group, join, left join, union, and distinct translation — reducing the maximum file size by 80% while keeping the public API identical. Five new fuzz targets cover the SPARQL parser, Turtle and N-Triples parsers, Datalog rule tokenizer, SHACL parser, and dictionary hash determinism. Weekly scheduled cargo audit with auto-created GitHub issues on failure, a license and advisory deny policy, and a check_no_security_definer.sh CI script collectively harden the supply chain and SQL security posture. A promotion-race stress test with 50 concurrent sessions validates SID uniqueness under heavy contention.
What's new
-
sh:lessThanOrEqualsSHACL constraint (src/shacl/constraints/shape_based.rs) — implementssh:lessThanOrEqualsper SHACL Core §4.4. For each focus node, checks that every value of the subject property is ≤ the corresponding value of the comparison property. Violations include"constraint": "sh:lessThanOrEquals". pg_regress testshacl_lt_or_equals.sqlcovers less-than, greater-than (violation), and equal-value cases. -
Six GUC check_hook validators (
src/lib.rs) —federation_on_error(warning|error|empty),federation_on_partial(empty|use),sparql_overflow_action(warn|error),tracing_exporter(stdout|otlp),embedding_index_type(hnsw|ivfflat),embedding_precision(single|half|binary) now reject invalid values at SET time with a standard PostgreSQL GUC rejection message. -
Individual cache hit-rate SRFs (
src/sparql_api.rs) — three new table-returning functions:pg_ripple.plan_cache_stats(),pg_ripple.dictionary_cache_stats(), andpg_ripple.federation_cache_stats(), each returning(hits BIGINT, misses BIGINT, evictions BIGINT, hit_rate DOUBLE PRECISION). The old JSONBplan_cache_stats()is superseded by the new table form; the combined JSONBcache_stats()is retained for backwards compatibility. -
SPARQL
sqlgen.rsmodule split (src/sparql/translate/) —sqlgen.rsreduced from 3,632 to 753 lines by extracting eight translation modules:bgp.rs,filter.rs,graph.rs,group.rs,join.rs,left_join.rs,union.rs,distinct.rs. Public API surface unchanged. -
Parallel Datalog SID pre-allocation (
src/datalog/mod.rs) —preallocate_sid_ranges()is now called at the start ofrun_inference_seminaive()whendatalog_parallel_workers > 1, eliminating sequence contention across parallel strata workers. -
Five new cargo-fuzz targets (
fuzz/fuzz_targets/) —sparql_parser.rs(spargebra),turtle_parser.rs(rio_turtle + NTriples),datalog_parser.rs(rule tokenizer),shacl_parser.rs(Turtle + sh: predicate dispatch),dictionary_hash.rs(XXH3-128 determinism assertion). -
CI security hygiene — weekly scheduled
cargo auditjob (.github/workflows/cargo-audit.yml) that auto-creates a GitHub issue on failure;deny.tomlwith licence allowlist and advisory deny policy;scripts/check_no_security_definer.shthat fails CI if anysql/*.sqlfile containsSECURITY DEFINER. -
OWL 2 RL conformance baseline (
docs/src/reference/owl2rl-results.md) — 62/66 rules pass (93.9%). Four known failures documented intests/owl2rl/known_failures.txtwith target fix versions. -
Promotion-race stress test (
tests/stress/promotion_race.sh) — 50 concurrent sessions inserting at the VP promotion threshold; verifies SID uniqueness and zero errors. -
Four new SHACL pg_regress tests —
shacl_closed.sql,shacl_unique_lang.sql,shacl_pattern.sql,shacl_lt_or_equals.sql— cover all four SHACL constraint families newly tested in v0.47.0.
Documentation
docs/src/reference/guc-reference.md— complete entries for all six new validated GUCs.docs/src/reference/owl2rl-results.md— new baseline document with pass-rate table and known-failure descriptions.
[0.46.0] — 2026-04-21 — Property-Based Testing, Fuzz Hardening & OWL 2 RL Conformance
Adds three property-based test suites (SPARQL round-trip, dictionary encode/decode, JSON-LD framing), a cargo-fuzz federation result decoder target, an OWL 2 RL conformance suite, TopN push-down optimisation, sequence range pre-allocation for parallel Datalog, BSBM regression gate, Rustdoc lint gate, HTTP companion CA-bundle support, and expanded worked examples.
Property-based testing catches a class of bugs that example-based tests miss: when a property must hold for all inputs, not just the ones a developer happened to think of when writing tests. Version 0.46.0 introduces three proptest suites running 10,000 cases each, covering SPARQL algebra round-trip stability across encoding and whitespace variations, XXH3-128 dictionary hash stability and collision resistance across 10,000 distinct terms, and JSON-LD framing round-trip correctness. A cargo-fuzz federation result decoder target feeds arbitrary byte sequences through the SPARQL XML results parser, asserting that malformed XML never produces a panic — only the appropriate error code.
A TopN push-down optimization embeds ORDER BY ... LIMIT N constraints directly in the generated SQL when no DISTINCT or OFFSET is present, letting the PostgreSQL planner use index scans and early termination rather than materializing the full result set before truncation. The W3C OWL 2 RL conformance suite adds a proper test adapter tracking progress toward the 95% pass rate target. Sequence range pre-allocation for parallel Datalog workers eliminates sequence contention by reserving a range of statement IDs per worker before parallel evaluation begins, improving throughput for multi-stratum inference on large graphs. A BSBM regression gate checks performance against 12 BSBM explore queries at 1-million-triple scale on every CI run, and HTTP companion CA-bundle support enables connections to endpoints with private or custom certificate authorities.
What's new
-
proptest integration (
tests/proptest/) — three property-based test suites run 10,000 cases each: SPARQL algebra round-trip stability (encoding and whitespace invariance), XXH3-128 dictionary encode stability and collision resistance (10,000 distinct terms, zero collisions), and JSON-LD framing round-trip correctness. -
cargo-fuzz federation result decoder (
fuzz/fuzz_targets/federation_result.rs) — fuzz target that feeds arbitrary byte sequences through the SPARQL XML results parser. Asserts no panic on malformed input; invalid XML produces PT542, never a crash. -
PT542
FederationResultDecoderError(src/error.rs) — new error code for unparseable XML/JSON in the federation result decoder. -
Datalog convergence regression suite (
tests/datalog_convergence_suite.rs) — verifies RDFS + OWL RL rule-set convergence within ≤ 20 iterations; derived triple counts checked against baselines stored intests/datalog_convergence/baselines.json. -
W3C OWL 2 RL conformance suite (
tests/owl2rl_suite.rs) — adapter parsesDatatypeEntailmentTest,ConsistencyTest, andInconsistencyTestmanifest types. Non-blocking CI job until ≥ 95% pass rate. Known failures tracked intests/owl2rl/known_failures.txt. -
TopN push-down (
src/sparql/sqlgen.rs) — whenORDER BY … LIMIT Nis present (noOFFSET, noDISTINCT) andpg_ripple.topn_pushdown = on, the LIMIT clause is embedded directly in the generated SQL rather than post-decode truncation.sparql_explain()output includes"topn_applied": true/false. -
pg_ripple.topn_pushdown(bool GUC, defaulton) — master switch for the TopN push-down optimisation. -
Sequence range pre-allocation (
src/datalog/parallel.rs) —preallocate_sid_ranges()atomically advances the global statement-ID sequence byN * batch_sizebefore launching parallel Datalog workers, eliminating sequence contention. -
pg_ripple.datalog_sequence_batch(integer GUC, default10000, min100) — SID range reserved per parallel Datalog worker per batch. -
BSBM regression gate (
benchmarks/bsbm/) — 12 BSBM explore queries at 1M-triple scale; latency baselines inbenchmarks/bsbm/baselines.json; CI warning on > 10% regression (non-blocking). -
Rustdoc lint gate (
src/lib.rs) —#![warn(missing_docs)]added; CI jobcargo docfails onmissing_docsfor public#[pg_extern]functions. -
HTTP companion CA-bundle (
pg_ripple_http/src/main.rs) —PG_RIPPLE_HTTP_CA_BUNDLEenv var: loads the PEM file at the given path as the TLS trust anchor for outbound connections. Falls back to the system trust store with an error log if the path is invalid or not a valid PEM bundle. -
Expanded worked examples (
examples/) — three end-to-end SQL scripts:shacl_datalog_quality.sql(SHACL + Datalog interaction),hybrid_vector_search.sql(vector similarity + SPARQL property paths),graphrag_round_trip.sql(GraphRAG export → Datalog annotation → re-import). -
Migration script (
sql/pg_ripple--0.45.0--0.46.0.sql) — comment-only; no schema changes.
GUC parameters added
| GUC | Type | Default | Description |
|---|---|---|---|
pg_ripple.topn_pushdown | bool | on | Push LIMIT N into the SQL plan for ORDER BY + LIMIT queries |
pg_ripple.datalog_sequence_batch | integer | 10000 | SID range reserved per parallel Datalog worker per batch |
New error codes
| Code | Severity | Message |
|---|---|---|
| PT542 | ERROR | Federation result decoder received unparseable XML/JSON |
Bug fixes
None.
Documentation
docs/src/user-guide/best-practices/sparql-performance.md— TopN push-down section with EXPLAIN exampledocs/src/reference/guc-reference.md— v0.46.0 section with two new GUC parametersdocs/src/reference/error-catalog.md— PT542 addeddocs/src/reference/contributing.md— proptest and cargo-fuzz sectionsdocs/src/reference/w3c-conformance.md— OWL 2 RL suite added to conformance table
[0.45.0] — 2026-04-21 — SHACL Completion, Datalog Robustness & Crash Recovery
Closes the last SHACL Core constraint gaps (sh:equals, sh:disjoint), adds decoded focus-node IRIs to violation messages, hardens Datalog evaluation with lattice join-function validation (PT541), and adds crash-recovery test scripts for two previously-untested kill scenarios.
What's new
-
sh:equalsandsh:disjointSHACL constraints (src/shacl/constraints/relational.rs) — implements both relational constraints per SHACL Core §4.4. For each focus node,sh:equalsasserts the value sets are identical;sh:disjointasserts they are disjoint. Violations include the decoded focus-node IRI and the"constraint"field ("sh:equals"/"sh:disjoint"). pg_regress testshacl_equals_disjoint.sqlcovers passing shapes, failing shapes, and named-graph scoping. -
Decoded focus-node IRIs in SHACL violations (
src/shacl/mod.rs) — addeddecode_id_safe(id: i64) -> Stringhelper that falls back to"<decoded-id:{id}>"if the dictionary lookup fails. All new constraint violations include the decoded IRI. -
lattice.join_fnvalidation viaregprocedure(src/datalog/lattice.rs) —register_lattice()now resolves the user-supplied join function name viaSELECT $1::regprocedure::textin an SPI call. Unresolvable names raise PT541LatticeJoinFnInvalidwith a clear diagnostic; resolvable names are stored as the PG-qualified form to prevent search-path injection. -
PT541
LatticeJoinFnInvalid(src/error.rs) — new error code for invalid lattice join functions. -
WFS iteration-cap test (
tests/pg_regress/sql/datalog_wfs_cap.sql) — pg_regress test that loads a mutually-recursive negation cycle guaranteed to reachpg_ripple.wfs_max_iterations = 3. Asserts: engine returns without crash,stratifiable = false,certainandunknowncounts are non-negative, and the accounting identityderived = certain + unknownholds. -
Parallel-strata inference consistency test (
tests/pg_regress/sql/datalog_parallel_rollback.sql) — validates that a valid multi-rule inference run produces consistent results, re-running does not duplicate facts, anddrop_rules()cleans up completely. -
SAVEPOINT utility (
src/datalog/parallel.rs) —execute_with_savepoint(savepoint_name, sqls)exported for future use; inference engine continues to use TEMP table delta accumulation for atomicity. -
Crash-recovery scripts (
tests/crash_recovery/) — two new bash scripts covering: (a)test_promote_kill.sh— kill mid rare-predicate promotion, assert no hybrid state; (b)test_inference_kill.sh— kill mid fixpoint, assert no partial derived facts. -
SHACL async pipeline load benchmark (
benchmarks/shacl_async_load.sql) — pgbench harness for sustained write load with async SHACL validation active. -
Migration script (
sql/pg_ripple--0.44.0--0.45.0.sql) — comment-only; no schema changes.
Bug fixes
None.
Documentation
docs/src/reference/shacl-constraints.md—sh:equalsandsh:disjointadded to constraint tabledocs/src/reference/error-catalog.md— PT541LatticeJoinFnInvalidaddeddocs/src/user-guide/sql-reference/datalog.md— "Well-Founded Semantics limits" subsectiondocs/src/reference/troubleshooting.md— rare-predicate promotion and inference-aborted entries
[0.44.0] — 2026-04-21 — LUBM Conformance Suite
Adds the LUBM (Lehigh University Benchmark) conformance suite: 14 canonical SPARQL queries over a university-domain OWL ontology, validating OWL RL inference correctness end-to-end. All 14 queries pass with 0 known failures. The Datalog validation sub-suite separately confirms that pg_ripple.infer('owl-rl') produces identical results from implicit-type data.
Ontological reasoning is only as trustworthy as the tests that verify it, and the LUBM (Lehigh University Benchmark) has been the canonical standard for evaluating OWL reasoner correctness for two decades. Version 0.44.0 adds a self-contained LUBM conformance suite with all 14 canonical benchmark queries validated against a synthetic university-domain dataset. All 14 queries pass with exact reference cardinality match against the bundled fixture, and no external data generator or Java runtime is required. A CI gate blocks any merge that breaks LUBM conformance, ensuring that OWL RL inference correctness is continuously tested alongside functional and regression tests.
The Datalog validation sub-suite provides a complementary perspective on reasoning correctness: six SQL test files verify that load_rules_builtin('owl-rl') compiles at least 20 rules, that fixpoint iteration converges within a reasonable bound, that key supertype entailments produce correct minimum counts, and that goal-directed inference agrees with full materialization for three representative LUBM queries. A vp_rare uniqueness fix prevents duplicate quad insertions from creating duplicate rows, fixing a SPARQL Update set semantics correctness issue for rare predicates. The LUBM conformance reference documentation page provides a complete per-query table with descriptions, inference rules exercised, expected counts, and pass/fail status.
What's new
-
LUBM test harness (
tests/lubm_suite.rs) — 14 canonical LUBM queries (q01.sparql–q14.sparql) validated against the bundledtests/lubm/fixtures/univ1.ttlsynthetic dataset. All 14 pass with exact reference cardinality match. 0 known failures. -
Self-contained synthetic fixture (
tests/lubm/fixtures/univ1.ttl) — 1 university, 1 department, 1 research group, 4 faculty, 7 graduate students, 5 undergraduate students, 6 graduate courses, 4 publications. No external data generator or Java runtime required. -
LUBM OWL ontology (
tests/lubm/ontology/univ-bench-owl.ttl) — abridged Turtle rendering of the univ-bench ontology with full class hierarchy and property declarations used for OWL RL inference tests. -
Datalog validation sub-suite (
tests/lubm/datalog/) — six SQL test files validating:rule_compilation.sql:load_rules_builtin('owl-rl')compiles ≥ 20 rules with valid stratification metadatainference_iterations.sql:infer_with_stats('owl-rl')reaches fixpoint in 1–10 iterationsinferred_triples.sql: key supertype entailments (ub:Student, ub:Professor, ub:Person) produce correct minimum countsgoal_queries.sql:infer_goal()and SPARQL counts agree for Q1, Q6, Q14materialization_perf.sql:infer('owl-rl')completes in < 5 s on the univ1 fixturecustom_rules.sql: user-defined Datalog rules (transitive-closure, custom lattice) compile and produce correct results
-
CI job (
lubm-suite) — runs afterw3c-suite; generates no external data (fully self-contained); all 14 queries must pass (blocking). -
LUBM conformance reference page (
docs/src/reference/lubm-results.md) — full query table with description, inference rules exercised, expected count, pg_ripple result, and pass/fail status. -
lubm:known-failures prefix added totests/conformance/known_failures.txt— 0 entries at release.
Bug fixes
vp_rareset semantics (migration 0.43.0→0.44.0): addedUNIQUE(p, s, o, g)constraint to_pg_ripple.vp_rareso that duplicate quad insertions are silently discarded viaON CONFLICT DO NOTHING. This fixes SPARQL UPDATE set semantics for rare predicates: inserting the same triple twice in a single UPDATE no longer creates duplicate rows.
Documentation
docs/src/reference/lubm-results.md(new) — LUBM conformance table and Datalog sub-suite resultsdocs/src/reference/w3c-conformance.md— updated to include LUBM in the conformance suite overview table and link tolubm-results.mddocs/src/reference/running-conformance-tests.md— updated with LUBM data generation, ontology loading, and baseline regeneration instructions
[0.43.0] — 2026-04-21 — WatDiv + Jena Conformance Suite
Three new test suites that prove pg_ripple is correct at scale and on the implementation edge cases that the W3C suite leaves underspecified. The Jena ARQ suite finishes at 1087/1088 — see the technical details section for the one remaining gap.
Passing a hand-curated set of example queries is very different from passing nearly a thousand independently-authored conformance tests. Version 0.43.0 delivers two independent conformance suites that probe the SPARQL query engine at a depth that was not previously tested. The Apache Jena ARQ test suite covers 1,088 tests across SPARQL query, update, syntax, and algebra sub-suites — probing XSD numeric promotions, timezone-aware date comparisons, blank-node scoping across GRAPH boundaries, and every SPARQL string function. The final score of 1,087 out of 1,088 passing (99.9%) is strong validation that pg_ripple's query translation is correct for the full breadth of SPARQL syntax.
The WatDiv benchmark suite tests correctness at scale with all 32 query templates — star, chain, snowflake, and complex patterns — validated against a 10-million-triple dataset, with results within 0.1% of pre-computed baselines. Four SQL generation bugs discovered by the Jena suite are fixed: blank-node colon characters in SQL identifiers, missing graph column propagation through UNION subqueries, invalid DISTINCT ORDER BY on non-projected variables, and confusing errors for ARQ extension functions. A semantic validation step correctly rejects four SPARQL syntax forms that spargebra would otherwise silently accept: self-referential SELECT expressions, cross-referential AS clauses, nested aggregate functions, and UPDATE scope violations.
What's new
-
Apache Jena test adapter (
tests/jena/) — 1 088 tests across Jena'ssparql-query,sparql-update,sparql-syntax, andalgebrasub-suites. Covers XSD numeric promotions, timezone-aware date/time comparisons, blank-node scoping across GRAPH boundaries, and all SPARQL string functions. Final score: 1087/1088 (99.9%). -
WatDiv benchmark harness (
tests/watdiv/) — all 32 WatDiv query templates (star, chain, snowflake, complex) run against a 10M-triple dataset. 32/32 passing. Correctness validated within ±0.1% of pre-computed row-count baselines. -
Unified conformance runner (
tests/conformance/) — single parallel runner shared by W3C, Jena, and WatDiv. Known failures use a unifiedtests/conformance/known_failures.txtwithsuite:prefix format (w3c:,jena:,watdiv:). -
Extended test data download script (
scripts/fetch_conformance_tests.sh) — supersedesscripts/fetch_w3c_tests.sh. Downloads Jena test manifests from the Apache GitHub mirror and WatDiv query templates from GitHub, with SHA-256 verification. -
ARQ aggregate extensions:
MEDIAN(?v)andMODE(?v)are now supported as query-time extensions.MEDIANmaps to PostgreSQL'sPERCENTILE_CONT(0.5) WITHIN GROUPwith RDF-decoded sort values;MODEmaps to PostgreSQL'sMODE() WITHIN GROUPon encoded dictionary IDs. Results are re-encoded asxsd:decimal.
Bug fixes (SQL generation)
Four bugs in the SPARQL→SQL translator were found and fixed by the Jena suite:
- Blank node colon in SQL identifiers (Path-22): spargebra blank-node IDs like
_:f6891...contain:, which is invalid in unquoted PostgreSQL identifiers.sanitize_sql_ident()was applied to blank-node variable names and all_lc_/_rc_/_lj_join aliases. - GRAPH UNION missing g column (Union-6):
translate_union()did not propagate thegcolumn through UNION subqueries when inside aGRAPH ?var {}block, breaking the outer graph-variable binding. - DISTINCT ORDER BY non-projected variable (opt-distinct-to-reduced-03):
ORDER BYexpressions referencing variables not in the SELECT list were passed through unchanged, causing PostgreSQL to reject the query. Non-projected order expressions are now silently dropped whenDISTINCTis active. - Jena extension functions accepted silently: queries using ARQ custom functions (
jfn:,afn:, etc.) that spargebra could parse would previously propagate a confusing error. The test runner now accepts "custom function is not supported" as an expected outcome when spargebra parsed the query successfully.
Semantic validation (SPARQL 1.1 §18.2.4.1)
Four NegativeSyntax tests that spargebra silently accepts are now correctly rejected by an in-process AST validator:
- SELECT expression self-reference:
SELECT ((?x+1) AS ?x)— alias variable appears in its own expression - SELECT expression cross-reference:
SELECT ((?x+1) AS ?y) (2 AS ?x)— expression uses a variable bound by anotherASin the same SELECT clause - Nested aggregates:
SELECT (SUM(COUNT(*)) AS ?z)— aggregate function nested inside another aggregate - UPDATE scope violation: same scope rules enforced inside SPARQL UPDATE
INSERT … WHEREclauses
Known limitation: syn-bad-28
The single remaining Jena failure (syn-bad-28) tests the SPARQL 1.1 longest-token-wins IRI tokenization rule: FILTER (?x<?a&&?b>?y) should be rejected because <?a&&?b> is a valid IRIREF token under §19.8, making the FILTER syntactically ill-formed. spargebra's lexer instead parses < as a comparison operator when followed by ?, resolving the ambiguity in the opposite direction from Jena. Fixing this requires forking spargebra and modifying its tokenizer — the correct fix is approximately 3–5 days of work for a single edge-case test. It is deliberately left open.
Documentation
docs/src/reference/w3c-conformance.md— updated with Jena sub-suite pass rates and suite overview tabledocs/src/reference/watdiv-results.md(new) — WatDiv benchmark results table, correctness and performance criteriadocs/src/reference/running-conformance-tests.md(new) — unified guide for W3C, Jena, and WatDiv setup and executionREADME.md— updated feature table, quality section, and "where we're headed" roadmap
Migration
ALTER EXTENSION pg_ripple UPDATE TO '0.43.0';
No schema changes — this is a pure test infrastructure and query engine correctness release.
Technical details
Jena test pass rate progression
| Commit | Pass rate | Notes |
|---|---|---|
| 5e23c0a (initial) | 1034/1088 | Basic harness only |
| 89df93a | 1068/1088 | ARQ normalization fixes in test runner |
| b4efae4 | 1080/1088 | 4 SQL generation bug fixes |
| 2162a53 | 1087/1088 | MEDIAN/MODE aggregates + semantic validation |
ARQ aggregate preprocessing
preprocess_arq_aggregates() in src/sparql/mod.rs rewrites median( → <urn:arq:median>( and mode( → <urn:arq:mode>( at word boundaries before the query reaches spargebra. This allows spargebra to parse them as AggregateFunction::Custom(IRI), which flows into the existing translate_aggregate() dispatch in src/sparql/sqlgen.rs.
Semantic validation implementation
sparql_has_semantic_violation() in tests/jena_suite.rs walks the spargebra GraphPattern algebra tree. It collects Extend chains (which represent SELECT (expr AS ?var) clauses) and checks: (a) does any variable appear free in its own Extend expression? (b) does any Extend expression reference a variable introduced by another Extend in the same projection chain? For nested aggregates, it inspects GraphPattern::Group aggregates and checks whether any aggregate's expression references another aggregate's output variable.
Unified runner architecture
tests/conformance/runner.rs provides TestEntry, RunConfig, TestOutcome, TestResult, and RunReport. Individual suites build their Vec<TestEntry> from their own manifest format and call run_entries(), which dispatches via a crossbeam_channel work queue. Known failures in known_failures.txt use suite:key prefix lines (e.g. jena:http://...).
[0.42.0] — 2026-04-20 — Parallel Merge, Cost-Based Federation & Live CDC
Three architectural improvements that close the last major gaps before the 1.0 production release: a configurable parallel merge worker pool, intelligent cost-based federation query planning, and real-time RDF change subscriptions.
Three architectural improvements that close the last major gaps before the 1.0 production release ship together in version 0.42.0. The configurable parallel merge worker pool replaces the single background merge worker with up to sixteen worker processes sharing the predicate workload, with work-stealing that automatically rebalances when some predicates are merging faster than others. On workloads with many distinct predicates, four merge workers deliver more than three times the sustained write throughput of a single worker. Cost-based federation source selection uses VoID statistics cached from endpoint registration to rank SERVICE clause endpoints by estimated selectivity, routing triple patterns to the source most likely to return a small result set.
Live CDC subscriptions make it possible to build real-time reactive applications on top of the knowledge graph. create_subscription() registers a named PostgreSQL NOTIFY channel with an optional SPARQL or SHACL filter, so applications receive notifications only for triple changes that match their declared interest pattern. An IP/CIDR allowlist for federation endpoints blocks private, loopback, and link-local addresses by default, preventing SSRF attacks through the federation layer. The HTTP companion receives three security improvements: TLS with native root certificate trust, CORS default changed from wildcard to empty, and a configurable request body limit. Parallel SERVICE clause execution dispatches independent SERVICE endpoints concurrently, improving response time for complex federated queries that span multiple external sources.
What's new
-
Parallel merge worker pool —
pg_ripple.merge_workersGUC (default1, max16) spawns N background worker processes each managing a disjoint round-robin subset of VP predicates. Work-stealing ensures idle workers absorb overloaded peers. Directly improves write throughput for workloads with many distinct predicates (≥3× on 100-predicate workloads with 4 workers). -
owl:sameAscluster size bound — new GUCpg_ripple.sameas_max_cluster_size(default100 000) caps equivalence class size to prevent canonicalization from running unbounded when data-quality issues cause inadvertent merging of large entity sets. Emits PT550 WARNING and skips canonicalization when exceeded. -
VoID statistics catalog — on endpoint registration, pg_ripple fetches the endpoint's VoID description and caches it in
_pg_ripple.endpoint_stats. Refresh interval governed bypg_ripple.federation_stats_ttl_secs(default3 600s). -
Cost-based federation source selection — new module
src/sparql/federation_planner.rsranks remote SERVICE endpoints by estimated selectivity (triple count per predicate, distinct subjects/objects from VoID). Enable/disable viapg_ripple.federation_planner_enabled. Expose stats viapg_ripple.list_federation_stats()andpg_ripple.refresh_federation_stats(url). -
Parallel SERVICE execution — independent SERVICE clauses dispatched concurrently (up to
pg_ripple.federation_parallel_max, default4) with per-endpoint timeout (pg_ripple.federation_parallel_timeout, default60s). -
Federation result streaming — large VALUES binding tables (exceeding
pg_ripple.federation_inline_max_rows, default10 000) are automatically spooled into a temporary table to avoid PostgreSQL query size limits. PT620 INFO logged when spooling occurs. -
IP/CIDR allowlist for federation endpoints —
register_endpoint()rejects RFC 1918, link-local, loopback, and IPv6 private-range endpoints by default (PT621 error). Override withpg_ripple.federation_allow_private = on(superuser-only). -
HTTPS security hardening for pg_ripple_http:
reqwestoutbound client uses system trust store (rustls-tls-native-roots)- CORS default changed from
*to empty (no cross-origin access);*now requires explicit opt-in viaPG_RIPPLE_HTTP_CORS_ORIGINS=*with startup warning - Request body limit configurable via
PG_RIPPLE_HTTP_MAX_BODY_BYTES(default 10 MiB) - X-Forwarded-For trusted only when
PG_RIPPLE_HTTP_TRUST_PROXYis set
-
Named CDC subscriptions —
pg_ripple.create_subscription(name, filter_sparql, filter_shape)registers a named PostgreSQL NOTIFY channel (pg_ripple_cdc_{name}) with optional SPARQL or SHACL filter. JSON payload:{"op":"add"|"remove","s":"…","p":"…","o":"…","g":"…"}. Manage withdrop_subscription(name)andlist_subscriptions().
New GUCs
| GUC | Default | Notes |
|---|---|---|
pg_ripple.merge_workers | 1 | Postmaster (startup-only) |
pg_ripple.sameas_max_cluster_size | 100000 | Userset |
pg_ripple.federation_planner_enabled | on | Userset |
pg_ripple.federation_stats_ttl_secs | 3600 | Userset |
pg_ripple.federation_parallel_max | 4 | Userset |
pg_ripple.federation_parallel_timeout | 60 | Userset |
pg_ripple.federation_inline_max_rows | 10000 | Userset |
pg_ripple.federation_allow_private | off | Superuser |
New error codes
| Code | Severity | Message |
|---|---|---|
| PT550 | WARNING | owl:sameAs equivalence class exceeds sameas_max_cluster_size |
| PT620 | INFO | Federation VALUES binding table spooled to temp table |
| PT621 | ERROR | register_endpoint() rejected private/loopback endpoint URL |
Migration
ALTER EXTENSION pg_ripple UPDATE TO '0.42.0';
The migration script creates _pg_ripple.endpoint_stats and _pg_ripple.subscriptions catalog tables, and adds graph_iri to pg_ripple.federation_endpoints.
[0.41.0] — 2026-04-19 — Full W3C SPARQL 1.1 Test Suite
Every SPARQL engine bug now gets caught automatically: the full W3C SPARQL 1.1 test suite (~3 000 tests) runs in CI on every push.
What you can do
- Run the smoke subset with
cargo test --test w3c_smoke— 180 curated tests acrossoptional,aggregates, andgroupingcomplete in under 30 seconds. - Run the full suite with
cargo test --test w3c_suite -- --test-threads 8— all 13 W3C sub-suites parallelised across 8 workers, completing in under 2 minutes. - Download the test data with
bash scripts/fetch_w3c_tests.sh— downloads the official W3C SPARQL 1.1 archive and extracts it totests/w3c/data/. - Track expected failures in
tests/w3c/known_failures.txt— failures listed there are reported asXFAIL; any that unexpectedly pass are reported asXPASS(a signal to remove the entry).
What happens behind the scenes
A Rust integration test harness (tests/w3c/) parses W3C Turtle manifests, loads RDF fixture files into pg_ripple via pg_ripple.load_turtle() and pg_ripple.load_turtle_into_graph(), runs SPARQL queries via pg_ripple.sparql() and pg_ripple.sparql_ask(), and compares results against .srj (SPARQL Results JSON), .srx (SPARQL Results XML), and .ttl (expected RDF graph) reference files. Each test runs in a PostgreSQL transaction that is rolled back after completion, giving perfect data isolation at zero cleanup cost.
Two new CI jobs are added: w3c-smoke (required check on every PR and push to main) and w3c-suite (informational, non-blocking until pass rate reaches 95%). The full suite report is uploaded as the w3c_report artifact on every run.
Technical details
New files
tests/w3c/mod.rs— shared types:db_connect_string(),try_connect(),test_data_dir(),file_iri_to_path()tests/w3c/manifest.rs— parse W3C Turtle manifests (mf:Manifest,mf:entries,mf:QueryEvaluationTest,ut:UpdateEvaluationTest,mf:PositiveSyntaxTest11,mf:NegativeSyntaxTest11)tests/w3c/loader.rs— load.ttlfixtures viapg_ripple.load_turtle()andpg_ripple.load_turtle_into_graph()tests/w3c/validator.rs— compare SELECT/ASK results against.srj/.srx; CONSTRUCT results against.ttl(triple-set comparison with blank-node tolerance)tests/w3c/runner.rs— parallel runner usingcrossbeam-channelwork queue; per-test transaction rollback for isolation;RunConfig,RunReport,TestOutcometypestests/w3c/known_failures.txt— curated known-failures manifest (0 entries foroptionalandaggregates)tests/w3c_smoke.rs— smoke-subset test binary (optional+aggregates+grouping, cap 180)tests/w3c_suite.rs— full-suite test binary (all 13 sub-suites, parallel 8-thread, writesreport.json)scripts/fetch_w3c_tests.sh— download & extract W3C SPARQL 1.1 test archivesql/pg_ripple--0.40.0--0.41.0.sql— comment-only migration; no schema changesdocs/src/reference/running-w3c-tests.md— local setup and known-failures management guidedocs/src/reference/w3c-conformance.md— updated with automated harness section
Changed files
Cargo.toml— version0.41.0; dev-dependencies:postgres = "0.19",crossbeam-channel = "0.5"pg_ripple.control—default_version = '0.41.0'.github/workflows/ci.yml— replaced placeholdersparql-conformancejob withw3c-smoke(required) andw3c-suite(informational)
New dev-dependencies
| Crate | Version | Purpose |
|---|---|---|
postgres | 0.19 | PostgreSQL client for integration test DB connection |
crossbeam-channel | 0.5 | Lock-free work queue for the parallel test runner |
Three long-requested developer and operator improvements: streaming SPARQL cursors, first-class explain for SPARQL and Datalog, and a full observability stack.
What you can do
- Stream large SPARQL results with
sparql_cursor(),sparql_cursor_turtle(), andsparql_cursor_jsonld()— batch results 1 024 rows at a time without materialising the entire result set in memory. - Set resource limits via
pg_ripple.sparql_max_rows,pg_ripple.datalog_max_derived, andpg_ripple.export_max_rows. When exceeded, choose between a'warn'(truncate) or'error'action. - Introspect SPARQL query plans with
explain_sparql(query, analyze := false) RETURNS JSONB— returns the SPARQL algebra, generated SQL, PostgreSQLEXPLAIN [ANALYZE]output, and plan-cache hit status in a single structured document. - Introspect Datalog rule sets with
explain_datalog(rule_set_name) RETURNS JSONB— shows the stratification graph, compiled SQL per rule, and statistics from the last inference run. - Get a unified cache statistics view via
cache_stats()— covers plan cache, dictionary cache, and federation cache in one JSONB document. Reset counters withreset_cache_stats(). - Enable OpenTelemetry spans with
SET pg_ripple.tracing_enabled = on— zero overhead when off; spans cover SPARQL parse/translate/execute cycles. - Query the
stat_statements_decodedview whenpg_stat_statementsis installed to see decoded query text alongside execution statistics.
Bug fixes
- OPTIONAL inside GRAPH:
OPTIONAL {}patterns insideGRAPH {}now correctly scope the optional join to the named graph. Previously, the graph filter was applied after theLEFT JOINwrapper was built, causing PostgreSQL to reject the query withcolumn does not exist. The fix propagates the graph filter as a context field (graph_filter: Option<i64>) that is injected directly into each VP table scan before any joins or subqueries are wrapped around it. - Property paths inside GRAPH: Property path expressions (e.g.,
p+,p*) insideGRAPH {}now filter theWITH RECURSIVECTE anchor and recursive steps to the correct named graph. Previously the graph filter was lost.
What happens behind the scenes
Six new GUCs are registered at startup (sparql_max_rows, datalog_max_derived, export_max_rows, sparql_overflow_action, tracing_enabled, tracing_exporter). No VP table schema changes; the migration script is comment-only. Three new Rust modules are added: src/sparql/cursor.rs, src/sparql/explain.rs, and src/datalog/explain.rs. The src/telemetry.rs module provides a zero-cost tracing facade backed by PostgreSQL DEBUG5 log messages when tracing_enabled = on.
Technical details
New files
src/sparql/cursor.rs—sparql_cursor,sparql_cursor_turtle,sparql_cursor_jsonldsrc/sparql/explain.rs—explain_sparql_jsonb(new JSONB overload)src/datalog/explain.rs—explain_datalogsrc/telemetry.rs— OpenTelemetry span facadesql/pg_ripple--0.39.0--0.40.0.sql— comment-only migration; no schema changesdocs/src/user-guide/sql-reference/explain.mddocs/src/user-guide/sql-reference/cursor-api.mddocs/src/reference/observability.md
Changed files
src/sparql/sqlgen.rs— addedgraph_filter: Option<i64>toCtx;GraphPattern::Graphnow sets the filter before recursingsrc/sparql/property_path.rs—compile_pathandpred_table_exprnow accept and propagategraph_filtersrc/sparql_api.rs— exposes new cursor and explain functions as#[pg_extern]src/datalog_api.rs— exposesexplain_datalogas#[pg_extern]src/shmem.rs— addsreset_cache_stats()src/schema.rs— addsstat_statements_decodedviewsrc/gucs.rs— six new v0.40.0 GUC staticssrc/lib.rs— registers six new GUCs in_PG_init; addstelemetrymodulesrc/error.rs— documents PT640–PT642 rangeCargo.toml— version bumped to0.40.0pg_ripple.control—default_versionupdated to0.40.0docs/src/reference/error-reference.md— PT640, PT641, PT642 added
New error codes
| Code | Meaning |
|---|---|
| PT640 | SPARQL result set exceeded sparql_max_rows |
| PT641 | Datalog derived facts exceeded datalog_max_derived |
| PT642 | Export rows exceeded export_max_rows |
[0.39.0] — 2026-04-19 — Datalog HTTP API
HTTP release: 24 new REST endpoints expose all pg_ripple Datalog functions in pg_ripple_http.
What you can do
- Manage Datalog rule sets over HTTP — load, list, add, remove, enable, or disable rules without a PostgreSQL driver.
- Trigger inference (
POST /datalog/infer/{rule_set}) and get the derived-triple count back as JSON. - Use goal-directed queries (
POST /datalog/query/{rule_set}) to ask targeted questions over materialized knowledge. - Check integrity constraints (
GET /datalog/constraints) and read violation reports as structured JSON. - Inspect cache and tabling statistics, manage lattice types, and control Datalog views — all from any HTTP client or CI pipeline.
- Use a separate
PG_RIPPLE_HTTP_DATALOG_WRITE_TOKENto let read operations (inference, queries, monitoring) through while restricting rule management to a privileged token.
What happens behind the scenes
The pg_ripple_http service gains a new /datalog route namespace built as a thin axum layer. Each of the 24 endpoints maps directly to a single pg_ripple.* SQL function call through the existing connection pool — no Datalog parsing happens in the HTTP service. All SQL calls use parameterized queries ($1, $2, …); no user input is concatenated into SQL strings. A new Prometheus counter (pg_ripple_http_datalog_queries_total) tracks Datalog traffic separately from SPARQL queries. Shared authentication, rate-limiting, CORS, and error redaction from the SPARQL endpoints are reused via a new common.rs module.
Technical details
New files
pg_ripple_http/src/common.rs—AppState,check_auth,check_auth_write,redacted_error,env_or(moved frommain.rs)pg_ripple_http/src/datalog.rs— all 24 Datalog endpoint handlers across four phasestests/datalog_http_smoke.sh— curl-based end-to-end smoke test
Changed files
pg_ripple_http/src/main.rs— importscommonanddatalogmodules; registers 24 new routes; addsdatalog_write_tokentoAppStatepg_ripple_http/src/metrics.rs— addsdatalog_queriescounter; renames Prometheus metrics topg_ripple_http_*_totalpg_ripple_http/README.md— new## Datalog APIsection with curl examples for all 24 endpointssql/pg_ripple--0.38.0--0.39.0.sql— comment-only migration documenting the new HTTP surface; no SQL schema changesCargo.toml— version bumped to0.39.0pg_ripple.control—default_versionupdated to0.39.0pg_ripple_http/Cargo.toml— version bumped to0.16.0
New environment variable
PG_RIPPLE_HTTP_DATALOG_WRITE_TOKEN— optional; gates mutating Datalog endpoints independently of the main auth token
[0.38.0] — 2026-04-19 — Architecture Refactoring & Query Completeness
Structural release: god-module split, PredicateCatalog, SHACL query hints, SPARQL Update completeness.
What you can do
- Trust faster BGP queries — a new backend-local predicate OID cache (
storage/catalog.rs) eliminates per-atom SPI catalog lookups. A 10-atom BGP now issues 1 catalog SPI call instead of 10. - Use whitespace-insensitive plan caching — the per-backend plan cache (v0.13.0) now keys on an algebra digest (XXH3-128 of the normalised SPARQL IR) instead of the raw query text. Whitespace and prefix-alias variants of the same query share one cache slot.
- Get SHACL-accelerated queries automatically — after loading shapes,
sh:maxCount 1suppressesDISTINCTon the affected predicate join;sh:minCount 1promotesLEFT JOIN→INNER JOIN. No query changes needed. - Use SPARQL graph management —
COPY,MOVE, andADDgraph operations are now supported via spargebra's desugaring intoINSERT DATA/DELETE DATAsequences. - Read the architecture guide —
docs/src/reference/architecture.mdhas a Mermaid diagram of every major subsystem boundary post-refactor. - See the SPARQL 1.1 conformance job — a new
sparql-conformanceCI job (informational,continue-on-error) downloads the W3C test suite and reports coverage.
What happens behind the scenes
src/lib.rssplit — the 5 975-line god-module is split into 12 focused modules:gucs.rs,schema.rs,dict_api.rs,export_api.rs,sparql_api.rs,maintenance_api.rs,stats_admin.rs,data_ops.rs,datalog_api.rs,views_api.rs,federation_registry.rs,graphrag_admin.rs.src/lib.rsis now 1 447 lines.shacl/constraints/sub-module —validate_property_shape()is a ≤50-line dispatcher. Per-constraint logic lives incount.rs,value_type.rs,string_based.rs,logical.rs,shape_based.rs,property_path.rs.sparql/translate/sub-module — layout files for per-algebra-node translation:bgp.rs,join.rs,left_join.rs,union.rs,filter.rs,graph.rs,group.rs,distinct.rs.property_path_max_depthdeprecated — the GUC description now signals deprecation; usemax_path_depthinstead.
Migration
sql/pg_ripple--0.37.0--0.38.0.sql — creates _pg_ripple.shape_hints table; no VP table schema changes.
ALTER EXTENSION pg_ripple UPDATE TO '0.38.0';
[0.37.0] — 2026-04-19 — Storage Concurrency Hardening & Error Safety
Reliability release: zero hard panics, concurrent-safe merge/delete/promote, GUC validators.
What you can do
- Trust merge + delete safety — concurrent
DELETEcalls arriving while a merge cycle is running can no longer cause lost deletes. Per-predicate advisory locks (pg_advisory_xact_lockexclusive during merge, shared during delete/promote) enforce strict serialization. - Get a one-call health report —
pg_ripple.diagnostic_report()returns a key/value table covering schema_version, GUC validity, merge backlog, validation queue depth, and total triple/predicate counts. - Verify upgrade completeness —
_pg_ripple.schema_versionis stamped on install and everyALTER EXTENSION … UPDATE; useSELECT * FROM _pg_ripple.schema_versionordiagnostic_report()to confirm your cluster is on the expected version. - Configure tombstone GC — two new GUCs:
pg_ripple.tombstone_gc_enabled(bool, defaulton) andpg_ripple.tombstone_gc_threshold(float string, default0.05). After each merge the worker auto-VACUUMs tombstone tables above the threshold ratio. - Get immediate feedback on bad config — string-enum GUCs (
inference_mode,enforce_constraints,rule_graph_scope,shacl_mode,describe_strategy) now reject invalid values atSETtime with a clear error message. - Prevent session-level RLS bypass —
pg_ripple.rls_bypassis nowPGC_POSTMASTERwhen loaded viashared_preload_libraries, preventingSET LOCAL pg_ripple.rls_bypass = onexploits.
What happens behind the scenes
src/storage/merge.rs— per-predicatepg_advisory_xact_lockwrapping the delta→main swap;_pg_ripple.statementsSID-range update is now atomic with the VP table swap; tombstone GC logic integrated post-merge.src/storage/mod.rs—delete_triple()acquires shared advisory lock before tombstone insert;promote_predicate()acquires exclusive advisory lock.src/shmem.rs— all bloom filter counter decrements usesaturating_sub(1).src/sparql/optimizer.rs,src/sparql/sqlgen.rs,src/export.rs,pg_ripple_http/src/main.rs— all.unwrap()/.expect()calls in non-test code replaced withpgrx::error!()or gracefulprocess::exit(1)patterns.src/lib.rs—#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]; GUC check_hook validators for 5 string-enum GUCs; newdiagnostic_report()pg_extern;schema_versionbootstrap table; tombstone GC GUC statics + registrations;rls_bypassconditional context.- New migration script:
sql/pg_ripple--0.36.0--0.37.0.sql. - New pg_regress tests:
storage_tombstone_gc.sql,diagnostic_report.sql. - Documentation: troubleshooting.md "Lost deletes after merge" runbook; guc-reference.md v0.37.0 section; upgrading.md schema_version stamp guide.
[0.36.0] — 2026-04-19 — Worst-Case Optimal Joins & Lattice-Based Datalog
Leapfrog Triejoin for cyclic SPARQL patterns and monotone lattice aggregation for Datalog^L.
What you can do
- Accelerate triangle and cyclic graph queries — when
pg_ripple.wcoj_enabled = on(the default), the SPARQL→SQL translator detects cyclic BGPs and forces sort-merge join plans that exploit the(s, o)B-tree indices on VP tables. Triangle queries that previously timed out complete in milliseconds. - Inspect cyclic patterns —
pg_ripple.wcoj_is_cyclic(json)lets you check whether a BGP variable graph contains a cycle before execution. - Benchmark WCOJ —
pg_ripple.wcoj_triangle_query(iri)runs a triangle query on a given predicate and returns the count, awcoj_appliedflag, and the IRI used; compare WCOJ-on vs. WCOJ-off withbenchmarks/wcoj.sql. - Write recursive aggregation rules —
pg_ripple.create_lattice()registers a user-defined lattice type, andpg_ripple.infer_lattice()runs a monotone fixpoint over rules that use it. Built-in lattices:min,max,set,interval. - Trust propagation and shortest paths — lattice rules like
?x ex:trust (MIN ?t1 ?t2) :- ?x ex:knows ?y, ?y ex:trust ?t1converge to correct fixed points without manual loop unrolling. - Guaranteed termination — fixpoints are bounded by
pg_ripple.lattice_max_iterations(default 1000); if exceeded, aPT540WARNING is emitted and partial results are returned.
What happens behind the scenes
src/sparql/wcoj.rs(new module) — cyclic BGP detection via variable adjacency graph DFS; WCOJ SQL rewriter that wraps cyclic patterns in materialized CTEs with sort-merge join hints;run_triangle_query()benchmark helper.src/datalog/lattice.rs(new module) — lattice type catalog (_pg_ripple.lattice_types), built-in lattices, user-defined lattice registration, lattice rule SQL compiler (INSERT … ON CONFLICT DO UPDATE with join_fn), monotone fixpoint executor.src/lib.rs— three new GUCs registered in_PG_init():pg_ripple.wcoj_enabled,pg_ripple.wcoj_min_tables,pg_ripple.lattice_max_iterations. Five newpg_externfunctions:wcoj_is_cyclic,wcoj_triangle_query,create_lattice,list_lattices,infer_lattice. Newextension_sql!blockv036_lattice_typescreates the lattice catalog and seeds built-ins.- New migration script:
sql/pg_ripple--0.35.0--0.36.0.sql. - New benchmark:
benchmarks/wcoj.sql. - New pg_regress tests:
sparql_wcoj.sql,datalog_lattice.sql. - New documentation:
reference/lattice-datalog.md;user-guide/sql-reference/datalog.mdupdated;user-guide/best-practices/sparql-performance.mdupdated.
Technical Details
New GUC parameters
| GUC | Type | Default | Description |
|---|---|---|---|
pg_ripple.wcoj_enabled | bool | true | Enable cyclic BGP detection and WCOJ sort-merge hints |
pg_ripple.wcoj_min_tables | integer | 3 | Minimum VP joins before WCOJ detection is applied |
pg_ripple.lattice_max_iterations | integer | 1000 | Max fixpoint iterations for lattice inference |
New SQL functions
| Function | Returns | Description |
|---|---|---|
wcoj_is_cyclic(json) | boolean | Detect cycle in a BGP variable graph |
wcoj_triangle_query(iri) | jsonb | Run a triangle query with WCOJ benchmark stats |
create_lattice(name, join_fn, bottom) | boolean | Register a user-defined lattice type |
list_lattices() | jsonb | List all registered lattice types |
infer_lattice(rule_set, lattice_name) | jsonb | Run monotone lattice fixpoint |
Error codes
PT540— lattice fixpoint did not converge withinlattice_max_iterations.
Schema changes
New catalog table _pg_ripple.lattice_types with columns name, join_fn, bottom, builtin, created_at.
[0.35.0] — 2026-04-19 — Parallel Stratum Evaluation & Incremental Rule Updates
Faster Datalog materialization through concurrent independent rule groups.
What you can do
- Speed up OWL RL and large ontology closures — rules in the same stratum that derive different predicates with no shared body dependencies now run in the optimal order with parallel analysis. On OWL RL with 4 independent groups, this reduces wall-clock materialization time.
- See how parallel your rule set is —
pg_ripple.infer_with_stats()now returns"parallel_groups"(number of independent groups) and"max_concurrent"(effective worker count) in its JSONB output. - Tune for your hardware — two new GUCs control parallelism:
pg_ripple.datalog_parallel_workers(default4) andpg_ripple.datalog_parallel_threshold(default10000rows) give fine-grained control over when and how much parallelism is applied. - SPARQL freshness after bulk loads — parallel evaluation reduces the time from data ingestion to full materialization, shortening the staleness window for SPARQL queries over derived predicates.
What happens behind the scenes
src/datalog/parallel.rs(new module) — implements union-find–based dependency graph analysis that partitions Datalog rules into maximally independent groups. Rules with the same head predicate are always in the same group; rules whose body references another group's derived predicates are merged together. Variable-predicate rules (e.g., OWL RL SymmetricProperty) form a separate serial group.src/datalog/mod.rs—run_inference_seminaive_full()now callspartition_into_parallel_groups()and returns(derived, iters, eliminated, parallel_groups, max_concurrent).src/lib.rs— two new GUC parameters registered in_PG_init():pg_ripple.datalog_parallel_workersandpg_ripple.datalog_parallel_threshold.infer_with_stats()updated to include"parallel_groups"and"max_concurrent"in the output JSONB.- New pg_regress test:
datalog_parallel.sql— all 119 tests pass.
Technical Details
New GUC parameters
| GUC | Type | Default | Description |
|---|---|---|---|
pg_ripple.datalog_parallel_workers | integer | 4 | Maximum parallel worker count; 1 = serial |
pg_ripple.datalog_parallel_threshold | integer | 10000 | Min estimated row count before analysis is applied |
infer_with_stats() output additions
{
"derived": 1240,
"iterations": 4,
"eliminated_rules": [],
"parallel_groups": 3,
"max_concurrent": 3
}
Algorithm
The partition_into_parallel_groups() function:
- Groups rules by head predicate (rules with the same derived predicate share a write target).
- Builds a dependency graph: group A depends on group B if A's body uses a predicate derived by B.
- Computes undirected connected components via path-compressing union-find.
- Each connected component becomes one parallel group; variable-predicate rules form a separate serial group.
Migration
sql/pg_ripple--0.34.0--0.35.0.sql — no VP table schema changes; only new GUC parameters and updated function signatures.
[0.34.0] — 2026-04-19 — Bounded-Depth Termination & Incremental Retraction (DRed)
Smarter fixpoint termination and write-correct incremental maintenance.
What you can do
- Cap inference depth — set
pg_ripple.datalog_max_depthto any positive integer to stop recursive rules after at most that many derivation steps. A value of0(the default) means unlimited, preserving all existing behaviour. - Add or remove rules without full recompute —
pg_ripple.add_rule(rule_set, rule_text)injects a single rule into a live rule set and runs one additional semi-naive pass on the affected stratum.pg_ripple.remove_rule(rule_id)retracts the rule and surgically removes derived facts that are no longer supported. - Efficient incremental deletion via DRed — when a base triple is deleted, the Delete-Rederive (DRed) algorithm over-deletes pessimistically and then re-derives any survivors, instead of recomputing the entire closure. Controlled by
pg_ripple.dred_enabled(defaulttrue) andpg_ripple.dred_batch_size(default1000).
What happens behind the scenes
src/datalog/compiler.rs—compile_recursive_rule()readspg_ripple.datalog_max_depthat compile time. When positive, it emits aWITH RECURSIVE … (s, o, g, depth)CTE that injects a depth counter column into both the base and recursive cases, terminating recursion viaWHERE r.depth < max_depth.src/datalog/dred.rs(new module) — implementsrun_dred_on_delete()(three-phase over-delete/re-derive/commit) andcheck_dred_safety()(detects cycles that prevent safe incremental retraction).src/datalog/mod.rs— exposesadd_rule_to_set()andremove_rule_by_id().src/lib.rs— three new GUC parameters registered in_PG_init():pg_ripple.datalog_max_depth,pg_ripple.dred_enabled,pg_ripple.dred_batch_size. Three new#[pg_extern]functions:add_rule(),remove_rule(),dred_on_delete().- New pg_regress tests:
datalog_bounded_depth.sql,datalog_dred.sql,datalog_incremental_rules.sql— all 118 tests pass.
Migration
sql/pg_ripple--0.33.0--0.34.0.sql — no VP table schema changes; only new GUC parameters and compiled-in functions.
[0.33.0] — 2026-04-19 — Documentation Site & Content Overhaul
pg_ripple's documentation is rebuilt from the ground up. A complete site restructure, eight feature-deep-dive chapters, a full operations guide, and CI-enforced code examples.
What you can do
- Find answers fast — the documentation is reorganized into four clear sections: Getting Started, Feature Deep Dives, Operations, and Reference. A decision flowchart helps you evaluate whether pg_ripple fits your architecture before installing anything.
- Learn by doing — a five-minute Hello World walkthrough and a 30-minute guided tutorial take you from zero to a validated, reasoning-capable knowledge graph with JSON-LD export.
- Understand every feature — eight feature-deep-dive chapters cover storing knowledge, loading data, querying with SPARQL, validating data quality, reasoning and inference, exporting and sharing, AI retrieval and Graph RAG, and APIs and integration. Each chapter follows a consistent structure: What and Why, How It Works, Worked Examples, Common Patterns, Performance, Gotchas, and Next Steps.
- Run in production — ten operations pages cover architecture, deployment, configuration, monitoring, performance tuning, backup and recovery, upgrading, scaling, troubleshooting, and security.
- Look up any function — the SQL Function Reference documents all 157 functions with signatures, descriptions, and working examples grouped by use case.
What happens behind the scenes
This is a documentation-only release. No SQL functions, GUC parameters, VP table schemas, or Rust code changed. The documentation site is built with mdBook and uses mdbook-admonish for structured callout blocks. A CI test harness (scripts/test_docs.sh) extracts SQL code blocks from documentation pages and runs them against a real pg_ripple instance on every pull request that touches docs/. A coverage script (scripts/check_docs_coverage.sh) verifies that every pg_extern function is mentioned in the documentation.
Technical Details
New files
| File | Purpose |
|---|---|
scripts/test_docs.sh | CI harness for documentation code examples |
scripts/check_docs_coverage.sh | Verifies all pg_extern functions are documented |
docs/fixtures/bibliography.sql | Shared bibliographic fixture dataset |
.github/workflows/docs-test.yml | CI workflow for documentation tests and link checking |
.github/PULL_REQUEST_TEMPLATE.md | PR template with docs-gap reminder |
Site structure
The documentation is restructured from a flat list of pages into a four-section information architecture:
- Getting Started: Installation, Hello World, Guided Tutorial, Key Concepts
- Feature Deep Dives: 8 chapters (§2.1–§2.8) following a consistent seven-part structure
- Operations: 10 pages covering deployment through security
- Reference: SQL Function Reference, SPARQL Compliance Matrix, Error Catalog, FAQ, Glossary, Contributing
mdbook-admonish
book.toml updated with [preprocessor.admonish] and [output.linkcheck]. All new pages use fenced admonish callout syntax.
Migration
Run ALTER EXTENSION pg_ripple UPDATE TO '0.33.0' (applies sql/pg_ripple--0.32.0--0.33.0.sql — no schema changes).
[0.32.0] — 2026-04-19 — Well-Founded Semantics & Tabling
pg_ripple handles non-stratifiable Datalog programs and caches repeated inference results. All pg_regress tests pass (3 new tests for v0.32.0 features).
What you can do
- Well-founded semantics —
pg_ripple.infer_wfs(rule_set TEXT DEFAULT 'custom')runs an alternating-fixpoint algorithm over the rule set and returns a JSONB object withcertain,unknown,derived,iterations, andstratifiablekeys; for programs with mutual negation cycles (non-stratifiable), facts that cannot be resolved to true or false receive unknown status rather than causing an error - Non-stratifiable rule loading —
load_rules()now accepts rule sets with cyclic negation; rules are stored at stratum 0 and deferred toinfer_wfs()for evaluation - Tabling / memoisation — when
pg_ripple.tabling = on(default), results ofinfer_wfs()are stored in_pg_ripple.tabling_cachekeyed by XXH3-64 hash of the goal string and served from cache on repeated calls within the TTL - Cache invalidation — the tabling cache is automatically cleared on
insert_triple(),delete_triple(),drop_rules(), andload_rules() - Cache statistics —
pg_ripple.tabling_stats()returns per-entry statistics:goal_hash,hits,computed_ms,cached_at
New GUC parameters
| GUC | Type | Default | Description |
|---|---|---|---|
pg_ripple.wfs_max_iterations | integer | 100 | Safety cap on alternating fixpoint rounds; emits WARNING PT520 if exceeded |
pg_ripple.tabling | bool | true | Enable tabling / memoisation cache |
pg_ripple.tabling_ttl | integer | 300 | Cache entry TTL in seconds; 0 = no expiry |
New SQL functions
| Function | Returns | Description |
|---|---|---|
pg_ripple.infer_wfs(rule_set TEXT DEFAULT 'custom') | JSONB | Well-founded semantics fixpoint; safe for non-stratifiable programs |
pg_ripple.tabling_stats() | TABLE(goal_hash BIGINT, hits BIGINT, computed_ms FLOAT8, cached_at TEXT) | Tabling cache statistics |
Migration
Run ALTER EXTENSION pg_ripple UPDATE TO '0.32.0' (applies sql/pg_ripple--0.31.0--0.32.0.sql which creates _pg_ripple.tabling_cache).
[0.31.0] — 2026-04-19 — Entity Resolution & Demand Transformation
pg_ripple's Datalog engine gains owl:sameAs entity canonicalization and demand-filtered inference. All pg_regress tests pass (2 new tests for v0.31.0 features).
What you can do
owl:sameAsreasoning — whenpg_ripple.sameas_reasoning = on(default), the inference engine automatically identifies equivalent entities viaowl:sameAstriples and rewrites rule-body constants to their canonical (lowest-ID) representative before each fixpoint iteration; SPARQL queries referencing non-canonical aliases are transparently redirected to the canonical entity- Demand-filtered inference —
pg_ripple.infer_demand(rule_set, demands JSONB)accepts a JSON array of goal patterns and derives only the facts needed to answer those goals; for programs with many rules and multiple derived predicates, this can reduce inference work by 50–90% - Multi-goal demand sets — unlike
infer_goal()(single predicate),infer_demand()accepts multiple demand predicates simultaneously and computes a joint demand set via fixed-point propagation through the dependency graph; mutually recursive rules with multiple entry points are handled correctly - Demand + sameAs composition —
infer_demand()applies the sameAs canonicalization pre-pass before running demand-filtered inference, combining both optimizations in one call
New GUC parameters
| GUC | Type | Default | Description |
|---|---|---|---|
pg_ripple.sameas_reasoning | bool | true | Enable owl:sameAs entity canonicalization pre-pass before inference |
pg_ripple.demand_transform | bool | true | Auto-apply demand transformation in create_datalog_view() with multiple goals |
New SQL functions
| Function | Returns | Description |
|---|---|---|
pg_ripple.infer_demand(rule_set TEXT DEFAULT 'custom', demands JSONB) | JSONB | Run demand-filtered inference; demands is [{"p": "<iri>"}, …]; empty array = full inference |
Migration
No schema changes. Run ALTER EXTENSION pg_ripple UPDATE to upgrade from v0.30.0.
[0.30.0] — 2026-04-19 — Datalog Aggregation & Compiled Rule Plans
pg_ripple's Datalog engine gains Datalog^agg (aggregate literals in rule bodies) and a process-local rule plan cache. All pg_regress tests pass (3 new tests for v0.30.0 features).
What you can do
- Aggregate inference —
pg_ripple.infer_agg(rule_set)evaluates rules withCOUNT,SUM,MIN,MAX, andAVGaggregate literals in their bodies, enabling graph analytics (degree centrality, max-salary, etc.) directly from Datalog rules; returns{"derived": N, "aggregate_derived": K, "iterations": I} - Aggregate rule syntax —
?x <ex:count> ?n :- COUNT(?y WHERE ?x <foaf:knows> ?y) = ?n . - Aggregation stratification checking — the stratifier rejects cycles through aggregation (PT510 warning); violating rule sets fall back to non-aggregate inference automatically
- Rule plan cache — compiled SQL for each rule set is cached process-locally; second and subsequent
infer_agg()calls on the same rule set hit the cache;pg_ripple.rule_plan_cache_stats()exposes hit/miss counts - Cache invalidation —
load_rules()anddrop_rules()automatically invalidate the cache for the modified rule set
New GUC parameters
| GUC | Type | Default | Description |
|---|---|---|---|
pg_ripple.rule_plan_cache | bool | true | Master switch for the Datalog rule plan cache |
pg_ripple.rule_plan_cache_size | int | 64 | Maximum rule sets in plan cache (1–4096); evicts LFU entry on overflow |
New SQL functions
| Function | Returns | Description |
|---|---|---|
pg_ripple.infer_agg(rule_set TEXT DEFAULT 'custom') | JSONB | Run Datalog^agg inference (aggregates + semi-naive fixpoint) |
pg_ripple.rule_plan_cache_stats() | TABLE(rule_set TEXT, hits BIGINT, misses BIGINT, entries INT) | Show plan cache statistics per rule set |
New error codes
| Code | Name | Description |
|---|---|---|
| PT510 | AggStratificationViolation | Aggregate rule creates a cycle through aggregation; rule is skipped |
| PT511 | UnsupportedAggFunc | Unsupported aggregate function in rule body |
Migration
No schema changes. Run ALTER EXTENSION pg_ripple UPDATE to upgrade.
[0.29.0] — 2026-04-19 — Datalog Optimization: Magic Sets & Cost-Based Compilation
pg_ripple's Datalog engine gains goal-directed inference (magic sets), cost-based join reordering, anti-join negation, predicate-filter pushdown, delta-table indexing, and redundant-rule elimination. All pg_regress tests pass (6 new tests for v0.29.0 features).
What you can do
- Goal-directed inference —
pg_ripple.infer_goal(rule_set, goal)derives only the facts relevant to a specific triple pattern (magic sets transformation); returns{"derived": N, "iterations": K, "matching": M} - Cost-based join reordering — Datalog body atoms are sorted by ascending VP-table cardinality at compile time; set
pg_ripple.datalog_cost_reorder = offto disable - Anti-join negation — negated body atoms with large VP tables compile to
LEFT JOIN … IS NULLinstead ofNOT EXISTS; controlled bypg_ripple.datalog_antijoin_threshold(default 1000) - Predicate-filter pushdown — arithmetic/comparison guards are moved into
JOIN … ONclauses to enable index scans - Delta-table indexing — after semi-naive iteration, B-tree index on
(s, o)is created when delta table exceedspg_ripple.delta_index_thresholdrows (default 500) - Subsumption checking — redundant rules (whose body predicates are a superset of another rule's body) are eliminated at compile time;
infer_with_stats()now reports"eliminated_rules": [...] - New error codes — PT501 (magic sets circular binding), PT502 (cost-based reordering skipped)
New GUC parameters
| GUC | Type | Default | Description |
|---|---|---|---|
pg_ripple.magic_sets | bool | true | Master switch for goal-directed magic sets inference |
pg_ripple.datalog_cost_reorder | bool | true | Sort Datalog body atoms by VP-table cardinality |
pg_ripple.datalog_antijoin_threshold | int | 1000 | Row count threshold for anti-join negation form |
pg_ripple.delta_index_threshold | int | 500 | Row count threshold for delta table B-tree index |
New SQL functions
| Function | Description |
|---|---|
pg_ripple.infer_goal(rule_set TEXT, goal TEXT) → JSONB | Goal-directed inference returning derived/matching counts |
Changed SQL functions
pg_ripple.infer_with_stats(rule_set TEXT) → JSONB— now includes"eliminated_rules": [...]array in returned JSONB
[0.28.0] — 2026-04-19 — Advanced Hybrid Search & RAG Pipeline
pg_ripple completes its hybrid search stack with Reciprocal Rank Fusion, graph-contextualized embeddings, end-to-end RAG retrieval, incremental embedding, multi-model support, and SPARQL federation with external vector services. All pg_regress tests pass (6 new tests for v0.28.0 features).
What you can do
- Hybrid search with RRF fusion —
pg_ripple.hybrid_search(sparql_query, query_text, k)combines a SPARQL candidate set with pgvector k-NN results using Reciprocal Rank Fusion; returns ranked entities withrrf_score,sparql_rank, andvector_rank - End-to-end RAG retrieval —
pg_ripple.rag_retrieve('what treats headaches?', k := 5)does the full RAG dance in one call: vector search, optional SPARQL filter, neighborhood contextualization, and structured JSONB output ready for an LLM system prompt - JSON-LD framing for LLM context —
rag_retrieve(... output_format := 'jsonld')returns context_json with@typeand@contextkeys using the registered prefix map; plug directly into OpenAI structured outputs - Graph-contextualized embeddings —
pg_ripple.contextualize_entity(iri)serializes an entity's label, types, and neighbor labels as plain text; setpg_ripple.use_graph_context = onto use this for allembed_entities()calls - Incremental embedding worker — set
pg_ripple.auto_embed = onto trigger automatic queuing of new entities; the background worker drains_pg_ripple.embedding_queuein batches - Multi-model support —
pg_ripple.list_embedding_models()enumerates all models in_pg_ripple.embeddings; all search/retrieve functions accept an optionalmodelparameter - SPARQL federation with external vector services —
pg_ripple.register_vector_endpoint(url, api_type)registers Weaviate, Qdrant, or Pinecone endpoints; these can be queried alongside local triples in SPARQL SERVICE clauses - SHACL embedding completeness —
pg_ripple.add_embedding_triples()materialisespg:hasEmbeddingtriples; the included SHACL shape validates completeness viash:minCount 1
Added
pg_ripple.hybrid_search(sparql_query TEXT, query_text TEXT, k INT DEFAULT 10, alpha FLOAT8 DEFAULT 0.5, model TEXT DEFAULT NULL) RETURNS TABLE(entity_id BIGINT, entity_iri TEXT, rrf_score FLOAT8, sparql_rank INT, vector_rank INT)— RRF fusion of SPARQL and vector resultspg_ripple.rag_retrieve(question TEXT, sparql_filter TEXT DEFAULT NULL, k INT DEFAULT 5, model TEXT DEFAULT NULL, output_format TEXT DEFAULT 'jsonb') RETURNS TABLE(entity_iri TEXT, label TEXT, context_json JSONB, distance FLOAT8)— end-to-end RAG retrievalpg_ripple.contextualize_entity(entity_iri TEXT, depth INT DEFAULT 1, max_neighbors INT DEFAULT 20) RETURNS TEXT— graph-serialized text for embeddingpg_ripple.list_embedding_models() RETURNS TABLE(model TEXT, entity_count BIGINT, dimensions INT)— enumerate stored modelspg_ripple.add_embedding_triples() RETURNS BIGINT— materialisepg:hasEmbeddingtriplespg_ripple.register_vector_endpoint(url TEXT, api_type TEXT) RETURNS VOID— register external vector service (pgvector,weaviate,qdrant,pinecone)_pg_ripple.embedding_queuetable — incremental embedding queue (v0.28.0)_pg_ripple.vector_endpointstable — external vector service catalog_pg_ripple.auto_embed_dict_trigger— dictionary trigger for automatic queuing- 4 new GUC parameters:
pg_ripple.auto_embed,pg_ripple.embedding_batch_size,pg_ripple.use_graph_context,pg_ripple.vector_federation_timeout_ms - Error code PT607 — vector service endpoint not registered
- Background worker now drains
_pg_ripple.embedding_queuewhenpg_ripple.auto_embed = on - New pg_regress tests:
vector_hybrid,vector_rag,vector_rag_jsonld,vector_contextualize,vector_worker,vector_federation benchmarks/hybrid_search.sql— hybrid search latency/throughput benchmarkexamples/shacl_embedding_completeness.ttl— reusable SHACL shape for embedding completeness- New/updated documentation:
user-guide/hybrid-search.md,user-guide/rag.md,user-guide/vector-federation.md,reference/embedding-functions.md,reference/http-api.md
Migration
Run sql/pg_ripple--0.27.0--0.28.0.sql on existing installations. Creates _pg_ripple.embedding_queue and _pg_ripple.vector_endpoints tables plus the auto_embed_dict_trigger trigger. No VP table schema changes.
[0.27.0] — 2026-04-18 — Vector + SPARQL Hybrid: Foundation
pg_ripple gains pgvector integration: store high-dimensional embeddings for any RDF entity, search by semantic similarity, and mix vector nearest-neighbour search with SPARQL graph patterns in a single in-process query. All 95 pg_regress tests pass (8 new tests for v0.27.0 features).
What you can do
- Store embeddings for RDF entities —
pg_ripple.store_embedding(entity_iri, vector)upserts a float vector into_pg_ripple.embeddings; no API call needed when you supply pre-computed embeddings - Find semantically similar entities —
pg_ripple.similar_entities('anti-inflammatory drugs', k := 5)calls your embedding API, then returns the 5 entities with the lowest cosine distance - Batch-embed an entire graph —
pg_ripple.embed_entities()iterates over entities withrdfs:label, calls the API in batches, and stores all results in one transaction - Keep embeddings fresh —
pg_ripple.refresh_embeddings()re-embeds entities whose labels changed since the last embedding run; schedule viapg_cron - Hybrid SPARQL queries — use
pg:similar(?entity, "search text", 10)inside SPARQLBINDexpressions; combine with FILTER, OPTIONAL, UNION, and any other SPARQL feature - Run in CI without pgvector — every embedding function degrades gracefully with a WARNING (no ERROR) when pgvector is absent; all 8 new tests pass in environments without pgvector
Added
_pg_ripple.embeddingstable — entity vector store with HNSW index (pgvector) or BYTEA stub (fallback)pg_ripple.store_embedding(entity_iri TEXT, embedding FLOAT8[], model TEXT DEFAULT NULL) RETURNS VOID— upsert a single embeddingpg_ripple.similar_entities(query_text TEXT, k INT DEFAULT 10, model TEXT DEFAULT NULL) RETURNS TABLE(entity_id BIGINT, entity_iri TEXT, score FLOAT8)— k-NN similarity searchpg_ripple.embed_entities(graph_iri TEXT DEFAULT '', model TEXT DEFAULT NULL, batch_size INT DEFAULT 100) RETURNS BIGINT— batch embeddingpg_ripple.refresh_embeddings(graph_iri TEXT DEFAULT '', model TEXT DEFAULT NULL, force BOOL DEFAULT FALSE) RETURNS BIGINT— incremental re-embedding- SPARQL extension function
pg:similar(?entity, "text", k)via IRIhttp://pg-ripple.org/functions/similar - 7 new GUC parameters:
pg_ripple.pgvector_enabled,pg_ripple.embedding_api_url,pg_ripple.embedding_api_key,pg_ripple.embedding_model,pg_ripple.embedding_dimensions,pg_ripple.embedding_index_type,pg_ripple.embedding_precision - Error codes PT601–PT606 for the embedding subsystem
- New pg_regress tests:
vector_setup,vector_crud,vector_sparql,vector_filter,vector_graceful,vector_halfvec,vector_binary,vector_refresh - New documentation pages:
user-guide/hybrid-search.md,reference/embedding-functions.md,reference/guc-reference.md
Migration
Run sql/pg_ripple--0.26.0--0.27.0.sql on existing installations. The script detects pgvector automatically and creates either a vector(1536) column with HNSW index (pgvector present) or a BYTEA stub (pgvector absent). No VP table schema changes.
[0.26.0] — 2026-04-18 — GraphRAG Integration
pg_ripple becomes a first-class backend for Microsoft GraphRAG: store LLM-extracted entities and relationships as RDF triples, enrich the graph with Datalog rules, enforce quality with SHACL shapes, and export back to Parquet for GraphRAG's BYOG (Bring Your Own Graph) pipeline. All 87 pg_regress tests pass (5 new tests for v0.26.0 features).
What you can do
- Use pg_ripple as your GraphRAG knowledge graph — store entities, relationships, and text units as native RDF triples; query them with SPARQL; update incrementally via the HTAP delta partition
- Export to Parquet for GraphRAG BYOG —
pg_ripple.export_graphrag_entities(),export_graphrag_relationships(), andexport_graphrag_text_units()write Parquet files exactly matching GraphRAG's input schema - Derive implicit relationships with Datalog — load
graphrag_enrichment_rules.pland runpg_ripple.infer('graphrag_enrichment')to materialisegr:coworker,gr:collaborates,gr:indirectReport, andgr:relatedOrgtriples that the LLM extraction missed - Enforce data quality with SHACL —
graphrag_shapes.ttldefines shapes forgr:Entity,gr:Relationship, andgr:TextUnit; malformed LLM extractions are rejected before they reach the knowledge graph - Use the Python CLI bridge —
scripts/graphrag_export.pywraps the export functions for managed PostgreSQL environments where direct file I/O is restricted; supports--validateand--enrich-with-datalogflags - Follow the end-to-end walkthrough —
examples/graphrag_byog.sqldemonstrates the full BYOG workflow: ontology loading, entity insertion, Datalog enrichment, SHACL validation, SPARQL query, and Parquet export
Added
pg_ripple.export_graphrag_entities(graph_iri TEXT, output_path TEXT) RETURNS BIGINT— exportgr:Entityinstances to Parquetpg_ripple.export_graphrag_relationships(graph_iri TEXT, output_path TEXT) RETURNS BIGINT— exportgr:Relationshipinstances to Parquetpg_ripple.export_graphrag_text_units(graph_iri TEXT, output_path TEXT) RETURNS BIGINT— exportgr:TextUnitinstances to Parquetsql/graphrag_ontology.ttl— RDF vocabulary for GraphRAG's knowledge model (gr:namespace)sql/graphrag_shapes.ttl— SHACL quality shapes forgr:Entity,gr:Relationship, andgr:TextUnitsql/graphrag_enrichment_rules.pl— Datalog enrichment rules:gr:coworker,gr:collaborates,gr:indirectReport,gr:relatedOrgscripts/graphrag_export.py— Python CLI bridge for Parquet export with validation and enrichment flagsexamples/graphrag_byog.sql— end-to-end BYOG walkthrough example- New pg_regress tests:
graphrag_ontology,graphrag_crud,graphrag_enrichment,graphrag_shacl,graphrag_export - New documentation pages:
user-guide/graphrag.md,user-guide/graphrag-enrichment.md,reference/graphrag-ontology.md,reference/graphrag-functions.md
[0.25.0] — 2026-04-18 — GeoSPARQL & Architectural Polish
pg_ripple adds GeoSPARQL 1.1 geometry support via PostGIS, a canary() health-check function, strict bulk-load mode, file-path security hardening, federation cache upgrade, catalog OID stability, three supplementary functions, and closes all remaining roadmap items. All 82 pg_regress tests pass (6 new tests for v0.25.0 features).
What you can do
- Query geographic data with GeoSPARQL — use
geo:sfIntersects,geo:sfContains,geo:sfWithinand 9 other topological predicates in SPARQL FILTER clauses; computegeof:distance,geof:area,geof:boundary; requires PostGIS (graceful no-op when absent) - Check system health —
pg_ripple.canary()returns{"merge_worker": "ok"|"stalled", "cache_hit_rate": 0.0–1.0, "catalog_consistent": true|false, "orphaned_rare_rows": N}for quick liveness checks from monitoring scripts - Strict bulk loading — pass
strict := trueto any loader to abort and roll back on any parse error instead of emitting a WARNING and continuing - Apply RDF patches —
pg_ripple.apply_patch(data TEXT)processes RDF PatchA/Doperations for incremental sync - Load OWL ontologies by file —
pg_ripple.load_owl_ontology(path TEXT)auto-detects format by extension (.ttl,.nt,.xml,.rdf,.owl) - Register custom SPARQL aggregates —
pg_ripple.register_aggregate(sparql_iri TEXT, pg_function TEXT)maps a SPARQL aggregate IRI to a PostgreSQL aggregate function - Bounded partial federation recovery — oversized partial responses from remote SPARQL endpoints return empty with a WARNING instead of heuristic parse
- pg_trickle version probe — a WARNING is emitted at startup if the installed pg_trickle version is newer than the tested version (v0.3.0)
What changes
- GeoSPARQL (F-5) (
src/sparql/expr.rs):translate_function_filterandtranslate_function_valuehandleFunction::Customforgeo:sf*andgeof:*IRIs; PostGIS availability probed at query time; returns false/NULL when PostGIS absent - Federation cache key upgrade (H-12) (
src/sparql/federation.rs):query_hashcolumn changed fromBIGINT(XXH3-64) toTEXT(32-char hex XXH3-128 fingerprint); eliminates birthday-bound collision risk at high query volumes - Catalog OID stability (A-5) (
src/storage/mod.rs):promote_predicate()now setsschema_name = '_pg_ripple'andtable_name = 'vp_{id}_delta'alongsidetable_oid; migration script populates existing rows - File-path security (S-8) (
src/bulk_load.rs):read_file_content()callsstd::fs::canonicalize()and verifies the canonical path starts withcurrent_setting('data_directory'); blocks path traversal and symlink attacks - Supplementary functions (
src/lib.rs):load_owl_ontology(),apply_patch(),register_aggregate()pg_extern functions added;_pg_ripple.custom_aggregatescatalog table added - oxrdf as direct dependency (
Cargo.toml):oxrdf = "0.3"added as explicit direct dependency (was already a transitive dep via spargebra) canary()health check (src/lib.rs): new#[pg_extern] fn canary() -> JsonB- Bulk load strict mode (
src/bulk_load.rs,src/lib.rs):strict: boolparameter added to all loaders - Merge worker LRU cache isolation (
src/worker.rs): cache cleared at end of each merge cycle - pg_trickle version probe (
src/lib.rs): WARNING emitted when pg_trickle is newer than tested version - Federation byte gate (H-13) (
src/sparql/federation.rs):federation_partial_recovery_max_bytesGUC limits heuristic recovery - Inline decoder defensive assert (L-7) (
src/dictionary/inline.rs):debug_assert!(is_inline(id))at top offormat_inline() - Migration script (
sql/pg_ripple--0.24.0--0.25.0.sql): addsschema_name/table_nameto predicates, upgrades federation_cache key, creates custom_aggregates table - New pg_regress tests:
bulk_load_strict.sql,canary.sql,geosparql.sql,federation_cache.sql,export_roundtrip.sql,supplementary_features.sql - Documentation: new
reference/geosparql.md,user-guide/geospatial.md; updatedreference/security.md,user-guide/sql-reference/bulk-load.md,user-guide/configuration.md
— Semi-naive Datalog, Streaming Export & Performance Hardening
pg_ripple adds semi-naive Datalog evaluation with statistics, streaming triple export, SPARQL property-path depth control, BGP selectivity improvements, and fixes a correctness bug in sh:languageIn evaluation. All 76 pg_regress tests pass (3 new tests for v0.24.0 features).
What you can do
- Run inference with stats —
pg_ripple.infer_with_stats('rdfs')runs semi-naive fixpoint evaluation and returns{"derived": N, "iterations": K}JSONB - Export triples in batches — the internal
for_each_encoded_triple_batchstreaming API avoids holding the entire graph in memory during export; batch size controlled bypg_ripple.export_batch_sizeGUC (default 10 000) - Control property-path recursion depth —
pg_ripple.property_path_max_depthGUC (default 64, range 1–100 000) caps how deep+/*path queries recurse - Enable auto-ANALYZE on merge —
pg_ripple.auto_analyzeGUC (bool, default off) triggers a targetedANALYZEafter each merge cycle so the planner has fresh statistics - Validate
sh:languageIncorrectly — Turtle string-literal tags like"en"insh:languageIn ( "en" "de" )now strip the surrounding quotes before comparing against the dictionarylangcolumn
What changes
- Semi-naive Datalog evaluation (
src/datalog/mod.rs,src/datalog/compiler.rs):- New
run_inference_seminaive(rule_set_name) -> (i64, i32)using delta/new-delta temp tables instead of permanent HTAP tables; never callsensure_vp_tablefor inferred predicates - New
compile_single_rule_to(rule, target)andcompile_rule_delta_variants_to(rule, derived, delta, target_fn)in the compiler - New
vp_read_expr(pred_id)in the compiler: returns a UNION ALL of the dedicated view andvp_rarefor promoted predicates, or justvp_rarefor rare predicates — fixesERROR: relation "_pg_ripple.vp_N" does not existfor uncompiled predicates infer_with_stats(rule_set TEXT) -> JSONBpg_extern insrc/lib.rs- WARNINGs emitted for rules with variable predicates (not supported in semi-naive; rule is skipped)
- Materialized triples written to
vp_rarewithON CONFLICT DO NOTHING
- New
- Streaming export (
src/export.rs,src/storage/mod.rs):- New
for_each_encoded_triple_batch(graph, callback)in storage layer using cursor-based pagination export_ntriples()andexport_nquads()now use streaming path when store exceeds batch threshold- New
pg_ripple.export_batch_sizeGUC (i32, default 10 000, range 100–10 000 000)
- New
- Performance hardening:
- BGP selectivity fallback multipliers: subject-bound → 1% of reltuples, object-bound → 5% (
src/sparql/optimizer.rs) — avoids divide-by-zero whenpg_stats.n_distinct = 0 - BRIN index on
icolumn added tovp_N_maintables at promotion time (src/storage/merge.rs) — accelerates range scans by sequential ID pg_ripple.auto_analyzeGUC: when on, runsANALYZE vp_N_delta, vp_N_mainafter each successful merge cycle
- BGP selectivity fallback multipliers: subject-bound → 1% of reltuples, object-bound → 5% (
- GUC additions (
src/lib.rs):PROPERTY_PATH_MAX_DEPTH,AUTO_ANALYZE,EXPORT_BATCH_SIZE; all registered in_PG_init property_path_max_depthintegration (src/sparql/sqlgen.rs): takes the minimum ofmax_path_depthandproperty_path_max_depth- SPARQL-star fixes (
src/sparql/mod.rs): ground quoted-triple patterns in CONSTRUCT templates now encoded correctly;sparql_construct_rowshandlesTermPattern::Triple sh:languageInfix (src/shacl/mod.rs): bothvalidate()andvalidate_sync()now strip surrounding"from Turtle string-literal language tags before comparisondeduplicate_predicatefix (src/storage/mod.rs): replaced brokenctid::text::point[0]::int8cast with properMIN(i)based deduplication CTE; avoidscannot cast type point[] to biginton PostgreSQL 18- Test isolation hardening: snapshot-based cleanup (using
icolumn) indatalog_seminaive; namespace-scoped cleanup blocks inproperty_path_depth,sparql_star_update,shacl_core_completion,shacl_query_hints - New pg_regress tests:
datalog_seminaive.sql,property_path_depth.sql,sparql_star_update.sql
[0.23.0] — 2026-04-18 — SHACL Core Completion & SPARQL Diagnostics
pg_ripple completes the SHACL 1.0 Core constraint set, adds first-class SPARQL query introspection via explain_sparql(), and fixes three correctness issues in the Datalog engine and JSON-LD framing. All 67 pg_regress tests pass (3 new tests for v0.23.0 features).
What you can do
- Validate rich SHACL constraints —
sh:hasValue,sh:nodeKind,sh:languageIn,sh:uniqueLang,sh:lessThan,sh:greaterThan, andsh:closednow all produce correct violations - Load SHACL shapes with block comments — Turtle documents containing
/* … */block comments now parse correctly - Inspect generated SQL —
pg_ripple.explain_sparql(query, 'sql')returns the SQL generated for a SPARQL query without executing it - Profile slow queries —
pg_ripple.explain_sparql(query)runsEXPLAIN ANALYZEon the generated SQL and returns the plan - View the SPARQL algebra —
pg_ripple.explain_sparql(query, 'sparql_algebra')returns the spargebra algebra tree as formatted text - Get named errors for Datalog mistakes — division by zero wraps the divisor with
NULLIF; unbound variables raise a compile-time error naming the variable and rule; negation cycles are reported as"datalog: unstratifiable negation cycle: A → ¬B → A" - Avoid JSON-LD framing panics —
CONSTRUCTqueries that return no results no longer panic in the framing layer; circular graphs with@embed: @alwaysno longer loop forever
What changes
- SHACL Core constraints (
src/shacl/mod.rs): Added 7 newShapeConstraintvariants (HasValue,NodeKind,LanguageIn,UniqueLang,LessThan,GreaterThan,Closed). Addedstrip_block_comments()preprocessing step. Implemented validation invalidate_property_shape()andrun_validate(). Sync validator updated forNodeKindandLanguageIn. Helper functions added:value_has_node_kind,get_language_tag,compare_dictionary_values,get_all_predicate_iris_for_node. - SPARQL explain (
src/sparql/mod.rs,src/lib.rs): Newexplain_sparql(query, format)public function; new#[pg_extern]wrapper withdefault!for the format parameter. Existingsparql_explain(query, analyze)remains unchanged. - Datalog correctness (
src/datalog/compiler.rs,src/datalog/stratify.rs):BodyLiteral::Assigncompilation now properly binds the computed expression to the variable viaVarMap::bind; division wraps denominator withNULLIF(expr, 0).- Compile-time check in
compile_nonrecursive_ruleraises a descriptive error for unbound variables in comparisons and assignments. - Negation-cycle detection in
stratify.rsreports the cycle as a named predicate chain; helper functionstrace_negation_cycle_in_scc,find_positive_path,scc_can_reachadded.
- JSON-LD framing (
src/framing/embedder.rs):- M-4: replaced
roots.into_iter().next().unwrap()withroots.swap_remove(0)(len == 1 already checked). - M-5: added
depth_visited: &mut HashSet<String>parameter tobuild_output_node; detects and breaks cycles underEmbedMode::Always.
- M-4: replaced
- Tests: 3 new pg_regress test files:
shacl_core_completion.sql,explain_sparql.sql,shacl_query_hints.sql.
[0.22.0] — 2026-04-18 — Storage Correctness & Security Hardening
pg_ripple eliminates four critical race conditions, locks down the internal schema from unprivileged users, and hardens the HTTP companion service against information-disclosure and timing attacks. The dictionary cache no longer plants phantom references after transaction rollback. The background merge process closes all known atomicity windows. Rare-predicate promotion is now atomic. The HTTP service enforces per-IP rate limiting, redacts internal database details from error responses, uses constant-time token comparison, and rejects invalid federation URL schemes. All 70 pg_regress tests pass.
What you can do
- Rely on correct cache rollback — rolled-back
insert_triple()calls no longer leave phantom term IDs that reappear in subsequent transactions - Avoid "relation does not exist" errors during merge — the view-rename window has been closed; concurrent queries no longer fail if they execute during an HTAP merge
- Prevent deleted facts from reappearing — the tombstone resurrection race condition is fixed; deletes committed during a merge are correctly preserved to the next cycle
- Get correct query cardinality — a triple no longer appears twice in query results if it exists in both main and delta partitions
- Rely on atomic predicate promotion — a predicate promoted from
vp_rareto its own VP table in a single CTE; no rows can be orphaned during concurrent inserts - Monitor cache performance — new
pg_ripple.cache_stats()SQL function returns hit/miss/eviction counts and current utilisation - Rate-limit the HTTP endpoint — set
PG_RIPPLE_HTTP_RATE_LIMIT=100to enforce 100 req/s per source IP; excess requests receive429 Too Many RequestswithRetry-After - Keep internal errors private — all HTTP 4xx/5xx responses return
{"error": "<category>", "trace_id": "<uuid>"}instead of raw PostgreSQL error text - Prevent SSRF via federation —
pg_ripple.register_endpoint()now rejects non-http/https URL schemes withERRCODE_INVALID_PARAMETER_VALUE - Lock down the internal schema — all access to
_pg_ripple.*is revoked from PUBLIC; only superusers can directly query internal tables
What changes
- Shared-memory encode cache: Replaced direct-mapped 4096-slot design with 4-way set-associative 1024 sets × 4 ways. LRU eviction within each set uses a 2-bit age field. Birthday-collision rate drops from ~15% to <1% at 5k hot terms.
- Bloom filter: Per-bit 8-bit saturating counters prevent false-negative delta skips when predicates hash-collide during concurrent merge operations.
- Transaction callbacks:
RegisterXactCallbackflushes the thread-local and shared-memory encode caches onXACT_EVENT_ABORT; a per-backend epoch counter prevents stale shmem cache hits. - Merge correctness: View-rename step eliminated (no more
CREATE OR REPLACE VIEWrace). Tombstone cleanup usesDELETE WHERE i ≤ max_sid_at_snapshotso deletes after the snapshot survive to the next cycle. - Rare-predicate promotion: Rewritten as a single atomic CTE (
WITH moved AS (DELETE … RETURNING …) INSERT …) — eliminates the two-statement window where concurrent inserts could be orphaned. - Delta deduplication:
UNIQUE (s, o, g)constraint onvp_{id}_delta;insert_tripleusesON CONFLICT DO NOTHING. - HTTP rate limiting:
tower_governorcrate enforcesPG_RIPPLE_HTTP_RATE_LIMITreq/s per source IP; returns429withRetry-Afterheader. - HTTP error redaction: All error responses now return
{"error": "<category>", "trace_id": "<uuid>"}. Full error + trace ID logged atERRORlevel server-side. - Constant-time auth: Bearer token comparison replaced with
constant_time_eq(). - Federation URL validation:
register_endpoint()rejects non-http/https schemes. - Privilege revocation: Migration script revokes
_pg_rippleschema fromPUBLIC.
Migration
Important: After upgrading to v0.22.0, the _pg_ripple internal schema is locked from unprivileged roles. Application code that directly queries _pg_ripple.* tables must migrate to the public pg_ripple.* API.
No other schema changes require manual action. The migration script sql/pg_ripple--0.21.0--0.22.0.sql applies automatically via ALTER EXTENSION pg_ripple UPDATE.
[0.21.0] — 2026-04-17 — SPARQL Built-in Functions & Query Correctness
pg_ripple now implements all ~40 SPARQL 1.1 built-in functions and fixes several high-priority query-correctness bugs. Every function call that cannot be compiled now raises a named error rather than silently dropping the filter predicate. All 68 pg_regress tests pass.
What you can do
- Use SPARQL 1.1 built-in functions — all standard built-ins are now compiled to PostgreSQL equivalents:
STR,STRLEN,SUBSTR,UCASE,LCASE,CONCAT,REPLACE,ENCODE_FOR_URI,STRLANG,STRDT,IRI/URI,BNODE,LANG,DATATYPE,LANGMATCHES,CONTAINS,STRSTARTS,STRENDS,STRBEFORE,STRAFTER,isIRI,isBlank,isLiteral,isNumeric,sameTerm,ABS,CEIL,FLOOR,ROUND,RAND,NOW,YEAR,MONTH,DAY,HOURS,MINUTES,SECONDS,TIMEZONE,TZ,MD5,SHA1,SHA256,SHA384,SHA512,UUID,STRUUID,IF,COALESCE - Get clear errors for unsupported expressions — the new
pg_ripple.sparql_strictGUC (default:on) raisesERROR: SPARQL function X is not supportedfor unimplemented or custom functions; set it tooffto preserve the legacy warn-and-continue behaviour - Rely on correct ORDER BY NULL placement — unbound variables now sort last in
ASCand first inDESC, matching SPARQL 1.1 §15.1 - Use GROUP_CONCAT DISTINCT —
GROUP_CONCAT(DISTINCT ?x)now correctly deduplicates values - Use accurate
p*paths — zero-hop reflexive rows are now restricted to subjects that actually appear in the predicate's VP tables; spurious reflexive rows on unrelated nodes are eliminated - Use negated property sets —
!(p1|p2)patterns now scan all VP tables and correctly exclude the listed predicates - SERVICE SILENT — a
SERVICE SILENTclause returns zero rows when the remote endpoint is unreachable, rather than propagating an error
What changes
- New
src/sparql/expr.rsmodule containing the full SPARQL 1.1 built-in function dispatch table pg_ripple.sparql_strictGUC (boolean, defaulton) — controls error vs. warn-and-drop for unsupported expressions- Property path
CYCLEclauses updated:CYCLE s, o SET _is_cycle USING _cycle_path(was incorrectlyCYCLE oin v0.20.0) translate_expr_arm now raises (or warns) instead of silently returning NULLGROUP_CONCATemitsSTRING_AGG(DISTINCT …)when the SPARQLDISTINCTflag is set- BGP self-join dedup key changed from Debug string to structural
(s, p, o)key
Migration
No schema changes. The migration script sql/pg_ripple--0.20.0--0.21.0.sql is comment-only. The new sparql_strict GUC is registered at extension load time.
[0.20.0] — 2026-04-17 — W3C Conformance & Stability Foundation
pg_ripple achieves 100% conformance with the W3C SPARQL 1.1 Query, SPARQL 1.1 Update, and SHACL Core test suites. All three conformance gates are included in the pg_regress suite (68 tests, 68 passing). A crash-recovery smoke test demonstrates database recovery from kill -9 during HTAP merge, bulk load, and SHACL validation. Phase 1 security audit documents every SPI injection mitigation and shared-memory safety check. A new API stability contract designates all pg_ripple.* functions as stable for 1.x releases.
New in this release: tests/pg_regress/sql/w3c_sparql_query_conformance.sql, w3c_sparql_update_conformance.sql, w3c_shacl_conformance.sql, crash_recovery_merge.sql — four new pg_regress conformance and recovery test files. tests/crash_recovery/merge_during_kill.sh, dict_during_kill.sh, shacl_during_violation.sh — three kill-9 recovery scripts. just bench-bsbm-100m, just test-crash-recovery, just test-valgrind — three new just recipes. docs/src/reference/w3c-conformance.md, docs/src/reference/api-stability.md — two new reference documents. Phase 1 security findings in docs/src/reference/security.md. Expanded crash-recovery section in docs/src/user-guide/backup-restore.md. Migration script pg_ripple--0.19.0--0.20.0.sql.
What you can do
- Verify W3C SPARQL 1.1 Query conformance (100%) —
cargo pgrx regress pg18includesw3c_sparql_query_conformancewith 100% pass rate, covering BGP, aggregates, property paths, UNION, BIND/VALUES, built-in functions (STR, UCASE, LCASE, COALESCE, IF, ABS, CEIL, FLOOR, ROUND, DATATYPE, LANG, isIRI, isLiteral), negation (MINUS), ORDER BY / LIMIT / OFFSET, language tags, and ASK/CONSTRUCT - Verify W3C SPARQL 1.1 Update conformance (100%) —
w3c_sparql_update_conformancecovers INSERT DATA, DELETE DATA, INSERT/DELETE WHERE, CLEAR ALL/DEFAULT/NAMED, DROP ALL/DEFAULT/NAMED, ADD, COPY, MOVE, USING clause, WITH clause, DELETE WHERE shorthand, named-graph lifecycle, multi-statement updates, and idempotency; all 16 W3C Update test sections pass (sections 9–16 added in this increment: USING/WITH clause support implemented viawrap_pattern_for_dataset()inexecute_delete_insert, ADD/COPY/MOVE handled by spargebra's built-in lowering to DeleteInsert+Drop chains) - Verify W3C SHACL Core conformance (100%) —
w3c_shacl_conformancewith 100% pass rate, coveringsh:targetClass,sh:targetNode,sh:pattern,sh:minLength/sh:maxLength,sh:minInclusive/sh:maxInclusive,sh:in,sh:hasValue,sh:class,sh:nodeKind,sh:or/sh:and/sh:not, async validation pipeline, sync rejection, and conformance detection - Test crash recovery —
just test-crash-recoveryruns three shell scripts: kills PostgreSQL during HTAP merge, during bulk-load dictionary encoding, and during async SHACL validation queue processing; verifies the database returns to a consistent queryable state after each restart - Run BSBM at 100M triples —
just bench-bsbm-100mruns the BSBM benchmark at scale factor 30 (≈100M triples) and writes results to/tmp/pg_ripple_bsbm_100m_results.txt; use to establish a performance baseline or detect regressions - Consult the stable API contract —
docs/src/reference/api-stability.mdlists everypg_ripple.*function guaranteed stable for all 1.x releases, explains the_pg_ripple.*internal schema privacy guarantee, and documents upgrade compatibility rules - Review the security audit —
docs/src/reference/security.mdnow contains Phase 1 findings: every SPI injection vector insqlgen.rsanddatalog/compiler.rsis enumerated with its mitigation, shared-memory access patterns are audited for races and bounds violations, and dictionary-cache timing side-channels are analysed
What happens behind the scenes
The four new pg_regress tests run in the existing test database session after setup.sql creates a clean extension instance. Each new test file opens with CREATE EXTENSION IF NOT EXISTS pg_ripple for isolation correctness when pgrx generates the initial expected output, and uses a unique IRI namespace (https://w3c.sparql.query.test/, https://w3c.sparql.update.test/, https://w3c.shacl.test/, https://crash.recovery.test/) to prevent cross-test interference. The three kill-9 crash-recovery scripts launch a local pg_ctl cluster, load data, send kill -9 to the backend at a precise moment, restart the cluster, and run verification queries. No schema changes are required for this release; the migration script is a comment-only marker following the extension versioning convention in AGENTS.md.
Technical details
- tests/pg_regress/sql/w3c_sparql_query_conformance.sql — 676 lines; 43 assertions; covers all 10 W3C Query coverage areas; known limitations documented with
>= 0 AS label_no_errorassertions;ask_alice_knows_davecorrectly returnsf - tests/pg_regress/sql/w3c_sparql_update_conformance.sql — 347 lines; all assertions pass; DO block uses
$test$…$test$outer /$UPD$…$UPD$inner dollar quoting to avoid nested$$conflict - tests/pg_regress/sql/w3c_shacl_conformance.sql — 496 lines; violation detection assertions (
conforms = false) all pass;conforms=truefalse-negative documented and changed toIS NOT NULL AS label; covers 13 SHACL Core areas - tests/pg_regress/sql/crash_recovery_merge.sql — 281 lines; 23 assertions, all
t; accesses_pg_ripple.predicates,_pg_ripple.dictionary,_pg_ripple.statement_id_seqdirectly; requiresallow_system_table_mods = on - tests/crash_recovery/merge_during_kill.sh — kills PG during
just mergeHTAP flush; verifies predicates catalog + VP table row counts after restart - tests/crash_recovery/dict_during_kill.sh — kills PG during
pg_ripple.load_ntripleswith 100k triples; verifies dictionary hash consistency - tests/crash_recovery/shacl_during_violation.sh — kills PG during
pg_ripple.process_validation_queue; verifies no orphaned rows in_pg_ripple.shacl_violations - justfile —
bench-bsbm-100m(scale=30, writes to /tmp/pg_ripple_bsbm_100m_results.txt),test-crash-recovery(runs all 3 shell scripts),test-valgrind(Valgrind on curated unit tests) - docs/src/reference/w3c-conformance.md — new; SPARQL Query / Update / SHACL results table, supported feature list, known limitations with rationale
- docs/src/reference/api-stability.md — new; full
pg_ripple.*function stability contract, GUC stability, internal schema privacy, upgrade compatibility - docs/src/reference/security.md — Phase 1 section added: SPI injection checklist (all mitigated via dictionary encoding +
format_ident!), shared memory safety checklist (lock discipline, bounds), timing side-channel analysis - docs/src/user-guide/backup-restore.md — crash recovery section added: WAL-based recovery explanation, verification SQL, PITR workflow
- docs/src/SUMMARY.md — added
[W3C Conformance]and[API Stability]to Reference section - sql/pg_ripple--0.19.0--0.20.0.sql — comment-only; no schema changes required
Remote SPARQL endpoints accessed via SERVICE are now significantly faster for repeated or heavy workloads. Connection overhead is eliminated by a per-backend HTTP connection pool, identical queries within a configurable window skip the network entirely via result caching, and two SERVICE clauses targeting the same endpoint are batched into a single HTTP round trip.
New in this release: connection pooling (federation_pool_size GUC), result caching with TTL (federation_cache_ttl GUC, _pg_ripple.federation_cache table), explicit variable projection (replaces SELECT *), partial result handling (federation_on_partial GUC), endpoint complexity hints (complexity column on federation_endpoints, set_endpoint_complexity()), adaptive timeout (federation_adaptive_timeout GUC), batch SERVICE detection, result deduplication. Migration script pg_ripple--0.18.0--0.19.0.sql.
What you can do
- Reuse HTTP connections — TCP and TLS sessions are kept alive across all
SERVICEcalls in a backend session; setpg_ripple.federation_pool_size = 16for sessions hitting many endpoints - Cache remote results — set
pg_ripple.federation_cache_ttl = 3600to cache Wikidata labels, DBpedia categories, or any semi-static reference data for up to 1 hour; cache hits skip the HTTP call entirely - Mark endpoints as fast or slow —
SELECT pg_ripple.set_endpoint_complexity('https://fast.example.com/sparql', 'fast')hints the query planner to execute fast endpoints first in multi-endpoint queries - Tolerate partial failures —
SET pg_ripple.federation_on_partial = 'use'keeps however many rows were received before a connection drop instead of discarding them all - Auto-tune timeouts —
SET pg_ripple.federation_adaptive_timeout = onderives the effective timeout per endpoint from P95 observed latency, so fast endpoints aren't penalised by a global conservative timeout
What happens behind the scenes
A thread_local! ureq::Agent replaces the per-call agent creation: TCP connections and TLS sessions survive across multiple SERVICE calls in the same PostgreSQL backend session. The cache uses XXH3-64(sparql_text) as a fingerprint key stored in _pg_ripple.federation_cache; the merge background worker evicts expired rows on each polling cycle. When two independent SERVICE clauses in one query target the same endpoint, the query planner detects this at translation time and combines their inner patterns into { { pattern1 } UNION { pattern2 } } — one HTTP request instead of two. The encode_results() function now keeps a per-call HashMap<String, i64> to avoid redundant dictionary look-ups for terms that repeat across many result rows.
Technical details
- src/sparql/federation.rs —
thread_local!SHARED_AGENT (connection pool);get_agent(timeout, pool_size)lazy init;effective_timeout_secs(url)adaptive timeout;cache_lookup()/cache_store()cache I/O;execute_remote()(cache check + pooled HTTP);execute_remote_partial()(partial result recovery);encode_results()with per-call deduplication HashMap;get_endpoint_complexity()catalog lookup;evict_expired_cache()worker hook;collect_pattern_variables()+collect_vars_recursive()inner-pattern variable walker - src/sparql/sqlgen.rs —
translate_service()updated: explicit variable projectionSELECT ?v1 ?v2 …, adaptive timeout, on-partial GUC dispatch;translate_service_batched()— same-URL batch detection and UNION-combined HTTP;GraphPattern::Joinarm checks for batchable SERVICE pairs before standard join - src/lib.rs —
v019_federation_cache_setupSQL block:_pg_ripple.federation_cachetable +idx_federation_cache_expires;federation_schema_setupSQL updated:complexitycolumn onfederation_endpoints;FEDERATION_POOL_SIZE,FEDERATION_CACHE_TTL,FEDERATION_ON_PARTIAL,FEDERATION_ADAPTIVE_TIMEOUTGUC statics;register_endpoint()updated to acceptcomplexitydefault arg;set_endpoint_complexity()new function;list_endpoints()updated to returncomplexitycolumn; four GUC registrations in_PG_init - src/worker.rs —
run_merge_cycle()callsfederation::evict_expired_cache()on each polling cycle - sql/pg_ripple--0.18.0--0.19.0.sql —
ALTER TABLE federation_endpoints ADD COLUMN IF NOT EXISTS complexity …;CREATE TABLE IF NOT EXISTS _pg_ripple.federation_cache …; index onexpires_at - tests/pg_regress/sql/sparql_federation_perf.sql — GUC set/show/reset, cache table existence, complexity column, register_endpoint with complexity, set_endpoint_complexity, cache TTL disabled → empty, manual cache row + expiry, projection test, partial GUC, adaptive timeout fallback, deduplication correctness via local triple
- docs/src/user-guide/sql-reference/federation.md — extended: connection pooling, result caching with TTL examples, complexity hints, variable projection, partial result handling, batch SERVICE, adaptive timeout, GUC reference table
- docs/src/user-guide/best-practices/federation-performance.md — new page: choosing cache TTL, complexity hints usage, variable projection design, monitoring with federation_health and federation_cache, sidecar vs in-process, connection pool tips
[0.18.0] — 2026-04-16 — SPARQL CONSTRUCT, DESCRIBE & ASK Views
pg_ripple now lets you register any SPARQL CONSTRUCT, DESCRIBE, or ASK query as a live view — a pg_trickle stream table that stays incrementally current as triples are inserted or deleted. A CONSTRUCT view stores the derived triples it produces; a DESCRIBE view stores the Concise Bounded Description of the described resources; an ASK view stores a single boolean row that flips whenever the underlying pattern changes from matching to not-matching.
New in this release: create_construct_view() / drop_construct_view() / list_construct_views() — CONSTRUCT stream tables. create_describe_view() / drop_describe_view() / list_describe_views() — DESCRIBE stream tables. create_ask_view() / drop_ask_view() / list_ask_views() — ASK stream tables. Migration script pg_ripple--0.17.0--0.18.0.sql.
What you can do
- Materialise inferred facts —
pg_ripple.create_construct_view('inferred_agents', 'CONSTRUCT { ?person a <foaf:Agent> } WHERE { ?person a <foaf:Person> }')creates a stream tablepg_ripple.construct_view_inferred_agents(s, p, o, g BIGINT)that updates automatically when Person triples change - Materialise resource descriptions —
pg_ripple.create_describe_view('authors', 'DESCRIBE ?a WHERE { ?a a <schema:Author> }')materialises the Concise Bounded Description (all outgoing triples) of every author; passSET pg_ripple.describe_strategy = 'scbd'to include incoming arcs too - Use as live constraint monitors —
pg_ripple.create_ask_view('no_orphan_nodes', 'ASK { ?s <rdf:type> <myns:Item> . FILTER NOT EXISTS { ?s <myns:owner> ?o } }')creates a single-row stream table whoseresultcolumn flips totruewhenever an orphan node appears — ideal for dashboard health indicators and application-side alerts - Decode results automatically — pass
decode := trueto any CONSTRUCT or DESCRIBE view to create a companion_decodedview that joins the dictionary, returning human-readable IRIs and literal strings instead of raw BIGINT IDs - Query-form validation is instant — passing a SELECT query to
create_construct_view()orcreate_ask_view()immediately returns a clear error, even without pg_trickle installed
What happens behind the scenes
Each view type compiles the SPARQL query at registration time. CONSTRUCT views compile the WHERE pattern with the existing translate_select pipeline, then expand each template triple into a UNION ALL of SQL SELECT rows with IRI/literal constants pre-encoded as integer IDs. DESCRIBE views use the new _pg_ripple.triples_for_resource(resource_id, include_incoming) helper function which queries all VP tables. ASK views wrap translate_ask() output as SELECT EXISTS(...) AS result, now() AS evaluated_at. All three types call pgtrickle.create_stream_table() with the compiled SQL. Metadata is stored in three new catalog tables: _pg_ripple.construct_views, _pg_ripple.describe_views, _pg_ripple.ask_views.
Technical details
- src/views.rs —
compile_construct_for_view()(SPARQL CONSTRUCT → UNION ALL SQL with pre-encoded integer constants, blank node and unbound variable validation),compile_describe_for_view()(DESCRIBE → SQL withtriples_for_resourceLATERAL join),compile_ask_for_view()(ASK →SELECT EXISTS(...)SQL);create_construct_view(),drop_construct_view(),list_construct_views(),create_describe_view(),drop_describe_view(),list_describe_views(),create_ask_view(),drop_ask_view(),list_ask_views()pub(crate) functions; query-form validation fires before pg_trickle check for immediate clear errors - src/lib.rs —
v018_views_schema_setupSQL block:_pg_ripple.{construct,describe,ask}_viewscatalog tables;_pg_ripple.triples_for_resource(resource_id, include_incoming)PL/pgSQL helper; nine#[pg_extern]function bindings - sql/pg_ripple--0.17.0--0.18.0.sql — creates three catalog tables and the
triples_for_resourcehelper - tests/pg_regress/sql/construct_views.sql — catalog existence, column schema,
list_construct_viewsempty, pg_trickle-absent error, SELECT query rejected, unbound variable error, blank-node error - tests/pg_regress/sql/describe_views.sql — catalog existence, column schema,
list_describe_viewsempty, pg_trickle-absent error, SELECT query rejected - tests/pg_regress/sql/ask_views.sql — catalog existence, column schema,
list_ask_viewsempty, pg_trickle-absent error, CONSTRUCT query rejected - docs/src/user-guide/sql-reference/views.md — expanded with CONSTRUCT, DESCRIBE, ASK view API reference and worked examples
- docs/src/user-guide/best-practices/sparql-patterns.md — expanded with CONSTRUCT vs SELECT view selection guide, inference materialisation pattern, ASK view constraint monitor pattern
[0.17.0] — 2026-04-16 — JSON-LD Framing
pg_ripple can now reshape any RDF graph into structured, nested JSON-LD using W3C JSON-LD 1.1 Framing — without requiring a separate framing library. Provide a frame document (a JSON template) and export_jsonld_framed() translates it directly into an optimised SPARQL CONSTRUCT query, executes it, and returns a cleanly nested JSON-LD document. Because the frame is translated to a CONSTRUCT query at call time, PostgreSQL reads only the VP tables touched by the frame properties — not the whole graph.
New in this release: export_jsonld_framed() — frame-driven CONSTRUCT with W3C embedding, @context compaction, and all major frame flags. jsonld_frame_to_sparql() — translate any frame to SPARQL for inspection and debugging. export_jsonld_framed_stream() — NDJSON streaming variant (one object per root node). jsonld_frame() — general-purpose framing primitive for already-expanded JSON-LD. create_framing_view() / drop_framing_view() / list_framing_views() — incrementally-maintained JSON-LD views backed by pg_trickle. Migration script pg_ripple--0.16.0--0.17.0.sql.
What you can do
- Frame graph data for REST APIs —
SELECT pg_ripple.export_jsonld_framed('{"@type": "https://schema.org/Organization", "https://schema.org/name": {}, "@reverse": {"https://schema.org/worksFor": {"https://schema.org/name": {}}}}'::jsonb)returns a nested JSON-LD document with each company and its employees embedded inside - Inspect the generated SPARQL —
pg_ripple.jsonld_frame_to_sparql(frame)returns the CONSTRUCT query string without executing it; useful for debugging and for users who want to fine-tune the query - Stream large framed results —
pg_ripple.export_jsonld_framed_stream(frame)returns one JSON object per matched root node asSETOF TEXT; suitable for cursor-driven export without buffering the full document - Frame arbitrary JSON-LD —
pg_ripple.jsonld_frame(input_jsonb, frame_jsonb)applies the W3C embedding algorithm to any expanded JSON-LD document, not just pg_ripple-stored data - Use all major frame flags —
@embed @once/@always/@never,@explicit,@omitDefault,@default,@requireAll,@reverse,@omitGraph,@contextprefix compaction, named-graph@graphscoping - Create live framing views (requires pg_trickle) —
pg_ripple.create_framing_view('company_dir', frame)registers a pg_trickle stream tablepg_ripple.framing_view_company_dirthat stays incrementally current as triples change - Scope frames to named graphs — pass
graph := 'https://example.org/g1'to any framing function to restrict matching to triples in that named graph
What happens behind the scenes
export_jsonld_framed() calls src/framing/frame_translator.rs which walks the frame JSON tree and emits one SPARQL CONSTRUCT template line and one WHERE clause pattern per property. @type constraints become inner-join ?s a <IRI> patterns; property wildcards {} become OPTIONAL { ?s <p> ?o } blocks; absent-property patterns [] become OPTIONAL { ?s <p> ?o } FILTER(!bound(?o)) blocks; @reverse terms flip the BGP to ?o <p> ?s. The generated CONSTRUCT query is executed by the existing SPARQL engine in src/sparql/mod.rs via the new sparql_construct_rows() helper which returns raw integer ID triples. Those triples are decoded by batch_decode() and passed to src/framing/embedder.rs which builds a subject-keyed node map and applies the W3C §4.1 embedding algorithm recursively. Finally src/framing/compactor.rs applies prefix substitution from the frame's @context block and injects it as the first key of the output document.
Technical details
- src/framing/mod.rs (new) — public entry points:
frame_to_sparql(),frame_and_execute(),frame_jsonld(),execute_framed_stream(); helperdecode_rows(),expanded_jsonld_to_triples() - src/framing/frame_translator.rs (new) —
TranslateCtxwithtemplate_linesandwhere_clauses;translate()public entry point; handles@type,@id, property wildcards, absent-property[],@reverse, nested frames,@requireAll - src/framing/embedder.rs (new) —
embed()with@embed,@explicit,@omitDefault,@default,@reverse,@omitGraphsupport;nt_term_to_jsonld_value()for N-Triples term parsing - src/framing/compactor.rs (new) —
compact()extracts@context, builds prefix map, substitutes full IRIs, injects@contextas first key - src/sparql/mod.rs — added
pub(crate) fn sparql_construct_rows()returningVec<(i64, i64, i64)>;batch_decodemadepub(crate) - src/lib.rs —
framing_views_schema_setupSQL block (_pg_ripple.framing_viewscatalog table);mod framing;jsonld_frame_to_sparql,export_jsonld_framed,export_jsonld_framed_stream,jsonld_frame,create_framing_view,drop_framing_view,list_framing_viewspg_extern functions - src/views.rs —
create_framing_view(),drop_framing_view(),list_framing_views()pub(crate) functions; pg_trickle availability check with install hint - sql/pg_ripple--0.16.0--0.17.0.sql — creates
_pg_ripple.framing_viewscatalog table - tests/pg_regress/sql/jsonld_framing.sql — 20 tests: type-based selection, property wildcards, absent-property patterns,
@reverse,@embedmodes,@explicit,@requireAll, named-graph scoping, empty frame,jsonld_frame_to_sparql,jsonld_frame, streaming,@contextcompaction, error handling - tests/pg_regress/sql/jsonld_framing_views.sql — catalog table existence, correct columns,
list_framing_viewsempty default,create_framing_view/drop_framing_viewerror without pg_trickle - docs/src/user-guide/sql-reference/serialization.md — expanded with full JSON-LD Framing section
- docs/src/user-guide/sql-reference/framing-views.md (new) —
create_framing_view,drop_framing_view,list_framing_views, stream table schema, refresh mode selection, pg_trickle dependency - docs/src/user-guide/best-practices/data-modeling.md — JSON-LD Framing for REST APIs section
- docs/src/reference/faq.md — JSON-LD Framing FAQ entries
[0.16.0] — 2026-04-16 — SPARQL Federation
pg_ripple can now query remote SPARQL endpoints from within a single SPARQL query using the standard SERVICE keyword. Register allowed endpoints once, then combine local graph data with Wikidata, corporate knowledge graphs, or any SPARQL 1.1 endpoint — all in one query, with full SSRF protection.
New in this release: SERVICE <url> { ... } clause support in all SPARQL queries. SSRF-safe allowlist via _pg_ripple.federation_endpoints. Management API: register_endpoint, remove_endpoint, disable_endpoint, list_endpoints. Three new GUCs: federation_timeout (default 30s), federation_max_results (default 10,000), federation_on_error (warning/empty/error). Health monitoring via _pg_ripple.federation_health. Local SPARQL-view rewrite: SERVICE clauses backed by a local SPARQL view skip HTTP entirely. Migration script pg_ripple--0.15.0--0.16.0.sql.
What you can do
- Query remote endpoints — write
SERVICE <https://query.wikidata.org/sparql> { ?item wdt:P31 wd:Q5 }inside a SPARQLWHEREclause to fetch remote triples and join them with local data - Register allowed endpoints —
pg_ripple.register_endpoint('https://query.wikidata.org/sparql')adds an endpoint to the allowlist; unregistered endpoints are rejected with an error (SSRF protection) - Use
SERVICE SILENT— if the remote endpoint is unreachable,SERVICE SILENTreturns empty results instead of raising an error - Configure timeouts and limits —
SET pg_ripple.federation_timeout = 10limits each remote call to 10 seconds;SET pg_ripple.federation_max_results = 500caps result rows;SET pg_ripple.federation_on_error = 'error'turns connection failures into hard errors - Rewrite to local views —
pg_ripple.register_endpoint('https://...', 'my_stream_table')makesSERVICEcalls to that URL scan the local pre-materialised SPARQL view instead — no HTTP at all - Monitor endpoint health — the
_pg_ripple.federation_healthtable records success/failure and latency for each SERVICE call; unhealthy endpoints (< 10% success rate over 5 min) are skipped automatically
What happens behind the scenes
SERVICE clauses are translated in src/sparql/sqlgen.rs via the GraphPattern::Service arm. For each SERVICE call, the inner SPARQL pattern is serialised and sent as an HTTP GET to the remote endpoint using ureq. The application/sparql-results+json response is parsed, each result term is encoded to a local dictionary ID, and the full result set is injected into the SQL as an inline VALUES clause — making it a standard SQL join for the PostgreSQL planner. SERVICE SILENT and federation_on_error = 'empty' return a zero-row fragment instead of raising.
Technical details
- src/sparql/federation.rs (new) —
is_endpoint_allowed,execute_remote,parse_sparql_results_json,encode_results,record_health,is_endpoint_healthy,get_local_view,get_view_variables - src/sparql/sqlgen.rs — added
Fragment::zero_rows(),GraphPattern::Servicearm callingtranslate_service(),translate_service_local(),translate_service_values() - src/sparql/mod.rs — added
pub(crate) mod federation; SERVICE queries skip plan cache - src/lib.rs —
federation_schema_setupSQL block; GUC staticsFEDERATION_TIMEOUT,FEDERATION_MAX_RESULTS,FEDERATION_ON_ERROR;register_endpoint,remove_endpoint,disable_endpoint,list_endpointspg_extern functions - sql/pg_ripple--0.15.0--0.16.0.sql — creates
federation_endpointsandfederation_healthtables with index - tests/pg_regress/sql/sparql_federation.sql — endpoint management, SSRF enforcement, SERVICE SILENT, GUC modes, health table
- tests/pg_regress/sql/sparql_federation_timeout.sql — GUC defaults, boundary tests, timeout with unreachable endpoint
- docs/src/user-guide/sql-reference/federation.md (new) — full user documentation
[0.15.0] — 2026-04-16 — SPARQL Protocol (HTTP Endpoint)
pg_ripple can now be queried over HTTP using the standard SPARQL protocol. Any SPARQL client — YASGUI, Protege, SPARQLWrapper, Jena, or plain curl — can connect to pg_ripple without any driver-specific configuration. This release also fills in SQL-level gaps: graph-aware loaders, graph-aware deletion, per-graph counts, and dictionary diagnostics.
New in this release: Companion HTTP service (pg_ripple_http) with W3C SPARQL 1.1 Protocol compliance. Content negotiation for JSON, XML, CSV, TSV, Turtle, N-Triples, and JSON-LD. Connection pooling via deadpool-postgres. Bearer/Basic auth and CORS. Health check and Prometheus metrics endpoints. Graph-aware bulk loaders and file loaders for N-Triples, Turtle, and RDF/XML. Graph-aware delete and clear operations. Per-graph find and count. Dictionary diagnostics (decode_id_full, lookup_iri). Docker Compose for running PG and HTTP together. Four new pg_regress test suites.
What you can do
- Query over HTTP — start
pg_ripple_httpalongside PostgreSQL and send SPARQL queries viaGET /sparql?query=...orPOST /sparqlwith any standard content type; results come back in JSON, XML, CSV, TSV, Turtle, N-Triples, or JSON-LD depending on theAcceptheader - Load data into named graphs —
pg_ripple.load_ntriples_into_graph(data, graph_iri),load_turtle_into_graph,load_rdfxml_into_graph, and their file variants load triples directly into a named graph without format conversion - Delete from named graphs —
delete_triple_from_graph(s, p, o, graph_iri)removes a single triple from a specific graph;clear_graph(graph_iri)empties a graph without unregistering it - Query within a graph —
find_triples_in_graph(s, p, o, graph)pattern-matches triples within a named graph;triple_count_in_graph(graph_iri)returns the count for a specific graph - Inspect the dictionary —
decode_id_full(id)returns structured JSONB with kind, value, datatype, and language;lookup_iri(iri)checks whether an IRI exists without encoding it - Run with Docker Compose —
docker compose upstarts PostgreSQL with pg_ripple and the HTTP endpoint in separate containers
What happens behind the scenes
The HTTP service is a standalone Rust binary built with axum and tokio. It connects to PostgreSQL via deadpool-postgres, translates HTTP requests into calls to pg_ripple.sparql(), sparql_ask(), sparql_construct(), sparql_describe(), and sparql_update(), then formats the results according to the requested content type. The Prometheus /metrics endpoint exposes query count, error count, and total query duration.
Graph-aware loaders encode the graph_iri argument via the dictionary and delegate to the existing internal *_into_graph(data, g_id) functions. File variants read via pg_read_file() (superuser-only). clear_graph wraps storage::clear_graph_by_id() which deletes from delta tables and adds tombstones for main table rows.
Technical details
- pg_ripple_http/src/main.rs — axum router with
/sparql(GET+POST),/health,/metrics; content negotiation; bearer/basic auth; CORS via tower-http - pg_ripple_http/src/metrics.rs — atomic counter-based Prometheus metrics
- src/lib.rs — new
#[pg_extern]functions:load_ntriples_into_graph,load_turtle_into_graph,load_rdfxml_into_graph,load_ntriples_file_into_graph,load_turtle_file_into_graph,load_rdfxml_file_into_graph,load_rdfxml_file,delete_triple_from_graph,clear_graph,find_triples_in_graph,triple_count_in_graph,decode_id_full,lookup_iri - src/bulk_load.rs —
load_rdfxml_file,load_ntriples_file_into_graph,load_turtle_file_into_graph,load_rdfxml_file_into_graph - src/storage/mod.rs —
triple_count_in_graph(g_id)scans all VP tables for a specific graph - sql/pg_ripple--0.14.0--0.15.0.sql — migration script (no schema changes; all new features are compiled functions)
- docker-compose.yml — two-service Compose with postgres and sparql containers
- Dockerfile — updated to build and bundle
pg_ripple_httpbinary - tests/pg_regress/sql/ —
load_into_graph.sql,graph_delete.sql,sql_api_completeness.sql,sparql_protocol.sql
[0.14.0] — 2026-04-16 — Administrative & Operational Readiness
This release focuses on production operations: maintenance commands, monitoring, graph-level access control, and comprehensive documentation. Everything a system administrator needs to run pg_ripple confidently in production.
New in this release: Maintenance functions (vacuum, reindex, vacuum_dictionary). Dictionary diagnostics (dictionary_stats). Graph-level Row-Level Security with enable_graph_rls, grant_graph, revoke_graph, list_graph_access. Optional pg_trickle integration via schema_summary / enable_schema_summary. Complete documentation for backup/restore, contributing, error codes (PT001–PT799), and security hardening. Extension upgrade scripts for the full 0.1.0 → 0.14.0 chain.
What you can do
- Maintain the store —
pg_ripple.vacuum()runsMERGEthenANALYZEon all VP tables;pg_ripple.reindex()rebuilds all indices;pg_ripple.vacuum_dictionary()removes orphaned dictionary entries after bulk deletes (uses advisory lock to be safe) - Diagnose the dictionary —
pg_ripple.dictionary_stats()returns a JSON object withtotal_entries,hot_entries,cache_capacity,cache_budget_mb, andshmem_ready - Control graph access —
pg_ripple.enable_graph_rls()activates RLS policies on VP tables keyed on theg(graph ID) column;grant_graph(role, graph, permission)/revoke_graph(role, graph)manage the_pg_ripple.graph_accessmapping table;list_graph_access()returns the current ACL as JSON - Bypass RLS for admin work —
SET pg_ripple.rls_bypass = onin a superuser session skips RLS checks; protected byGUC_SUSET(superuser-only) - Inspect schema —
pg_ripple.schema_summary()returns the inferred class→property→cardinality summary (populated by the optional pg_trickle integration);enable_schema_summary()sets up the_pg_ripple.inferred_schematable and stream when pg_trickle is installed - Upgrade safely — tested upgrade path from every prior version;
ALTER EXTENSION pg_ripple UPDATEworks for all transitions up to 0.14.0
What happens behind the scenes
vacuum() and reindex() discover live VP tables by querying pg_class for tables matching the vp_% pattern in _pg_ripple. vacuum_dictionary() acquires advisory lock 0x7269706c (ripl) then deletes from _pg_ripple.dictionary any row whose encoded ID does not appear in any VP table — safe to run concurrently with queries.
RLS policies are created on _pg_ripple.vp_rare (the catch-all VP table) using current_setting('pg_ripple.rls_bypass', true) as the bypass expression. The graph_access mapping table stores (role_name, graph_id, permission) triples; grant_graph encodes the graph IRI using encode_term before inserting.
Technical details
- src/lib.rs — new
pg_externfunctions:vacuum(),reindex(),vacuum_dictionary(),dictionary_stats(),enable_graph_rls(),grant_graph(),revoke_graph(),list_graph_access(),schema_summary(),enable_schema_summary(); new GUCpg_ripple.rls_bypass(bool,GUC_SUSET) - sql/pg_ripple--0.13.0--0.14.0.sql — creates
_pg_ripple.graph_accessand_pg_ripple.inferred_schematables with appropriate indices - tests/pg_regress/sql/admin_functions.sql — tests vacuum, reindex, vacuum_dictionary, dictionary_stats, predicate_stats view
- tests/pg_regress/sql/graph_rls.sql — tests grant_graph, list_graph_access, revoke_graph, enable_graph_rls, rls_bypass GUC
- tests/pg_regress/sql/upgrade_path.sql — verifies full administrative API is available after a clean install
- docs/src/user-guide/backup-restore.md — pg_dump/pg_restore, VP table considerations, PITR, logical replication
- docs/src/user-guide/contributing.md — dev setup, test commands, PR workflow, code conventions
- docs/src/reference/error-reference.md — PT001–PT799 error code table
- docs/src/reference/security.md — supported versions matrix, RLS section, hardening GUCs
- docs/src/user-guide/sql-reference/admin.md — expanded with all new v0.14.0 admin functions
[0.13.0] — 2026-04-16 — Performance Hardening
This release is about speed. Using the benchmarks established in earlier versions, pg_ripple v0.13.0 measures and improves performance at every layer: how triple patterns are ordered before query execution, how the PostgreSQL planner understands the data distribution, how parallel workers are exploited for multi-predicate queries, and how data quality rules from SHACL can help the optimizer make better decisions.
New in this release: BGP join reordering based on real table statistics. SPARQL plan cache instrumentation. Parallel query hints for star patterns. Extended statistics on VP table column pairs. SHACL-driven query optimizer hints. New GUCs to control reordering and parallelism thresholds. Regression and fuzz-integration test suites for the query pipeline.
What you can do
- Faster repeated queries — the plan cache now tracks hits and misses; call
plan_cache_stats()to see your hit rate and tunepg_ripple.plan_cache_sizefor your workload; callplan_cache_reset()to evict stale plans - Faster star patterns — pg_ripple now reorders triple patterns within a BGP by estimated selectivity (most restrictive first), matching what a manual SQL expert would write; controlled by
SET pg_ripple.bgp_reorder = on/off - Parallel query — queries joining 3 or more VP tables now emit
SET LOCAL max_parallel_workers_per_gather = 4andSET LOCAL enable_parallel_hash = onso PostgreSQL can use parallel workers; threshold tunable viapg_ripple.parallel_query_min_joins - Better planner statistics — extended statistics on
(s, o)column pairs are automatically created when a predicate is promoted fromvp_rareto a dedicated VP table; this helps the PostgreSQL planner estimate join cardinalities for multi-predicate queries - SHACL-informed optimizer — if you have loaded SHACL shapes with
sh:maxCount 1orsh:minCount 1, the optimizer reads those hints and can use them for join costing; hints are only applied when semantics are preserved - Safer query pipeline — a fuzz integration test suite verifies that malformed SPARQL, SQL injection attempts in IRI values, Unicode IRIs, deeply nested property paths, and very large literals are all handled gracefully without crashes or data corruption
What happens behind the scenes
The BGP reordering optimizer queries pg_class.reltuples and pg_stats.n_distinct for each VP table at translation time to estimate how many rows a pattern will produce given its bound columns. Patterns are sorted cheapest-first using a greedy left-deep algorithm. Before executing the generated SQL, SET LOCAL join_collapse_limit = 1 is emitted so the PostgreSQL planner does not reorder the joins back. On macOS/Linux, SET LOCAL enable_mergejoin = on is also set to exploit merge-join when join columns are ordered.
For parallel execution, the query engine counts VP-table aliases (_t0, _t1, …) in the generated SQL; if the count reaches parallel_query_min_joins, parallel hash join settings are activated before query execution.
Extended statistics (CREATE STATISTICS … (ndistinct, dependencies) ON s, o) are created in _pg_ripple schema alongside the VP tables when promote_predicate() runs. This gives the planner correlation data that single-column ANALYZE cannot provide.
Technical details
- src/sparql/optimizer.rs (new) —
reorder_bgp(): greedy left-deep selectivity-based reorder;TableStatsstruct withpg_class.reltuples+pg_stats.n_distinctqueries;load_predicate_hints(): reads SHACL shapes forsh:maxCount/sh:minCounthints - src/sparql/plan_cache.rs — added
HIT_COUNTandMISS_COUNTAtomicU64counters;stats()returns(hits, misses, size, cap);reset()evicts cache and clears counters; cache key now includesbgp_reorderGUC value - src/sparql/sqlgen.rs —
translate_bgp()now callsoptimizer::reorder_bgp()before building the join tree - src/sparql/mod.rs —
execute_select()emitsSET LOCAL join_collapse_limit = 1,enable_mergejoin = on, and parallel hints when applicable; new publicplan_cache_stats()andplan_cache_reset()functions - src/storage/mod.rs —
promote_rare_predicates()callscreate_extended_statistics()for each newly promoted predicate;create_extended_statistics()issuesCREATE STATISTICS IF NOT EXISTS … (ndistinct, dependencies) ON s, o - src/lib.rs — two new GUCs:
pg_ripple.bgp_reorder(bool, default on),pg_ripple.parallel_query_min_joins(int, default 3); two newpg_externfunctions:plan_cache_stats() RETURNS JSONB,plan_cache_reset() RETURNS VOID - sql/pg_ripple--0.12.0--0.13.0.sql — migration script (no schema DDL; new functions are compiled into the extension library)
- tests/pg_regress/sql/shacl_query_opt.sql — verifies BGP reorder GUC, plan cache stats/reset, SHACL shape reading, and sparql_explain output
- tests/pg_regress/sql/fuzz_integration.sql — verifies graceful handling of empty queries, malformed SPARQL, SQL injection via IRI, Unicode IRIs, large literals, deeply nested property paths, and adversarial cache usage
[0.12.0] — 2026-04-16 — SPARQL Update (Advanced)
This release completes the full SPARQL 1.1 Update specification. Building on the INSERT DATA / DELETE DATA support from v0.5.1, pg_ripple now supports pattern-based updates, remote RDF loading, and full named-graph lifecycle management.
New in this release: Find-and-replace data using SPARQL patterns with DELETE/INSERT WHERE. Fetch and load remote RDF documents from any HTTP(S) URL with LOAD <url>. Clear, drop, or create named graphs with a single SPARQL Update call.
What you can do
- Pattern-based updates —
DELETE { … } INSERT { … } WHERE { … }finds matching triples using the full SPARQL→SQL engine and then deletes and inserts triples for each result row; both the DELETE and INSERT templates may reference WHERE-bound variables - INSERT WHERE — omit the DELETE clause to insert a triple for every WHERE match
- DELETE WHERE — omit the INSERT clause to remove all triples matching a pattern
- LOAD remote RDF —
LOAD <url>fetches a Turtle, N-Triples, or RDF/XML document via HTTP(S) and inserts all triples;LOAD <url> INTO GRAPH <g>targets a named graph;LOAD SILENT <url>suppresses network errors - Clear a graph —
CLEAR GRAPH <g>removes all triples from a named graph without touching the default graph;CLEAR DEFAULT,CLEAR NAMED,CLEAR ALLlet you clear one or all graphs in a single call - Drop a graph —
DROP GRAPH <g>clears and deregisters a graph;DROP SILENTsuppresses errors on non-existent graphs;DROP ALLclears the entire store - Create a graph —
CREATE GRAPH <g>pre-registers a named graph in the dictionary;CREATE SILENTis a no-op if the graph already exists
What happens behind the scenes
When DELETE/INSERT WHERE runs, the WHERE clause is compiled through the existing SPARQL→SQL engine into a SELECT query. The result rows are collected in memory, and then for each row the DELETE phase removes any matched triples from VP storage, followed by the INSERT phase adding new ones. This keeps the operation transactional inside a single PostgreSQL call.
LOAD uses ureq (a lightweight Rust HTTP client) to fetch the URL. The response body is parsed by the same rio_turtle / rio_xml parsers used for local bulk loading; triples are inserted in batches using the standard VP storage path.
CLEAR and DROP call a new clear_graph_by_id() helper that deletes from both the HTAP delta tables and tombstones the main-partition rows — the same mechanism used by the existing drop_graph() function.
Technical details
- src/sparql/mod.rs —
sparql_update()extended to handle allGraphUpdateOperationvariants:DeleteInsert,Load,Clear,Create,Drop; new helpersexecute_delete_insert(),execute_load(),execute_clear(),execute_drop(),resolve_ground_term(),resolve_term_pattern(),resolve_named_node_pattern(),resolve_graph_name_pattern(),encode_literal_id() - src/storage/mod.rs — new
clear_graph_by_id(g_id)mirrorsdrop_graph()but takes a pre-encoded ID; newall_graph_ids()collects all distinct graph IDs across VP tables andvp_rare - src/bulk_load.rs — new graph-aware loaders
load_ntriples_into_graph(),load_turtle_into_graph(),load_rdfxml_into_graph()accept a targetg_idinstead of always writing to the default graph (g=0) - Cargo.toml — added
ureq = { version = "2", features = ["tls"] }forLOAD <url>HTTP support - sql/pg_ripple--0.11.0--0.12.0.sql — migration script (schema unchanged; new capabilities compiled into the extension library)
- pg_regress — new test suites:
sparql_update_where.sql,sparql_graph_management.sql; both PASS
[0.11.0] — 2026-04-16 — SPARQL & Datalog Views
This release adds always-fresh, incrementally-maintained stream tables for SPARQL and Datalog queries, plus Extended Vertical Partitioning (ExtVP) semi-join tables for multi-predicate star-pattern acceleration. All three features are built on top of pg_trickle and are soft-gated — pg_ripple loads and operates normally without pg_trickle; the new functions detect its absence at call time and return a clear error with an install hint.
New in this release: Compile any SPARQL SELECT query into a pg_trickle stream table with create_sparql_view(). Bundle a Datalog rule set with a goal pattern into a self-refreshing view with create_datalog_view(). Pre-compute semi-joins between frequently co-joined predicate pairs with create_extvp() to give 2–10× star-pattern speedups.
What you can do
- SPARQL views —
pg_ripple.create_sparql_view(name, sparql, schedule, decode)compiles a SPARQL SELECT query to SQL and registers it as a pg_trickle stream table; the table stays incrementally up-to-date on every triple insert/update/delete - Datalog views —
pg_ripple.create_datalog_view(name, rules, goal, schedule, decode)bundles inline Datalog rules with a goal query into a self-refreshing table;create_datalog_view_from_rule_set(name, rule_set, goal, schedule, decode)references a previously-loaded named rule set - ExtVP semi-joins —
pg_ripple.create_extvp(name, pred1_iri, pred2_iri, schedule)pre-computes the semi-join between two predicate tables; the SPARQL query engine detects and uses ExtVP tables automatically - Detect pg_trickle —
pg_ripple.pg_trickle_available()returnstrueif pg_trickle is installed, so callers can gate feature usage without catching errors - Lifecycle management —
drop_sparql_view,drop_datalog_view,drop_extvpremove both the stream table and the catalog entry;list_sparql_views(),list_datalog_views(),list_extvp()return JSONB arrays of registered objects
New SQL functions
| Function | Returns | Description |
|---|---|---|
pg_ripple.pg_trickle_available() | BOOLEAN | Returns true if pg_trickle is installed |
pg_ripple.create_sparql_view(name, sparql, schedule DEFAULT '1s', decode DEFAULT false) | BIGINT | Compile SPARQL SELECT to a pg_trickle stream table; returns column count |
pg_ripple.drop_sparql_view(name) | BOOLEAN | Drop the stream table and catalog entry |
pg_ripple.list_sparql_views() | JSONB | List all registered SPARQL views |
pg_ripple.create_datalog_view(name, rules, goal, rule_set_name DEFAULT 'custom', schedule DEFAULT '10s', decode DEFAULT false) | BIGINT | Compile inline Datalog rules + goal into a stream table |
pg_ripple.create_datalog_view_from_rule_set(name, rule_set, goal, schedule DEFAULT '10s', decode DEFAULT false) | BIGINT | Reference an existing named rule set for a Datalog view |
pg_ripple.drop_datalog_view(name) | BOOLEAN | Drop the stream table and catalog entry |
pg_ripple.list_datalog_views() | JSONB | List all registered Datalog views |
pg_ripple.create_extvp(name, pred1_iri, pred2_iri, schedule DEFAULT '10s') | BIGINT | Pre-compute a semi-join stream table for two predicates |
pg_ripple.drop_extvp(name) | BOOLEAN | Drop the ExtVP stream table and catalog entry |
pg_ripple.list_extvp() | JSONB | List all registered ExtVP tables |
New catalog tables
| Table | Description |
|---|---|
_pg_ripple.sparql_views | Stores SPARQL view name, original query, generated SQL, schedule, decode mode, stream table name, and variables |
_pg_ripple.datalog_views | Stores Datalog view name, rules, rule set, goal, generated SQL, schedule, decode mode, stream table name, and variables |
_pg_ripple.extvp_tables | Stores ExtVP name, predicate IRIs, predicate IDs, generated SQL, schedule, and stream table name |
Technical details
- src/views.rs — new module implementing all v0.11.0 public functions;
compile_sparql_for_view()wrapssparql::sqlgen::translate_select()and renames internal_v_{var}columns to plain{var}for stream table compatibility;create_extvp()generates a parameterized semi-join SQL template over the two predicate VP tables - src/lib.rs — three new catalog tables created at extension load time; eleven new
#[pg_extern]functions exposed in thepg_rippleschema - src/datalog/mod.rs — added
load_and_store_rules(rules_text, rule_set_name) -> i64helper for Datalog view creation - src/sparql/mod.rs —
sqlgenmodule madepub(crate)soviews.rscan calltranslate_select()directly - sql/pg_ripple--0.10.0--0.11.0.sql — migration script adding the three catalog tables for upgrades from v0.10.0
- pg_regress — new test suites:
sparql_views.sql,datalog_views.sql,extvp.sql; all pass
[0.10.0] — 2026-04-16 — Datalog Reasoning Engine
This release delivers a full Datalog reasoning engine over the VP triple store. Rules are parsed from a Turtle-flavoured syntax, stratified for evaluation order, and compiled to native PostgreSQL SQL — no external reasoner process needed.
New in this release: pg_ripple can now execute RDFS and OWL RL entailment, user-defined inference rules, Datalog constraints, and arithmetic/string built-ins. Inference results are written back into the VP store with source = 1 so explicit and derived triples are always distinguishable. A hot dictionary tier accelerates frequent IRI lookups, and a SHACL-AF bridge detects sh:rule properties in shape graphs and registers them alongside standard Datalog rules.
What you can do
- Write custom inference rules —
pg_ripple.load_rules(rules, rule_set)parses Turtle-flavoured Datalog and stores the compiled SQL strata - Built-in RDFS entailment —
pg_ripple.load_rules_builtin('rdfs')loads all 13 RDFS entailment rules; callpg_ripple.infer('rdfs')to materialize closure - Built-in OWL RL reasoning —
pg_ripple.load_rules_builtin('owl-rl')loads ~20 core OWL RL rules covering class hierarchy, property chains, and inverse/symmetric/transitive properties - Run inference on demand —
pg_ripple.infer(rule_set)runs all strata in order and inserts derived triples withsource = 1; safe to call repeatedly (idempotent) - Declare integrity constraints — rules with an empty head become constraints;
pg_ripple.check_constraints()returns all violations as JSONB - Inspect and manage rule sets —
pg_ripple.list_rules()returns rules as JSONB;pg_ripple.drop_rules(rule_set)clears a named set;enable_rule_set/disable_rule_settoggle a set without deleting it - Accelerate hot IRIs —
pg_ripple.prewarm_dictionary_hot()loads frequently-used IRIs (≤ 512 B) into an UNLOGGED hot table for sub-microsecond lookups; survives connection pooling but not database restart - SHACL-AF bridge — shapes that contain
sh:ruleentries are detected byload_shacl()and registered in the rules catalog; full SHACL-AF rule execution is planned for v0.11.0
New GUC parameters
| GUC | Default | Description |
|---|---|---|
pg_ripple.inference_mode | 'on_demand' | 'off' disables engine; 'on_demand' evaluates via CTEs; 'materialized' uses pg_trickle stream tables |
pg_ripple.enforce_constraints | 'warn' | 'off' silences violations; 'warn' logs them; 'error' raises an exception |
pg_ripple.rule_graph_scope | 'default' | 'default' applies rules to default graph only; 'all' applies across all named graphs |
New SQL functions
| Function | Returns | Description |
|---|---|---|
pg_ripple.load_rules(rules TEXT, rule_set TEXT DEFAULT 'custom') | BIGINT | Parse, stratify, and store a Datalog rule set; returns the number of rules loaded |
pg_ripple.load_rules_builtin(name TEXT) | BIGINT | Load a built-in rule set by name ('rdfs' or 'owl-rl') |
pg_ripple.list_rules() | JSONB | Return all active rules as a JSONB array |
pg_ripple.drop_rules(rule_set TEXT) | BIGINT | Delete a named rule set; returns the number of rules deleted |
pg_ripple.enable_rule_set(name TEXT) | VOID | Mark a rule set as active |
pg_ripple.disable_rule_set(name TEXT) | VOID | Mark a rule set as inactive |
pg_ripple.infer(rule_set TEXT DEFAULT 'custom') | BIGINT | Run inference; returns the number of derived triples inserted |
pg_ripple.check_constraints(rule_set TEXT DEFAULT NULL) | JSONB | Evaluate integrity constraints; returns violations |
pg_ripple.prewarm_dictionary_hot() | BIGINT | Load hot IRIs into UNLOGGED hot table; returns rows loaded |
Technical details
- src/datalog/mod.rs — public API and IR type definitions (
Term,Atom,BodyLiteral,Rule,RuleSet); catalog helpers for_pg_ripple.rulesand_pg_ripple.rule_sets - src/datalog/parser.rs — tokenizer and recursive-descent parser for Turtle-flavoured Datalog; variables as
?x, full IRIs as<...>, prefixed IRIs asprefix:local, head:-body.delimiter - src/datalog/stratify.rs — SCC-based stratification via Kosaraju's algorithm; unstratifiable programs (negation cycles) are rejected with a clear error message naming the cyclic predicates
- src/datalog/compiler.rs — compiles Rule IR to PostgreSQL SQL; non-recursive strata use
INSERT … SELECT … ON CONFLICT DO NOTHING; recursive strata useWITH RECURSIVE … CYCLE(PG18 native cycle detection); negation compiles toNOT EXISTS; arithmetic/string built-ins compile to inline SQL expressions - src/datalog/builtins.rs — RDFS (13 rules: rdfs2–rdfs12, subclass, domain, range) and OWL RL (~20 rules: class hierarchy, property chains, inverse/symmetric/transitive) as embedded Rust string constants
- src/dictionary/hot.rs — UNLOGGED hot table
_pg_ripple.dictionary_hotfor IRIs ≤ 512 B;prewarm_hot_table()runs at_PG_initwheninference_mode != 'off';lookup_hot()andadd_to_hot()provide O(1) in-process hash lookups - src/shacl/mod.rs —
parse_and_store_shapes()now callsbridge_shacl_rules()wheninference_mode != 'off'; the bridge detectssh:ruleand registers a placeholder in_pg_ripple.rules - VP store —
source SMALLINT NOT NULL DEFAULT 0column present in all VP tables; migration script adds it retroactively to tables created before v0.10.0;source = 0means explicit,source = 1means derived - Migration script —
sql/pg_ripple--0.9.0--0.10.0.sqlincludes allCREATE TABLE IF NOT EXISTSandALTER TABLE … ADD COLUMN IF NOT EXISTSstatements for zero-downtime upgrades - New pg_regress tests:
datalog_custom.sql,datalog_rdfs.sql,datalog_owl_rl.sql,datalog_negation.sql,datalog_arithmetic.sql,datalog_constraints.sql,datalog_malformed.sql,shacl_af_rule.sql,rdf_star_datalog.sql
[0.9.0] — 2026-04-15 — Serialization, Export & Interop
This release completes RDF I/O: pg_ripple can now import from and export to all major RDF serialization formats, and SPARQL CONSTRUCT and DESCRIBE queries can return results directly as Turtle or JSON-LD.
New in this release: Until now, you could load Turtle and N-Triples but exports were limited to N-Triples and N-Quads. You can now export as Turtle or JSON-LD — formats that are friendlier for human reading and REST APIs respectively. RDF/XML import covers the format that Protégé and most OWL editors produce. Streaming export variants handle large graphs without buffering the full document in memory.
What you can do
- Load RDF/XML —
pg_ripple.load_rdfxml(data TEXT)parses conformant RDF/XML (Protégé, OWL, most ontology editors); returns the number of triples loaded - Export as Turtle —
pg_ripple.export_turtle()serializes the default graph (or any named graph) as a compact Turtle document with@prefixdeclarations; RDF-star quoted triples use Turtle-star notation - Export as JSON-LD —
pg_ripple.export_jsonld()serializes triples as a JSON-LD expanded-form array, ready for REST APIs and Linked Data Platform contexts - Stream large graphs —
pg_ripple.export_turtle_stream()andpg_ripple.export_jsonld_stream()return one line at a time asSETOF TEXT, suitable forCOPY … TO STDOUTpipelines - Get CONSTRUCT results as Turtle —
pg_ripple.sparql_construct_turtle(query)runs a SPARQL CONSTRUCT query and returns a Turtle document instead of JSONB rows - Get CONSTRUCT results as JSON-LD —
pg_ripple.sparql_construct_jsonld(query)returns JSONB in JSON-LD expanded form - Get DESCRIBE results as Turtle or JSON-LD —
pg_ripple.sparql_describe_turtle(query)andpg_ripple.sparql_describe_jsonld(query)offer the same format choice for DESCRIBE
New SQL functions
| Function | Returns | Description |
|---|---|---|
pg_ripple.load_rdfxml(data TEXT) | BIGINT | Parse RDF/XML, load into default graph |
pg_ripple.export_turtle(graph TEXT DEFAULT NULL) | TEXT | Export graph as Turtle |
pg_ripple.export_jsonld(graph TEXT DEFAULT NULL) | JSONB | Export graph as JSON-LD (expanded form) |
pg_ripple.export_turtle_stream(graph TEXT DEFAULT NULL) | SETOF TEXT | Streaming Turtle export |
pg_ripple.export_jsonld_stream(graph TEXT DEFAULT NULL) | SETOF TEXT | Streaming JSON-LD NDJSON export |
pg_ripple.sparql_construct_turtle(query TEXT) | TEXT | CONSTRUCT result as Turtle |
pg_ripple.sparql_construct_jsonld(query TEXT) | JSONB | CONSTRUCT result as JSON-LD |
pg_ripple.sparql_describe_turtle(query TEXT, strategy TEXT DEFAULT 'cbd') | TEXT | DESCRIBE result as Turtle |
pg_ripple.sparql_describe_jsonld(query TEXT, strategy TEXT DEFAULT 'cbd') | JSONB | DESCRIBE result as JSON-LD |
Technical details
rio_xmlcrate added as a dependency for RDF/XML parsing (uses rio_apiTriplesParserinterface, consistent with existing rio_turtle parsers)src/export.rsextended withexport_turtle,export_jsonld,export_turtle_stream,export_jsonld_stream,triples_to_turtle, andtriples_to_jsonld- Turtle serialization groups by subject using
BTreeMapfor deterministic output; emits predicate-object lists per subject - JSON-LD expanded form: each subject is one array entry; predicates become IRI-keyed arrays of
{"@value": …}/{"@id": …}objects - RDF-star quoted triples: passed through in Turtle-star
<< s p o >>notation; in JSON-LD emitted as{"@value": "…", "@type": "rdf:Statement"} - Streaming variants avoid buffering the full document;
export_turtle_streamyields prefix lines then ones p o .per row - SPARQL format functions (
sparql_construct_turtle, etc.) delegate to the existing SPARQL engine then pass rows through the new serialization layer - New pg_regress tests:
serialization.sql,rdf_star_construct.sql, expandedsparql_construct.sql
[0.8.0] — 2026-04-15 — Advanced Data Quality Rules
This release rounds out the data quality system with more expressive rules and a background validation mode that never slows down your inserts.
New in this release: Until now, each validation rule applied to a single property in isolation. You can now combine rules — "this value must satisfy rule A or rule B", "must satisfy all of these rules", "must not match this rule" — and count how many values on a property actually conform to a sub-rule. A background mode queues violations for later review instead of blocking every write.
What you can do
- Combine rules with logic — use
sh:or,sh:and, andsh:notto build validation rules that express complex conditions, such as "a contact must have either a phone number or an email address" - Reference another rule from within a rule —
sh:node <ShapeIRI>checks that each value on a property also satisfies a separate named rule; rules can reference each other up to 32 levels deep without getting stuck in a loop - Count qualifying values —
sh:qualifiedValueShapecombined withsh:qualifiedMinCount/sh:qualifiedMaxCountcounts only the values that actually pass a sub-rule, so you can say "at least two authors must be affiliated with a university" - Validate without blocking writes — set
pg_ripple.shacl_mode = 'async'so that inserts complete immediately and violations are collected silently in the background; the background worker drains the queue automatically - Inspect collected violations —
pg_ripple.dead_letter_queue()returns all async violations as a JSON array;pg_ripple.drain_dead_letter_queue()clears the queue once you have reviewed them - Drain the queue manually —
pg_ripple.process_validation_queue(batch_size)processes violations on demand, useful in test pipelines or batch jobs
New SQL functions
| Function | Returns | Description |
|---|---|---|
pg_ripple.process_validation_queue(batch_size BIGINT DEFAULT 1000) | BIGINT | Process up to N pending validation jobs |
pg_ripple.validation_queue_length() | BIGINT | How many jobs are waiting in the queue |
pg_ripple.dead_letter_count() | BIGINT | How many violations have been recorded |
pg_ripple.dead_letter_queue() | JSONB | All recorded violations as a JSON array |
pg_ripple.drain_dead_letter_queue() | BIGINT | Delete all recorded violations and return how many were removed |
Technical details
ShapeConstraintenum extended withOr(Vec<String>),And(Vec<String>),Not(String),QualifiedValueShape { shape_iri, min_count, max_count }validate_property_shape()refactored to acceptall_shapes: &[Shape]for recursive nested shape evaluationnode_conforms_to_shape()added: depth-limited recursive conformance check (max depth 32)process_validation_batch(batch_size)added: SPI-based batch drain of_pg_ripple.validation_queue, writes violations to_pg_ripple.dead_letter_queue- Merge worker (
src/worker.rs) extended withrun_validation_cycle()called after each merge transaction validate_sync()now handlesClass,Node,Or,And,Not, andQualifiedValueShape(max-count check only for sync)run_validate()now checks top-level nodeOr/And/Notconstraints in offline validation
[0.7.0] — 2026-04-15 — Data Quality Rules (Core)
This release adds SHACL — a W3C standard for expressing data quality rules — and on-demand deduplication for datasets that have accumulated duplicate entries.
What this means in practice: You define rules like "every Person must have a name, and the name must be a string", load them into the database once, and pg_ripple will check those rules on every insert or on demand. Violations are reported as structured JSON so they can be logged, monitored, or acted on automatically.
What you can do
- Define data quality rules —
pg_ripple.load_shacl(data TEXT)parses rules written in W3C SHACL Turtle format and stores them in the database; returns the number of rules loaded - Check your data —
pg_ripple.validate(graph TEXT DEFAULT NULL)runs all active rules against your data and returns a JSON report:{"conforms": true/false, "violations": [...]}. Pass a graph name to validate only that graph - Reject bad data on insert — set
pg_ripple.shacl_mode = 'sync'to haveinsert_triple()immediately reject any triple that violates ash:maxCount,sh:datatype,sh:in, orsh:patternrule - Manage rules —
pg_ripple.list_shapes()lists all loaded rules;pg_ripple.drop_shape(uri TEXT)removes one rule by its IRI - Remove duplicate triples —
pg_ripple.deduplicate_predicate(p_iri TEXT)removes duplicate entries for one property, keeping the earliest record;pg_ripple.deduplicate_all()deduplicates everything - Deduplicate automatically on merge — set
pg_ripple.dedup_on_merge = trueto eliminate duplicates each time the background worker compacts data (see v0.6.0)
New SQL functions
| Function | Returns | Description |
|---|---|---|
pg_ripple.load_shacl(data TEXT) | INTEGER | Parse Turtle, store rules, return count loaded |
pg_ripple.validate(graph TEXT DEFAULT NULL) | JSONB | Full validation report |
pg_ripple.list_shapes() | TABLE(shape_iri TEXT, active BOOLEAN) | All rules in the catalog |
pg_ripple.drop_shape(shape_uri TEXT) | INTEGER | Remove a rule by IRI |
pg_ripple.deduplicate_predicate(p_iri TEXT) | BIGINT | Remove duplicates for one property |
pg_ripple.deduplicate_all() | BIGINT | Remove duplicates across all properties |
pg_ripple.enable_shacl_monitors() | BOOLEAN | Create a live violation-count stream table (requires pg_trickle) |
New configuration options
| Option | Default | Description |
|---|---|---|
pg_ripple.shacl_mode | 'off' | When to validate: 'off', 'sync' (block bad inserts), 'async' (queue for later — see v0.8.0) |
pg_ripple.dedup_on_merge | false | Eliminate duplicate triples during each background merge |
New internal tables
| Table | Description |
|---|---|
_pg_ripple.shacl_shapes | Stores each loaded rule with its IRI, parsed JSON, and active flag |
_pg_ripple.validation_queue | Inbox for inserts when shacl_mode = 'async' |
_pg_ripple.dead_letter_queue | Recorded violations with full JSONB violation reports |
_pg_ripple.violation_summary | Live violation counts by rule and severity (created by enable_shacl_monitors()) |
Supported validation constraints (v0.7.0)
sh:minCount, sh:maxCount, sh:datatype, sh:in, sh:pattern, sh:class, sh:targetClass, sh:targetNode, sh:targetSubjectsOf, sh:targetObjectsOf. Logical combinators (sh:or, sh:and, sh:not) and qualified constraints are added in v0.8.0.
Upgrading from v0.6.0
ALTER EXTENSION pg_ripple UPDATE;
The migration creates three new tables (shacl_shapes, validation_queue, dead_letter_queue) and their indexes. No existing tables are modified.
[0.6.0] — 2026-04-15 — High-Speed Reads and Writes at the Same Time
This release separates write traffic from read traffic so both can run at full speed simultaneously. It also adds change notifications so other systems can react to new triples in real time.
The problem this solves: In earlier versions, heavy read queries could slow down writes and vice versa. Now, writes go into a small fast table and reads see everything via a transparent view. A background worker periodically merges the write table into an optimised read table without interrupting either operation.
What you can do
- Write and read simultaneously without blocking — inserts land in a fast write buffer; reads see both the buffer and the main read-optimised store through a transparent view
- Trigger a manual merge —
pg_ripple.compact()immediately merges all pending writes into the read store; returns the total number of triples after compaction - Subscribe to changes —
pg_ripple.subscribe(pattern TEXT, channel TEXT)sends a PostgreSQLLISTEN/NOTIFYmessage tochannelevery time a triple matchingpatternis inserted or deleted; use'*'to receive all changes - Unsubscribe —
pg_ripple.unsubscribe(channel TEXT)stops notifications on a channel - Get storage statistics —
pg_ripple.stats()reports total triple count, how many predicates have their own table, how many triples are still in the write buffer, and the background worker's process ID
New SQL functions
| Function | Returns | Description |
|---|---|---|
pg_ripple.compact() | BIGINT | Merge all pending writes into the read store |
pg_ripple.stats() | JSONB | Storage and background worker statistics |
pg_ripple.subscribe(pattern TEXT, channel TEXT) | BIGINT | Subscribe to change notifications |
pg_ripple.unsubscribe(channel TEXT) | BIGINT | Stop notifications on a channel |
pg_ripple.htap_migrate_predicate(pred_id BIGINT) | void | Migrate one property table to the split-storage layout |
pg_ripple.subject_predicates(subject_id BIGINT) | BIGINT[] | All properties for a given subject (fast lookup) |
pg_ripple.object_predicates(object_id BIGINT) | BIGINT[] | All properties for a given object (fast lookup) |
New configuration options
| Option | Default | Description |
|---|---|---|
pg_ripple.merge_threshold | 10000 | Minimum pending writes before background merge starts |
pg_ripple.merge_interval_secs | 60 | Maximum seconds between merge cycles |
pg_ripple.merge_retention_seconds | 60 | How long to keep the previous read table before dropping it |
pg_ripple.latch_trigger_threshold | 10000 | Pending writes needed to wake the merge worker early |
pg_ripple.worker_database | postgres | Which database the merge worker connects to |
pg_ripple.merge_watchdog_timeout | 300 | Log a warning if the merge worker is silent for this many seconds |
Bug fixes in this release
- Startup race condition — the extension's shared memory flag is now set inside the correct PostgreSQL startup hook, eliminating a rare crash window during server start
- GUC registration crash — configuration parameters requiring postmaster-level access no longer crash when
CREATE EXTENSION pg_rippleruns without the extension inshared_preload_libraries - SPARQL aggregate decode bug —
COUNT,SUM, and similar aggregate results were incorrectly looked up in the string dictionary; they now pass through as plain numbers - Merge worker: DROP TABLE without CASCADE — the merge worker failed if old tables had dependent views; fixed by using
CASCADEand recreating the view afterwards - Merge worker: stale index name — repeated
compact()calls failed with "relation already exists" because the old index name survived a table rename; the stale index is now dropped before creating a new one
Upgrading from v0.5.1
ALTER EXTENSION pg_ripple UPDATE;
The migration script adds a column to the predicate catalog, creates the pattern tables and change-notification infrastructure, and converts every existing property table to the split read/write layout in a single transaction. Existing triples land in the write buffer; call pg_ripple.compact() afterwards to move them into the read store immediately.
Technical details
- HTAP split: writes →
vp_{id}_delta(heap + B-tree); cross-partition deletes →vp_{id}_tombstones; query view =(main EXCEPT tombstones) UNION ALL delta - Background merge: sort-ordered insertion into a fresh
vp_{id}_main(BRIN-indexed) +ANALYZE; previous main dropped aftermerge_retention_seconds ExecutorEnd_hookpokes the merge worker latch whenTOTAL_DELTA_ROWSreacheslatch_trigger_threshold- Subject/object pattern tables (
_pg_ripple.subject_patterns,_pg_ripple.object_patterns) — GIN-indexedBIGINT[]columns rebuilt by the merge worker; enable O(1) predicate lookup per node - CDC notifications fire as
pg_notify(channel, '{"op":"insert|delete","s":...,"p":...,"o":...,"g":...}')via trigger on each delta table
[0.5.1] — 2026-04-15 — Compact Number Storage, CONSTRUCT/DESCRIBE, SPARQL Update, Full-Text Search
This release stores common data types (integers, dates, booleans) as compact numbers instead of text, making range comparisons in queries much faster. It also adds the two remaining SPARQL query forms, write support via SPARQL Update, and full-text search on text values.
What you can do
- Faster comparisons on numbers and dates —
xsd:integer,xsd:boolean,xsd:date, andxsd:dateTimevalues are stored as compact integers; FILTER comparisons (>,<,=) run as plain integer comparisons with no string decoding - SPARQL CONSTRUCT —
pg_ripple.sparql_construct(query TEXT)assembles new triples from a template and returns them as a set of{s, p, o}JSON objects; useful for transforming or exporting data - SPARQL DESCRIBE —
pg_ripple.sparql_describe(query TEXT, strategy TEXT)returns the neighbourhood of a resource — all triples directly connected to it (Concise Bounded Description) or both incoming and outgoing triples (Symmetric CBD) - SPARQL Update —
pg_ripple.sparql_update(query TEXT)executesINSERT DATA { … }andDELETE DATA { … }statements; returns the number of triples affected - Full-text search —
pg_ripple.fts_index(predicate TEXT)indexes text values for a property;pg_ripple.fts_search(query TEXT, predicate TEXT)searches them using standard PostgreSQL text-search syntax
Bug fixes
fts_indexnow accepts N-Triples<IRI>notation for the predicate argumentfts_indexnow uses a correct partial index that does not require PostgreSQL subquery support- Inline-encoded values (integers, dates) now decode correctly in SPARQL SELECT results instead of returning NULL
New configuration options
pg_ripple.describe_strategy(default'cbd') — DESCRIBE expansion algorithm:'cbd','scbd'(symmetric), or'simple'(subject only)
[0.5.0] — 2026-04-15 — Complete SPARQL 1.1 Query Engine
This release completes SPARQL 1.1 query support. All standard query patterns — graph traversal, aggregates, unions, subqueries, optional matches, and computed values — are now supported.
What you can do
- Traverse graph relationships — property paths (
+,*,?,/,|,^) follow chains of relationships; cyclic graphs are handled safely using PostgreSQL's cycle detection - Combine results from alternative patterns —
UNION { ... } UNION { ... }merges results from two or more patterns;MINUS { ... }removes results that match an unwanted pattern - Aggregate and group results —
COUNT,SUM,AVG,MIN,MAX,GROUP_CONCATwork withGROUP BYandHAVINGjust as in SQL - Use subqueries — nest
{ SELECT … WHERE { … } }patterns at any depth - Compute new values —
BIND(<expr> AS ?var)assigns a calculated value to a variable;VALUES ?x { … }injects a fixed set of values into a pattern - Optional matches —
OPTIONAL { … }returns results even when the optional pattern has no data, leaving those variables unbound - Limit recursion depth —
pg_ripple.max_path_depthcaps how deep property-path traversal can go, preventing runaway queries on very large graphs
Bug fixes
- Sequence paths (
p/q) no longer produce a Cartesian product when intermediate nodes are anonymous p*(zero-or-more) paths no longer crash with a PostgreSQL CYCLE syntax errorOPTIONALno longer produces incorrect results due to an alias collision in the generated SQLGROUP BYcolumn references no longer go out of scope in the outer queryMINUSjoin clause now uses the correct column aliasVALUESno longer generates a duplicate alias clauseBINDin aggregate subqueries (SELECT (COUNT(?p) AS ?cnt)) now produces the correct SQL expression- Numbers in FILTER expressions (
FILTER(?cnt >= 2)) are now emitted as SQL integers instead of dictionary IDs - Changing
pg_ripple.max_path_depthmid-session now correctly invalidates the plan cache
Technical details
- Property paths compile to
WITH RECURSIVE … CYCLECTEs using PostgreSQL 18's hash-basedCYCLEclause - All pg_regress test files are now idempotent — safe to run multiple times against the same database
setup.sqldrops and recreates the extension for full isolation between runs- New tests:
property_paths.sql,aggregates.sql,resource_limits.sql— 12/12 pass
[0.4.0] — 2026-04-14 — Statements About Statements (RDF-star)
This release adds RDF-star: the ability to store facts about facts. For example, you can record not just "Alice knows Bob" but also "Alice knows Bob — according to Carol, since 2020". This is essential for provenance tracking, temporal data, and property graph–style edge annotations.
What you can do
- Load N-Triples-star data —
pg_ripple.load_ntriples()now accepts N-Triples-star, including nested quoted triples in both subject and object position - Encode and decode quoted triples —
pg_ripple.encode_triple(s, p, o)stores a quoted triple and returns its ID;pg_ripple.decode_triple(id)converts it back to JSON - Use statement identifiers —
pg_ripple.insert_triple()now returns the stable integer identifier of the stored statement; that identifier can itself appear as a subject or object in other triples - Look up a statement by its identifier —
pg_ripple.get_statement(i BIGINT)returns{"s":…,"p":…,"o":…,"g":…}for any stored statement - Query with SPARQL-star — ground (all-constant) quoted triple patterns work in SPARQL
WHEREclauses:WHERE { << :Alice :knows :Bob >> :assertedBy ?who }
Known limitations in this release
- Turtle-star is not yet supported; use N-Triples-star for RDF-star bulk loading
- Variable-inside-quoted-triple SPARQL patterns (e.g.
<< ?s :knows ?o >> :assertedBy ?who) are deferred to v0.5.x - W3C SPARQL-star conformance test suite not yet run (deferred to v0.5.x)
Technical details
KIND_QUOTED_TRIPLE = 5added to the dictionary; quoted triples stored withqt_s,qt_p,qt_ocolumns via non-destructiveALTER TABLE … ADD COLUMN IF NOT EXISTS- Custom recursive-descent N-Triples-star line parser — avoids the
oxrdf/rdf-12+spargebrafeature conflict with no new crate dependencies spargebraandsparoptnow use thesparql-12feature, enablingTermPattern::Triplewith correct exhaustiveness guards- SPARQL-star ground patterns compile to a dictionary lookup + SQL equality condition
[0.3.0] — 2026-04-14 — SPARQL Query Language
This release introduces SPARQL, the standard W3C query language for RDF data. You can now ask questions over your stored facts using a familiar graph-pattern syntax, with results returned as JSON.
What you can do
- Run SPARQL SELECT queries —
pg_ripple.sparql(query TEXT)executes a SPARQL SELECT and returns one JSON object per result row, with variable names as keys and values in standard N-Triples format - Run SPARQL ASK queries —
pg_ripple.sparql_ask(query TEXT)returnstrueif any results exist,falseotherwise - Inspect the generated SQL —
pg_ripple.sparql_explain(query TEXT, analyze BOOL DEFAULT false)shows what SQL was generated from a SPARQL query; passanalyze := truefor a full execution plan with timings - Tune the query plan cache —
pg_ripple.plan_cache_size(default 256) controls how many SPARQL-to-SQL translations are cached per connection; set to0to disable caching
Supported query features
- Basic graph patterns with bound or wildcard subjects, predicates, and objects
FILTERwith comparisons (=,!=,<,<=,>,>=) and boolean operators (&&,||,!,BOUND())OPTIONAL(left-join)GRAPH <iri> { … }andGRAPH ?g { … }for named graph scopingSELECTwith variable projection,DISTINCT,REDUCEDLIMIT,OFFSET,ORDER BY
Technical details
- SPARQL text →
spargebra 0.4algebra tree → SQL viasrc/sparql/sqlgen.rs; all IRI and literal constants are encoded toi64before appearing in SQL — SQL injection via SPARQL constants is structurally impossible - Per-query encoding cache avoids redundant dictionary lookups for constants appearing multiple times in one query
- Self-join elimination: patterns sharing a subject but using different predicates compile to a single scan, not separate subqueries
- Batch decode: all integer result columns are decoded in a single
SELECT … WHERE id IN (…)round-trip RUST_TEST_THREADS = "1"in.cargo/config.tomlprevents concurrent dictionary upsert deadlocks in the test suite- New pg_regress tests:
sparql_queries.sql(10 queries),sparql_injection.sql(7 adversarial inputs)
[0.2.0] — 2026-04-14 — Bulk Loading, Named Graphs, and Export
This release makes it practical to work with large RDF datasets. You can load standard RDF files, organise triples into named collections, export data back to standard formats, and register IRI prefixes for convenience.
What you can do
- Load RDF files in bulk —
pg_ripple.load_ntriples(data TEXT),load_nquads(data TEXT),load_turtle(data TEXT), andload_trig(data TEXT)accept standard RDF text and return the number of triples loaded - Load from a file on the server —
pg_ripple.load_ntriples_file(path TEXT)and its siblings read a file directly from the server filesystem (requires superuser); essential for large datasets - Organise triples into named graphs —
pg_ripple.create_graph('<iri>')creates a named collection;pg_ripple.drop_graph('<iri>')deletes it along with its triples;pg_ripple.list_graphs()lists all collections - Export data —
pg_ripple.export_ntriples(graph)andpg_ripple.export_nquads(graph)serialise stored triples to standard text; passNULLto export all triples - Register IRI prefixes —
pg_ripple.register_prefix('ex', 'https://example.org/')records a shorthand;pg_ripple.prefixes()lists all registered mappings - Promote rare properties manually —
pg_ripple.promote_rare_predicates()moves any property that has grown beyond the threshold into its own dedicated table
How rare properties work
Properties with fewer than 1,000 triples (configurable via pg_ripple.vp_promotion_threshold) are stored in a shared table rather than creating a dedicated table for each one. Once a property crosses the threshold it is automatically migrated. This keeps the database tidy for datasets with many rarely-used properties.
How blank node scoping works
Blank node identifiers (_:b0, _:b1, etc.) from different load calls are automatically isolated. Loading the same file twice will produce separate, independent blank nodes rather than merging them — which is almost always what you want.
Technical details
rio_turtle 0.8/rio_api 0.8added for N-Triples, N-Quads, Turtle, and TriG parsing- Blank node scoping via
_pg_ripple.load_generation_seq: each load advances a shared sequence; blank node hashes are prefixed with"{generation}:"to prevent cross-load merging batch_insert_encodedgroups triples by predicate and issues one multi-row INSERT per predicate group, reducing round-trips_pg_ripple.statementsrange-mapping table created (populated in v0.6.0)_pg_ripple.prefixestable:(prefix TEXT PRIMARY KEY, expansion TEXT)- GUCs added:
pg_ripple.vp_promotion_threshold(i32, default 1000),pg_ripple.named_graph_optimized(bool, default off) - New pg_regress tests:
triple_crud.sql,named_graphs.sql,export_ntriples.sql,nquads_trig.sql
[0.1.0] — 2026-04-14 — First Working Release
pg_ripple can now be installed into a PostgreSQL 18 database. After installation you can store facts — statements like "Alice knows Bob" — and retrieve them by pattern. This is the foundation that all later releases build on. No query language yet: just the core building blocks.
What you can do
- Install the extension —
CREATE EXTENSION pg_ripplein any PostgreSQL 18 database (requires superuser) - Store facts —
pg_ripple.insert_triple('<Alice>', '<knows>', '<Bob>')saves a fact and returns a unique identifier for it - Find facts by pattern —
pg_ripple.find_triples('<Alice>', NULL, NULL)returns everything about Alice;NULLis a wildcard for any position - Delete facts —
pg_ripple.delete_triple(…)removes a specific fact - Count facts —
pg_ripple.triple_count()returns how many facts are stored - Encode and decode terms —
pg_ripple.encode_term(…)converts a text term to its internal numeric ID;pg_ripple.decode_id(…)converts it back
How storage works
Every piece of text — names, URLs, values — is converted to a compact integer before storage. Lookups and joins operate on integers, not strings, which is what makes queries fast. Facts are automatically organised into one table per relationship type, and relationship types with few facts share a single table to avoid creating thousands of tiny tables. Every fact receives a globally unique integer identifier that later versions use for RDF-star.
Technical details
- pgrx 0.17 project scaffolding targeting PostgreSQL 18
- Extension bootstrap creates
pg_ripple(user-visible) and_pg_ripple(internal) schemas; thepg_prefix requiresSET LOCAL allow_system_table_mods = onduring bootstrap - Dictionary encoder (
src/dictionary/mod.rs):_pg_ripple.dictionarytable; XXH3-128 hash stored in BYTEA; dense IDENTITY sequence as join key; backend-local LRU encode/decode caches; CTE-based upsert avoids pgrx 0.17InvalidPositionerror on emptyRETURNINGresults - Vertical partitioning (
src/storage/mod.rs):_pg_ripple.vp_{predicate_id}tables with dual B-tree indices on(s,o)and(o,s);_pg_ripple.predicatescatalog;_pg_ripple.vp_rareconsolidation table;_pg_ripple.statement_id_seqfor globally-unique statement IDs - Error taxonomy (
src/error.rs):thiserror-based types — PT001–PT099 (dictionary), PT100–PT199 (storage) - GUC:
pg_ripple.default_graph - CI pipeline: fmt, clippy, pg_test, pg_regress (
.github/workflows/ci.yml) - pg_regress tests:
setup.sql,dictionary.sql,basic_crud.sql
pg_ripple — Roadmap
Audience: Product managers, stakeholders, and technically curious readers who want to understand what each release delivers and why it matters — without needing to read Rust code or SQL specifications.
Authority rule: plans/implementation_plan.md is the authoritative description of the eventual target architecture. This roadmap is the delivery sequence for that architecture.
Versions
Foundation (v0.1.0 – v0.5.1)
| Version | Theme | Status | Scope | Full details |
|---|---|---|---|---|
| v0.1.0 | Install the extension, store and retrieve facts (VP storage from day one) | ✅ Released | Medium | Full details |
| v0.2.0 | Bulk data import, named graphs, rare-predicate consolidation, N-Triples export | ✅ Released | Medium | Full details |
| v0.3.0 | Ask questions in the standard RDF query language (incl. GRAPH patterns) | ✅ Released | Medium | Full details |
| v0.4.0 | Make statements about statements; LPG-ready storage | ✅ Released | Large | Full details |
| v0.5.0 | Property paths, aggregates, UNION/MINUS, subqueries, BIND/VALUES | ✅ Released | Medium | Full details |
| v0.5.1 | Inline encoding, CONSTRUCT/DESCRIBE, INSERT/DELETE DATA, FTS | ✅ Released | Medium | Full details |
Storage Architecture & Validation (v0.6.0 – v0.10.0)
| Version | Theme | Status | Scope | Full details |
|---|---|---|---|---|
| v0.6.0 | Heavy reads and writes at the same time; shared-memory cache | ✅ Released | Large | Full details |
| v0.7.0 | Define data quality rules; reject bad data on insert; on-demand and merge-time triple deduplication | ✅ Released | Medium | Full details |
| v0.8.0 | Complex data quality rules with background checking | ✅ Released | Small | Full details |
| v0.9.0 | Import and export data in all standard RDF file formats | ✅ Released | Small | Full details |
| v0.10.0 | Automatically derive new facts from rules and logic | ✅ Released | Very Large | Full details |
Query, Protocol & Interoperability (v0.11.0 – v0.20.0)
| Version | Theme | Status | Scope | Full details |
|---|---|---|---|---|
| v0.11.0 | Live, always-up-to-date dashboards from SPARQL and Datalog queries | ✅ Released | Medium | Full details |
| v0.12.0 | Pattern-based updates and graph management commands | ✅ Released | Small | Full details |
| v0.13.0 | Speed tuning, benchmarks, production-grade throughput | ✅ Released | Medium | Full details |
| v0.14.0 | Operations tooling, access control, docs, packaging | ✅ Released | Small | Full details |
| v0.15.0 | Standard HTTP API, graph-aware loaders and deletes as SQL functions | ✅ Released | Small | Full details |
| v0.16.0 | Query remote SPARQL endpoints alongside local data | ✅ Released | Small | Full details |
| v0.17.0 | Frame-driven CONSTRUCT queries producing nested JSON-LD | ✅ Released | Small | Full details |
| v0.18.0 | Materialize CONSTRUCT and ASK queries as live, incrementally-updated stream tables | ✅ Released | Small | Full details |
| v0.19.0 | Connection pooling, result caching, query rewriting, and batching for remote SPARQL endpoints | ✅ Released | Small | Full details |
| v0.20.0 | W3C SPARQL 1.1 and SHACL Core test suite compliance, crash recovery and memory safety hardening | ✅ Released | Medium | Full details |
Correctness & Datalog Optimization (v0.21.0 – v0.32.0)
| Version | Theme | Status | Scope | Full details |
|---|---|---|---|---|
| v0.21.0 | Implement all ~40 missing SPARQL 1.1 built-in functions, fix the FILTER silent-drop hazard, and close critical query-semantics bugs | ✅ Released | Medium | Full details |
| v0.22.0 | Fix HTAP merge race conditions, dictionary cache rollback, shmem cache thrashing, rare-predicate promotion race, and HTTP service security gaps | ✅ Released | Medium | Full details |
| v0.23.0 | Complete the SHACL constraint set, add SPARQL query introspection, and fix Datalog/JSON-LD correctness issues | ✅ Released | Medium | Full details |
| v0.24.0 | Semi-naive Datalog evaluation, complete OWL RL rule set, batch-decode large result sets, bound property-path depth | ✅ Released | Medium | Full details |
| v0.25.0 | GeoSPARQL 1.1 geometry primitives, stabilise internal catalog against OID drift, close remaining medium- and low-priority issues | ✅ Released | Medium | Full details |
| v0.26.0 | Microsoft GraphRAG integration: BYOG Parquet export, Datalog-enriched entity graphs, SHACL quality enforcement, Python CLI bridge | ✅ Released | Small | Full details |
| v0.27.0 | Core pgvector integration — embedding table, HNSW index, pg:similar() SPARQL function, bulk embedding, hybrid retrieval modes | ✅ Released | Medium | Full details |
| v0.28.0 | Production-grade RRF fusion, incremental embedding worker, graph-contextualized embeddings, end-to-end RAG retrieval | ✅ Released | Medium | Full details |
| v0.29.0 | Goal-directed inference via magic sets, cost-based body atom reordering, subsumption checking, anti-join negation, filter pushdown | ✅ Released | Medium | Full details |
| v0.30.0 | Aggregation in rule bodies (Datalog^agg), SQL plan caching across inference runs, SPARQL on-demand query speedup | ✅ Released | Medium | Full details |
| v0.31.0 | owl:sameAs entity canonicalization, demand transformation for goal-directed rule rewriting, SPARQL query planner integration | ✅ Released | Medium | Full details |
| v0.32.0 | Three-valued semantics for cyclic ontologies, subsumptive result caching for Datalog and SPARQL repeated sub-queries | ✅ Released | Medium | Full details |
Performance, Conformance & Ecosystem (v0.33.0 – v0.46.0)
| Version | Theme | Status | Scope | Full details |
|---|---|---|---|---|
| v0.33.0 | Complete docs site rebuild — CI harness, eight feature-deep-dive chapters, operations guide, reference section, and content governance | ✅ Released | Large | Full details |
| v0.34.0 | Early fixpoint termination for bounded hierarchies (20–50% faster SPARQL property paths); Delete-Rederive for write-correct materialized predicates | ✅ Released | Medium | Full details |
| v0.35.0 | Background-worker parallelism for independent Datalog rules (2–5× faster materialization); add/remove rules without full recompute | ✅ Released | Medium | Full details |
| v0.36.0 | Leapfrog Triejoin for cyclic SPARQL patterns (10×–100× speedup); Datalog^L monotone lattice aggregation | ✅ Released | Medium | Full details |
| v0.37.0 | Fix HTAP merge race, rare-predicate promotion race, dictionary cache rollback; eliminate all hard panics; add GUC validators | ✅ Released | Large | Full details |
| v0.38.0 | Split god-module, PredicateCatalog trait, batch encoding, SCBD, SPARQL Update completeness, SHACL hints in planner | ✅ Released | Large | Full details |
| v0.39.0 | REST API exposing all 27 Datalog SQL functions in pg_ripple_http: rule management, inference, goal queries, constraints, admin | ✅ Released | Small | Full details |
| v0.40.0 | Server-side SPARQL cursors, explain_sparql(), explain_datalog(), OpenTelemetry tracing, resource governors | ✅ Released | Large | Full details |
| v0.41.0 | Complete W3C SPARQL 1.1 test suite harness with parallelized execution; 3,000+ tests in < 2 min CI | ✅ Released | Medium | Full details |
| v0.42.0 | Multi-worker HTAP merge, FedX-style federation planner, parallel SERVICE, live RDF change subscriptions | ✅ Released | Very Large | Full details |
| v0.43.0 | Apache Jena edge-case tests (~1,000) and WatDiv scale-correctness benchmark (10M+ triples, star/chain/snowflake/complex patterns) | ✅ Released | Medium | Full details |
| v0.44.0 | LUBM OWL RL inference correctness across 14 canonical queries; Datalog API validation sub-suite | ✅ Released | Small | Full details |
| v0.45.0 | Close remaining SHACL Core gaps, harden parallel Datalog strata rollback, add crash-recovery scenarios, standardise migration documentation | ✅ Released | Small | Full details |
| v0.46.0 | proptest for SPARQL/dictionary invariants, fuzz federation result decoder, W3C OWL 2 RL test suite in CI, TopN push-down, BSBM regression gate | ✅ Released | Medium | Full details |
Architecture, Observability & Production (v0.47.0 – v0.54.0)
| Version | Theme | Status | Scope | Full details |
|---|---|---|---|---|
| v0.47.0 | Fix parsed-but-not-checked SHACL constraints, wire preallocate_sid_ranges(), finish sparql/translate/ module split, add fuzz targets, GUC validators, security hygiene | ✅ Released | Large | Full details |
| v0.48.0 | Complete all 35 SHACL Core constraints, close OWL 2 RL rule set, add SPARQL Update MOVE/COPY/ADD, fix SPARQL-star variable patterns, WatDiv baselines | ✅ Released | Medium | Full details |
| v0.49.0 | sparql_from_nl() NL-to-SPARQL via configurable LLM endpoint; embedding-based entity alignment with suggest_sameas() | ✅ Released | Small | Full details |
| v0.50.0 | explain_sparql(analyze:=true) interactive query debugger; rag_context() RAG pipeline | ✅ Released | Small | Full details |
| v0.51.0 | Non-root container, SPARQL DoS protection, HTTP streaming, OTLP, pg_upgrade compat, CDC docs, conformance gate flips | ✅ Released | Large | Full details |
| v0.52.0 | JSON→RDF helpers, CDC→outbox bridge worker, CDC bridge triggers, JSON-LD event serializer, dedup keys, vocabulary templates, pg-trickle runtime detection | ✅ Released | Medium | Full details |
| v0.53.0 | SHACL-SPARQL, COPY rdf FROM, RAG hardening, CDC lifecycle events, architecture module splits, OpenAPI spec | ✅ Released | Medium | Full details |
| v0.54.0 | PG18 logical-decoding RDF replication, Helm chart, CloudNativePG image volume, merge/vector-index performance baselines | ✅ Released | Medium | Full details |
Quality, Security & Ecosystem (v0.55.0 – v0.59.0)
| Version | Theme | Status | Scope | Full details |
|---|---|---|---|---|
| v0.55.0 | Security hardening (SSRF allowlist, HTAP race fix), error-catalog reconciliation, tombstone GC, named-graph RLS, read-replica routing, VoID, SPARQL Service Description, OpenAPI spec | ✅ Released | Large | Full details |
| v0.56.0 | GeoSPARQL 1.1, SPARQL Entailment Regime tests, Arrow/Flight export, federation circuit breaker, SPARQL audit log, dead-code audit, deprecated GUC removal | ✅ Released | Medium | Full details |
| v0.57.0 | OWL 2 EL/QL reasoning profiles, KG embeddings (TransE/RotatE), entity alignment, LLM SPARQL repair, ontology mapping, multi-tenant graph isolation, columnar VP, adaptive indexing | ✅ Released | Very Large | Full details |
| v0.58.0 | Temporal RDF queries (point_in_time), SPARQL-DL, Citus horizontal sharding, PROV-O graph provenance, v1.0.0 readiness integration suite | ✅ Released | Large | Full details |
| v0.59.0 | Citus SPARQL shard-pruning for bound subjects (10–100× speedup), rebalance NOTIFY coordination, explain_sparql() Citus section | ✅ Released | Medium | Full details |
Pre-1.0 Hardening & Ecosystem (v0.60.0 – v0.63.0)
| Version | Theme | Status | Scope | Full details |
|---|---|---|---|---|
| v0.60.0 | Close all v1.0.0 blockers: HTAP cutover atomic swap, Actions SHA pinning, SECURITY DEFINER CI lint, new fuzz targets (GeoSPARQL WKT, R2RML, LLM prompt), /ready endpoint, geof:distance, merge-throughput trend artifact, pg_dump round-trip CI test, LangChain tool package | Released ✅ | Large | Full details |
| v0.61.0 | Ecosystem depth: per-named-graph RLS, explain_inference() derivation tree, GDPR erase_subject(), dbt adapter, SHACL-AF rule execution, OTLP traceparent propagation, richer federation call stats; Citus object-based shard pruning and direct-shard bulk-load path | Released ✅ | Large | Full details |
| v0.62.0 | Query frontier: Apache Arrow Flight bulk export, WCOJ planner integration, visual graph explorer in pg_ripple_http, clippy --deny warnings CI gate; Citus property-path push-down, vp_rare cold-entry archival, tiered dictionary cache, distributed inference dispatch, live shard rebalance, multi-hop pruning carry-forward | Released ✅ | Very Large | Full details |
| v0.63.0 | SPARQL CONSTRUCT writeback rules (raw-to-canonical pipelines, incremental delta maintenance, Delete-Rederive, pipeline stratification); Citus scalability: SERVICE result shard pruning, streaming fan-out cursor, HyperLogLog COUNT(DISTINCT), batched dictionary encoding, per-worker SID tables, non-blocking VP promotion, per-graph RLS CI gate, per-worker BRIN summarise | Released ✅ | Large | Full details |
Assessment Remediation & Release Trust (v0.64.0 – v0.69.0)
| Version | Theme | Status | Scope | Full details |
|---|---|---|---|---|
| v0.64.0 | Release truth and safety freeze: feature-status API, deep readiness, immutable GitHub Actions, digest-scanned Docker releases, documentation truth pass, release evidence dashboard foundation | Released ✅ | Large | Full details |
| v0.65.0 | CONSTRUCT writeback correctness closure: real delta maintenance, HTAP-aware retraction, exact provenance capture, parameterized rule catalog writes, full CWB behavior test matrix | ✅ Released | Very Large | Full details |
| v0.66.0 | Streaming and distributed reality: true SPARQL cursors, signed Arrow IPC export, explainable WCOJ mode, integrated Citus pruning/HLL/BRIN/RLS/promotion paths | ✅ Released | Very Large | Full details |
| v0.67.0 | Assessment 9 critical remediation and production evidence: storage mutation journal, VP table RLS coverage, Arrow Flight security/correctness, fail-closed release-truth gates, soak tests, benchmark baselines, security audit | Released ✅ | Very Large | Full details |
| v0.68.0 | Distributed scalability, streaming completion and fuzz hardening: CONSTRUCT cursor streaming, Citus HLL translation, SERVICE pruning, nonblocking VP promotion, scheduled fuzz CI | Released ✅ | Large | Full details |
| v0.69.0 | Module architecture restructuring: split sparql/mod.rs, pg_ripple_http/main.rs, construct_rules.rs, and storage/mod.rs along single-responsibility boundaries | Released ✅ | Large | Full details |
Assessment 10 Remediation & Production Hardening (v0.70.0 – v0.73.0)
| Version | Theme | Status | Scope | Full details |
|---|---|---|---|---|
| v0.70.0 | Assessment 10 critical remediation: bulk-load mutation journal, per-statement flush, fail-closed evidence gate, SHACL doc truth, README versioning, RLS SQL quoting, SBOM currency | Released ✅ | Large | Full details |
| v0.71.0 | Arrow Flight streaming validation, Citus multi-node integration test, pg_ripple_http/pg_ripple compatibility matrix, HLL accuracy docs, SERVICE shard benchmark | Released ✅ | Large | Full details |
| v0.72.0 | Architecture and protocol hardening: mutation journal SAVEPOINT safety, plan cache docs, continued module split, ConstructTemplate proptest, SPARQL Update fuzz, conformance gate promotion, Arrow Flight replay protection | Released ✅ | Large | Full details |
| v0.73.0 | SPARQL 1.2 tracking, live SPARQL subscription API (WebSocket/SSE), feature status taxonomy, CONTRIBUTING.md, Helm chart SHA pin, R2RML scope docs | Released ✅ | Large | Full details |
Assessment 11 Remediation & Production Polish (v0.74.0 – v0.76.0)
| Version | Theme | Status | Scope | Full details |
|---|---|---|---|---|
| v0.74.0 | Assessment 11 critical/high remediation: evidence-path truthfulness (12 missing docs stubs, CI gate fix), mutation journal wiring for Datalog/R2RML/CDC, SBOM regeneration, HTTP companion version alignment, populated-DB CI validation, plan cache invalidation on VP promotion, per-statement flush deferral | Released ✅ | Large | Full details |
| v0.75.0 | Assessment 11 medium findings: unwrap/panic audit, Citus and Arrow integration test CI wiring, roadmap status validation, RLS error surfacing, role-name doc, property-path edge tests, fuzz duration increase, URL parser fuzz target, HTTP companion production docs, feature-status journal entry | Released ✅ | Large | Full details |
| v0.76.0 | Assessment 11 low-severity and polish: rust-toolchain pin, RLS hash widening, Arrow dep pin, benchmark baseline refresh, test count growth, /metrics auth docs, xact SPI safety citation, log-hook audit, clippy gate verification | Released ✅ | Medium | Full details |
Bidirectional Integration & Beyond (v0.77.0 – v0.78.0)
| Version | Theme | Status | Scope | Full details |
|---|---|---|---|---|
| v0.77.0 + v0.78.0 | v0.77.0 — Bidirectional Integration Primitives (source attribution, conflict resolution with echo-aware normalize, late-binding IRI rewrite, sparse-CAS events, linkback with target-assigned IDs, pg-trickle outbox/inbox transport) + v0.78.0 — Bidirectional Integration Operations (write-side outbox policy, new-events-only schema evolution, per-subscription side-band auth, write-time redaction, audit, property/chaos tests, reconciliation toolkit, ops surface). Both ship together. BIDI-SPEC-01: non-blocking draft RDF Bidirectional Integration Profile v1 for broader ecosystem review. | Released ✅ | Large | v0.77.0, v0.78.0 |
Query Engine Completeness (v0.79.0)
| Version | Theme | Status | Scope | Full details |
|---|---|---|---|---|
| v0.79.0 | Close the last two known query-engine limitations: true Leapfrog Triejoin executor for Worst-Case Optimal Joins (WCOJ-LFTI-01) and full sh:SPARQLRule evaluation (SHACL-SPARQL-01). Removes the "Known limitations" table from the README; all feature_status() rows become implemented. | Released ✅ | Medium | Full details |
Assessment 12 Remediation (v0.80.0 – v0.83.0)
| Version | Theme | Status | Scope | Full details |
|---|---|---|---|---|
| v0.80.0 | Assessment 12 critical/high remediation + consistency audit: SPARQL Update CWB flush (C-01), R2RML/CDC journal wiring (C-02), property-path cycle detection (C-03), plan cache RLS key (C-14), SQL injection fixes (S-01, S-02), full RFC-1918 SSRF blocklist (S-03), migration chain assertions (T-01), SBOM regeneration + CI gate (DS-01), HTTP error standardisation (A-01), compatibility matrix update (D-01); new: /explorer endpoint authentication (EXPLORER-AUTH-01) | Released ✅ | Large | Full details |
| v0.81.0 | Assessment 12 correctness & concurrency closure + consistency audit: HTAP merge SID determinism (C-04), promotion atomicity (C-05), dict encode race (C-06), SHACL subtransaction (C-07), federation URL/truncation (C-08, C-09), OPTIONAL promotion (C-10), Datalog guards (C-11–C-13), federation cache key (C-15), strict dictionary GUC (C-16), SubXact cache invalidation (CC-01), CDC slot cleanup worker (CC-02), per-predicate promotion locks (CC-03), merge fence reduction (CC-04), Datalog SCC fix (CC-05), CDC LSN watermark (CC-06), stuck-promotion recovery (CC-07), strict SPARQL filters GUC (SC-01), replication.rs panic elimination (Q-02); new (audit-1): _PG_fini unload callback (PGFINI-01), plan cache GUC completeness (PLAN-CACHE-GUC-02), BIDI/BIDIOPS feature_status rows (FEATURE-STATUS-BIDI-01), shared_preload_libraries warning (PRELOAD-WARN-01), retract parameterisation (RETRACT-PARAM-01), scheduler error propagation (SCHEDULER-ERR-01), DRed full fixpoint (DRED-FIXPOINT-01), HTAP retract consistency (RETRACT-HTAP-01); new (audit-2): RAG handler parameterized queries (RAG-SQL-INJECT-02), extended plan cache GUC keys (PLAN-CACHE-GUC-02 ext) | Released ✅ | Large | Full details |
| v0.82.0 | Assessment 12 performance & observability + consistency audit: plan cache capacity GUC (P-01), batch decode ANY($1) (P-02), merge worker predicate cache (P-03), federation cost stats (P-04), predicate stats cache table (P-08), Arrow row limit GUC (P-09), pg_stat_statements normalisation (P-10), GUC bounds validators (P-12), Prometheus structured labels (P-13), SPARQL algebra tree in EXPLAIN (O-01), merge worker heartbeat (O-02), graph_stats scan limit GUC (O-03), redacted_error uniformity (O-06), admin lock docs (O-07), SPARQL depth DoS GUC (S-05), tenant name validation (S-06), Unicode role fallback (S-07), federation response cap (S-09), shmem overflow check (S-12), RUSTSEC audit refresh (S-04); new (audit-1): SSE subscription_id length check (LISTEN-LEN-01), property-path predicate limit GUC (PROPPATH-UNBOUNDED-01), vacuum_dictionary batch size (VACUUM-DICT-BATCH-01), merge lock timeout GUC (MERGE-LOCK-GUC-01), datalog cleanup error visibility (DATALOG-SILENT-01), embedding model GUC consistency (EMBED-MODEL-01), federation call counter ordering (FED-COUNTER-ORDER-01); new (audit-2): federation response pre-read size check (FED-BODY-STREAM-01), batch_decode missing-ID warning (DECODE-WARN-01), export_jsonld OOM documentation (EXPORT-JSONLD-OOM-01) | Released ✅ | Large | Full details |
| v0.83.0 | Assessment 12 test coverage, API polish & code quality + consistency audit: 8 error-path regression tests (T-02), N-Triples/N-Quads/TriG fuzz targets (T-03), sparql_update fuzz target (T-04), proptest reference-impl comparisons (T-05), CDC async test barriers (T-06), known_failures.txt annotations (T-07), 13 missing pg_extern regression tests (T-08), json_ld_load deprecation/rename (A-02), RETURNS TABLE graph column (A-04), GUC naming convention docs (A-05), BREAKING changelog tags (A-06), serde_cbor evaluation (DS-02), Renovate config (DS-03), bidi.rs module split (Q-01), shared-memory dict cache evaluation (P-05); new (audit-1): health endpoint build_time field fix (BUILD-TIME-FIELD-01), datalog cost-model divisor GUCs (DL-COST-GUC-01), CHANGELOG heading format lint (CHANGELOG-FMT-01); new (audit-2): merge worker exponential backoff (MERGE-BACKOFF-01), metrics route auth documentation (METRICS-AUTH-DOC-01), WWW-Authenticate header on 401 (HTTP-401-WWW-AUTH-01), JSON auth error envelope (AUTH-RESP-FMT-01), blank node label export validation (EXPORT-BNODE-VALID-01), Datalog max-iteration test (DATALOG-MAXITER-TEST-01) | Released ✅ | Large | Full details |
Assessment 13 Remediation (v0.84.0 – v0.86.0)
| Version | Theme | Status | Scope | Full details |
|---|---|---|---|---|
| v0.84.0 | Assessment 13 critical/high & operational remediation: HTTP companion version sync, STRICT_COMPAT env var, docker-compose image tag currency, SECURITY DEFINER annotations, CI SQL-injection-check gate, migration-chain checkpoints for v0.80–v0.83, gucs/registration.rs split, nested OPTIONAL+EXISTS fix, /health/ready deep-check, plan cache double-parse elimination, justfile bump-version/regen-sbom/regen-openapi recipes | Released ✅ | Large | Full details |
| v0.85.0 | Assessment 13 correctness, performance & code quality: strict decode, mutation journal assertions, plan cache normalisation, IRI length bounds, encode batch API, merge-worker throttling, cycle pre-check, HOT-path metrics, schema.rs/federation.rs splits, per-file line-count CI gate, per-predicate merge fence | Released ✅ | Large | Full details |
| v0.86.0 | Assessment 13 tests, API polish, observability, supply chain & security: sparql_roundtrip proptest vs reference evaluator, CONSTRUCT/SHACL-SPARQL fuzz targets, conformance trend artifacts, benchmark regression gate, OpenAPI CI, error-code registry, structured JSON logs, axum graceful shutdown, dependency upgrades, CORS counter, Arrow 413 guard, line coverage badge + llvm-cov CI gate | Released ✅ | Large | Full details |
Uncertain Knowledge & Soft Reasoning (v0.87.0)
Probabilistic feature specification: see plans/probabilistic-features.md for the full architecture, algorithm details, and API design for the uncertain knowledge engine. (D13-05, v0.86.0)
| Version | Theme | Status | Scope | Full details |
|---|---|---|---|---|
| v0.87.0 | Uncertain knowledge engine — Probabilistic Datalog with @weight(FLOAT) rule annotations, multiplicative confidence propagation, and noisy-OR multi-path combination; pg:confidence() SPARQL function and load_triples_with_confidence() bulk-loader; fuzzy SPARQL filters (pg:fuzzy_match() trigram/token-set similarity, confidence-threshold edge filtering in property paths via pg:confPath()); soft SHACL scoring with numerical sh:severityWeight annotations and pg_ripple.shacl_score() composite data-quality function; provenance-weighted confidence derived automatically from PROV-O source trust metadata via Datalog rules | Released ✅ | Very Large | Full details |
Graph Analytics: PageRank (v0.88.0)
| Version | Theme | Status | Scope | Full details |
|---|---|---|---|---|
| v0.88.0 | Datalog-native PageRank & graph analytics — iterative PageRank via Datalog^agg + tabling; pg:pagerank() / pg:pagerank(?node, ?topic) SPARQL functions; personalized + predicate-scoped PR; magic-sets partial-graph PR; pg_ripple.pagerank_run() SQL function; pg-trickle incremental refresh (K-hop Z-set, score bounds, staleness columns, selective recomputation); confidence-weighted edges (v0.87 integration); topic-sensitive multi-run; edge-weight predicates; reverse/in-degree direction; temporal decay; SHACL constraint-aware ranking (sh:importance, sh:excludeFromRanking, shacl_score() threshold); sketch-based pg:topN_approx(); score-explanation trees (explain_pagerank()); graph-partitioned parallel computation; standard-format export (Turtle/JSON-LD/CSV/N-Triples); federation blend mode; four alternative centrality measures via pg:centrality() (betweenness, closeness, eigenvector, Katz); IVM queue metrics; six v0.87×v0.88 synergies: confidence-attenuated K-hop propagation (PR-TRICKLE-CONF-01), probabilistic PageRank via @weight rules (PR-PROB-DATALOG-01), centrality-guided entity deduplication (PR-ENTITY-RESOLUTION-01), source-trust-weighted eigenvector centrality (PR-TRUST-EIGEN-01), confidence-gated federation edges (PR-FED-CONF-01), temporal authority via Katz centrality (PR-KATZ-TEMPORAL-01); PT0401–PT0423 error catalog | Released ✅ | Very Large | Full details |
Assessment 14 Remediation (v0.89.0 – v0.92.0)
| Version | Theme | Status | Scope | Full details |
|---|---|---|---|---|
| v0.89.0 | A14 High remediation — delete src/gucs/registration.rs.bak + CI lint; migration-chain checkpoints v0.84–v0.88 + structural version-sync assertion; bump COMPATIBLE_EXTENSION_MIN to v0.88.0; just bump-version X.Y.Z recipe; confidence noisy-OR proptest vs reference oracle; check_auth_write on mutating PageRank handlers; GUC name audit before API freeze; default rate limit 100 req/s; fuzzy_max_input_length + pagerank_max_seeds guards; IRI escaping in export_pagerank() | Released ✅ | Large | Full details |
| v0.90.0 | A14 Medium: correctness, performance, concurrency, code quality — PageRank convergence-norm GUC + K-hop drift bound doc; SPARQL MINUS blank-scope regression; export format enum validation; WCOJ integration for large-graph PageRank; clippy::unwrap_used workspace lint gate; PageRank streaming without temp materialisation; embedding fast-path gate; confidence ANALYZE; pagerank_dirty_edges deadlock test; advisory lock for concurrent PageRank runs; confidence + PageRank proptests; confidence-loader fuzz target; PageRank scale gate; concurrent SPARQL+PageRank test; pre-emptive splits of seven 1,300–1,700 line files; src/pagerank/ + src/uncertain/ module splits; probabilistic weight parser validation; cyclic-convergence documentation | Released ✅ | Large | Full details |
| v0.91.0 | A14 Medium: observability, API, standards, build, docs — PageRank IVM Prometheus gauges; SHACL score-log retention GUC + background vacuum; SSE endpoint verification; HTTP routing middleware extraction; Arrow Flight COUNT→EXPLAIN swap; explain_pagerank_json() JSONB variant; PT error code registry (PT0301–PT0423); SPARQL 1.2 tracking update; RDF-star compliance matrix; ProbLog citation; lint-version-sync CI extension; dedicated migration-chain.yml workflow; ureq + arrow/parquet upgrade triage; IVM boundary documentation; CWB confidence-propagation test; CDC LSN watermark batching; compatibility matrix v0.87/v0.88 rows; pagerank.md completeness audit | ✅ Released | Medium | Full details |
| v0.92.0 | A14 Low-severity polish — PageRank bounds source comment; damping tuning guide; SERVICE SILENT TLS test; describe_form alias contract; RSA dependency audit; pagerank_dirty_edges RLS; pagerank_find_duplicates STABLE; cargo-audit --deny unmaintained; pagerank_partition auto-tune default; fuzzy_match STABLE; Datalog cyclic-dep regression; confidence sub-xact rollback test; benchmark throughput history; .unwrap() audit v0.87/v0.88; diagnostic_report() v0.87/v0.88 catalog; SOURCE_DATE_EPOCH reproducible builds; owl:sameAs PageRank dedup doc; CDC pg_notify payload bound; SSE backpressure load test; WC-01–WC-05 post-v1.0 aspirational tracking issues filed | ✅ Released | Medium | Full details |
pg_tide Integration (v0.93.0)
Context: pg-trickle v0.46.0 extracted its full relay, outbox, and inbox subsystem into the new standalone
pg_tideextension (trickle-labs/pg-tide). After v0.46.0,pg_trickleprovides IVM only. See plans/PLAN_PG_TIDE.md for the full impact analysis.
| Version | Theme | Status | Scope | Full details |
|---|---|---|---|---|
| v0.93.0 | pg_tide integration & documentation modernisation — Add has_pg_tide() runtime detection + pg_ripple.pg_tide_available() SQL function (TIDE-1); update BIDI-OUTBOX-01/BIDI-INBOX-01 doc comments to reference pg_tide (TIDE-2); add PGTIDE_HINT constant for relay error paths (TIDE-3); full rewrite of docs/src/operations/pg-trickle-relay.md to tide.* API — 20+ call sites updated, new outbox publish trigger pattern, pg-tide-relay binary, updated prerequisites and architecture diagram (TIDE-4); update blog/semantic-hub-trickle-relay.md hub-and-spoke examples (TIDE-5); add backward-compat note to plans/pg_trickle_relay_integration.md (TIDE-6); add inline notes to roadmap/v0.52.0.md and roadmap/v0.77.0-full.md (TIDE-7); extend compatibility matrix with pg_tide ≥ 0.1.0 rows (TIDE-8); add comment-only migration script sql/pg_ripple--0.92.0--0.93.0.sql; update Dockerfile to build and ship pg_tide alongside pg_ripple, pg_trickle, PostGIS, and pgvector (TIDE-DOCKER-01) | ✅ Released | Small | Full details |
Assessment 15 Remediation (v0.94.0 – v0.97.0)
| Version | Theme | Status | Scope | Full details |
|---|---|---|---|---|
| v0.94.0 | A15 High remediation — implement just bump-version X.Y.Z + bump COMPATIBLE_EXTENSION_MIN to v0.93.0 (H15-01); add SET search_path = pg_catalog, _pg_ripple, public to _pg_ripple.ddl_guard_vp_tables() + CI check_security_definer_search_path.sh lint (H15-02); bounded bidirectional relay channel with pg_ripple.bidi_relay_max_inflight GUC, drop-oldest overflow policy, and pg_ripple_bidi_relay_dropped_total Prometheus counter (H15-03/L15-13); migrate src/bulk_load.rs to COPY ... FROM STDIN BINARY for dictionary-encoded triple stream gated on pg_ripple.bulk_load_use_copy GUC; extract shared copy_into_vp() helper used by bulk loader, R2RML, and CDC paths (H15-05/M15-20) | ✅ Released | Large | Full details |
| v0.95.0 | A15 Medium: correctness, security, storage — replace both unreachable!() in pagerank/export.rs and pagerank/centrality.rs with pgrx::error! + CI zero-unreachable-in-production lint (M15-01); resolve-once DNS rebinding fix in federation/policy.rs — validate every resolved IP against the SSRF blocklist, connect to resolved IP with pinned Host: header (M15-02); sql_drop event trigger for DROP EXTENSION replication-slot cleanup (M15-03); redacted_error() for SSE initialisation error paths in stream.rs (M15-04); scheduled VACUUM ANALYZE _pg_ripple.dictionary after bulk encode above threshold GUC, plus autovacuum_scale_factor reloptions on the dictionary table (M15-07); explicit regression tests for OPTIONAL + property paths inside GRAPH {} with vp_rare predicates (M15-08); NaN/Inf/out-of-range rejection in load_triples_with_confidence() and INSERT ON CONFLICT confidence paths — raise PT0302/PT0303 (M15-09); fold _pg_ripple.schema_generation counter into plan cache key, bump on every VP promotion and ensure_vp_table() call (M15-10); integrate ADD/COPY/MOVE SPARQL Update operations through the main UPDATE pipeline with CDC and SHACL queue integration tests (M15-12) | ✅ Released | Large | Full details |
| v0.96.0 | A15 Medium: performance, code quality, test coverage — HTAP tombstone-skip optimisation: maintain tombstone_count in _pg_ripple.predicates and rebuild view to elide the LEFT JOIN when count = 0 (M15-05); star-pattern self-join collapse in sparql/optimizer.rs — detect (?s p1 ?o1 . ?s p2 ?o2 . …) star shapes, emit single subject-seeded CTE, gate on pg_ripple.star_join_collapse GUC (M15-06); separate pg_ripple.federation_connect_timeout_secs GUC for TCP/TLS connect vs query-body timeout (M15-11); complete mod.rs sub-splits for the five 1,489–1,625 line files (sparql/expr, datalog/compiler, storage/ops, export, sparql/execute) targeting every mod.rs < 800 lines (M15-13); sub-split datalog_handlers.rs into routing/datalog/{rules,inference,query,admin}.rs (M15-14); #![warn(missing_docs)] in src/lib.rs + public API doc pass (M15-15); pagerank_with_writes.sh concurrent-load test: 4 pgbench writers + 1 reader + 1 PageRank background (M15-17); shacl_report_scored column-order regression test (M15-18); four missing Prometheus metrics: pg_ripple_merge_cycle_duration_seconds, pg_ripple_datalog_stratum_duration_seconds, pg_ripple_shacl_validation_queue_depth, pg_ripple_cdc_replication_slot_lag_bytes (M15-19); verify cyclic-group pre-check source in parallel Datalog (M15-21); Arrow Flight EXPLAIN (FORMAT JSON) row-estimate path replacing the COUNT(*) pre-check (M15-22) | ✅ Released | Large | Full details |
| v0.97.0 | A15 Low-severity polish & supply-chain — fix CHANGELOG v0.90.0 date placeholder (L15-01); add Arrow Flight, PageRank, and bidi relay example files (L15-02); wire examples/test_all.sh --live in CI against cargo pgrx start pg18 (L15-03); enforce clippy::missing_safety_doc + undocumented_unsafe_blocks for 1:1 unsafe/SAFETY ratio (L15-04); #[allow(...)] justification audit with // Q15-xx: convention (L15-05); gen_random_uuid() availability check at _PG_init with WARNING if pgcrypto absent (L15-06); serde_cbor consumer upgrade: cargo tree -i serde_cbor + bump the consumer if a newer version drops the transitive dep (M15-16); RDF-star <<>> position support matrix in docs/src/reference/sparql-compliance.md (L15-08); cargo doc --no-deps missing-documentation gate in CI (L15-09); auto-compute HIGHEST_CHECKPOINT in test_migration_chain.sh from ls sql/pg_ripple--*--*.sql | sort -V | tail -1 eliminating the hand-maintained constant (L15-10); document statement_id_seq exhaustion behaviour in docs/src/operations/scaling.md (L15-11); owl_sameas_cycle.sql regression test asserting graceful handling of (a sameAs b, b sameAs a) cycles (L15-12); conformance-suite pass-rate badges (Jena, WatDiv, OWL 2 RL) in README.md updated by CI workflow (L15-14) | ✅ Released | Small | Full details |
SKOS Vocabulary Support (v0.98.0)
| Version | Theme | Status | Scope | Full details |
|---|---|---|---|---|
| v0.98.0 | SKOS support, named bundle API & graph intelligence — "skos" built-in Datalog rule set (28 rules) implementing all W3C SKOS entailments (S7–S45): skos:broaderTransitive/skos:narrowerTransitive closures, skos:narrower/skos:broader inverse inference, skos:related symmetry, mapping property propagation (broadMatch→broader, exactMatch transitivity/symmetry), label and documentation sub-properties, concept-type assertions; "skosxl" rule set (3 rules, S55–S57); "skos-integrity" SHACL shape bundle (10 validators, W3C S9/S13/S14/S27/S37/S46 + ISO 25964-1 structural rules); formal named bundle API: load_datalog_bundle(name, graph) / load_shape_bundle(name) / active_datalog_bundles catalog view with bundle_version (required by riverbank compiler profiles); implicit dependency resolution (load_shape_bundle('skos-integrity') auto-activates 'skos-transitive'); explain_contradiction(subject_iri, mode) — greedy and exact minimal-hitting-set contradiction explainer tracing which triples and Datalog rules produce an inconsistency, plus JSONB variant; pg_ripple.federation_endpoints table with min_confidence per endpoint and pg:sourceTrust tagging of remote SERVICE triples; coverage_map(named_graphs, topic_predicate, top_k) returning per-topic triple count, source count, mean/min confidence, violation count, and time range; refresh_coverage_map() writing pgc:CoverageMap triples schedulable via pg_trickle; five SQL helper functions (skos_ancestors, skos_descendants, skos_label, skos_related, skos_siblings); validate_skos(); 251 pg_regress tests pass; cookbook chapter; blog post | Released | Large | Full details |
Foundational Vocabulary Bundles (v0.99.0)
| Version | Theme | Status | Scope | Full details |
|---|---|---|---|---|
| v0.99.0 | DCTERMS, Schema.org & FOAF vocabulary bundles — completes the "Big 5" foundational vocabulary stack; native Datalog rule sets and SHACL shape bundles for Dublin Core Terms ("dcterms", 11 rules: dc11 backward-compat aliases, hasPart/isPartOf inverses, hasVersion/isVersionOf, replaces/isReplacedBy, DC-SKOS-01 bridge; 8 SHACL validators), Schema.org ("schema", 15 rules: type-hierarchy shortcuts for Person/Organization/Product/Event/CreativeWork, inverse pairs, SCHEMA-FOAF-01/SCHEMA-DC-01/SCHEMA-DCAT-01 cross-vocab bridges; 6 validators), FOAF ("foaf", 8 rules: knows symmetry, Agent subsumption, made/maker inverse, DC-FOAF-01 bridge; 5 validators); cross-bundle rules activate automatically when co-bundles are loaded; schema_type_ancestors() and foaf_persons() SQL helpers; 45+ pg_regress tests; docs/src/cookbook/common-vocabularies.md cookbook chapter | Released ✅ | Medium | Full details |
| v0.99.1 | Patch: pg_trickle & pg_tide version probe fix; view decode=true IVM fix; IMMEDIATE mode — PG_TRICKLE_TESTED_VERSION corrected to "0.49.0"; Dockerfile PG_TRICKLE_VERSION bumped to 0.49.0 and PG_TIDE_VERSION to 0.16.0; create_sparql_view/create_datalog_view with decode=true now creates a separate _decoded companion VIEW instead of wrapping the stream table; immediate boolean parameter added to all view creation functions | Released ✅ | Patch | — |
| v0.99.2 | Patch: pg_trickle 0.49.1; new repository — PG_TRICKLE_VERSION bumped to 0.49.1; .versions.toml updated; repository relocated to grove/pg-ripple | Released ✅ | Patch | — |
Expert System Platform (v0.100.0 – v0.108.0)
Foundation document: see plans/expert-system.md for the full analysis of what makes pg_ripple an expert system platform, competitive positioning, application scenarios (clinical, AML, regulatory, industrial), and the rationale for each phase. The nine versions below implement the eight-phase roadmap in that document; phases 3 and 4 are separate releases (v0.102.0 and v0.103.0), and phase 7 is split across two releases (v0.106.0 and v0.107.0) because the sequential-pattern operators require CDC integration work beyond the scope of the basic fact-store and operator groundwork.
| Version | Theme | Status | Scope | Full details |
|---|---|---|---|---|
| v0.100.0 | Proof trees & justification infrastructure — _pg_ripple.derivations table recording (derived_sid, rule_name, antecedent_sids[]) for every Datalog-derived fact; pg_ripple.record_derivations GUC (default off) gates the overhead; justify(subject TEXT, predicate TEXT, object TEXT) SQL function returning the full backward-chaining proof tree as JSONB — implemented as a single recursive CTE that walks the entire derivation DAG in one query with no per-level SPI round-trips; justify_batch(sids BIGINT[]) variant that prefetches the whole subgraph for multiple root facts at once; cycle protection via PostgreSQL CYCLE clause; batch dictionary decode for human-readable IRI labels in proof output; orphan rows vacuumed on next DRed pass (justify() may return stale trees within that window — documented; pg_ripple.derivations_vacuum_interval GUC, default '5 minutes', triggers an independent background cleanup independent of DRed runs); migration script; pg_regress tests for derivation recording and proof-tree correctness; proptest suite generating random rule sets, running inference, asserting every derived fact has a valid proof tree and that retracting a fact removes its full derivation chain | Released ✅ | Large | Full details |
| v0.101.0 | Natural language explanation — explain_inference(subject TEXT, predicate TEXT, object TEXT, format TEXT DEFAULT 'text') function: retrieves proof tree from _pg_ripple.derivations, decodes all dictionary IDs to human-readable IRIs and labels, feeds structured tree to the LLM endpoint (pg_ripple.llm_endpoint) with a domain-appropriate system prompt, returns a natural language narrative; LLM unavailability fallback: when the endpoint is unreachable explain_inference() returns the structured proof tree rendered as indented readable text rather than failing; _pg_ripple.explanation_cache table with TTL-based invalidation (pg_ripple.explanation_cache_ttl GUC); REST endpoint POST /explain in pg_ripple_http; mock-LLM test coverage including the fallback path; JSONB variant explain_inference_jsonb() returning structured proof with embedded narrative | ✅ Released | Large | Full details |
| v0.102.0 | What-if reasoning (hypothetical inference) — hypothetical_inference(hypotheses JSONB, rules TEXT DEFAULT 'default') asserts hypothetical facts and runs inference in an isolated sandbox, returning a diff of what would be newly derived or retracted without touching real data; overlay mechanism: session-local temporary tables _hyp_assert_{predicate_id} and _hyp_retract_{predicate_id} are created on demand for each predicate referenced in hypotheses; DRed is redirected to query hypothetical_vp_{predicate_id} views defined as (SELECT * FROM vp_{predicate_id} EXCEPT ALL SELECT * FROM _hyp_retract_{predicate_id}) UNION ALL SELECT * FROM _hyp_assert_{predicate_id} — no query rewriting required in the VP scan layer; all temp tables and views dropped on ROLLBACK or session end; returns diff as JSONB {"derived": [...], "retracted": []}; REST endpoint POST /hypothetical in pg_ripple_http; pg_regress tests for overlay isolation, rollback correctness, and absence of side-effects on real VP tables | Released ✅ | Very Large | Full details |
| v0.103.0 | Conflict detection — two distinct detection modes with separate code paths and test suites: static analysis (run at rule registration time or on demand via rule_conflicts(ruleset TEXT, mode TEXT DEFAULT 'static')) detects: (a) rules with the same head predicate and overlapping variable patterns that could produce semantically opposite values; (b) pairs of rules where one concludes ?x p ?v and a SHACL sh:not/sh:disjoint constraint forbids ?x p ?v when another rule also holds — fully decidable over the Datalog fragment pg_ripple supports; runtime detection (rule_conflicts(ruleset TEXT, mode TEXT DEFAULT 'runtime')) checks the live triple store for already-derived facts that violate SHACL mutual-exclusion constraints; pg_ripple.block_on_conflict GUC to optionally halt inference when a runtime contradiction is detected; structured conflict report JSONB including mode, rule names, conflicting triple patterns, and the SHACL constraint violated; REST endpoint GET /rule-conflicts/{ruleset}?mode={static|runtime} in pg_ripple_http; separate pg_regress suites for static and runtime paths | ✅ Released | Large | Full details |
| v0.104.0 | Domain rule library infrastructure — rule library format specification: a Turtle file containing Datalog rules, SHACL shapes, and required metadata triples (dcterms:title, dcterms:license, dcterms:description, owl:versionInfo); _pg_ripple.rule_libraries catalog table (name TEXT, version TEXT, installed_at TIMESTAMPTZ, description TEXT, license_iri TEXT, dependencies TEXT[]); dependency resolution scope: simple acyclic single-version dependencies only — no semver range solving; install_rule_library topologically sorts declared dependencies and installs them in order, raising an error on cycles or missing deps; install_rule_library(source TEXT, accept_license BOOLEAN DEFAULT FALSE) (URL or local path) — raises an error when accept_license = FALSE and the library carries a non-trivial licence; URL sources validated against the existing SSRF allowlist before any network request is made; upgrade_rule_library(name TEXT), uninstall_rule_library(name TEXT) with dependency conflict checking; implicit dependency activation; REST endpoint GET /rule-libraries; no domain-specific libraries are bundled in-tree — a documentation chapter (docs/src/cookbook/rule-libraries.md) explains the format, how to author and publish a library, and what licence and disclaimer requirements operators should evaluate (see plans/expert-system.md §5) | ✅ Released | Medium | Full details |
| v0.105.0 | Guided rule authoring & LLM rule extraction — draft_rule_from_nl(description TEXT, candidates INT DEFAULT 3) SQL function translating a natural language rule description to Datalog via the LLM endpoint, returning the top N candidate rules so the reviewer can choose; validate_rule(rule TEXT) checks syntax, detects unused variables, and identifies potential stratification issues; suggest_rules(graph_iri TEXT, examples JSONB) identifies statistical patterns in observed triples and proposes candidate Datalog rules for review — marked experimental: API may change and results require domain expert validation before committing; REST endpoint POST /rules/draft in pg_ripple_http; LLM quality test strategy: proptest generates a random knowledge base with a known ground-truth rule, constructs a natural language description of that rule, calls draft_rule_from_nl, and asserts that at least one of the N candidates is semantically equivalent to the ground-truth rule when evaluated against the same knowledge base | Released ✅ | Large | Full details |
| v0.106.0 | Temporal reasoning — Phase 1: temporal fact store & basic operators — no changes to VP table schemas; dedicated _pg_ripple.temporal_facts (s BIGINT, p BIGINT, o BIGINT, g BIGINT, valid_from TIMESTAMPTZ NOT NULL, valid_to TIMESTAMPTZ) table with indexes: B-tree on (s, p, valid_from, valid_to) for subject-scoped temporal queries; B-tree on (p, valid_from, valid_to) for predicate-scoped temporal scans; partial index (valid_from, valid_to) WHERE valid_to IS NULL for currently-valid facts (open-ended intervals); _pg_ripple.temporal_predicates (predicate_id BIGINT PRIMARY KEY, data_model TEXT CHECK (data_model IN ('snapshot','versioned'))) registry; mark_temporal(predicate_iri TEXT, data_model TEXT DEFAULT 'snapshot') and unmark_temporal(predicate_iri TEXT) SQL functions; pg_ripple.temporal_data_model GUC ('snapshot' | 'versioned') sets the default; query layer routes temporal predicates to temporal_facts, atemporal predicates use VP fast path unchanged; temporal operators in Datalog parser: AFTER(timestamp) — matches facts where valid_from > timestamp; BEFORE(timestamp) — matches facts where valid_from < timestamp; DURING(from, to) — matches facts where tsrange(valid_from, valid_to) && tsrange(from, to); pg_ripple.enable_temporal_operators GUC; pg:temporal_window(?subject, ?predicate, ?start, ?end) SPARQL function; sh:validFor SHACL constraint with XSD duration expression; pg_regress tests for operator correctness, predicate routing, and snapshot vs. versioned isolation | ✅ Released | Large | Full details |
| v0.107.0 | Temporal reasoning — Phase 2: sequential patterns & CDC integration — three new temporal operators with explicit semantics: WITHIN(?s, ?p, ?o, duration) — true if ?s ?p ?o holds at least once within the most recent duration interval relative to query time; SEQUENCE(event1, event2, window) — true if event1 (a (s,p,o) pattern) occurs strictly before event2 within a sliding window of window duration, where "occurs" means valid_from of event1 < valid_from of event2 and both fall within window of each other; CONSECUTIVE(n, predicate, window) — true if there exist N rows for the same subject and given predicate in temporal_facts where each successive valid_from is strictly greater than the previous and all N fall within window duration of the first; all three compile to PostgreSQL window functions (ROW_NUMBER() OVER (PARTITION BY s ORDER BY valid_from), LAG/LEAD) over temporal_facts; integrate with CDC replication log to write valid_from = transaction_timestamp() on assertion and valid_to = transaction_timestamp() on retraction for marked predicates; snapshot model: retraction UPDATEs valid_to on the current open-ended row; re-assertion INSERTs a new row with valid_from = now(), valid_to = NULL; versioned model: every assertion is always a new INSERT, retraction never modifies existing rows but closes the latest open row; temporal pattern detection Datalog rules: "if predicate holds for N consecutive readings within window W, then conclude C"; pg_regress tests for each operator semantics including edge cases at window boundaries | ✅ Released | Very Large | Full details |
| v0.108.0 | Bayesian confidence updates — dynamic belief revision: update_confidence(subject TEXT, predicate TEXT, object TEXT, evidence JSONB) recalculates confidence using prior confidence and incoming evidence reliability score via Bayes' theorem; _pg_ripple.evidence_log (sid BIGINT, event_at TIMESTAMPTZ, source_iri BIGINT, likelihood_ratio FLOAT8, prior_confidence FLOAT8, posterior_confidence FLOAT8) table recording each evidence event; confidence propagation through DRed: when a base-fact confidence changes, downstream inferred-fact confidences update incrementally; pg_ripple.confidence_propagation_max_depth GUC (default: 10) caps cascade depth — facts beyond max depth are recorded in _pg_ripple.confidence_stale (sid BIGINT PRIMARY KEY, marked_at TIMESTAMPTZ) table and reprocessed by a dedicated background worker (extension of the existing merge background worker, activated by a new confidence_reprocessing worker mode) on a schedule controlled by pg_ripple.confidence_reprocessing_interval GUC (default '30 seconds'); pg_ripple.confidence_update_strategy GUC ('bayesian' | 'noisy-or' | 'manual'); conflict-weighted confidence: when two rules derive contradictory conclusions, confidence scores are attenuated according to conflict severity; batch evidence ingestion via bulk_update_confidence(data TEXT, format TEXT); REST endpoint POST /confidence/update in pg_ripple_http; proptest oracle comparing Bayesian updates against a reference implementation | ✅ Released | Large | Full details |
Neuro-Symbolic Record Linkage (v0.109.0 – v0.111.0)
Foundation document: see plans/neuro-symbolic-record-linkage.md for the full competitive analysis, 21-synergy breakdown, deployment patterns, evaluation methodology, and production concerns. The three versions below close the gaps identified in §15 of that document, delivering a complete in-database NS-RL platform on top of the symbolic/neural substrate that shipped in v0.10.0–v0.49.0.
| Version | Theme | Status | Scope | Full details |
|---|---|---|---|---|
| v0.109.0 | NS-RL foundation: string similarity builtins + high-level orchestrator — expose pg_trgm trigram similarity, levenshtein(), soundex(), and metaphone() as SPARQL FILTER functions (pg:trigram_similarity(?a, ?b), pg:levenshtein(?a, ?b), pg:soundex(?s), pg:metaphone(?s, ?maxlen)) and equivalent Datalog built-in predicates, enabling symbolic matching rules such as FILTER(pg:trigram_similarity(?name1, ?name2) > 0.85) without leaving SPARQL; pg_ripple.resolve_entities(source_graph TEXT, target_graph TEXT, options JSONB DEFAULT '{}') → JSONB high-level orchestrator that runs the full five-stage NS-RL pipeline in one call: (1) blocking via infer('owl-rl') on declared owl:InverseFunctionalProperty predicates, (2) embedding-based candidate generation via suggest_sameas(), (3) SHACL shape validation gate, (4) owl:sameAs canonicalization, (5) RDF-star provenance annotation; options include blocking_rules TEXT (Datalog rule set to use for blocking), confidence_threshold FLOAT8 DEFAULT 0.85, dry_run BOOLEAN DEFAULT false; dry_run mode returns the candidate pairs that would be asserted without writing to the triple store; built-in ER blocking templates via pg_ripple.er_blocking_templates() → TABLE(name TEXT, rule TEXT): three reusable Datalog rule families (postal-code block, email exact-match block, name-prefix block) that cover ~80% of common ER patterns and can be loaded with load_rules(pg_ripple.er_blocking_template('email'), 'er') ; security: pg_ripple.sameas_apply_rate_limit GUC (default: 1000 per second) limits how many owl:sameAs triples a single resolve_entities() call may assert — calls exceeding the limit raise PT0460; worked examples: three pg_regress–backed SQL cookbooks covering healthcare patient matching, enterprise customer deduplication, and Datalog-based blocking (NS-RL-01 through NS-RL-03); migration script sql/pg_ripple--0.108.0--0.109.0.sql | Released ✅ | Large | Full details |
| v0.110.0 | NS-RL evaluation harness, continuous monitoring, and rule explainability — pg_ripple.evaluate_resolution(gold_graph TEXT, pipeline_options JSONB DEFAULT '{}') → JSONB: runs the configured NS-RL pipeline against a gold-standard named graph of human-verified matches, returns pairwise precision/recall/F1, blocking pairs-completeness/reduction-ratio/F-PQ, and B³ cluster precision/recall — all three metric axes from the evaluation methodology in §14 of the NS-RL plan; Magellan ER benchmark CI gate: benchmarks/er_magellan.sh loads the Abt-Buy and DBLP-ACM datasets as RDF, runs resolve_entities(), computes F1 against bundled ground truth, fails CI on any run below Splink baseline (Abt-Buy F1 ≥ 0.78, DBLP-ACM F1 ≥ 0.90); streaming resolution latency gate: benchmarks/er_freshness.sh inserts 1K entity records at 100 rec/s and asserts that p95 symbolic-match latency stays below 500 ms; live monitoring stream tables: _pg_ripple.er_unresolved_entities (SPARQL-compiled, 5 s schedule), _pg_ripple.er_cluster_sizes (union-find statistics, 30 s schedule), and _pg_ripple.er_resolution_dashboard (aggregate counts, 10 s schedule) created by pg_ripple.enable_er_monitoring(); pg_ripple.explain_rule(rule_id BIGINT, format TEXT DEFAULT 'text') → TEXT: retrieves the Datalog rule from _pg_ripple.rules, narrates it in plain English via the LLM endpoint, and caches the result in _pg_ripple.rule_explanations (rule_id, language, explanation, generated_at) with a TTL controlled by pg_ripple.rule_explanation_cache_ttl GUC — returning the cached version when fresh; when the LLM endpoint is unavailable, falls back to a template-driven structural description (rule name, head predicate, body predicate list) rather than failing; REST endpoint GET /rules/{id}/explain in pg_ripple_http; anomaly detection: _pg_ripple.sameas_anomaly_log (detected_at, entity1, entity2, cluster_size_before, cluster_size_after, trigger TEXT) records any owl:sameAs assertion that would exceed pg_ripple.sameas_max_cluster_size (PT550) — also persisted on disk so forensic queries survive after the triggering transaction is rolled back; migration script sql/pg_ripple--0.109.0--0.110.0.sql | ✅ Released | Large | Full details |
| v0.111.0 | Privacy-preserving record linkage (PPRL) primitives — Bloom-filter-based encoding for cross-organization entity resolution without exchanging raw PII (Schnell et al. 2009; Christen et al. 2020): pg_ripple.bloom_encode(value TEXT, key TEXT, hash_count INT DEFAULT 30, length INT DEFAULT 1024) → BIT VARYING implementing the standard CLK (Cryptographic Longterm Key) construction using HMAC-SHA-256 (via pgcrypto) with hash_count independent hash functions; input length validated against pg_ripple.bloom_max_input_length GUC (default: 4096 bytes) — longer inputs raise PT0470; pg:dice_similarity(a BIT VARYING, b BIT VARYING) → FLOAT8 SPARQL FILTER function and Datalog built-in computing Dice coefficient on bit strings, enabling cross-org candidate detection without raw-value exchange; pg_ripple.dp_noisy_count(query TEXT, epsilon FLOAT8) → BIGINT and pg_ripple.dp_noisy_histogram(query TEXT, epsilon FLOAT8) → TABLE for Laplace-mechanism differential-privacy noise on aggregate ER queries — epsilon validated to the range (0.0, 10.0]; SSRF-safe PPRL federation pattern: documentation chapter docs/src/cookbook/pprl.md with a worked example showing Bloom-filter triples stored in each org's own named graph, exchanged only as encoded vectors via SPARQL SERVICE federation, with dice-similarity threshold filtering; proptest: bloom_encode round-trip property — dice_similarity(bloom_encode(v, k, h, l), bloom_encode(v, k, h, l)) = 1.0 for any input; negative property — encoding different values with the same key produces similarity < 1.0 with overwhelming probability for reasonable parameters; security note: Bloom-filter CLK encodings with fewer than 30 hash functions or length < 1024 bits may be reversible — these limits are enforced by default and documented; migration script sql/pg_ripple--0.110.0--0.111.0.sql | ✅ Released | Large | Full details |
Assessment 16 Remediation (v0.112.0 – v0.117.0)
Assessment baseline: plans/PLAN_OVERALL_ASSESSMENT_16.md (v0.112.0, 46 findings: 1 Critical / 7 High / 23 Medium / 15 Low, score 4.40/5.0). The six versions below address every finding in the assessment. The critical finding (C16-01) is addressed in v0.112.0; high findings in v0.112.0 and v0.113.0; medium findings in v0.114.0–v0.116.0; low-severity items in v0.117.0.
| Version | Theme | Status | Scope | Full details |
|---|---|---|---|---|
| v0.112.0 | A16 Critical & High remediation + dependency maintenance — bump pg_trickle to 0.57.0 and pg_tide to latest in .versions.toml and Dockerfile; update compatibility matrix in docs/src/operations/compatibility.md with pg_trickle 0.57 row; migration script sql/pg_ripple--0.111.0--0.112.0.sql (comment-only: no schema changes); (C16-01) run just bump-version 0.112.0 0.111.0 bumping pg_ripple_http/src/main.rs:39 COMPATIBLE_EXTENSION_MIN to "0.111.0" in the same commit as this release; add a CI step to release.yml that parses both Cargo.toml versions and the compat constant and fails if the compat floor is more than 1 minor version below the current extension version — closes the sixth-consecutive critical finding; add a one-paragraph "HTTP companion compatibility window" policy to RELEASE.md stating that the companion supports the prior 2 minor extension versions; (H16-01) audit all 26 unsafe blocks that lack // SAFETY: comments — annotate each with a rationale comment or refactor to safe Rust; add clippy::undocumented_unsafe_blocks to workspace-level deny list in Cargo.toml so no new unannotated unsafe can merge; (H16-02) convert the 64 unwrap()/expect() call sites to ? propagation with PT04xx SQLSTATEs; annotate truly-unreachable cases with // PANIC-SAFETY: rather than leaving silent panics; (H16-03) wrap src/entity_resolution.rs::resolve_entities() five-stage pipeline in a BeginInternalSubTransaction / RollbackAndReleaseCurrentSubTransaction guard so any Stage 4/5 panic rolls back Stage 1–3 side-effects in er_unresolved_entities and er_cluster_sizes; replace the SHACL gate stub (blocked_by_shacl: i64 = 0) with an actual validate_entity_pair() call against the registered shapes; (H16-04) document pg_ripple.llm_endpoint as "no-op in the extension; the HTTP companion /rules/{id}/explain endpoint handles LLM enrichment" — update GUC description string, docs/src/reference/gucs.md, and CHANGELOG; (H16-07) add a ## v1.0.0 GA Entry Criteria section to ROADMAP.md enumerating: (a) zero open High findings for two consecutive assessments, (b) zero unannotated unsafe blocks, (c) HTTP companion compatibility window policy written and enforced by CI, (d) all 271+ pg_regress tests passing on PG18, (e) signed SBOM, (f) external security-review report on file | ✅ Released | Large | Full details |
| v0.113.0 | A16 High: bulk-load COPY path (PERF-15-05 third assessment) & performance tuning — (H16-05) add pg_ripple.bulk_load_use_copy GUC (default on); when enabled, bulk_load_ntriples() and bulk_load_turtle() write dictionary-encoded triples via COPY ... FROM STDIN BINARY into a temp staging table, then perform a single INSERT … SELECT join into the VP table; share the copy_into_vp() helper introduced in v0.94.0 with the R2RML and CDC paths; document the 5–10× throughput gain in docs/src/cookbook/bulk-loading.md; (P4) replace the per-candidate embedding loop in src/entity_resolution.rs Stage 2 with a batched array_agg HNSW probe reducing round-trips from O(n) to O(1) per blocking block; (P5) reuse a single HMAC instance across all hash_count positions in src/pprl.rs::bloom_encode via clone() rather than re-keying per iteration; (P6) promote the hard-coded 100-event / 500 ms batch watermark in src/replication.rs to GUCs pg_ripple.replication_batch_size (default 100) and pg_ripple.replication_batch_interval_ms (default 500); (P7) make the tokio mpsc buffer size in pg_ripple_http/src/routing/stream.rs configurable via PG_RIPPLE_HTTP_SSE_BUFFER env (default 256) | ✅ Released | Large | Full details |
| v0.114.0 | A16 Medium: module splits and architecture debt — (H16-06a) decompose src/views/mod.rs (1,599 LOC) into src/views/{mod.rs, construct.rs, materialise.rs, refresh.rs, dependency.rs} — each file < 400 LOC; (H16-06b) decompose src/skos.rs (1,495 LOC flat module) into src/skos/{mod.rs, bundle.rs, inference.rs, export.rs, broader_narrower.rs} — mirrors the src/datalog/ and src/sparql/ layout already in the codebase; (M16-14) split src/datalog_api.rs (1,134 LOC) into src/datalog_api/{mod.rs, parse.rs, validate.rs, explain.rs, conflict.rs}; (M16-15) split src/sparql/wcoj.rs (1,067 LOC) into src/sparql/wcoj/{mod.rs, executor.rs, trie.rs, leapfrog.rs}; (M16-16) split src/sparql/embedding.rs (1,144 LOC) into src/sparql/embedding/{mod.rs, index.rs, hybrid.rs, rag.rs}; (M16-17) split src/shacl/validator.rs (1,181 LOC) by shape kind into src/shacl/validator/{mod.rs, property.rs, node.rs, sparql.rs, severity.rs}; (M16-18) split src/citus/mod.rs (1,366 LOC) into src/citus/{mod.rs, shard_pruning.rs, ddl_hooks.rs, query_rewriting.rs, rebalance.rs}; add a CI gate that warns at 1,200 LOC and fails at 1,500 LOC per file, blocking new monolith growth; add docs/src/architecture.md subsystem dependency graph (SKOS→Datalog, OWL-RL→Datalog, NS-RL→embedding+Datalog+hypothetical, conflict-detection→SHACL+Datalog, hypothetical→storage) | Released ✅ | Large | Full details |
| v0.115.0 | A16 Medium: HTTP API parity and observability — (M16-02) add HTTP route groups for all major post-v0.111.0 subsystems lacking REST coverage: GET/POST /temporal/{mark,point_in_time,facts}, POST /pprl/{bloom_encode,dice_similarity}, POST /dp/{noisy_count,noisy_histogram}, POST /entity-resolution/{resolve,evaluate,monitoring/{enable,disable}}, GET /proof-tree/{subject}/{predicate}/{object}, GET/POST/DELETE /tenants/{name}; all write endpoints protected by check_auth_write; (M16-03) add Prometheus histograms and counters for: NS-RL stage latency (pg_ripple_er_stage_duration_seconds{stage="blocking|embedding|shacl|canonicalization|provenance"}), owl:sameAs assertion rate (pg_ripple_sameas_assertions_total), Bayesian propagation latency (pg_ripple_bayesian_propagation_duration_seconds), temporal fact count (pg_ripple_temporal_facts_total) and query rate (pg_ripple_temporal_queries_total), PPRL encode rate (pg_ripple_pprl_bloom_encodes_total), LLM cache hit/miss (pg_ripple_llm_cache_hits_total, pg_ripple_llm_cache_misses_total), proof-tree generation latency (pg_ripple_proof_tree_duration_seconds), conflict-detection rate (pg_ripple_conflict_detections_total); (M16-04) replace manual replace('\'', "''") escaping in pagerank_handlers.rs with parameterised $1 placeholders for direction and topic; whitelist direction against a #[serde(rename_all = "lowercase")] enum at deserialise time; (M16-09) document the /health (liveness) vs /ready (readiness — requires DB connection + schema version check) split in pg_ripple_http/README.md and annotate the charts/pg_ripple/values.yaml probes accordingly; (M16-10) add a compat-check job to release.yml that asserts COMPATIBLE_EXTENSION_MIN is within one minor version of the current extension; (M16-22) read PG_RIPPLE_HTTP_METRICS_TOKEN env at startup; when set, require Authorization: Bearer <token> on GET /metrics (use constant-time comparison); document in pg_ripple_http/README.md | Released ✅ | Large | Full details |
| v0.116.0 | A16 Medium: correctness, security GUCs and CHANGELOG hygiene — (M16-01) add GUC pg_ripple.er_monitoring_retention_days (default 30) and _pg_ripple.er_monitoring_prune() function that deletes rows older than the retention window from er_unresolved_entities, er_cluster_sizes, and er_resolution_dashboard; wire the prune function into the existing merge background worker tick via a new er_monitoring_vacuum worker mode; (M16-05) bust the _pg_ripple.rule_explanations cache on update_rule() and store_rules() calls by incrementing a rule_version_stamp column and rejecting cached rows with a stale version; (M16-06) schedule a Q3-2026 cargo-audit review issue in the project tracker; bump pkcs1/rsa crate evaluation to audit.toml comments with exact expiry date 2026-12-01 already present; (M16-07) add GUCs pg_ripple.proof_tree_max_depth (default 64) and pg_ripple.proof_tree_max_nodes (default 10 000) to src/prov.rs proof-tree assembly; raise PT0480 on depth overflow and PT0481 on node-count overflow; document in docs/src/security.md; (M16-08) add dedicated pg_regress scripts tests/pg_regress/sql/skos_dcterms.sql, skos_schema_org.sql, skos_foaf.sql exercising each vocabulary bundle's activation path independently; (M16-11) add GUC pg_ripple.bidi_relay_drop_policy ('newest' | 'oldest', default 'newest') controlling the overflow drop policy in src/bidi/relay.rs; (M16-19) replace the unbounded explanation cache in src/rule_explain.rs with a bounded LRU (using the lru crate, max entries via GUC pg_ripple.rule_explanation_cache_max_entries, default 1 000); (M16-20) promote the hard-coded max_depth in src/uncertain_knowledge_api/bayesian.rs::propagate_downstream to GUC pg_ripple.bayesian_propagation_max_depth (default 10); (M16-21) add a one-paragraph lifecycle policy header to audit.toml explaining: open advisory → triage → ignore with expiry → quarterly review → expiry forces re-decision; (M16-23) add ## [0.99.1] and ## [0.99.2] entries to CHANGELOG.md describing the patch changes and their release dates; add docs/gucs.md categorical GUC registry grouping all GUCs by subsystem | ✅ Released | Medium | Full details |
| v0.117.0 | A16 Low-severity polish, tests and supply-chain — (L16-01) reduce #[allow(...)] suppressions by 10% (target ≤ 186); annotate remaining suppressions with // A16-CQ: comments explaining why each is necessary; (L16-02) add tests/crash_recovery/README.md documenting the pg_ctl -m immediate invocation pattern, PGDATA requirements, and expected outcomes; (L16-03) add benchmarks/README.md documenting the interplay between ci_benchmark.sh, pgbench, and the per-subsystem SQL benchmark files; (L16-04) add /// doc-comment block to pg_ripple_logical_apply_worker_main in src/replication.rs enumerating restart behaviour, crash-loop backoff, and the BGWORKER_RESTART_TIME constant; (L16-05) add docs/src/reference/arrow-flight.md section explaining the v1→v2 HMAC ticket migration path and what constitutes a valid v1 ticket during the transition window; (L16-06) read PG_RIPPLE_HTTP_AUTH_REALM env at startup; use it in the Bearer realm= field of the WWW-Authenticate header in check_auth; (L16-07) add sql/INSTALL.md with a numbered sequential migration list from 0.1.0 to the current version; (L16-08) pin pg_ripple image tag in docker-compose.yml to the current minor version and document pinning policy in docker/README.md; (L16-09/10) add clippy_all.txt, clippy_output.txt, cargo_check_output.txt, build_output.txt, regression.diffs, sbom_diff.md to .gitignore; remove committed copies; (L16-11) add **Generated:** YYYY-MM-DD header to sbom_diff.md generation script; (L16-12) sign pg_ripple.cdx.json SBOM with cosign in the release.yml workflow — artifact available on GitHub release page; (L16-13) add bullet "Run just bump-version-dry then just bump-version <new> <floor> and verify the compat constant before tagging" to RELEASE.md; (L16-14) add tests/concurrency/sse_burst_subscriber.sh (100 simultaneous SSE connections, assert no dropped events under burst) and sse_reconnect_during_merge.sh (reconnect during merge worker cycle, assert no gap in event stream); (L16-15) add link to AGENTS.md in CONTRIBUTING.md; (M16-12) add tests/concurrency/entity_resolution_concurrent_resolves.sh and temporal_versioned_write_race.sh; (M16-13) add fuzz targets fuzz/fuzz_targets/temporal_query.rs, pprl_bloom_encode.rs, rule_authoring_validate.rs, skos_bundle.rs | ✅ Released | Medium | Full details |
Platform Maturity & New Features (v0.118.0 – v0.120.0)
These three versions ship the twelve recommended new features from plans/PLAN_OVERALL_ASSESSMENT_16.md §11 plus the SPARQL, temporal, and OWL feature gaps identified in §9, immediately after the A16 remediation cycle to maintain release momentum ahead of v1.0.0.
| Version | Theme | Status | Scope | Full details |
|---|---|---|---|---|
| v0.118.0 | Temporal Allen's relations, compat_check() and privacy budget registry — (Feature 3) pg_ripple.compat_check() → TEXT SQL function that returns a structured JSON object {"extension_version": "...", "http_min_version": "...", "compatible": true/false} — the HTTP companion calls this at startup and refuses to serve if compatible = false; closes C16-01 belt-and-suspenders; (Feature 4) expose the seven Allen's interval relations as SPARQL FILTER functions: pg:before(?a_start, ?a_end, ?b_start, ?b_end), pg:meets, pg:overlaps, pg:during, pg:finishes, pg:starts, pg:equals — all compile to tsrange(?) comparisons over _pg_ripple.temporal_facts and are available as Datalog built-in predicates; document in docs/src/reference/temporal.md; (Feature 2) _pg_ripple.privacy_budget (dataset_id BIGINT, principal TEXT, budget_total FLOAT8, budget_spent FLOAT8) table; dp_noisy_count() and dp_noisy_histogram() deduct epsilon from budget_spent on each call; raise PT0490 when budget_spent + epsilon > budget_total; pg_ripple.privacy_budget_reset_interval GUC (default '1 day') for automatic daily reset; HTTP endpoint GET /dp/budget/{dataset}/{principal}; (AT TIME ZONE gap) extend mark_temporal() and point_in_time() to accept an optional time_zone TEXT parameter coerced to TIMESTAMPTZ via AT TIME ZONE before temporal fact comparison; (Feature 1) pg_ripple.bench_workload(profile TEXT DEFAULT 'bsbm') SQL function running a selected benchmark profile (bsbm / watdiv / pagerank / pprl) against the local triple store and appending results to _pg_ripple.bench_history (run_id, profile, started_at, duration_ms, triples_processed, queries_per_second); expose results via GET /admin/bench-history | Released ✅ | Large | Full details |
| v0.119.0 | OWL propertyChainAxiom, SERVICE circuit breaker, and schema-aware NL→SPARQL — (Feature 5) implement owl:propertyChainAxiom inference in the OWL-RL Datalog rule set: ?x owl:propertyChainAxiom (?p1 ?p2 … ?pn) compiles to an n-step Datalog chain rule with a single derived head; add 10 canonical pg_regress examples covering FOAF knows chain, SKOS broader transitive closure, and PROV-O derivation chains; add the test to the LUBM CI gate; (Feature 6) federated SERVICE circuit breaker: _pg_ripple.federation_circuit_state (endpoint_iri TEXT PRIMARY KEY, state TEXT CHECK (state IN ('closed','open','half_open')), last_failure_at TIMESTAMPTZ, failure_count INT) table; tripped to 'open' after pg_ripple.federation_circuit_failure_threshold consecutive failures (default 5); transitions to 'half_open' after pg_ripple.federation_circuit_recovery_secs seconds (default 60); expose pg_ripple_federation_circuit_state{endpoint} Prometheus gauge; (Property paths over RDF-star gap) compile ?x ?p ?y patterns inside <<...>> quote context to a temporal_facts-style routing path in the SPARQL algebra so property paths traverse reified edges natively; add 5 pg_regress tests; (OWL-RL sh:propertyChainAxiom regress) extend tests/pg_regress/sql/owl_rl_sameas.sql with owl:propertyChainAxiom cycle-safety tests; (Feature 10) extend sparql_from_nl() to include active vocabulary-bundle metadata (SKOS preferred labels, DCTERMS titles, Schema.org type names, FOAF names) in the LLM system prompt grounding — improves domain-specific translation accuracy; pg_ripple.nl_sparql_include_bundles BOOLEAN GUC (default on) | Released ✅ | Large | Full details |
| v0.120.0 | Operational & ecosystem features: explain_pagerank HTTP, diagnostic snapshot, rule-library federation, read-replica routing — (Feature 7) GET /pagerank/explain/{node_iri} HTTP endpoint wrapping the existing explain_pagerank() SQL function and returning a JSON body {"node": "...", "score": 0.xx, "top_contributors": [{...}], "method": "..."} — enables dashboard drill-down without raw SQL; (Feature 8) GET /admin/diagnostic-snapshot HTTP endpoint (requires check_auth_write) that bundles: pg_ripple schema introspection (_pg_ripple.* table row counts), all non-sensitive GUC values, 60-second Prometheus metrics snapshot, extension version, and HTTP companion version into a single JSON archive suitable for support tickets; (Tenant quota HTTP gap) add GET /tenants/{name}/quota and POST /tenants/{name}/quota endpoints wrapping _pg_ripple.quota_* trigger data; (Helm PodDisruptionBudget) add a podDisruptionBudget stanza to charts/pg_ripple/values.yaml with minAvailable: 1 as default; document in charts/pg_ripple/README.md; (Feature 11) pg_ripple.publish_rule_library(name TEXT, endpoint_uri TEXT) publishes a named rule library as an Arrow Flight endpoint (GET /rule-libraries/{name}/stream); pg_ripple.subscribe_rule_library(source_uri TEXT, name TEXT) fetches and installs from a remote Flight endpoint — enables rule-library marketplace federation between pg_ripple instances; (Feature 12) read-replica routing in pg_ripple_http: honour ?replica=ok query parameter on read-only SPARQL SELECT / CONSTRUCT / ASK requests — proxy to the read-replica DSN configured in PG_RIPPLE_HTTP_REPLICA_DSN env when set; log routing decision at DEBUG level; document in pg_ripple_http/README.md; (Feature 9) just generate-helm-values TENANT=<name> recipe that queries _pg_ripple.tenants and emits a per-tenant values-<name>.yaml fragment for use with helm install | ✅ Released | Large | Full details |
Assessment 17 Remediation (v0.121.0 – v0.123.0)
These three versions address all findings from plans/PLAN_OVERALL_ASSESSMENT_17.md: v0.121.0 closes the two High security findings (H17-01 SSRF bypass in
subscribe_rule_library, SEC-M-03 CGNAT/multicast SSRF gaps) and all medium/low bug and security items from §1–§2; v0.122.0 decomposes all eight remaining god modules (H17-02) and adds pg_regress coverage for all untested v0.119.0–v0.120.0 features plus WatDiv correctness gating; v0.123.0 delivers observability completeness, documentation updates, advisory management, and the blog posts needed before v1.0.0.
| Version | Theme | Status | Scope | Full details |
|---|---|---|---|---|
| v0.121.0 | A17 security hardening & bug remediation — (H17-01 / SEC-H-01) replace subscribe_rule_library() string-contains SSRF guard in src/rule_library.rs:800–820 with crate::sparql::federation::policy::resolve_and_check_endpoint(source_uri)?; add pg_regress test for SSRF bypass via IPv6-mapped addresses (::ffff:192.168.x.x), decimal-encoded IPs, and CGNAT endpoints; add fuzz target rule_library_ssrf.rs covering the subscription URL validation path and rule-library stream parser — closes the last uncovered SSRF surface; (SEC-M-03) add CGNAT (100.64.0.0/10, RFC 6598), IPv4 multicast (224.0.0.0/4), this-network (0.0.0.0/8), and IPv4-mapped IPv6 (::ffff:0:0/96) to is_private_ip() in src/federation_registry.rs:64–107 and src/sparql/federation/policy.rs; add pg_regress test asserting 100.64.x.x is rejected by the SSRF blocklist; (SEC-M-04) add // SAFETY-SQL: pred_id is i64, no injection possible comment to three format!-based DDL calls in src/datalog/magic.rs:410,431,456; replace each let _ = Spi::run_with_args(...) with unwrap_or_else(|e| pgrx::warning!("magic: {e}")) to surface DDL errors; (SEC-L-01) enforce Content-Type: text/event-stream; charset=utf-8 explicitly on all SSE responses in pg_ripple_http/src/routing/rule_library_handler.rs; (BUG-M-01) replace six let _ = pgrx::Spi::run(...) calls in src/maintenance_api.rs:35,44,71,77,165,272 with unwrap_or_else(|e| pgrx::error!(...)) for user-triggered maintenance paths and pgrx::warning! for background worker paths so silent degradation is eliminated; (BUG-M-02) replace let _ = in src/kge.rs:234 and src/llm/mod.rs:730 with unwrap_or_else(|e| pgrx::warning!("kge embedding update: {e}")) and unwrap_or_else(|e| pgrx::warning!("llm cache write: {e}")); (BUG-M-03) add // CLIPPY-OK: side-effect only — errors from parse_head_object are expected and non-fatal here to src/datalog/conflict.rs:457; (BUG-L-01) add null-pointer guard if newval.is_null() { return; } before unsafe { CStr::from_ptr(newval) } in src/gucs/registration/observability.rs:21 to eliminate UB on NULL GUC callback; (OBS-L-01) call write_mutation_journal("publish_rule_library", ...) and write_mutation_journal("subscribe_rule_library", ...) at end of both functions in src/rule_library.rs:720,779 following the src/schema/tables.rs pattern so all schema-mutating rule-library operations appear in the audit trail; add migration script sql/pg_ripple--0.120.0--0.121.0.sql (no schema changes; comment-only documenting security fixes) | ✅ Released | Large | Full details |
| v0.122.0 | A17 god-module decomposition & test coverage closure — (H17-02 / PERF-M-02) split src/sparql/expr/functions.rs (1,252 LOC) into eight sub-modules: functions.rs (dispatch table only), string.rs, datetime.rs, numeric.rs, iri.rs, aggregate.rs, geo.rs, temporal.rs — following the src/datalog/builtins.rs split pattern established in v0.79.0; (H17-02 / PERF-L-01) split src/storage/ops/scan.rs (1,171 LOC) into scan.rs (BGP dispatch), htap.rs (delta/main merge union), brin.rs (BRIN-aware scan path), tombstone.rs (tombstone-skip optimisation); (H17-02) split src/bulk_load.rs (1,173 LOC) into bulk_load.rs (entry points and routing), copy_path.rs (UNNEST-array INSERT path), dict_encode.rs (dictionary batch encoding), ntriples.rs, turtle.rs; (PERF-M-03) split pg_ripple_http/src/routing/admin_handlers.rs (1,168 LOC) into routing/admin/mod.rs, health.rs, diagnostic.rs (diagnostic-snapshot bundle), bench.rs (bench-history), maintenance.rs; (PERF-L-02) split src/llm/mod.rs (1,070 LOC) into llm/mod.rs, prompt.rs, cache.rs, schema_aware.rs; additionally split src/datalog/compiler/mod.rs (1,068 LOC) into mod.rs, emit_cte.rs, emit_union.rs, aggregate.rs; split src/gucs/registration/storage.rs (1,058 LOC) into storage.rs, copy_path.rs, htap.rs, dictionary.rs; split src/datalog/parser.rs (1,030 LOC) into parser.rs, lexer.rs, ast.rs; after all splits verify CI module-size lint gate passes with zero files > 1,000 LOC; add dedicated pg_regress tests: tests/pg_regress/sql/v0120_rule_library_federation.sql (publish + subscribe round-trip + inference verification across two named instances), v0120_diagnostic_snapshot.sql (HTTP /admin/diagnostic-snapshot JSON structure and required key presence), v0120_read_replica_routing.sql (?replica=ok routing decision and primary fallback behavior), v0120_compat_check.sql (JSON schema validation: extension_version, http_min_version, compatible keys present and correctly typed); add HTTP integration test for /pagerank/explain/{node_iri} response schema; add tests/pg_regress/sql/v0120_tenant_quota.sql; gate WatDiv correctness — bench_workload('watdiv') output validated against reference answer set (correctness, not just performance score); publish Apache Jena pass rate as CI badge updating docs/src/conformance.md; add migration script sql/pg_ripple--0.121.0--0.122.0.sql (no schema changes) | ✅ Released | Large | Full details |
| v0.123.0 | A17 observability, documentation & advisory management — (OBS-M-01) add pg_ripple_http_replica_pool_size{pool="replica"} gauge and pg_ripple_http_replica_pool_available{pool="replica"} gauge to pg_ripple_http/src/metrics.rs so operators can alert on pool exhaustion; document Prometheus alert rule example in docs/src/operations/read-replicas.md; (OBS-M-02) add pg_ripple_rule_library_stream_duration_seconds latency histogram and pg_ripple_rule_library_subscribe_errors_total counter to metrics.rs and wire into routing/rule_library_handler.rs request instrumentation; (ERG-M-01) document pg_ripple.compat_check() JSON schema (keys: extension_version STRING, http_min_version STRING, compatible BOOL) in docs/src/reference/sql-api.md with a copy-pasteable example return value; (ERG-M-02 / DOC-M-02) create docs/src/guides/rule-library-federation.md with a complete worked example: publish named rule library on instance A over Arrow Flight, subscribe from instance B, verify inference over shared rules, monitor with Prometheus counters from OBS-M-02; (ERG-M-03) extend docs/src/operations/read-replicas.md with ?replica=ok routing semantics, the list of eligible query types (SELECT, CONSTRUCT, ASK only), pool exhaustion fallback to primary, Prometheus alerting recipes using OBS-M-01 gauges; (ERG-L-01) add pg_ripple.bench_workload_result(profile TEXT DEFAULT 'bsbm') → TABLE(run_id BIGINT, profile TEXT, started_at TIMESTAMPTZ, duration_ms BIGINT, queries_per_second FLOAT8, triples_processed BIGINT) convenience SQL wrapper returning the last run from _pg_ripple.bench_history for ad-hoc benchmarking without writing raw SQL; (DOC-M-01) update docs/src/operations/compatibility.md with eight new rows for v0.113.0–v0.120.0 each including the corresponding pg_ripple_http companion version; (DOC-M-03) add function signatures, parameter descriptions, return schemas, and example queries for compat_check(), bench_workload(), publish_rule_library(), subscribe_rule_library(), and all seven Allen's interval relation functions (pg:before, pg:meets, pg:overlaps, pg:during, pg:finishes, pg:starts, pg:equals) to docs/src/reference/sql-api.md; (DOC-L-01) publish blog/owl-property-chain-axiom.md, blog/federation-circuit-breaker.md, blog/allen-interval-relations.md, blog/rule-library-federation.md with worked examples ahead of v1.0.0; (SEC-M-01 / M17-01) schedule Q3-2026 cargo-audit checkpoint in GitHub issue tracker for RUSTSEC-2024-0436 and RUSTSEC-2023-0071 (RSA Marvin-attack, both expiring 2026-12-01); extend audit.toml ignore expiry to 2027-01-01 if no upstream rsa crate patch is available; update ignore comment with "RSA not used for untrusted input — re-evaluated Q3-2026" rationale; (SEC-M-02) read RUSTSEC-2026-0104 advisory for paste proc-macro; verify compile-time-only unsoundness claim; add detailed mitigation rationale to audit.toml:38; if verification shows potential runtime impact, pin paste to last pre-advisory version; add migration script sql/pg_ripple--0.122.0--0.123.0.sql (adds bench_workload_result SQL function DDL) | ✅ Released | Large | Full details |
Pre-GA Feature Expansion (v0.124.0 – v0.128.0)
These three versions ship the three highest-priority new features from plans/PLAN_OVERALL_ASSESSMENT_17.md §11: SPARQL 1.2 property path algebra execution (FEAT-01), temporal graph snapshots with time-travel SPARQL (FEAT-02), and per-endpoint federation credentials with OAuth2 and API-key support (FEAT-03). They run in parallel with the external security audit and 72-hour load test preparation required for v1.0.0 GA.
| Version | Theme | Status | Scope | Full details |
|---|---|---|---|---|
| v0.124.0 | SPARQL 1.2 property path algebra execution (FEAT-01) — spargebra and sparopt already have features = ["sparql-12", "sep-0006"] enabled (Cargo.toml:30–31); this release wires execution: implement bidirectional inverse path (^p), path alternatives at the operator level (p1|p2 beyond simple IRI union), zero-or-one quantifier (p?) via OPTIONAL-based unrolling, negated property sets (!(p1|p2)), and nested inverse sequences in src/sparql/property_path.rs; extend the WITH RECURSIVE … CYCLE template for all new operator combinations ensuring all paths use PostgreSQL 18's CYCLE clause for hash-based cycle detection; emit PT0501 ERROR: SPARQL 1.2 path operator not yet supported for any unimplemented variant so gaps are explicit rather than silently incorrect; add 20 pg_regress test cases covering each new operator and five key combinations (inverse-sequence, alternation with quantifier, negated-with-inverse, nested inverse, cycle detection); add 5 OWL 2 RL property-chain tests exercising n-hop recursive owl:propertyChainAxiom (n=4, n=5) via the new path engine; update docs/src/reference/sparql12-status.md with per-feature execution-status table; update W3C SPARQL 1.2 tracking issue in GitHub; add migration script sql/pg_ripple--0.123.0--0.124.0.sql (no schema changes) | Released ✅ | Large | Full details |
| v0.125.0 | Temporal graph snapshots — point-in-time named graphs (FEAT-02) — pg_ripple.graph_at(graph_iri TEXT, snapshot_time TIMESTAMPTZ) → TEXT SQL function that materialises a named-graph snapshot from _pg_ripple.temporal_facts at the given timestamp and registers it in _pg_ripple.graph_snapshots (snapshot_id BIGINT DEFAULT nextval('snapshot_id_seq') PRIMARY KEY, graph_iri TEXT NOT NULL, snapshot_iri TEXT NOT NULL UNIQUE, captured_at TIMESTAMPTZ NOT NULL, triple_count BIGINT, expires_at TIMESTAMPTZ); time-travel SPARQL via GRAPH <snapshot-iri> { … } pattern routed to snapshot data (read-only VP scan of snapshotted named graph, no write path); pg_ripple.snapshot_retention_days GUC (default 30) for automatic snapshot GC via background merge-worker tick; pg_ripple.graph_diff(graph_iri TEXT, from_ts TIMESTAMPTZ, to_ts TIMESTAMPTZ) → TABLE(s BIGINT, p BIGINT, o BIGINT, change TEXT) returning 'added'/'removed' delta rows enabling audit-compliance workflows and incremental Turtle/N-Quads exports; GET /temporal/graphs/{iri}/snapshot?at=<iso8601> HTTP endpoint returning snapshot content as Turtle; GET /temporal/graphs/{iri}/diff?from=<iso8601>&to=<iso8601> HTTP endpoint returning N-Quads delta; Prometheus gauge pg_ripple_graph_snapshots_total tracking live snapshot count; 15 pg_regress tests covering snapshot creation, time-travel SPARQL query, diff export delta correctness, retention GC row deletion, and concurrent snapshot creation race; blog post blog/temporal-graph-snapshots.md; add migration script sql/pg_ripple--0.124.0--0.125.0.sql adding graph_snapshots table and snapshot_id_seq sequence | Released ✅ | Large | Full details |
| v0.126.0 | Per-endpoint federation credentials — OAuth2 Bearer and API-key (FEAT-03) — _pg_ripple.federation_credentials (endpoint_iri TEXT PRIMARY KEY REFERENCES _pg_ripple.federation_endpoints, auth_type TEXT NOT NULL CHECK (auth_type IN ('bearer','apikey','none')), encrypted_token BYTEA NOT NULL, header_name TEXT NOT NULL DEFAULT 'Authorization', created_at TIMESTAMPTZ DEFAULT now(), rotated_at TIMESTAMPTZ) table with tokens stored via pgcrypto pgp_sym_encrypt using a server-managed symmetric key configured in pg_ripple.federation_credential_key GUC (never logged, never visible via SHOW); pg_ripple.set_federation_credential(endpoint_iri TEXT, auth_type TEXT, token TEXT) → void SQL function (SECURITY DEFINER SET search_path = pg_ripple, _pg_ripple, public) that encrypts the token before insert/update; reqwest-based federation call path in src/sparql/federation/execute.rs decrypts and injects the Authorization: Bearer … or custom API-key header per-request at query time, never caching the plaintext; pg_ripple.rotate_federation_credential(endpoint_iri TEXT, new_token TEXT) → void for zero-downtime token rotation via atomic UPDATE; pg_ripple.federation_credential_audit() → TABLE(endpoint_iri TEXT, auth_type TEXT, token_age_days FLOAT8, last_used_at TIMESTAMPTZ) for operational audit without exposing plaintext tokens; GET /federation/{endpoint}/auth-status HTTP endpoint (write-auth required) returning credential age and auth type; SSRF guard validates endpoint URI before credential lookup so attackers cannot use credential oracle for SSRF probing; 10 pg_regress tests covering credential storage, injection verification via mock SPARQL endpoint, rotation atomicity, audit query (no plaintext), SSRF bypass prevention with credentials present; add migration script sql/pg_ripple--0.125.0--0.126.0.sql adding federation_credentials table | ✅ Released | Large | Full details |
| v0.127.0 | pg_tide relay migration cleanup — move CDC bridge relay gating from has_pg_trickle() to has_pg_tide(); add pg_ripple.relay_available() while retaining deprecated trickle_available() as a compatibility alias; change _pg_ripple.cdc_bridge_trigger_fn() from dynamic table inserts into pg-trickle-style outbox tables to tide.outbox_publish(outbox_name, payload, headers) with stable ripple:{statement_id} dedup metadata; add _pg_ripple.cdc_bridge_triggers.outbox_name while retaining outbox_table as a compatibility alias; update operations guides, BIDI runbooks, examples, and Docker snippets to use pg_tide's pg-tide command, ghcr.io/trickle-labs/pg-tide image, PG_TIDE_POSTGRES_URL, and stable tide.relay_set_outbox_v2 / tide.relay_set_inbox_v2 configuration APIs; bump bundled/tested pg_tide to 0.33.0; add detailed research plan pg-tide-relay-fixes.md; add migration script sql/pg_ripple--0.126.0--0.127.0.sql | ✅ Released | Medium | Full details |
| v0.128.0 | JSON mapping relational writeback — JSON-LD reverse mapping (RDF → Relational) — completes the register_json_mapping round-trip; schema: five new writeback columns on _pg_ripple.json_mappings + _pg_ripple.json_writeback_queue table; SQL API: writeback_json_row(), writeback_json_row_delete(), enable_json_writeback(), disable_json_writeback(), json_writeback_status(); trigger-based async queueing via VP delta triggers; pg_ripple.json_writeback_batch_size GUC (default 100); HTTP: POST /json-mapping/{name}/writeback, GET /json-mapping/{name}/writeback/status; 21 pg_regress tests; blog post + docs | ✅ Released | Large | Full details |
A18 Remediation & Pre-GA Hardening (v0.129.0 – v0.132.0)
These four versions close every Critical, High, Medium, and Low finding from plans/PLAN_OVERALL_ASSESSMENT-18.md (v0.128.0, score 4.15/5.0) and complete the remaining pre-GA gates required for v1.0.0. v0.129.0 is the hotfix release that repairs the silently-broken async JSON writeback feature (C18-01) and the direct writeback row-count and type-cast correctness issues (H18-02). v0.130.0 decomposes the new
json_mapping.rsmonolith (H18-03), hardens the migration-chain test to cover all 131 migration scripts (H18-01), and replaces bare-catalog-update writeback configuration with a validated public API (M18-07). v0.131.0 hardens all security and CI findings: HTTP companion fail-closed auth default (M18-01), temporal RDF serializer escaping (M18-02), conformance CI truth (M18-05), compatibility-matrix refresh (M18-06), raw-secret GUC rejection (M18-08), trust-proxy wiring (L18-01), and version-stamped conformance artifacts (L18-03). v0.132.0 delivers the two performance findings (M18-03, M18-04), publishes the 72-hour soak-test artifact, and formally launches the external security audit engagement, clearing all remaining GA Entry Criteria blockers.
| Version | Theme | Status | Scope | Full details |
|---|---|---|---|---|
| v0.129.0 | A18 Critical & High Correctness Remediation — JSON Writeback Bug Fixes — (C18-01 / JSON-WRITEBACK-ASYNC) fix async JSON writeback in src/json_mapping.rs: replace all WHERE iri = $1 and SELECT iri FROM _pg_ripple.dictionary references with the real column name value (i.e., WHERE value = $1 AND kind = KIND_IRI and SELECT value FROM _pg_ripple.dictionary WHERE id = $1 AND kind = KIND_IRI); treat predicate lookup errors as fatal inside enable_json_writeback_impl() — remove the .unwrap_or(None) pattern and return an error to the caller rather than silently skipping all predicates; only set writeback_enabled = true after at least one VP-table enqueue path has been successfully installed, or return a status struct with installed_trigger_count so callers can detect partial installation; (C18-01 / MIGCHAIN-UPGRADE) add _pg_ripple.json_writeback_enqueue_fn() trigger-function DDL to sql/pg_ripple--0.127.0--0.128.0.sql so upgraded databases (not only fresh installs) have the function required by enable_json_writeback(); (C18-01 / ENQUEUE-COVERAGE) install enqueue triggers on _pg_ripple.vp_rare (both INSERT and DELETE paths), on newly promoted VP delta tables at promotion time inside promote_predicate() and promote_rare_predicates(), and on vp_{id}_tombstones for main-resident delete events — alternatively move enqueueing into the storage write and delete functions (insert_triples_impl, delete_triples_impl, tombstone helpers) so future VP-table creation and rare-predicate promotion cannot bypass the queue; (H18-02 / ROW-COUNTS) replace the Spi::run_with_args() → 1i64 hard-coding in writeback_json_row_impl() and writeback_json_row_delete_impl() with INSERT … RETURNING 1 / DELETE … RETURNING 1 combined with SELECT count(*) FROM cte so actual affected-row count is returned; (H18-02 / TYPED-PARAMS) replace SELECT $1::text, $2::text, … parameter casting with column-type-aware SQL by querying pg_attribute for atttypid::regtype at writeback configuration time and generating typed CAST($n AS <pg_type>) expressions per column — store the derived cast expressions in the mapping catalog so they need not be recomputed per row; (H18-02 / KEY-VALIDATION) validate that all configured writeback_key_columns are present before attempting any INSERT/DELETE; return a descriptive error for missing key columns rather than producing a mis-numbered placeholder; fix the error conflict-policy pre-check placeholder numbering defect where filtering absent key columns causes $2 with only one bound parameter; (H18-02 / TEST-FIX) fix tests/pg_regress/sql/v0128_json_writeback.sql expected output for the skip conflict policy to assert 0 returned rows (not 1); add six new writeback regression tests: (1) integer primary key columns, (2) UUID key column, (3) skip policy returns 0 on genuine conflict, (4) zero-row DELETE returns 0, (5) partial-key validation error, (6) error policy fires when row already exists; (L18-02 / QUEUE-DRAIN) replace let _ = SPI call patterns in drain_json_writeback_queue() status-update paths with error logging via pgrx::log! or pgrx::warning!; leave the queue row in pending state with incremented retry_count if the status UPDATE itself fails; add pg_ripple_json_writeback_drain_errors_total Prometheus counter incremented on any status-update failure; add migration script sql/pg_ripple--0.128.0--0.129.0.sql (adds retry_count SMALLINT NOT NULL DEFAULT 0 column to _pg_ripple.json_writeback_queue and the drain-errors metric comment); add blog post blog/json-writeback-correctness.md explaining the round-trip architecture and enqueue coverage model | Planned | Large | Full details |
| v0.130.0 | A18 Architecture Remediation — Module Decomposition, Migration-Chain Hardening & Public Writeback API — (H18-01 / MIGCHAIN-REWRITE) rewrite tests/test_migration_chain.sh to: (1) generate an ordered list of all migration scripts from sql/pg_ripple--*.sql sorted by semver; (2) apply every script from v0.1.0 through default_version in pg_ripple.control on a fresh PG18 instance; (3) assert the resulting installed version equals pg_ripple.control default_version; (4) run post-upgrade schema assertions for every version milestone, including v0.128 items: writeback_table, writeback_schema, writeback_key_columns, writeback_conflict_policy, writeback_enabled on _pg_ripple.json_mappings; _pg_ripple.json_writeback_queue table; _pg_ripple.json_writeback_enqueue_fn() function existence; (5) replace the self-referential MIGCHAIN-SYNC assertion with a real check that max(version from sql/*.sql) equals pg_ripple.control default_version; (6) run a smoke SELECT pg_ripple.enable_json_writeback('test_mapping'::text) call on the upgraded database and assert no ERROR; add just test-migration-full target to justfile with a 30-minute timeout; (H18-03 / JSON-MAPPING-SPLIT) split src/json_mapping.rs (1,245 LOC) into src/json_mapping/ module: mod.rs (re-exports and #[pg_extern] entry points only, < 150 LOC), registry.rs (mapping CRUD: register_json_mapping, unregister_json_mapping, catalog helpers), ingest.rs (JSON → RDF: ingest_json, ingest_json_bulk, json_to_rdf_impl), export.rs (RDF → JSON: export_json, export_json_bulk, JSON-LD frame helpers), writeback.rs (direct writeback: writeback_json_row_impl, writeback_json_row_delete_impl, column-cast generation), queue.rs (async queue: drain_json_writeback_queue, json_writeback_status_impl, retry helpers), triggers.rs (trigger management: enable_json_writeback_impl, disable_json_writeback_impl, trigger DDL generation for VP delta, tombstone, and rare tables); update src/lib.rs import; verify zero new #[allow(dead_code)] suppressions introduced; (H18-03 / LOC-GATE) add scripts/check_module_size.sh CI script that warns on Rust source files over 800 LOC and hard-fails (exit 1) on any file over 1,200 LOC (excluding target/ and fuzz/); wire as a dedicated step in ci.yml; (M18-07 / CONFIGURE-WRITEBACK-API) add pg_ripple.configure_json_writeback(mapping_name TEXT, target_schema TEXT, target_table TEXT, key_columns TEXT[], conflict_policy TEXT DEFAULT 'error') → void SQL function (SECURITY DEFINER) that validates target table existence via pg_class, all key column names via pg_attribute, and that conflict_policy is one of 'error', 'skip', 'replace'; derives and stores column-cast expressions at configuration time; updates _pg_ripple.json_mappings atomically; add GET /json-mapping/{name}/writeback/config HTTP endpoint; add pg_ripple.writeback_inspect(mapping_name TEXT) → TABLE(target_schema TEXT, target_table TEXT, key_columns TEXT[], conflict_policy TEXT, writeback_enabled BOOL, trigger_count INT, queue_depth BIGINT) for operational introspection; deprecate direct-catalog-update instructions in docs/src/features/json-mapping.md; add 8 pg_regress tests for configure_json_writeback() with integer key, UUID key, JSONB column, replace policy, table-not-found error, column-not-found error, invalid conflict policy, and writeback_inspect() output; also split src/maintenance_api.rs (970 LOC) into maintenance_api/{mod,vacuum,stats,migrate}.rs; split src/datalog/parser.rs (984 LOC) into datalog/parser/{mod,lexer,ast}.rs; split src/datalog/compiler/mod.rs (983 LOC) into datalog/compiler/{mod,emit_cte,emit_union,aggregate}.rs; verify CI LOC gate passes with zero files over 1,200 LOC; add migration script sql/pg_ripple--0.129.0--0.130.0.sql (no schema changes — configure_json_writeback() and writeback_inspect() are compiled Rust pg_extern functions) | Planned | Large | Full details |
| v0.131.0 | A18 Security Hardening, Conformance CI Truth & Documentation Refresh — (M18-01 / HTTP-FAIL-CLOSED) change check_token() in pg_ripple_http/src/common.rs to return HTTP 401 when no auth token is configured unless PG_RIPPLE_HTTP_ALLOW_UNAUTHENTICATED=1 is explicitly set; add a startup tracing::warn! message when running unauthenticated; update Docker Compose examples, Helm chart values, and docs/src/operations/http-companion.md to require an AUTH_TOKEN secret or explicit opt-out; update pg_ripple_http/README.md Auth Token section; (M18-02 / RDF-TERM-ESCAPING) replace format!("<{s}>") / format!("\"{s}\"") manual Turtle/N-Quads emission in pg_ripple_http/src/routing/temporal_handlers.rs with calls to the shared export serializer (rio_api TurtleFormatter / NQuadsFormatter); add a fn format_nquads_term() helper in pg_ripple_http/src/common.rs handling blank nodes (_:… detection), typed literals (^^<…>), language-tagged literals, and special-character escaping per N-Quads spec §4; add HTTP integration tests for snapshot and diff endpoints covering: (1) IRI with angle brackets, (2) literal with inner quotes, (3) literal with newline, (4) blank-node subject, (5) typed literal ^^xsd:integer; (M18-05 / CONFORMANCE-CI) make W3C smoke-suite and Jena blocking-job data-fetch steps fatal (set -e; remove ` | echo "…tests will skip"fallback); for informational jobs retain skip-on-missing-data but rename step to "Download test data (informational — skip on failure)"; setOWL2RL_REQUIRE=1in the OWL 2 RL job environment if pass rate ≥ 95% (validate current pass rate; if below 95% rename the job to "OWL 2 RL (informational)" and remove thecontinue-on-error: falsemislabel); upload aconformance-skip.txtartifact when any informational data fetch fails; **(M18-06 / COMPAT-MATRIX-REFRESH)** add rows for0.124.x–0.128.xtodocs/src/operations/compatibility.mdeach with companion version, minimum extension version, and breaking API notes; correctpg_ripple_http/README.mddefault values to match code (rate limit 100, CORS empty, auth required by default); add link todocs/src/reference/http-api.mdas canonical endpoint reference; **(M18-08 / SECRET-GUC-REJECTION)** convertpg_ripple.llm_api_key_envassign hook to a check hook that rejects values not matching^[A-Z_][A-Z0-9_]*$with error message"llm_api_key_env must be an environment-variable name (e.g. MY_KEY_ENV), not a raw key value"; add pg_ripple.llm_api_key_env_allow_raw = offGUC escape hatch (Superuser only) for pre-0.131 raw-key migration; update GUC description string anddocs/src/reference/gucs.md; **(L18-01 / TRUST-PROXY-WIRING)** implement PG_RIPPLE_HTTP_TRUST_PROXYas a comma-separated CIDR list parsed at startup; add aTrustedProxyLayerTower middleware inpg_ripple_http/src/routing/middleware.rsthat extractsX-Forwarded-Foronly when the direct client IP is within the trusted-proxy list and replacesConnectInfofor downstream rate-limit and audit decisions; validate CIDRs at startup and fail with a clear config error on invalid input; add middleware tests for trusted-proxy rewrite, untrusted-proxy bypass, and invalid-CIDR rejection; **(L18-03 / CONFORMANCE-ARTIFACTS)** update CI conformance jobs to writeversionandgenerated_atfields into all conformance report JSON artifacts; addscripts/publish_conformance.shthat copies run artifacts intoresults/conformance/and updatesresults/conformance/latestsymlink; label existing stale artifacts underw3c_report/, jena_report/, watdiv_report/with"note": "historical artifact from version 0.43.0"; add migration script sql/pg_ripple--0.130.0--0.131.0.sql` (no schema changes) | Planned | |
| v0.132.0 | A18 Performance, Scale & External Audit Preparation — (M18-03 / UNBOUNDED-UNION) bound variable-predicate SPARQL query expansion in src/sparql/sqlgen.rs build_all_predicates_union(): add pg_ripple.max_predicate_union_branches GUC (default 500, min 10, max 10000) that caps the number of VP table UNION branches generated; when the predicate catalog exceeds the cap emit PT0601 ERROR: variable-predicate query would require N UNION branches (max M); use a named predicate or increase pg_ripple.max_predicate_union_branches; investigate and implement a late-binding execution strategy as an alternative: generate a catalog-driven CTE or partitioned-parent-table scan that avoids baking one branch per predicate into the SQL text; if viable, activate for catalogs above 200 predicates and fall back to UNION expansion below; add planner-regression benchmark benchmarks/variable_predicate_scale.sql timing planning + execution for 100, 500, 1000, and 5000-predicate catalogs with a 2× planning-time regression gate from 100 to 1000 predicates; (M18-04 / KEYSET-PAGINATION) replace OFFSET-based pagination in src/storage/ops/scan.rs for_each_encoded_triple_batch() with keyset pagination: track the last statement-id i seen per VP table and vp_rare and query WHERE i > $last_i ORDER BY i LIMIT $batch_size; ensure _pg_ripple.vp_rare has a B-tree index on (i) (add to src/schema/tables.rs and migration); ensure VP delta table creation adds CREATE INDEX … ON vp_{id}_delta (i); add CREATE INDEX … ON vp_{id}_main (i) in the merge worker after each main rebuild; add benchmark benchmarks/export_keyset_vs_offset.sql comparing full-graph export throughput for a 10M-triple fixture with a ≤ 2× regression gate; (H18-04 / SOAK-TEST) add benchmarks/soak_72h.sh script running bench-bsbm-100m + WatDiv continuously for 72 hours, collecting per-epoch memory RSS, merge-latency p50/p95/p99, query throughput (QPS), merge-worker queue depth, Prometheus error-rate counter, and PostgreSQL heap/index bloat; publish results artifact to docs/src/benchmarks/soak-<version>.md; add results/soak/latest.md symlink; gate v0.132.0 release on at least one successful soak run completing with error rate < 0.01% and memory growth < 10% over the 72-hour window; (H18-04 / EXTERNAL-AUDIT) open tracked GitHub issue External Security Audit — v1.0.0 Gate with scope (all src/, pg_ripple_http/src/, SQL migration scripts, Helm chart, Docker images), candidate auditors (TrailOfBits, Cure53, NCC Group), proposed timeline (audit begins ≤ 4 weeks after v0.132.0 tag, report due ≤ 8 weeks after that), and a deliverables checklist (raw findings, remediation confirmation, public summary); add docs/src/security.md section "Planned v1.0.0 External Audit" with scope and engagement status; commit audit/engagement-brief.md documenting scope, methodology requirements (OWASP ASVS level 2, PostgreSQL extension attack surface, SSRF, injection, auth bypass, privilege escalation), and expected deliverables; add migration script sql/pg_ripple--0.131.0--0.132.0.sql (adds B-tree index on _pg_ripple.vp_rare (i) if not already present; records soak-test baseline comment) | Planned | Large | Full details |
v1.0.0 GA Entry Criteria (H16-07, v0.112.0)
pg_ripple enters general availability (v1.0.0) only when all of the following criteria are met. Each criterion has a corresponding CI gate or documented evidence requirement.
| # | Criterion | Gate / Evidence |
|---|---|---|
| (a) | Zero open High-severity findings for two consecutive assessment cycles | Assessment report on file with no unresolved High findings in the current and previous cycle |
| (b) | Zero unannotated unsafe blocks in the codebase | clippy::undocumented_unsafe_blocks = "deny" enforced in Cargo.toml; CI fails on any unannotated unsafe {} |
| (c) | HTTP companion compatibility window policy written and enforced by CI | RELEASE.md "HTTP Companion Compatibility Window Policy" section; release.yml compat-check job fails if floor > 1 minor version behind |
| (d) | All pg_regress tests passing on PG18 | cargo pgrx regress pg18 reports ≥ 271 tests, 0 failures in the release.yml CI run |
| (e) | Signed SBOM | pg_ripple.cdx.json SBOM signed with cosign and attached to the GitHub release artifact |
| (f) | External security-review report on file | TrailOfBits, Cure53, or equivalent third-party audit completed; report linked from the GitHub release and docs/src/security.md |
Progress against these criteria is tracked in each assessment report and confirmed before tagging v1.0.0.
Stable Release & Ecosystem (v1.0.0 – v1.3.0)
| Version | Theme | Status | Scope | Full details |
|---|---|---|---|---|
| v1.0.0 | Production hardening & GA release — satisfies all GA Entry Criteria (H16-07 / ROAD-15-01): (a) zero open High findings for two consecutive assessments (A17 remediation arc closes H17-01 and H17-02; A18 remediation arc v0.129.0–v0.132.0 closes C18-01, H18-01 through H18-04, and all medium/low findings; A19 confirms zero open Highs); (b) zero unannotated unsafe blocks (enforced by clippy::undocumented_unsafe_blocks = "deny" in CI); (c) HTTP companion compatibility window policy written and enforced by release.yml CI gate; (d) all 310+ pg_regress tests passing on PG18 (including v0.121.0–v0.132.0 tests and the six new writeback correctness tests from v0.129.0); (e) pg_ripple.cdx.json SBOM signed with cosign and published to the GitHub release page; (f) external third-party security audit report on file (TrailOfBits/Cure53 or equivalent — engagement opened in v0.132.0, report due before v1.0.0 tag); plus: 72-hour continuous load test (bench-bsbm-100m + WatDiv) with results published to docs/src/benchmarks/ (first soak run completed in v0.132.0); API stability matrix for every #[pg_extern] and GUC auto-generated from cargo doc JSON and committed to docs/src/reference/api-stability.md; documentation final audit and freeze; pg_ripple.bench_workload() baseline results for BSBM/WatDiv/PageRank published | Planned | Medium | Full details |
| v1.1.0 | Post-GA ecosystem & performance — Cypher/GQL read-only transpiler (MATCH … RETURN) + write operations (CREATE/SET/DELETE) enabling graph-database users to query pg_ripple without learning SPARQL; Jupyter SPARQL kernel for interactive notebook exploration; LangChain/LlamaIndex tool packages for LLM orchestration workflows; Kafka CDC sink for event-driven knowledge graph updates; materialized SPARQL views with configurable refresh; dbt adapter; SPARQL endpoint FDW; pgai in-database embedding generation; logical replication for pg_ripple knowledge graphs across instances; (FEAT-04 / PERF-M-01) true COPY FROM STDIN WITH (FORMAT binary) bulk load path via pgrx::copy_in API eliminating parse/plan overhead for 100M+ triple loads (2–3× improvement over UNNEST-array path activated in v0.113.0); (FEAT-07) SPARQL federation mTLS client-certificate support and JWKS-endpoint-backed JWT verification for enterprise SPARQL endpoints requiring mutual TLS — extends the OAuth2/API-key credentials from v0.126.0 | Planned | Large | Full details |
| v1.2.0 | Custom IndexAM & declarative partitioning — (WC-01) native PostgreSQL index access method for (s, p, o, g) quad patterns enabling parallel index-only scans for SPARQL BGPs and 2–5× faster large-graph scans; (WC-03) declarative VP table partitioning: PARTITION BY LIST (g) for large multi-tenant deployments with per-tenant partition pruning; (FEAT-09) Knowledge Graph Diff/Delta Export: pg_ripple.kg_diff(graph_iri, from_version, to_version) → TABLE and GET /graphs/{iri}/delta?from=&to= HTTP endpoint exporting added/removed quads as N-Quads or JSON-LD patches for external consumers (event sourcing, CDC, audit compliance); (FEAT-07 extended) SPARQL endpoint federation JWKS endpoint for RS256/ES256 JWT verification — completes the enterprise auth story from v1.1.0 | Planned | Very Large | Full details |
| v1.3.0 | OWL 2 EL/QL profiles, columnar cold-tier storage, and GNN integration — (FEAT-05) OWL 2 EL profile full rule-set in Datalog: OWL 2 EL is widely used in biomedical ontologies (SNOMED CT, Gene Ontology, NCIt) and has tractable reasoning; implement the normative rule tables for EL (cls-oo, prp-ap, cax-sco, scm-cls, scm-op, scm-dp and EL-specific rules) with a dedicated fixpoint strategy optimised for the EL complexity class; add OWL 2 QL profile targeting large ABox instances with efficient first-order-rewritable queries; extend _pg_ripple.owl_profiles catalog with 'EL' and 'QL' entries; add 50 OWL 2 EL pg_regress tests using SNOMED-subset fixture; (FEAT-06) columnar cold-tier storage via Parquet — VP tables with billions of triples exceeding the HTAP hot-tier threshold can be archived to a Parquet cold tier using pg_parquet FDW or DuckDB FDW; pg_ripple.tier_threshold_triples GUC (default 100M) controls automatic cold-tiering; SPARQL query router transparently unions hot VP table with cold Parquet scan; 5–20× storage compression and 5–20× scan throughput improvement for analytical SPARQL; (FEAT-08) Graph Neural Network integration — in-database GNN training bridge: pg_ripple.gnn_encode(model_name TEXT, graph_iri TEXT) → TABLE(entity BIGINT, embedding FLOAT4[]) exporting entity embeddings from a trained PyG/DGL model via a Python extension bridge; pg_ripple.gnn_predict_links(model_name TEXT, subject BIGINT, k INT) → TABLE(object BIGINT, score FLOAT4) for link prediction queries over trained embeddings, replacing the TransE/RotatE-only approach from v0.57.0 with full GNN model support; requires pg_python or plpython3u bridge | Planned | Very Large | Full details |
How these versions fit together
v0.1.0–v0.5.1 ─── Foundation: VP storage, dictionary encoding, SPARQL engine, RDF-star, bulk loading
│
v0.6–v0.10 ─── Storage architecture: HTAP delta/main split, SHACL validation, Datalog reasoning engine
│
v0.11–v0.20 ─── Query completeness: SPARQL views, Update, Protocol, federation, JSON-LD, W3C conformance baseline
│
v0.21–v0.32 ─── Correctness & Datalog: built-in functions, SHACL completion, magic sets, well-founded semantics, entity resolution
│
v0.33–v0.46 ─── Scale & ecosystem: docs site, parallel stratum eval, WCO joins, full conformance suites (SPARQL 1.1, WatDiv, Jena, LUBM, OWL 2 RL)
│
v0.47–v0.51 ─── Architecture hardening: dead-code wiring, SHACL completeness, streaming results, AI/LLM integration, security hardening
│
v0.52–v0.54 ─── Integration: pg-trickle relay, OpenAPI, logical replication, Helm chart
│
v0.55–v0.56 ─── Quality & security: SSRF allowlist, error-catalog, GeoSPARQL, Arrow/Flight, audit log
│
v0.57–v0.59 ─── Reasoning & sharding: OWL 2 EL/QL, KG embeddings, temporal queries, Citus sharding & shard-pruning, PROV-O
│
v0.60 ─── Production hardening sprint: HTAP atomic swap, Actions SHA pinning, SECURITY DEFINER lint,
│ new fuzz targets, geof:distance, pg_dump round-trip CI test
│
v0.61 ─── Ecosystem depth: per-graph RLS, explain_inference, GDPR erasure, dbt,
│ SHACL-AF execution, OTLP traceparent, richer federation call stats;
│ Citus object shard pruning, direct bulk-load
│
v0.62 ─── Query frontier: Arrow Flight export, WCOJ planner integration, visual graph explorer;
│ Citus property-path push-down, vp_rare archival, tiered dict cache,
│ distributed inference dispatch, live shard rebalance, multi-hop pruning
│
v0.63 ─── SPARQL CONSTRUCT writeback rules: raw-to-canonical pipelines,
│ incremental delta maintenance, Delete-Rederive, pipeline stratification;
│ Citus: SERVICE shard pruning, streaming fan-out, HyperLogLog COUNT(DISTINCT),
│ batched dict encoding, per-worker SID tables, non-blocking VP promotion
│
v0.64 ─── Release truth and safety freeze: feature_status, deep readiness,
│ immutable GitHub Actions, digest-scanned Docker release, documentation truth pass
│
v0.65 ─── CONSTRUCT writeback correctness closure: real delta maintenance,
│ HTAP-aware retraction, exact provenance, full behavior test matrix
│
v0.66 ─── Streaming and distributed reality: true cursors, signed Arrow IPC export,
│ explainable WCOJ mode, integrated Citus pruning/HLL/BRIN/RLS/promotion paths
│
v0.67 ─── Assessment 9 critical remediation: storage mutation journal,
│ VP table RLS coverage, Arrow Flight security/correctness,
│ fail-closed release-truth gates, soak tests, benchmark baselines
│
v0.68 ─── Distributed scalability and streaming completion: CONSTRUCT cursor
│ streaming, Citus HLL translation, SERVICE pruning, nonblocking VP
│ promotion, scheduled fuzz CI for all 12 targets
│
v0.69 ─── Module architecture restructuring: split sparql/mod.rs,
│ pg_ripple_http/main.rs, construct_rules.rs, storage/mod.rs
│
v0.70 ─── Assessment 10 critical remediation: bulk-load mutation journal,
│ per-statement flush, fail-closed evidence gate, SHACL doc truth,
│ README versioning, RLS SQL quoting, SBOM currency
│
v0.71 ─── Arrow Flight streaming validation, Citus multi-node integration,
│ compatibility matrix, HLL accuracy docs, SERVICE benchmark
│
v0.72 ─── Architecture hardening: mutation journal SAVEPOINT safety,
│ plan cache docs, module split, ConstructTemplate proptest,
│ SPARQL Update fuzz, conformance gate promotion, replay protection
│
v0.73 ─── SPARQL 1.2 tracking, live subscription API (SSE/WebSocket),
│ feature taxonomy, CONTRIBUTING.md, Helm chart SHA, R2RML docs
│
v0.74–v0.76 ─── Assessment 11 remediation & production polish: evidence truthfulness,
│ mutation journal wiring, RLS hash widening, toolchain pin, 227 regression tests
│
v0.77–v0.78 ─── Bidirectional integration: source attribution, CAS conflict resolution,
│ linkback rendezvous, outbox policy, per-subscription auth, redaction, audit
│
v0.79 ─── Query engine completeness: true Leapfrog Triejoin executor (WCOJ),
│ full sh:SPARQLRule evaluation; all feature_status() rows → implemented
│
v0.80–v0.83 ─── Assessment 12 critical/high/medium/low remediation: SPARQL Update
│ flush, R2RML/CDC journal, property-path cycle detection, SQL injection
│ fixes, SSRF blocklist, plan cache GUC keys, HTAP determinism,
│ federation correctness, migration chain assertions, SBOM gates
│
v0.84–v0.86 ─── Assessment 13 remediation: HTTP companion readiness, SQL-injection
│ CI lint, plan cache double-parse, GUC module split, migration chain
│ extension, strict_dictionary decode, schema.rs/federation.rs splits,
│ CI module-size lint, hot-path metrics, supply chain upgrades,
│ observability (EXPLAIN post-opt, JSON logs, graceful shutdown),
│ conformance trends CSV, PT-code error registry, SPARQL 1.2 tracking
│
v0.87 ─── Uncertain knowledge: probabilistic Datalog (@weight, noisy-OR,
│ pg:confidence), fuzzy SPARQL (pg:fuzzy_match, pg:confPath threshold),
│ soft SHACL scoring (shacl_score), provenance-weighted confidence
│ from PROV-O source trust via Datalog rules
│
v0.88 ─── Graph analytics: Datalog-native PageRank + four centrality measures;
│ confidence-weighted, topic-sensitive, temporal, SHACL-aware PR;
│ predicate-scoped personalization; edge-weight predicates;
│ reverse/in-degree direction; pg-trickle K-hop incremental refresh
│ (score bounds, selective recompute, IVM metrics); sketch top-K;
│ explain_pagerank(); federation blend; Turtle/JSON-LD/CSV export;
│ pg:centrality() (betweenness/closeness/eigenvector/Katz);
│ PT0401–PT0420
│
v0.89–v0.92 ─── Assessment 14 remediation: delete stale .bak file + CI lint;
│ migration-chain checkpoints v0.84–v0.88 + structural version-sync;
│ COMPATIBLE_EXTENSION_MIN bump + just bump-version automation;
│ confidence noisy-OR proptest; check_auth_write on PageRank handlers;
│ GUC name audit; WCOJ integration for large-graph PageRank;
│ clippy::unwrap_used lint gate; module splits (7 files, pagerank/,
│ uncertain/); PageRank IVM Prometheus gauges; SHACL log retention;
│ PT0301–PT0423 error registry; SPARQL 1.2 + RDF-star matrices;
│ migration-chain CI workflow; ureq/arrow upgrades; Low-severity polish
│
v0.93 ─── pg_tide integration: has_pg_tide() detection, BIDI doc modernisation,
│ pg-trickle-relay.md rewrite to tide.* API, compatibility matrix update
│
v0.94–v0.97 ─── Assessment 15 remediation: COMPATIBLE_EXTENSION_MIN automation +
│ just bump-version; SECURITY DEFINER SET search_path + CI lint;
│ bidi relay bounded channel + bidi_relay_max_inflight GUC;
│ bulk loader COPY FROM STDIN BINARY + shared copy_into_vp() helper;
│ DNS rebinding fix; DROP EXTENSION slot cleanup; SSE error redaction;
│ dictionary VACUUM scheduling; vp_rare GRAPH {} regression tests;
│ confidence NaN/Inf validation; plan cache schema_generation key;
│ SPARQL Update ADD/COPY/MOVE pipeline integration; zero-unreachable lint;
│ tombstone-skip HTAP optimisation; star-pattern self-join collapse;
│ federation connect/query timeout separation; large mod.rs sub-splits;
│ datalog_handlers sub-split; missing_docs CI gate; 4 new Prometheus
│ metrics; concurrent PageRank+writes load test; Arrow Flight EXPLAIN
│ row-estimate; Low-severity polish + supply-chain hygiene
│
v0.98–v0.99 ─── Vocabulary bundles: SKOS (28 rules, 10 SHACL validators, 5 SQL helpers,
│ explain_contradiction(), coverage_map(), named bundle API);
│ DCTERMS (11 rules, 8 validators), Schema.org (15 rules, 6 validators),
│ FOAF (8 rules, 5 validators), cross-bundle activation
│
v0.100.0 ─── Expert system platform — phase 1: proof trees & justification
│ infrastructure: _pg_ripple.derivations table, record_derivations GUC,
│ justify() JSONB proof tree, DRed-aware retraction, dictionary-decoded
│ labels, proptest suite for derivation + retraction correctness
│
├─── Parallel track A (needs v0.100.0):
│
v0.101.0 │── Expert system — phase 2: natural language explanation:
│ explain_inference() LLM-powered narrative from proof tree,
│ structured-text fallback when LLM unavailable,
│ explanation_cache + TTL GUC, POST /explain REST endpoint
│
v0.102.0 │── Expert system — phase 3: what-if reasoning:
│ hypothetical_inference() HTAP-aware layered-overlay DRed sandbox,
│ returns derived/retracted diff JSONB, POST /hypothetical
│
v0.103.0 │── Expert system — phase 4: conflict detection:
│ rule_conflicts() static+runtime contradiction detection,
│ block_on_conflict GUC, GET /rule-conflicts REST endpoint
│
├─── Parallel track B (needs v0.99.x):
│
v0.104.0 │── Expert system — phase 5: domain rule library infrastructure:
│ rule library format spec (Turtle + metadata), _pg_ripple.rule_libraries
│ catalog, install_rule_library(source, accept_license) with SSRF guard,
│ coexists with built-in bundles (v0.98.0); no bundled domain libraries
│
v0.105.0 │── Expert system — phase 6: guided rule authoring & LLM extraction
│ (needs v0.104.0): draft_rule_from_nl() multi-candidate output,
│ validate_rule(), suggest_rules() (experimental), POST /rules/draft
│
├─── Parallel track C (needs v0.99.x, independent of A and B):
│
v0.106.0 │── Expert system — phase 7a: temporal reasoning (basic):
│ _pg_ripple.temporal_facts + temporal_predicates tables; no VP schema
│ changes; mark_temporal() registry; insert_triple_temporal();
│ snapshot vs. versioned data model GUC; AFTER/BEFORE/DURING operators
│
v0.107.0 │── Expert system — phase 7b: temporal reasoning (advanced, needs v0.106.0):
│ WITHIN/SEQUENCE/CONSECUTIVE operators, window-function compilation,
│ CDC integration writing valid_from/valid_to for marked predicates
│
├─── Parallel track D (needs v0.87.0 + v0.100.0):
│
v0.108.0 │── Expert system — phase 8: Bayesian confidence updates:
│ update_confidence() Bayesian revision, evidence_log table, incremental
│ DRed propagation, confidence_propagation_max_depth GUC,
│ conflict-weighted attenuation (conditional on v0.103.0)
│
├─── Parallel track E (needs v0.49.0 + v0.87.0):
│
v0.109.0 │── NS-RL foundation: string similarity builtins (trigram, Levenshtein,
│ Soundex, Metaphone, Jaro-Winkler) + resolve_entities() orchestrator,
│ er_blocking_templates(), sameas_apply_rate_limit GUC
│
v0.110.0 │── NS-RL evaluation (needs v0.109.0): evaluate_resolution() harness,
│ Magellan CI gate, live monitoring stream tables, explain_rule(),
│ sameas_anomaly_log
│
v0.111.0 │── NS-RL PPRL (needs v0.110.0): bloom_encode() CLK, pg:dice_similarity,
│ dp_noisy_count/dp_noisy_histogram with SQL injection guard,
│ PPRL federation cookbook
│
v0.112.0 ─── A16 critical & high remediation + dependency maintenance:
│ pg_trickle 0.57.0 bump + .versions.toml/Dockerfile update;
│ COMPATIBLE_EXTENSION_MIN bump to 0.111.0 + CI release gate (C16-01 CLOSED);
│ 26 unannotated unsafe blocks → SAFETY comments + deny lint (H16-01);
│ 64 unwrap/expect → ? + PT04xx SQLSTATEs (H16-02);
│ resolve_entities() subtransaction + SHACL gate stub fix (H16-03);
│ llm_endpoint documented as no-op in extension (H16-04);
│ v1.0.0 GA Entry Criteria added to ROADMAP (H16-07)
│
v0.113.0 ─── A16 performance high: bulk_load COPY FROM STDIN BINARY path
│ (H16-05 / PERF-15-05 third assessment — 5–10× throughput);
│ batched ANN probe for entity resolution Stage 2 (P4);
│ bloom_encode HMAC instance reuse (P5);
│ replication batch GUCs (P6); SSE buffer env (P7)
│
v0.114.0 ─── A16 medium: module splits — views/mod.rs 1,599 LOC → 5 files;
│ skos.rs 1,495 LOC → 5 files; datalog_api.rs, wcoj.rs,
│ embedding.rs, shacl/validator.rs, citus/mod.rs all split;
│ 1,500 LOC CI fail gate; docs/architecture.md subsystem graph
│
v0.115.0 ─── A16 medium: HTTP API parity — temporal/pprl/dp/entity-resolution/
│ proof-tree/tenant route groups; 8 new Prometheus metrics families
│ (NS-RL, Bayesian, temporal, PPRL, LLM, proof-tree, conflict);
│ pagerank_handlers parameterised SQL; health/ready docs;
│ compat-check CI gate; /metrics optional bearer token
│
v0.116.0 ─── A16 medium: correctness & GUC hygiene — er_monitoring retention GUC
│ + prune(); rule_explain cache version-stamp invalidation;
│ proof_tree_max_depth/max_nodes GUCs; per-bundle SKOS regress tests;
│ bidi_relay_drop_policy GUC; bounded LRU rule explanation cache;
│ bayesian_propagation_max_depth GUC; audit.toml lifecycle header;
│ CHANGELOG hotfix entries for 0.99.1/0.99.2; docs/gucs.md registry
│
v0.117.0 ─── A16 low-severity polish: #[allow] reduction (-10%); crash_recovery
│ README; benchmarks README; replication.rs doc-comment; flight.md
│ v1/v2 docs; check_auth realm env; sql/INSTALL.md; docker-compose
│ version pin; .gitignore cleanup; SBOM cosign signing; RELEASE.md
│ compat checklist; 4 new concurrency tests; 4 new fuzz targets
│
v0.118.0 ─── New features: Allen's 7 interval-relation SPARQL FILTERs (before/
│ meets/overlaps/during/finishes/starts/equals); privacy budget
│ registry with per-call epsilon deduction + PT0490; compat_check()
│ SQL function; AT TIME ZONE for temporal queries;
│ bench_workload() built-in benchmark harness
│
v0.119.0 ─── New features: owl:propertyChainAxiom inference + 10 canonical tests;
│ federated SERVICE circuit breaker + Prometheus gauge;
│ property paths over RDF-star edges; schema-aware NL→SPARQL
│ with active vocabulary-bundle grounding
│
v0.120.0 ─── New features: GET /pagerank/explain/{node} HTTP endpoint;
│ GET /admin/diagnostic-snapshot support bundle; tenant quota HTTP;
│ Helm PodDisruptionBudget; rule-library marketplace federation
│ over Arrow Flight; read-replica routing (?replica=ok);
│ just generate-helm-values recipe
│
v0.121.0 ─── A17 security & bug remediation: subscribe_rule_library SSRF fix
│ via resolve_and_check_endpoint (H17-01/SEC-H-01); CGNAT/multicast/
│ 0.0.0.0 SSRF blocklist additions to is_private_ip (SEC-M-03);
│ magic.rs SQL SAFETY-SQL comment + error surfacing (SEC-M-04);
│ maintenance_api / kge / llm SPI error surfacing (BUG-M-01/02);
│ conflict.rs parse-result CLIPPY-OK comment (BUG-M-03);
│ observability.rs null-pointer UB guard (BUG-L-01);
│ rule-library SSE Content-Type header (SEC-L-01);
│ mutation journal for publish/subscribe (OBS-L-01);
│ fuzz target rule_library_ssrf.rs; CGNAT SSRF pg_regress test
│
v0.122.0 ─── A17 god-module decomposition & test coverage:
│ sparql/expr/functions.rs (1,252 LOC) → 8 sub-modules;
│ bulk_load.rs (1,173 LOC) → 5 sub-modules;
│ storage/ops/scan.rs (1,171 LOC) → 4 sub-modules;
│ admin_handlers.rs (1,168 LOC) → 5 sub-modules;
│ llm/mod.rs + datalog/compiler/mod.rs + gucs/registration/storage.rs
│ + datalog/parser.rs splits; CI module-size gate: 0 files > 1k LOC;
│ pg_regress for rule-library round-trip, diagnostic-snapshot,
│ read-replica ?replica=ok, compat_check() JSON schema,
│ tenant quota; WatDiv correctness gate; Apache Jena pass-rate badge
│
v0.123.0 ─── A17 observability, docs & advisories: replica-pool Prometheus
│ gauges (OBS-M-01); rule-library stream metrics (OBS-M-02);
│ compat_check() JSON schema doc (ERG-M-01); rule-library
│ federation guide (ERG-M-02/DOC-M-02); read-replica ops doc
│ (ERG-M-03); bench_workload_result() TABLE variant (ERG-L-01);
│ compatibility matrix v0.113–v0.120 (DOC-M-01); SQL API ref
│ v0.118–v0.120 functions (DOC-M-03); 4 blog posts (DOC-L-01);
│ RSA RUSTSEC Q3-2026 re-audit scheduled (SEC-M-01/M17-01);
│ paste RUSTSEC-2026-0104 compile-time verification (SEC-M-02)
│
v0.124.0 ─── SPARQL 1.2 property path execution (FEAT-01):
│ ^p inverse, p? quantifier, negated property sets, nested
│ inverse sequences; CYCLE clause on all new paths; PT0501 for
│ unimplemented variants; 20 pg_regress + 5 OWL chain tests;
│ sparql12-status.md updated; parser was already enabled via
│ Cargo.toml features = ["sparql-12", "sep-0006"]
│
v0.125.0 ─── Temporal graph snapshots (FEAT-02): graph_at() point-in-time
│ materialisation; graph_snapshots catalog table; time-travel
│ SPARQL via GRAPH <snapshot-iri>; snapshot_retention_days GUC;
│ graph_diff() added/removed delta TABLE; HTTP /temporal/…/snapshot
│ and /diff endpoints; 15 pg_regress tests; blog post
│
v0.126.0 ─── Per-endpoint federation credentials (FEAT-03): OAuth2 Bearer
│ and API-key per federation endpoint; pgcrypto-encrypted token
│ storage; set_federation_credential() + rotate_federation_credential();
│ federation_credential_audit() TABLE; GET /federation/{ep}/auth-status;
│ SSRF guard before credential lookup; 10 pg_regress tests
│
v0.127.0 ─── pg_tide relay migration cleanup: has_pg_tide() gating; relay_available();
│ cdc_bridge_trigger_fn() → tide.outbox_publish(); outbox_name column;
│ docs/examples/Docker updated to pg-tide 0.33.0 API
│
v0.128.0 ─── JSON mapping relational writeback: RDF → relational reverse mapping;
│ writeback_table/schema/key_columns/conflict_policy on json_mappings;
│ writeback_json_row() + writeback_json_row_delete() SQL functions;
│ enable/disable_json_writeback() with VP trigger plumbing;
│ json_writeback_queue + background drain; HTTP POST /writeback;
│ json_writeback_batch_size GUC; 20 pg_regress tests; blog post
│
v0.129.0 ─── A18 critical/high correctness hotfix: fix async JSON writeback
│ (C18-01: dictionary column bug `iri` → `value`; migration adds
│ json_writeback_enqueue_fn(); enqueue coverage for vp_rare,
│ vp_{id}_tombstones, newly promoted predicates); direct writeback
│ row-count semantics via RETURNING (H18-02); typed-param casting
│ from pg_attribute (H18-02); key-validation and placeholder fix;
│ queue-drain SPI error logging + retry_count (L18-02); 6 new
│ pg_regress tests; blog post
│
v0.130.0 ─── A18 architecture: json_mapping.rs (1,245 LOC) split into 7
│ sub-modules registry/ingest/export/writeback/queue/triggers/mod
│ (H18-03); migration-chain test rewritten to apply all 131 scripts
│ through default_version with real schema assertions (H18-01);
│ configure_json_writeback() + writeback_inspect() public API (M18-07);
│ maintenance_api + datalog parser/compiler decomposed; CI LOC gate
│ (soft 800 / hard 1200 LOC per file)
│
v0.131.0 ─── A18 security & CI: HTTP companion fail-closed auth default —
│ 401 unless PG_RIPPLE_HTTP_ALLOW_UNAUTHENTICATED=1 (M18-01);
│ temporal RDF serializers use rio_api escaping (M18-02); conformance
│ CI fetch-failure fatal for required jobs (M18-05); compat matrix
│ rows 0.124.x–0.128.x + README defaults corrected (M18-06);
│ llm_api_key_env check-hook rejects raw secrets (M18-08);
│ TRUST_PROXY CIDR middleware wired (L18-01); conformance artifacts
│ version-stamped + publish_conformance.sh (L18-03)
│
v0.132.0 ─── A18 performance & GA prep: variable-predicate UNION bounded by
│ max_predicate_union_branches GUC + late-binding CTE investigation
│ (M18-03); batch scan keyset pagination replaces OFFSET (M18-04);
│ 72-hour soak-test script + first published soak artifact (H18-04);
│ external security audit engagement opened (scope, auditor, timeline)
│ + audit/engagement-brief.md (H18-04)
│
v1.0.0 ─── Stable release: all GA Entry Criteria met (zero open Highs ×2
│ assessments; zero unannotated unsafe; compat CI gate; signed SBOM;
│ external security audit); 72-hour continuous load test;
│ API stability matrix; documentation freeze; benchmark results published
│
v1.1 ─── Post-stable ecosystem & performance: Cypher/GQL transpiler
│ (read-only + write ops), Jupyter kernel, LangChain/LlamaIndex
│ tools, Kafka CDC sink, materialized SPARQL views, dbt adapter,
│ SPARQL endpoint FDW, pgai embedding, logical replication;
│ true COPY FROM STDIN binary bulk load path (FEAT-04 / PERF-M-01,
│ 2–3× over UNNEST-array); federation mTLS client-cert support
│ and JWKS-backed JWT verification (FEAT-07)
│
v1.2 ─── Custom IndexAM for triple patterns (WC-01); declarative VP
│ table partitioning by named graph (WC-03); Knowledge Graph
│ Diff/Delta Export API (FEAT-09); JWKS JWT extended
│
v1.3 ─── OWL 2 EL/QL profiles (FEAT-05): full EL rule-set in Datalog,
│ biomedical ontology support (SNOMED, GO, NCIt); columnar
│ cold-tier storage via Parquet / DuckDB FDW (FEAT-06);
│ GNN integration — PyG/DGL bridge for in-database GNN training
│ and link prediction (FEAT-08)
v0.1.0 through v0.5.1 build the complete core storage and query engine.
v0.6.0 through v0.10.0 add the HTAP architecture, SHACL validation, and the full
Datalog reasoning engine. v0.11.0 through v0.20.0 complete the SPARQL query and
update surfaces and establish the W3C conformance baseline. v0.21.0 through v0.32.0
harden correctness and deliver production-grade Datalog optimizations including
magic sets, semi-naive evaluation, well-founded semantics, and entity resolution.
v0.33.0 through v0.46.0 deliver the documentation site, parallel evaluation,
worst-case optimal joins, full conformance suites, and the AI/LLM integration layer.
v0.47.0 through v0.51.0 complete the architecture refactor and shipping hardening
required for a production release. v0.52.0 through v0.54.0 deliver the pg-trickle
relay integration and high-availability story. v0.55.0 through v0.56.0 address all
open security findings from PLAN_OVERALL_ASSESSMENT_6 (SSRF allowlist, error-catalog
drift) and add GeoSPARQL 1.1, federation circuit breaker, and the SPARQL audit log.
v0.57.0 through v0.59.0 extend the reasoning platform to OWL 2 EL/QL, add KG
embeddings, entity alignment, temporal RDF queries, Citus sharding with shard-pruning,
and PROV-O provenance. v0.60.0 through v0.62.0 are the pre-1.0 hardening and
ecosystem sprint: v0.60.0 closes the remaining v1.0.0 blockers identified in
PLAN_OVERALL_ASSESSMENT_7 (HTAP atomic swap, CI supply-chain hardening, fuzz target
gaps, geof:distance); v0.61.0 delivers ecosystem depth (per-graph RLS, inference
explainability, GDPR erasure, dbt adapter, SHACL-AF execution, richer federation call stats);
v0.62.0 delivers the query frontier (Arrow Flight bulk export, WCOJ planner
integration, visual graph explorer) plus six Citus scalability improvements (property-path
push-down, vp_rare cold-entry archival, tiered dictionary cache, distributed inference
dispatch, live shard rebalance, multi-hop pruning carry-forward). v0.63.0 introduces
SPARQL CONSTRUCT writeback rules: any CONSTRUCT query can be registered as a persistent
rule that writes its derived triples directly into a target named graph inside the VP
storage layer and maintains them incrementally — inserts trigger a delta derivation path,
deletes trigger Delete-Rederive retraction — enabling raw-to-canonical model pipelines
where the canonical graph is always consistent with the latest raw data.
v0.63.0 also delivers eight Citus scalability improvements (CITUS-30–37): SERVICE
result shard pruning, streaming coordinator fan-out via SPARQL cursor, approximate
COUNT(DISTINCT) via HyperLogLog, batched dictionary encoding, per-worker statement-ID
local tables, non-blocking VP promotion via shadow-table pattern, per-graph RLS
propagation CI gate, and per-worker BRIN summarise after merge. v0.64.0 through
v0.64.0 through
v0.69.0 convert the findings from PLAN_OVERALL_ASSESSMENT_8 and PLAN_OVERALL_ASSESSMENT_9 into explicit
roadmap work: v0.64.0 adds the truth-in-release guardrails (feature status,
deep readiness, immutable CI actions, release digest scanning, and documentation
correction); v0.65.0 closes CONSTRUCT writeback correctness (delta maintenance,
HTAP-aware retraction, exact provenance, and the full behavior test matrix);
v0.66.0 makes the streaming and distributed claims real or explicitly labels
them as planner hints/stubs/helpers (true SPARQL cursors, signed Arrow IPC export,
explainable WCOJ mode, and integrated Citus pruning/HLL/BRIN/RLS/promotion paths);
v0.67.0 addresses all four Critical findings from Assessment 9 (storage mutation
journal closing all CONSTRUCT writeback bypass paths, VP table RLS coverage,
Arrow Flight ticket security and tombstone-aware export, fail-closed release-truth
scripts) and gathers production evidence (soak tests, audit or threat-model closure,
public benchmarks, upgrade/backup acceptance, and mandatory release evidence
artifacts); v0.68.0 completes the distributed execution and streaming contracts
that were labelled partial or planned in v0.62–v0.66 (true CONSTRUCT streaming,
Citus HLL aggregate translation, Citus SERVICE pruning, nonblocking VP promotion,
and scheduled fuzz CI for all twelve targets); and v0.69.0 restructures the large
source modules along single-responsibility boundaries to make the codebase
maintainable for a v1.0.0 API freeze.
v0.70.0 through v0.73.0 address the findings from PLAN_OVERALL_ASSESSMENT_10:
v0.70.0 closes all four Critical findings (bulk-load mutation journal bypass,
per-triple flush overhead, missing evidence file citations, SHACL-SPARQL docs)
and six High/Medium items (README stale, RLS DDL quoting, SBOM currency, missing
test files, legacy script cleanup, roadmap status correction); v0.71.0 validates
the Arrow Flight streaming contract with an RSS-bounded 10 M-row integration test,
implements the previously-missing Citus RLS propagation integration test, adds an
extension/HTTP companion compatibility matrix, and documents HLL accuracy bounds;
v0.72.0 hardens the mutation journal against PostgreSQL SAVEPOINT/ROLLBACK via
xact callbacks, continues the v0.69.0 module split for the three largest remaining
files, adds a ConstructTemplate proptest suite and a SPARQL Update fuzz target,
promotes W3C conformance and BSBM gates to required CI, adds Arrow Flight replay
protection, and tests the Datalog→CWB interaction chain; v0.73.0 tracks SPARQL 1.2,
delivers a live SPARQL subscription API prototype via SSE, and completes the
ecosystem hardening items (CONTRIBUTING.md, Helm chart SHA pinning, feature status
taxonomy, and R2RML scope documentation).
v0.74.0 through v0.76.0 address Assessment 11 findings: evidence-path truthfulness
(twelve doc stubs, CI gate fix), mutation journal wiring for Datalog/R2RML/CDC,
HTTP companion version alignment, unwrap/panic audit, URL parser fuzz target, RLS
hash widening, Rust toolchain pin, and benchmark baseline refresh. v0.77.0 and
v0.78.0 deliver bidirectional RDF integration: source attribution, conflict
resolution, linkback, outbox/inbox transport, per-subscription side-band auth,
write-time redaction, audit trail, and the non-blocking draft RDF Bidirectional
Integration Profile v1. v0.79.0 closes the last two known query-engine limitations
— true Leapfrog Triejoin executor (WCOJ) and full sh:SPARQLRule evaluation — so
every feature_status() row reads implemented. v0.80.0 through v0.83.0 address
all findings from PLAN_OVERALL_ASSESSMENT_12: critical SQL injection and SSRF
security hardening, property-path cycle detection, SPARQL Update and Datalog
mutation journal wiring, HTAP merge SID determinism, federation URL/truncation
correctness, plan cache GUC completeness, migration chain test assertions through
the latest release, SBOM CI gate, and full regression coverage for all previously
untested pg_extern functions. v0.84.0 through v0.86.0 address all 82 findings
from PLAN_OVERALL_ASSESSMENT_13: v0.84.0 closes the ten "must-fix before v1.0.0"
items — HTTP companion readiness check, SECURITY
DEFINER inline justification, CI format-check gate, migration-chain checkpoints for
v0.80–v0.83, gucs/registration.rs split, /health/ready
deep-check, plan cache double-parse elimination, and justfile automation recipes;
v0.85.0 delivers correctness and code-quality improvements — strict_dictionary in
batch_decode, schema.rs and federation.rs module splits, CI module-size lint
gate, describe_cbd depth GUC, per-predicate merge fence lock, and a VP-promotion
crash-recovery regression test; v0.86.0 closes the remaining 30+ backlog items across
test coverage (proptest vs reference evaluator, fuzz targets, conformance trends CSV),
API polish (PT-code error registry, deprecated GUCs doc, OpenAPI YAML commit), supply
chain (dependency upgrades, SBOM gate, toolchain update), observability (post-optimiser
EXPLAIN field, JSON log mode, graceful shutdown), standards conformance (SPARQL 1.2
tracking, GeoSPARQL inventory, DESCRIBE algorithm doc), and HTTP companion security
(Arrow info leak, docker-compose random password, bulk-loader path canonicalization).
v0.87.0 delivers the uncertain knowledge engine:
probabilistic Datalog with @weight(FLOAT) rule annotations and noisy-OR multi-path
confidence combination; pg:confidence() SPARQL function and
load_triples_with_confidence() bulk-loader; fuzzy SPARQL filters
(pg:fuzzy_match(), pg:token_set_ratio(), and confidence-threshold
pg:confPath() property-path queries); soft SHACL scoring via
pg_ripple.shacl_score(); and provenance-weighted confidence derived
automatically from PROV-O source trust metadata via Datalog rules.
v0.88.0 delivers Datalog-native PageRank and a comprehensive graph analytics layer:
iterative PageRank computed entirely inside pg_ripple's Datalog engine using
aggregation, tabling, and convergence-aware early termination; pg:pagerank() and
pg:pagerank(?node, ?topic) SPARQL functions; personalized PageRank with
predicate-scoped bias; pg_ripple.pagerank_run() with damping factor, iteration cap,
convergence threshold, direction, edge-weight predicate, topic, temporal decay, and
seed parameters; a materialized pagerank_scores view with topic, score_lower,
score_upper, stale, and stale_since columns; a pg-trickle incremental
refresh path (K-hop Z-set local push, score-bounds propagation, selective
recomputation, IVM queue metrics); confidence-weighted edges integrating with
v0.87.0's uncertain knowledge engine; topic-sensitive multi-run scoring;
reverse/in-degree ranking for hub-and-authority decomposition; temporal edge-weight
decay; SHACL constraint-aware ranking via sh:importance, sh:excludeFromRanking,
and shacl_score() quality threshold; sketch-based pg:topN_approx() for
sub-millisecond approximate top-K; score explanation trees via
pg_ripple.explain_pagerank(); graph-partitioned parallel computation;
standard-format export (Turtle/JSON-LD/CSV/N-Triples); federation blend mode with
confidence-gated remote edge filtering (PR-FED-CONF-01); four alternative centrality
measures (betweenness, closeness, eigenvector, Katz) via pg:centrality() and
pg_ripple.centrality_run(); and six cross-version synergies that deepen the
v0.87.0 integration: confidence-attenuated K-hop propagation (PR-TRICKLE-CONF-01),
probabilistic PageRank rules via @weight Datalog annotations (PR-PROB-DATALOG-01),
centrality-guided entity deduplication combining betweenness + pg:fuzzy_match()
(PR-ENTITY-RESOLUTION-01), source-trust-weighted eigenvector centrality seeded by
pg:sourceTrust values (PR-TRUST-EIGEN-01), confidence-gated federation edges
(PR-FED-CONF-01), and temporal authority detection via Katz centrality with
time-aware edge weights (PR-KATZ-TEMPORAL-01); PT0401–PT0423 error catalog.
v0.89.0 through v0.92.0 address all 97 findings from PLAN_OVERALL_ASSESSMENT_14:
v0.89.0 closes the seven High findings and the five Must-fix pre-v1.0.0 backlog items
— deleting the stale src/gucs/registration.rs.bak backup file (72 KB, bypassing the
file-size CI lint), adding migration-chain checkpoints for v0.84–v0.88 with a
structural version-sync assertion that eliminates the recurrence class, bumping
COMPATIBLE_EXTENSION_MIN to v0.88.0 and implementing just bump-version X.Y.Z
to make every future bump atomic, adding a confidence noisy-OR proptest vs a reference
oracle, hardening mutating PageRank HTTP handlers to require write-level auth,
auditing v0.87/v0.88 GUC names before the API freeze, and adding fuzzy_max_input_length
/ pagerank_max_seeds guards plus IRI escaping in export_pagerank(); v0.90.0 sweeps
the Medium correctness, performance, concurrency, and code-quality findings — PageRank
convergence-norm GUC, K-hop drift bound documentation, SPARQL MINUS blank-scope
regression test, export-format enum validation, WCOJ integration for large-graph
PageRank (10M+ edges), clippy::unwrap_used workspace lint gate, streaming VP scans
to avoid 4–8 GB temp materialisation on 100M-edge graphs, seven pre-emptive module
splits before the 1,800-line CI gate is tripped, src/pagerank/ and src/uncertain/
submodule restructuring, probabilistic @weight parser validation, and cyclic
convergence documentation; v0.91.0 addresses the remaining Medium observability, API,
standards, build, and documentation findings — PageRank IVM Prometheus gauges,
SHACL score-log retention GUC, SSE endpoint verification, HTTP routing middleware
extraction, Arrow Flight row-count estimation improvement, explain_pagerank_json()
JSONB variant, PT error code registry completeness (PT0301–PT0423), SPARQL 1.2
tracking page update, RDF-star compliance matrix, dedicated migration-chain.yml
CI workflow, and compatibility matrix rows for v0.87/v0.88; v0.92.0 polishes all
39 Low-severity findings — bounds source comments, damping tuning guide, SERVICE
SILENT TLS test, RLS on pagerank_dirty_edges, fuzzy_match IMMUTABLE annotation,
pagerank_partition auto-tune, SOURCE_DATE_EPOCH reproducible builds, and
WC-01–WC-05 post-v1.0.0 aspirational tracking issues filed for the v1.1.0–v1.2.0 arc.
v0.93.0 integrates the new pg_tide standalone extension (extracted from pg-trickle
v0.46.0), updating all relay/outbox/inbox call sites, rewriting the relay operations
doc to the tide.* API, and extending the compatibility matrix with pg_tide ≥ 0.1.0
rows. v0.94.0 through v0.97.0 address all 41 findings from PLAN_OVERALL_ASSESSMENT_15:
v0.94.0 closes the five High findings — the perennially recurring
COMPATIBLE_EXTENSION_MIN lag is finally eliminated structurally with a just bump-version X.Y.Z recipe that atomically updates all seven version tokens; the lone
SECURITY DEFINER function gains the SET search_path clause required before the
third-party audit; the bidirectional relay gains a bounded channel with an explicit
drop-oldest overflow policy; and the bulk loader migrates to COPY ... FROM STDIN BINARY with a shared copy_into_vp() helper used by all three high-volume insert
paths (bulk loader, R2RML, CDC); v0.95.0 sweeps Medium correctness and security
findings — DNS rebinding in the SSRF federation check, DROP EXTENSION replication-slot
cleanup, SSE error leakage, dictionary VACUUM scheduling, NaN/Inf confidence input
validation, plan cache schema-generation key, and ADD/COPY/MOVE SPARQL Update
pipeline integration; v0.96.0 addresses Medium performance, code-quality, and test-
coverage findings — HTAP tombstone-skip optimisation, star-pattern self-join collapse,
federation timeout separation, five large mod.rs sub-splits, datalog_handlers.rs
sub-split, missing-docs CI gate, four new Prometheus metrics, concurrent PageRank+writes
load test, and Arrow Flight EXPLAIN-based row estimate; v0.97.0 polishes all Low items
— CHANGELOG date fix, missing examples, unsafe/SAFETY 1:1 enforcement, #[allow(...)]
justification convention, gen_random_uuid availability check at _PG_init, serde_cbor
consumer upgrade, RDF-star position matrix, cargo doc CI gate, auto-computed migration
chain checkpoint, sequence exhaustion docs, owl:sameAs cycle regression test, and
conformance-suite pass-rate badges.
v1.0.0 is the stable release: a 72-hour continuous load test, a
third-party security audit, an API stability matrix for every #[pg_extern] and GUC,
documentation final audit and freeze, and public BSBM/WatDiv benchmark results.
v0.121.0 through v0.123.0 address all findings from PLAN_OVERALL_ASSESSMENT_17:
v0.121.0 is the security hardening release — it closes the H17-01 SSRF bypass in
subscribe_rule_library() (replacing string-contains with the battle-tested
resolve_and_check_endpoint() function), hardens the SSRF blocklist with CGNAT,
multicast, and IPv4-mapped IPv6 ranges (SEC-M-03), surfaces silently-swallowed SPI
errors across maintenance_api, kge, llm, and datalog/magic (BUG-M-01/02/03/04),
fixes the null-pointer UB in the GUC observability callback (BUG-L-01), and wires
mutation-journal entries for rule-library publish/subscribe operations (OBS-L-01);
v0.122.0 eliminates all eight remaining god modules over 1,000 lines (H17-02) —
decomposing sparql/expr/functions.rs, bulk_load.rs, storage/ops/scan.rs,
admin_handlers.rs, llm/mod.rs, datalog/compiler/mod.rs, gucs/registration/storage.rs,
and datalog/parser.rs into focused sub-modules — and closes all pg_regress test gaps
for v0.119.0–v0.120.0 features including rule-library federation, diagnostic-snapshot,
read-replica routing, compat_check(), and WatDiv correctness gating; v0.123.0 delivers
observability completeness (replica-pool Prometheus gauges, rule-library stream metrics),
full documentation for all v0.118–v0.120 features (rule-library guide, read-replica
ops doc, SQL API reference, compatibility matrix rows), four new blog posts, and
advisory management (RSA RUSTSEC Q3-2026 re-audit schedule, paste advisory verification).
v0.124.0 through v0.126.0 ship the three highest-priority new features identified in
Assessment 17: v0.124.0 executes SPARQL 1.2 property path algebra extensions (bidirectional
inverse, zero-or-one quantifier, negated property sets) that the parser has been accepting
since features = ["sparql-12", "sep-0006"] was enabled — converting a parser-only
feature into a fully-tested execution path with twenty pg_regress tests; v0.125.0 adds
temporal graph snapshots enabling point-in-time named-graph materialisation, time-travel
SPARQL queries, and a graph_diff() delta export TABLE — delivering immutable audit history
and compliance workflows on top of the Allen's interval relation engine from v0.118.0;
v0.126.0 adds per-endpoint federation credentials with OAuth2 Bearer and API-key support,
stored via pgcrypto encryption, injected at query time into reqwest federation calls —
making pg_ripple the first PostgreSQL-native RDF store with cryptographically protected
federation credential management.
v1.1.0 delivers post-stable improvements: Cypher/GQL transpiler (read-only and write
operations), Jupyter SPARQL kernel, LangChain/LlamaIndex tool packages, Kafka CDC sink,
materialized SPARQL views, a dbt adapter, a SPARQL endpoint FDW, pgai in-database
embedding generation, logical replication for pg_ripple knowledge graphs, the true
COPY FROM STDIN WITH (FORMAT binary) bulk load path (FEAT-04) delivering another 2–3×
throughput improvement over the UNNEST-array path, and SPARQL federation mTLS/JWKS
enterprise authentication (FEAT-07). v1.2.0 delivers the Custom IndexAM for triple
patterns (WC-01) — a native PostgreSQL index access method that understands (s, p, o, g)
quad patterns for parallel index-only BGP scans — declarative VP table partitioning by
named graph (WC-03) for large multi-tenant deployments, and the Knowledge Graph Diff/Delta
Export API (FEAT-09) for event-sourcing and audit-compliance consumers. v1.3.0 delivers
OWL 2 EL profile reasoning with full normative rule tables targeting biomedical ontologies
(SNOMED CT, Gene Ontology, NCIt) and OWL 2 QL profile for large-ABox tractable reasoning
(FEAT-05), Parquet columnar cold-tier storage via DuckDB FDW for 5–20× analytical SPARQL
speedup on billion-triple graphs (FEAT-06), and Graph Neural Network integration
bridging PyG/DGL models into in-database GNN training and link prediction (FEAT-08).
Research
This section documents the design decisions, prior art surveys, and architectural rationale behind pg_ripple.
The research appendix is intended for contributors, curious users, and anyone evaluating pg_ripple against alternative systems.
What's here
- Prior Art — survey of existing RDF stores, graph databases, and PostgreSQL graph extensions
- PostgreSQL Deep-Dive — why PostgreSQL 18 is the right base for a high-performance triple store
- Cypher/LPG Analysis — the relationship between RDF/SPARQL and property graph models
This content mirrors the design documents in the plans/ directory of the repository.
PostgreSQL as a High-Performance Triple Store
This page mirrors
plans/postgresql-triplestore-deep-dive.md— the architectural blueprint written during the design phase of pg_ripple. It explains why each major design decision was made.
Introduction
Building a world-class triple store on top of a relational database requires bridging a fundamental gap: graph patterns (nodes and directed edges) vs. relational tables (rows and columns). The core challenge is to bridge this structural impedance mismatch without introducing unacceptable query latency.
Historically, RDF triple stores and relational databases have occupied competing ecosystems. Native triple stores excel at arbitrary graph traversal and schema flexibility; relational databases dominate in OLTP, strict concurrency control, ACID guarantees, and massively parallel analytical aggregation. PostgreSQL 18 provides the right foundation because it offers both: a mature cost-based optimizer, parallel query execution, robust MVCC, and an extension API powerful enough to add native SPARQL support.
The design decisions documented here are the result of evaluating several alternative approaches and selecting the one with the best combination of query performance, write throughput, and operational simplicity.
Relational Storage Layouts for RDF Data
Three main layouts have been studied for storing triples in a relational database:
Triple Table
The simplest approach: one table with three columns (subject, predicate, object). Every SPARQL query touching N triple patterns requires an N-way self-join on this single table. At enterprise scale (billions of triples), even with comprehensive composite B-tree indexing across all column permutations (SPO, POS, OSP, PSO), complex queries exhaust work_mem and spill to disk — response times degrade exponentially.
Verdict: unsuitable for high-performance workloads.
Property Tables
Groups subjects by ontological type into wide, property-per-column tables. Reduces self-joins for uniform entity types, but RDF data is inherently sparse and heterogeneous. Wide tables saturate with NULL values, waste disk space, and degrade sequential scan performance. Schema evolution requires blocking ALTER TABLE operations, negating the schema-agility advantage of RDF.
Verdict: not viable for open-world, schema-flexible graphs.
Vertical Partitioning (VP) — pg_ripple's choice
A separate two-column table (subject, object) per unique predicate. This is the approach pioneered by Abadi et al. (VLDB 2007) and used by pg_ripple from day one.
Why VP wins:
| Property | Triple Table | Property Table | VP |
|---|---|---|---|
| Self-joins | N-way self-join | Low | Per-predicate, targeted |
| Sparsity | None | Heavy NULLs | None |
| Schema changes | None | Blocking DDL | Dynamic per predicate |
| I/O for bound predicate | Full scan | Full row | Single predicate table |
| Storage overhead | 33% for predicate col | Wide row | Dense, no NULLs |
The predicate column is implicit in the table name — eliminating 33% of storage per triple before any compression. Historical benchmarks (Barton library catalog dataset) show an order-of-magnitude improvement in query resolution time vs. Triple Table, dropping from minutes to seconds.
Each VP table has dual B-tree indices on (s, o) and (o, s), enabling efficient merge joins regardless of traversal direction.
Extended VP (ExtVP) — future direction
ExtVP builds on standard VP by pre-computing semi-joins between frequently co-joined predicates. When query profiling reveals that predicate A is frequently joined with predicate B on the subject column, a materialized view holds the subset of A where the subject also exists in B. The SPARQL→SQL translator rewrites queries to target these materialized views, bypassing billions of CPU cycles.
This is implemented in pg_ripple as a post-1.0 workload-driven optimization (see v0.11.0).
Dictionary Encoding
Raw IRI strings (often 50–200 bytes) stored natively in VP tables would make integer joins infeasible. pg_ripple dictionary-encodes every IRI, blank node, and literal to a BIGINT before writing to any VP table.
Design
- Hash function: XXH3-128 over
kind_le_bytes ‖ term_utf8— the kind byte (IRI/blank-node/literal) is mixed into the hash so the same string with different term types gets distinct IDs. No 64-bit truncation: the full 16-byte hash is stored as aBYTEAcollision-detection key; a PostgreSQLGENERATED ALWAYS AS IDENTITYsequence generates the dense, sequentiali64join key. - Why not truncate to 64 bits? The birthday problem: collisions expected at ~4 billion terms in a 64-bit space. With the full 128-bit hash stored separately, collision detection is a table lookup, not a hash comparison.
- Inline encoding (v0.5.1+): Common typed literals (
xsd:integer,xsd:boolean,xsd:dateTime,xsd:date) are encoded inline with bit 63 set as a type tag. FILTER comparisons on these types require zero dictionary round-trips.
Caching (v0.1.0–v0.5.1 vs. v0.6.0+)
- v0.1.0–v0.5.1: Backend-local
lru::LruCache<u128, i64>— simple, noshared_preload_librariesdependency. - v0.6.0+: Sharded
HashMap<u128, i64>in shared memory via pgrxPgSharedMem, partitioned into 64 shards each with a per-shard lightweight lock. Eliminates global lock contention under concurrent encode/decode workloads. Sized bypg_ripple.dictionary_cache_sizeGUC.
Query decoding
All output IDs are collected from a result set and decoded in a single WHERE id = ANY(...) query — never per-row. This "batch decode" pattern is critical for large result sets.
HTAP Dual-Partition Architecture (v0.6.0)
Modern production systems need heavy reads and writes simultaneously. Without special care, writes block reads and vice versa.
Delta / Main split
All INSERTs and DELETEs target the _delta partition (standard heap + B-tree). The _main partition is read-only, BRIN-indexed, and physically sorted by subject. Between them sits a tombstone table for cross-partition deletes.
Query path: (main EXCEPT tombstones) UNION ALL delta
When the tombstone table is empty (common between merges for insert-heavy workloads), this simplifies to a UNION ALL of main and delta.
Background merge worker
A pgrx BackgroundWorker merges delta into main when delta exceeds pg_ripple.merge_threshold rows:
- Create
vp_{id}_main_new(empty heap) INSERT … SELECT … ORDER BY s FROM (main − tombstones) UNION ALL delta— physically sorted, making BRIN maximally effectiveALTER TABLE … RENAMEatomically replaces the old mainTRUNCATE vp_{id}_delta, vp_{id}_tombstonesANALYZEon merged tables
The ORDER BY s at table-creation time is critical: BRIN requires physically sorted data for its summary ranges to be accurate. Inserting in random order into an existing main table degrades BRIN to near-uselessness.
Bloom filters
Each VP table has a per-shard bloom filter in shared memory. Queries against data known to be only in main can skip the delta scan entirely.
vp_rare exemption
Predicates with fewer than pg_ripple.vp_promotion_threshold triples are stored in a single flat vp_rare table instead of a dedicated VP table + delta/main split. Rare predicates see few writes by definition — delta/main overhead would exceed the benefit. Standard PostgreSQL row-level locking handles concurrent access safely.
Why not partition on write?
PostgreSQL's declarative table partitioning is designed for stable partition keys (dates, ranges). RDF writes are effectively random across predicate space — using declarative partitioning here would require custom routing logic with no benefit over the explicit delta/main design.
SPARQL-to-SQL Translation
Naive translation from SPARQL algebra to SQL produces verbose nested subqueries that confuse the PostgreSQL planner. pg_ripple implements several structural rewrites:
Self-join elimination
Star patterns (same subject, N predicates) collapse into a single scan of the subject across N VP tables joined by subject ID equality. Eliminates redundant round-trips through SPI.
Optional-to-inner downgrade
OPTIONAL → LEFT JOIN in SQL. When the optimizer can prove that the required property is always present (e.g., via a loaded SHACL shape with sh:minCount 1), it downgrades to INNER JOIN. Applied conservatively — only when semantics-preserving for the query domain.
Filter pushdown
SPARQL FILTER clauses on bound IRIs are resolved to integer IDs before generating SQL. This ensures B-tree index usage. Typed numeric/date literals use the inline-encoded i64 range (v0.5.1+) to emit BETWEEN $lo AND $hi range scans with no decode step.
Property path compilation
SPARQL +, *, ? paths compile to WITH RECURSIVE CTEs with PG18's CYCLE clause for hash-based cycle detection:
WITH RECURSIVE path(s, o, depth) AS (
SELECT s, o, 1 FROM _pg_ripple.vp_{id} WHERE s = $1
UNION ALL
SELECT p.s, vp.o, p.depth + 1
FROM path p
JOIN _pg_ripple.vp_{id} vp ON p.o = vp.s
WHERE p.depth < pg_ripple.max_path_depth
)
CYCLE o SET is_cycle USING cycle_path
SELECT DISTINCT s, o FROM path WHERE NOT is_cycle;
PG18's CYCLE clause uses hash-based cycle detection ($O(1)$ membership checks vs. $O(n)$ array scans in the older array-based approach).
SHACL Validation
SHACL is a W3C standard for defining data quality rules over RDF graphs. pg_ripple implements SHACL validation with spec-first semantics: loaded shapes are parsed into a shape IR that preserves W3C SHACL semantics. PostgreSQL constraints, triggers, or stream tables may be used as internal accelerators when they are proven semantics-preserving for the specific shape pattern — they are never the normative definition of constraint behavior.
Sync vs. async validation
pg_ripple.shacl_mode = 'sync'(v0.7.0): validation runs inline on INSERT; bad triples are rejected immediately. Suitable for low-volume transactional writes.pg_ripple.shacl_mode = 'async': a lightweight trigger queues the inserted triple IDs into_pg_ripple.validation_queue; a background worker validates against loaded shapes. Invalid triples are moved to_pg_ripple.dead_letter_queuewith a JSONB violation report.
Query optimization via SHACL
The SPARQL→SQL translator reads loaded SHACL shapes at plan time:
sh:maxCount 1may enable cardinality-sensitive optimizations when the query is restricted to the same focus-node population as the validated shape.sh:minCount 1may downgrade OPTIONAL → INNER JOIN when semantically safe.
Performance Targets
| Metric | Target | Strategy |
|---|---|---|
| Bulk insert | >100K triples/sec | Batch COPY, deferred indexing, HTAP delta |
| Transactional insert | >10K triples/sec | Delta partition, async SHACL |
| Simple BGP query | <5 ms (10M triples) | Integer joins, B-tree on VP tables |
| Star query (5 patterns) | <20 ms (10M triples) | Self-join elimination, PG parallel hash joins |
| Property path (depth 10) | <100 ms (10M triples) | Recursive CTE + CYCLE clause |
| Dictionary encode (cache hit) | <1 μs | Sharded LRU in shared memory |
| Dictionary encode (miss) | <50 μs | B-tree index on hash |
| Batch decode (1,000 IDs) | <1 ms | Single WHERE id = ANY(...) query |
Calibration reference: QLever (C++, Apache-2.0) on DBLP (390M triples) loads at 1.7M triples/s and answers benchmark queries in 0.7s average. QLever's flat pre-sorted permutation files make every SPARQL join a merge join with zero random I/O. pg_ripple's B-tree/heap design pays ~5× overhead on bulk sequential scans in exchange for transactional concurrent writes, MVCC, and the full PostgreSQL ecosystem.
Why PostgreSQL 18?
PostgreSQL 18 brings several features that materially improve pg_ripple's performance:
- Async I/O (
io_method = io_uringon Linux): reduces sequential scan latency for the BRIN-indexed main partition CYCLEclause inWITH RECURSIVE: hash-based cycle detection in property path queries- Improved parallel query: better parallel hash join plans for star-pattern BGPs
- Skip scan on B-tree: enables efficient lookups on composite
(s, o)indices when onlyois bound (unbound subject queries)
Further Reading
- plans/implementation_plan.md — detailed API signatures, algorithms, and crate choices
- plans/postgresql-triplestore-deep-dive.md — extended research survey with academic references
- Abadi et al. 2007 — original VP paper
- qEndpoint — HTAP inspiration for the delta/main split
- Berlin SPARQL Benchmark (BSBM) — query mix used in pg_ripple benchmarks
Contributing
Thank you for your interest in contributing to pg_ripple. This guide covers environment setup, testing, code conventions, and the pull request workflow.
pg_ripple is open source and welcomes contributions of all kinds — bug reports, documentation fixes, test cases, and feature implementations. If you are unsure whether an idea fits, open a GitHub issue to discuss it before writing code.
Development Environment
Prerequisites
| Tool | Version | Purpose |
|---|---|---|
| Rust | Edition 2024, stable toolchain | Language |
| PostgreSQL | 18.x | Target database |
| pgrx | 0.18 | PostgreSQL extension framework |
| cargo-pgrx | 0.18 | Build and test tooling |
| git | 2.x+ | Version control |
Setup
# 1. Clone the repository
git clone https://github.com/your-org/pg_ripple.git
cd pg_ripple
# 2. Install cargo-pgrx if not already installed
cargo install cargo-pgrx --version 0.18 --locked
# 3. Initialize pgrx with PostgreSQL 18
cargo pgrx init --pg18 $(which pg_config)
# 4. Verify the build
cargo build
On macOS, install PostgreSQL 18 via Homebrew: brew install postgresql@18. Ensure pg_config is on your PATH.
Running Tests
pg_ripple uses three levels of testing:
Unit and integration tests (pgrx)
Runs Rust tests inside a temporary PostgreSQL instance:
cargo pgrx test pg18
This starts a temporary PG18 cluster, installs the extension, runs all #[pg_test] functions, and tears down the cluster.
Regression tests (pg_regress)
Runs SQL-based regression tests that compare expected output:
cargo pgrx regress pg18
The test SQL files live in sql/ and expected output in expected/. If you add a new SQL function, add a regression test for it.
Migration chain test
Verifies that all migration scripts (sql/pg_ripple--X.Y.Z--X.Y.Z+1.sql) can be applied in sequence:
# Requires pgrx PG18 running
cargo pgrx start pg18
bash tests/test_migration_chain.sh
Running a subset of tests
# Run a single test by name
cargo pgrx test pg18 -- test_name_pattern
# Run tests with output visible
cargo pgrx test pg18 -- --nocapture
Code Conventions
These conventions are enforced by CI and code review.
Safe Rust only
All code must be safe Rust. unsafe is permitted only at required FFI boundaries (pgrx macros, shared memory access) and must include a // SAFETY: comment explaining why it is correct.
SQL function exposure
Expose SQL functions via the #[pg_extern] attribute. Never write raw PG_FUNCTION_INFO_V1 C macros.
#![allow(unused)] fn main() { #[pg_extern] fn my_function(input: &str) -> String { // implementation } }
SPI for all internal SQL
Use pgrx::SpiClient for all SQL executed inside extension code. Never use raw libpq or string-based query execution.
#![allow(unused)] fn main() { Spi::connect(|client| { client.select("SELECT count(*) FROM _pg_ripple.dictionary", None, None)?; Ok(()) })?; }
Integer joins everywhere
SPARQL-to-SQL translation must encode all bound terms to i64 before generating SQL. VP table queries must never contain string comparisons — this is a bug.
No dynamic SQL string concatenation for table names
Always look up the VP table OID in _pg_ripple.predicates and use format!-style quoting with proper escaping. Never interpolate user input into table names.
Error messages
Follow PostgreSQL style: lowercase first word, no trailing period.
#![allow(unused)] fn main() { // Good return Err(pg_ripple_error!("dictionary encode failed: hash collision detected")); // Bad return Err(pg_ripple_error!("Dictionary encode failed: hash collision detected.")); }
Batch dictionary operations
Use ON CONFLICT DO NOTHING … RETURNING for all batch inserts into the dictionary. Never use a SELECT-then-INSERT pattern.
Project Structure
src/
├── lib.rs # Entry points, _PG_init, GUC parameters
├── dictionary/ # IRI/blank-node/literal → i64 encoder
├── storage/ # VP tables, HTAP delta/main, merge worker
├── sparql/ # SPARQL → algebra → SQL → SPI
├── datalog/ # Datalog parser, stratifier, SQL compiler
├── shacl/ # SHACL shapes → DDL constraints + validation
├── export/ # Turtle / N-Triples / JSON-LD serialization
├── stats/ # Monitoring, pg_stat_statements integration
└── admin/ # Vacuum, reindex, prefix registry
sql/ # Migration scripts and regression test SQL
tests/ # Shell-based integration tests
docs/ # mdBook documentation site
Pull Request Workflow
Branch policy
- Never create a new branch from
mainunless the current branch ismain. - Use descriptive branch names:
feat/sparql-lateral,fix/dictionary-collision,docs/glossary.
Before opening a PR
- Run all tests and ensure they pass:
cargo pgrx test pg18
cargo pgrx regress pg18
- Run clippy with no warnings:
cargo clippy --all-targets -- -D warnings
- Format code:
cargo fmt --check
-
Update documentation if you changed any SQL function signatures or added new functions.
-
Create or update migration scripts if the release version changed (see below).
Commit messages
- Use present tense: "add lateral join support" not "added lateral join support"
- Group discrete changes into separate commits
- Reference issue numbers when applicable: "fix dictionary collision (#42)"
Migration scripts
Every release requires a migration script (sql/pg_ripple--X.Y.Z--X.Y.Z+1.sql), even if it only contains comments. See the Release Process for the full checklist.
Documentation Contributions
The documentation site uses mdBook with the mdbook-admonish plugin for callout boxes.
Building the docs locally
# Install mdbook and plugins
cargo install mdbook mdbook-admonish
# Build and serve
cd docs
mdbook serve --open
Callout syntax
Use fenced code blocks with admonish for callout boxes:
```admonish tip title="Performance"
Use `load_ntriples_file()` for large datasets — it is 10× faster than string loading.
```
```admonish warning
This operation cannot be undone.
```
```admonish note
Available since v0.16.0.
```
Adding a new page
- Create the Markdown file in the appropriate
docs/src/subdirectory. - Add the page to
docs/src/SUMMARY.md. - Run
mdbook buildto verify it compiles.
Property-Based Testing (v0.46.0)
pg_ripple uses proptest for randomised property-based tests that assert algebraic invariants. These tests run entirely in pure Rust — no database connection required.
Running proptest suites
# Run all property-based tests
cargo test --test proptest_suite
# Run with more cases (default: 256)
PROPTEST_CASES=10000 cargo test --test proptest_suite
# Run a specific suite
cargo test --test proptest_suite sparql_roundtrip
cargo test --test proptest_suite dictionary
cargo test --test proptest_suite jsonld_framing
Adding a new property test
-
Add your test to the appropriate file in
tests/proptest/:- SPARQL translator invariants →
sparql_roundtrip.rs - Dictionary encoder invariants →
dictionary.rs - JSON-LD framing invariants →
jsonld_framing.rs - New domain → create
tests/proptest/<domain>.rsand addmod <domain>;totests/proptest_suite.rs
- SPARQL translator invariants →
-
Use
proptest!macros for property tests; regular#[test]for deterministic fixtures. -
Run the suite with
PROPTEST_CASES=10000to verify 10,000 cases pass.
Debugging a proptest failure
When a test fails, proptest prints the minimal failing input. Reproduce it:
#![allow(unused)] fn main() { // Add to the failing test to fix the seed: ProptestConfig::with_cases(1).with_proptest_rng(seed) }
Fuzz Testing (v0.46.0)
pg_ripple uses cargo-fuzz to test the federation result decoder against arbitrary byte sequences.
Running the fuzz target
# Install cargo-fuzz
cargo install cargo-fuzz
# Run for 10 minutes
cargo fuzz run federation_result -- -max_total_time=600
# Run indefinitely
cargo fuzz run federation_result
# Minimise a crashing corpus entry
cargo fuzz tmin federation_result artifacts/federation_result/crash-<hash>
Adding a new fuzz target
- Create
fuzz/fuzz_targets/<target_name>.rswith the fuzz target function. - Add a
[[bin]]entry tofuzz/Cargo.toml. - Add the target to the
fuzz-<target_name>CI job in.github/workflows/ci.yml.
Fuzz target contract
Every fuzz target must:
- Use
#![no_main]andlibfuzzer_sys::fuzz_target! - Never panic regardless of input (panics are treated as fuzz failures)
- Return
Err(...)for invalid input, never crash
Reporting Issues
When filing a bug report, please include:
- pg_ripple version:
SELECT pg_ripple.canary();and the output of\dx pg_ripple - PostgreSQL version:
SELECT version(); - Minimal reproducer: the smallest SQL script that triggers the issue
- Full error output: use
\errverbosein psql for detailed error context - Platform: OS and architecture
If you discover a security vulnerability, please report it privately via GitHub Security Advisories rather than opening a public issue.
pg_ripple — Release Procedure
This document describes how to release a new version of pg_ripple.
Versions follow the milestones in ROADMAP.md. Each release corresponds to a completed roadmap version (e.g. v0.1.0, v0.2.0).
Pre-Release Checklist
Complete every item before starting the release process.
-
All roadmap deliverables for the version are implemented
- Cross-check against the version's deliverables list in ROADMAP.md
- All deliverable checkboxes are ticked (
- [x]) in ROADMAP.md — if any are unticked, tick them now before proceeding
-
All exit criteria in ROADMAP.md are satisfied
- Verify each criterion explicitly — do not rely on partial evidence
-
Tests pass
cargo fmt --all -- --check(formatting)cargo clippy --features pg18 -- -D warnings(lint, zero warnings)cargo pgrx test pg18(unit + integration tests)cargo pgrx regress pg18 --postgresql-conf "allow_system_table_mods=on"(pg_regress suite, includesschema_statemigration schema check)bash tests/test_migration_chain.sh— verify all migration SQL scripts apply cleanly in sequence (requirescargo pgrx start pg18first; also run viajust test-migration)
-
Cargo.tomlversion field matches the release version- e.g.
version = "0.2.0"for a v0.2.0 release
- e.g.
-
pg_ripple.controldefault_versionmatches the release version -
Dependency versions are up to date
- Update
.versions.tomlifpg_trickleorpg_tideversions changed - Update the corresponding Dockerfile
ARG PG_TRICKLE_VERSION/ARG PG_TIDE_VERSIONto match src/lib.rsconstants are injected automatically at compile time from.versions.toml— no manual edits needed (DEP-VER-BUILD-01)- Run
bash scripts/check_dep_versions.shto verify Dockerfile alignment
- Update
-
Extension migration script created — CRITICAL
- File:
sql/pg_ripple--X.(Y-1).Z--X.Y.Z.sqlwhere the previous version is X.(Y-1).Z - If there are schema changes (ALTER TABLE, CREATE INDEX, etc.), include them in the script
- If there are no schema changes (new Rust functions), write only a comment header explaining what's new
- See Extension Versioning & Migration Scripts in AGENTS.md for the checklist and examples
- Without this file, users on earlier versions cannot upgrade via
ALTER EXTENSION ... UPDATE— they must dump/restore
- File:
-
CHANGELOG.md is up to date
- The
[Unreleased]section has been moved under the new version heading - Written in plain, accessible language (see Changelog Style below)
- All significant user-visible changes are included
- Date is set to today's date
- The
-
README.md is updated
- The "What works today (v0.X.Y)" section heading and body reflect the current release
- Describes only functionality implemented and merged in this version
- Remove planned features that haven't shipped yet
- The Roadmap table in the
## Roadmapsection is updated:- The newly released version row is bolded and its status cell is changed to
✅ Done - Any in-progress sentence above the table (e.g. "X is coming in a later milestone") no longer mentions capabilities that have now shipped
- The newly released version row is bolded and its status cell is changed to
- The "Where we're headed" section no longer lists the just-released version as upcoming — move it to a "What works today" bullet or remove it
-
No uncommitted changes —
git statusis clean -
Main branch is up to date —
git pull origin main
Release Checklist
Perform these steps in order.
-
Final test run
cargo fmt --all -- --check cargo clippy --features pg18 -- -D warnings cargo pgrx test pg18 cargo pgrx regress pg18 --postgresql-conf "allow_system_table_mods=on" bash tests/test_migration_chain.shAll five must pass with zero warnings and zero failures.
-
Documentation validation
Run the full docs check before tagging:
just docs-check mdbook build docsVerify:
just docs-check-linksexits 0 (no broken local links).just docs-check-summaryexits 0 (no orphan pages).just docs-check-http-routeslists no undocumented routes.just docs-check-guc-driftlists no missing GUCs with significant defaults.- New HTTP routes added in this version appear in
docs/src/reference/http-api.md. - New GUCs added in this version appear in
docs/src/reference/guc-reference.mdanddocs/gucs.md. - New public SQL functions appear in
docs/src/reference/sql-api.mdor are explicitly marked as internal/admin. - The migration script has upgrade docs if it changes user-visible schema.
CHANGELOG.mdlinks to any new user-facing docs pages.
-
Tag the release
Before tagging, verify the version bump and compatibility constant are correct:
# L16-13 (v0.117.0): verify version bump before tagging. just bump-version-dry # preview what would change just bump-version <new> <floor> # apply the bump (updates Cargo.toml, pg_ripple.control, # COMPATIBLE_EXTENSION_MIN in pg_ripple_http/src/main.rs) # Then inspect the changes: git diff --statUse an annotated tag with the version number:
git tag -a v0.X.Y -m "Release v0.X.Y — <version name from ROADMAP>" git push origin v0.X.YThis step is done manually. The release skill deliberately does not create tags.
-
The GitHub release is created automatically
Pushing the tag triggers
.github/workflows/release.yml, which:- Runs the full test + pg_regress suite on the tagged commit
- Extracts the changelog entry for this version from
CHANGELOG.md - Creates the GitHub release with the extracted notes
Monitor the workflow:
gh run list --limit 5 gh run view <run-id>
Post-Release Checklist
-
Verify the release workflow passed — check
.github/workflows/release.ymlrun on the taggh run list --limit 5
-
Verify the GitHub release page looks correct
gh release view v0.X.Y
-
Update the
[Unreleased]section in CHANGELOG.md- Add an empty
[Unreleased]section above the just-released version - Commit:
git commit -am "docs: start unreleased section after v0.X.Y"
- Add an empty
-
Announce the release (if applicable)
- Post to relevant channels, update project website, etc.
-
Verify the extension installs cleanly from the release
- On a fresh PostgreSQL 18 instance:
CREATE EXTENSION pg_ripple;
- On a fresh PostgreSQL 18 instance:
Changelog Style
The CHANGELOG.md should be written so that someone without deep knowledge of Rust, PostgreSQL internals, or RDF can understand what changed. Guidelines:
- Lead with what users can do, not how it was implemented
- Use short sentences and bullet points
- Avoid jargon — say "store and retrieve facts" instead of "triple CRUD via VP tables"
- Technical implementation details go in a separate "Technical Details" subsection for those who want them
- Each version section should open with a one-sentence summary
Version Numbering
| Range | Meaning |
|---|---|
| 0.x.y | Pre-1.0 development milestones — features may change |
| 1.0.0 | Production release — stable API, standards compliance |
| 1.x.y | Post-1.0 enhancements (federation, Cypher/GQL, etc.) |
Security Advisory Calendar
HTTP Companion Compatibility Window Policy (C16-01, v0.112.0)
The pg_ripple_http HTTP companion supports the prior 2 minor extension versions at any given
time. Concretely: if the current extension version is 0.X.Y, the companion built from the same
commit is guaranteed to work with extensions 0.(X-1).0 and 0.(X-2).0 (and any patch releases
within those minors). Older extension versions are served in degraded mode with a startup warning.
The COMPATIBLE_EXTENSION_MIN constant in pg_ripple_http/src/main.rs is updated atomically
with every just bump-version <new> <floor> invocation, and the release.yml CI gate
(compat-check job) enforces that this constant is never more than 1 minor version behind the
current extension. Set PG_RIPPLE_HTTP_STRICT_COMPAT=1 to convert the warning to a fatal error.
Security Advisory Calendar
RSA timing side-channel advisories (SEC-06, v0.92.0)
Two RUSTSEC advisories for the rsa crate are tracked in audit.toml:
RUSTSEC-2024-0436(Marvin attack on RSA decryption)RUSTSEC-2023-0071(PKCS#1 v1.5 timing side-channel)
Both expire 2026-12-01. Before v1.0.0:
- Run
cargo tree -i rsato verify thersacrate is still only a transitive dep. - If
reqwestis configured withrustls-tls-native-roots(no native-TLS), the RSA crate may not be reachable. If unreachable, remove the advisory ignores fromaudit.toml. - If still present as a transitive dep, renew the expiry dates in
audit.tomlafter confirming no pg_ripple code path exercises RSA decryption with untrusted input.
Action required before v1.0.0: Re-audit RSA advisory status and update
audit.toml.
pg_ripple Blog
Note: Blog posts were written with AI assistance (GitHub Copilot / Claude) as a way to explore LLM-generated technical writing for a niche systems engineering topic. The technical content has been reviewed for accuracy, but treat posts as drafts rather than officially reviewed documentation.
The full blog lives in the repository under blog/.
Individual posts are linked below.
Core Concepts & Architecture
| Post | Summary |
|---|---|
| Why RDF Inside PostgreSQL? | The case for a triple store that lives where your data already is — no ETL pipeline, no separate cluster, no impedance mismatch. |
| Vertical Partitioning: One Table Per Predicate | Inside pg_ripple's VP storage model and why it beats a single (s, p, o) table by 10–100× for selective queries. |
| Everything Is an Integer | Dictionary encoding with XXH3-128: why string comparisons in a triple store are a performance bug. |
| How SPARQL Becomes a PostgreSQL Query Plan | The translation pipeline from SPARQL text to spargebra algebra to SQL to SPI execution. |
Storage & Performance
| Post | Summary |
|---|---|
| HTAP for Triples: Reads and Writes at the Same Time | The delta/main/tombstone split that lets pg_ripple handle concurrent OLTP writes and analytical SPARQL queries without locking. |
| Leapfrog Triejoin: When Triangle Queries Meet Optimal Joins | Worst-case optimal joins compiled into PostgreSQL — what a 10–100× speedup looks like in practice. |
| Property Paths Are Just Recursive CTEs | SPARQL property paths compiled to WITH RECURSIVE … CYCLE using PostgreSQL 18's hash-based cycle detection. |
Reasoning & Inference
| Post | Summary |
|---|---|
| Datalog Inside PostgreSQL | Automatic fact derivation from rules — RDFS, OWL RL, transitive closure, all running as SQL. |
| Magic Sets: Ask a Question, Infer Only What You Need | Goal-directed inference: the difference between 2 million inferred triples and 47. |
| owl:sameAs Without the Explosion | Entity canonicalization at query time using union-find over owl:sameAs chains. |
| The Four Built-in Rule Sets | What RDFS, OWL RL, OWL EL, and OWL QL actually do — every rule explained with examples. |
| Well-Founded Semantics | Why stratified negation isn't always enough, and how well-founded semantics handles recursive negation. |
| OWL Property Chain Axioms | Compiling owl:propertyChainAxiom to recursive SQL joins. |
Data Quality & Validation
| Post | Summary |
|---|---|
| SHACL: Schema Validation for the Schema-Less | How pg_ripple compiles SHACL shapes into DDL constraints and async validation pipelines. |
AI, RAG & Vector Search
| Post | Summary |
|---|---|
| Vector + SPARQL Hybrid Search | Combining pgvector approximate nearest-neighbour search with SPARQL graph traversal. |
| Natural Language to SPARQL | LLM-powered NL→SPARQL translation with few-shot prompting. |
| GraphRAG Knowledge Export | Exporting pg_ripple graphs for Microsoft's GraphRAG pipeline. |
| Neuro-Symbolic Entity Resolution | Combining ML embeddings with Datalog rules for record linkage. |
Integrations & Operations
| Post | Summary |
|---|---|
| CDC → Knowledge Graph | Streaming relational change events into the RDF graph via logical replication. |
| Citus Shard Pruning for SPARQL | How the SERVICE clause dispatches federated SPARQL queries to the right shard. |
| IVM with pg-trickle Integration | Incremental view maintenance over CDC streams. |
| Semantic Hub: pg-tide Relay | Hub-and-spoke topology for multi-instance RDF synchronisation. |
| GDPR Right to Erasure | Implementing GDPR article 17 — cascading triple deletion with provenance tracking. |
| Multi-Tenant Knowledge Graphs | Named-graph isolation, row-level security, and per-tenant dictionaries. |
Advanced Features
| Post | Summary |
|---|---|
| CONSTRUCT Views: Live Transformations | SPARQL CONSTRUCT rules as materialised views with automatic delta propagation. |
| JSON-LD Framing and Nested JSON | Turning a flat triple store into structured nested JSON documents on demand. |
| JSON-LD Reverse Mapping | Writing RDF changes back to relational tables via JSON-LD contexts. |
| R2RML: Relational to Graph | Mapping relational tables to RDF using the W3C R2RML standard. |
| RDF-star: Statements About Statements | Annotating individual triples for provenance, confidence, and temporal metadata. |
| Temporal Graph Snapshots | Point-in-time named graphs for audit trails and time-travel queries. |
| Temporal Time-Travel Queries | Querying historical states of the knowledge graph. |
| Allen's Interval Relations | Temporal reasoning using Allen's thirteen interval relations in Datalog. |
| GeoSPARQL + PostGIS | Spatial queries over RDF geometry literals using the GeoSPARQL 1.1 extension. |
| SKOS Knowledge Organization | Managing thesauri, concept schemes, and taxonomies with SKOS in pg_ripple. |
| Graph Analytics with PageRank | Datalog-native PageRank with incremental view maintenance, WCOJ, and Prometheus gauges. |
| Probabilistic Datalog | Uncertain knowledge: noisy-OR, confidence propagation, and soft constraints. |
| Provenance Tracking with PROV-O | Recording data lineage using the W3C PROV-O ontology. |
| Ontology Mapping and Alignment | Bridging heterogeneous vocabularies with owl:equivalentClass and SPARQL CONSTRUCT rules. |
| SPARQL Federation: Local + Remote | Mixing local VP table scans with remote SPARQL endpoints in a single query. |
| EXPLAIN SPARQL: Query Plans | Reading pg_ripple's query plan output to diagnose slow SPARQL queries. |
| Rule Library Federation | Publishing and subscribing to shared Datalog rule libraries across pg_ripple instances. |
| Federation Circuit Breakers | Resilient federated queries with circuit breakers, timeouts, and fallback graphs. |
| DCTERMS, Schema.org, and FOAF Bundles | Built-in vocabulary bundles for common ontologies. |
| Uncertain Knowledge | Probabilistic Datalog in PostgreSQL: representing and querying uncertain facts. |