Skip to content

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.

text
                     ┌─────────────────────────────────────────────┐
 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 record

Modules

AreaSourceWhat it does
Entry pointsrc/lib.rs_PG_init, version() / build_id(), the refuse helper, and the wiring of the schema SQL into the install script.
Dictionary and quadssrc/storage/ dict.rs, hexastore.rs, partition.rsTerm interning, quad writes, add_graph, serialised partition DDL.
Graph lifecyclesrc/storage/graphs.rsIRI ↔ id mapping, clear_graph, copy_graph, move_graph, drop_graph, carve_graph and their IRI overloads.
Loadingsrc/storage/loader.rs, src/storage/staged/, src/storage/construct_ingest.rs, src/storage/txn_guard.rsTurtle, 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 counterssrc/storage/shmem_cache.rs, src/storage/stats.rs, src/query/plan_cache.rsThe shared-memory term cache, the per-backend prepared-plan cache, stats() and shmem_reset().
Custodysrc/storage/lock.rs, inventory.rs, freshness.rs, integrity.rsGraph write locks, graph_inventory() and orphan_partitions(), materialisation freshness, graph_integrity().
Identity and exportsrc/storage/canon.rs, fd1.rs, export.rsgraph_digest (W3C RDFC-1.0), structural_digest (first-degree), export_graph, graph_manifest.
Querysrc/query/ parser.rs, executor.rs, path.rs, guc.rs, values_graph_guard.rssparql_parse, the algebra → SQL translator, SELECT / ASK / CONSTRUCT / DESCRIBE / UPDATE execution, property paths, registration of the pgrdf.* settings.
Inferencesrc/inference/reasonable.rsmaterialize: OWL 2 RL through the reasonable crate; the RDFS profile in pgRDF's own code.
Validationsrc/validation/ shacl.rs, pgrdf_sparql.rsvalidate: SHACL Core through rudof's shacl crate, plus pgRDF's own SHACL-SPARQL evaluator.
Surfacesrc/surface.rs, src/surface_manifest.tsvsurface(): 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

  1. The dictionary is the source of truth for term identity. Every id in _pgrdf_quads refers to a row in _pgrdf_dictionary. There are no foreign keys (they would slow the write path); loaders resolve ids before inserting, and graph_integrity() audits a graph after the fact.
  2. One partition per graph. _pgrdf_quads is LIST-partitioned on graph_id. Clearing or dropping a graph is a partition operation (TRUNCATE, DETACH + DROP), so its cost does not grow with the number of rows.
  3. Inferred triples are flagged, not separate. They live in the same partition as the asserted ones with is_inferred = true. materialize replaces them; export_graph and the digests ignore them; SPARQL and validate see them.
  4. Every write path checks the graph lock. Loaders, SPARQL UPDATE, the lifecycle functions and materialize call storage::lock::require_unlocked before touching a graph. Reads are never blocked.
  5. 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, …). XX000 is 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.
  6. Validation and digests never write.
  7. The exported surface is declared. src/surface_manifest.tsv is compiled into the library and served by surface(). A #[pg_test] compares it with the extension-owned functions in pg_proc in 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

CrateRole
pgrx =0.19.2PostgreSQL extension framework (SPI, shared memory, background workers, SQL generation).
spargebraSPARQL 1.1 query and update parser.
oxrdf, oxttlRDF terms; Turtle, N-Triples, TriG and N-Quads parsing and serialisation.
reasonableOWL 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_rdfSHACL Core validation (the rudof project).
rayonParallel parsing and term resolution in the bulk loader (pure CPU work; no SPI inside parallel regions).
sha2, serde_jsonDigests; 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.

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