Architecture
pgRDF is a PostgreSQL 18 extension written in Rust with pgrx. It runs as a shared library (pgrdf.so) loaded into ordinary PostgreSQL backends: there is no sidecar and no external process. Everything it stores lives in ordinary tables in the pgrdf schema, so transactions, MVCC, WAL, backups and replication behave as they do for any other table.
┌─────────────────────────────────────────────┐
PostgreSQL client ─►│ PostgreSQL backend │
(psql, a driver) │ │
│ pgrdf.so │
│ ├─ query SPARQL → algebra → SQL │
│ ├─ storage dictionary, quads, loaders,│
│ │ lifecycle, locks, digests │
│ ├─ inference OWL 2 RL, RDFS │
│ └─ validation SHACL Core, SHACL-SPARQL │
│ │
│ shared memory (when preloaded) │
│ term cache, statistics counters, │
│ staged-loader job control │
└──────────────────────┬──────────────────────┘
│ SPI
▼
pgrdf._pgrdf_dictionary term ↔ BIGINT id
pgrdf._pgrdf_quads LIST-partitioned by graph_id
pgrdf._pgrdf_graphs graph IRI, lock, materialisation recordModules
| Area | Source | What it does |
|---|---|---|
| Entry point | src/lib.rs | _PG_init, version() / build_id(), the refuse helper, and the wiring of the schema SQL into the install script. |
| Dictionary and quads | src/storage/ dict.rs, hexastore.rs, partition.rs | Term interning, quad writes, add_graph, serialised partition DDL. |
| Graph lifecycle | src/storage/graphs.rs | IRI ↔ id mapping, clear_graph, copy_graph, move_graph, drop_graph, carve_graph and their IRI overloads. |
| Loading | src/storage/loader.rs, src/storage/staged/, src/storage/construct_ingest.rs, src/storage/txn_guard.rs | Turtle, N-Triples, TriG and N-Quads ingest: the standard, parallel bulk, streaming and staged (background-worker) loaders; re-ingest of CONSTRUCT rows; the transaction-block guard for the staged path. |
| Caches and counters | src/storage/shmem_cache.rs, src/storage/stats.rs, src/query/plan_cache.rs | The shared-memory term cache, the per-backend prepared-plan cache, stats() and shmem_reset(). |
| Custody | src/storage/lock.rs, inventory.rs, freshness.rs, integrity.rs | Graph write locks, graph_inventory() and orphan_partitions(), materialisation freshness, graph_integrity(). |
| Identity and export | src/storage/canon.rs, fd1.rs, export.rs | graph_digest (W3C RDFC-1.0), structural_digest (first-degree), export_graph, graph_manifest. |
| Query | src/query/ parser.rs, executor.rs, path.rs, guc.rs, values_graph_guard.rs | sparql_parse, the algebra → SQL translator, SELECT / ASK / CONSTRUCT / DESCRIBE / UPDATE execution, property paths, registration of the pgrdf.* settings. |
| Inference | src/inference/reasonable.rs | materialize: OWL 2 RL through the reasonable crate; the RDFS profile in pgRDF's own code. |
| Validation | src/validation/ shacl.rs, pgrdf_sparql.rs | validate: SHACL Core through rudof's shacl crate, plus pgRDF's own SHACL-SPARQL evaluator. |
| Surface | src/surface.rs, src/surface_manifest.tsv | surface(): every exported function with its stability class. |
Module-level documentation lives in the //! comments at the top of each file.
How a call flows
pgrdf.sparql(q). spargebra parses the query into SPARQL algebra. The executor walks the algebra into its own plan (BGP triples, FILTERs, OPTIONAL / MINUS / UNION blocks, VALUES, BINDs, graph scopes, solution modifiers) and emits one SQL statement over _pgrdf_quads. Constants are resolved to dictionary ids and passed as $N parameters, so the SQL text depends only on the query's shape. The plan cache prepares that text once per backend; the statement runs through SPI, and each row comes back as a JSONB object. See Query.
pgrdf.materialize(g). The graph's asserted rows are read back as RDF triples, handed to the reasoner, and the entailed triples that were not already asserted are written into the same partition with is_inferred = true, replacing the previous run's. See Inference.
pgrdf.validate(d, s). Both graphs are read back, the shapes are compiled, and the data graph is validated in process; the report is returned as JSONB and nothing is written. See Validation.
Loading. Parsers turn the input into terms; terms are interned in the dictionary (through the per-call and shared-memory caches); quads are written in batches into the graph's partition. Large N-Triples files can go through the staged loader instead, which uses a pool of background workers. See Storage.
Invariants
- The dictionary is the source of truth for term identity. Every id in
_pgrdf_quadsrefers to a row in_pgrdf_dictionary. There are no foreign keys (they would slow the write path); loaders resolve ids before inserting, andgraph_integrity()audits a graph after the fact. - One partition per graph.
_pgrdf_quadsis LIST-partitioned ongraph_id. Clearing or dropping a graph is a partition operation (TRUNCATE,DETACH+DROP), so its cost does not grow with the number of rows. - Inferred triples are flagged, not separate. They live in the same partition as the asserted ones with
is_inferred = true.materializereplaces them;export_graphand the digests ignore them; SPARQL andvalidatesee them. - Every write path checks the graph lock. Loaders, SPARQL UPDATE, the lifecycle functions and
materializecallstorage::lock::require_unlockedbefore touching a graph. Reads are never blocked. - Refusals are typed. A deliberate refusal is raised through
crate::refuse(code, message), which reports a PostgreSQL ERROR with a semantic SQLSTATE (55P03,22023,0A000,42704, …).XX000is meant for internal faults, although a few older refusal paths still use it with a descriptive message. The codes are listed on Errors and diagnostics. - Validation and digests never write.
- The exported surface is declared.
src/surface_manifest.tsvis compiled into the library and served bysurface(). A#[pg_test]compares it with the extension-owned functions inpg_procin both directions, so an unclassified export or a stale row fails the suite.
Shared memory and preloading
_PG_init runs once per process. It always registers the pgrdf.* settings. When pgRDF is listed in shared_preload_libraries, it also runs in the postmaster and reserves three shared-memory structures: the term cache, the cross-backend counters behind stats() (cache, plan-cache and completeness counters), and the job-control segment used by the staged loader's worker pool.
Without preloading the extension still installs and most functions work, but the term cache is a no-op, the staged loader is unavailable, and pgrdf.stats()->'shmem_ready' is false.
Main dependencies
| Crate | Role |
|---|---|
pgrx =0.19.2 | PostgreSQL extension framework (SPI, shared memory, background workers, SQL generation). |
spargebra | SPARQL 1.1 query and update parser. |
oxrdf, oxttl | RDF terms; Turtle, N-Triples, TriG and N-Quads parsing and serialisation. |
reasonable | OWL 2 RL forward-chaining reasoner. pgRDF uses a fork, applied through [patch.crates-io] in Cargo.toml, that lets it share the RDF 1.2 feature shacl requires. |
shacl, rudof_rdf | SHACL Core validation (the rudof project). |
rayon | Parallel parsing and term resolution in the bulk loader (pure CPU work; no SPI inside parallel regions). |
sha2, serde_json | Digests; JSONB output. |
Deployment
The release artifact is the .so plus the control and SQL files for PostgreSQL 18 on glibc Linux. See Packaging for the file layout and the install guide for the supported routes. Local builds are covered in Development.