Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

RDF over application tables

Use GraphEngine for Cypher, Gremlin, and SPARQL over the same application tables. Add RDF vocabulary to GraphMapping: each rule describes a subject, predicate, object, and optional graph. SPARQL patterns resolve to scans and joins of those sources. OrchidDB does not create or populate a triple store. An ontology or explicit rdf:type root is not required for direct column mappings.

The complete Rust example creates a small application table, queries typed RDF terms, updates through SPARQL, reads the change through Cypher in the same transaction, and rolls it back. Use the dependencies in installation.

Map columns and composite identities

Given customers(tenant BIGINT, id BIGINT, name VARCHAR), register its schema and map its name column:

use std::sync::Arc;
use arrow::datatypes::{DataType, Field, Schema};
use orchiddb::engine::GraphEngine;
use orchiddb::ir::rel::mapping::{GraphMapping, NodeMapping, schema_only_provider};
use orchiddb::ir::rel::rdf_mapping::{RdfMapping, RdfTermMapping as Term};

let connection = duckdb::Connection::open("application.duckdb")
    .map_err(|e| e.to_string())?;
let mut mapping = GraphMapping::new();
mapping.register_table("customers", schema_only_provider(Arc::new(Schema::new(vec![
    Field::new("tenant", DataType::Int64, false),
    Field::new("id", DataType::Int64, false),
    Field::new("name", DataType::Utf8, true),
]))));
mapping.map_node(NodeMapping::table("Customer", "customers", ["tenant", "id"])
    .property("name", "name"));
mapping.map_rdf(RdfMapping::table(
    "customers", Term::template("urn:customer:", ["tenant", "id"]),
    "https://example.com/name", Term::literal("name"),
).writable(["tenant", "id"]));
let mut graph = GraphEngine::mapped(connection, Arc::new(mapping))?;
let result = graph.sparql_query(
    "SELECT ?name WHERE { ?customer <https://example.com/name> ?name }",
    "default",
).await?;

Term::iri(column) reads existing IRIs. Term::template(prefix, columns) encodes ordered key components as UTF-8 hexadecimal separated by /: tenant 1, id 7 become urn:customer:31/37. Binary key columns use their original bytes. The encoding is reversible and escapes separators. A null component emits no statement. Blank-node templates add a declared scope.

Literal rules infer RDF datatypes from registered column types. Use the Literal variant to specify a datatype, constant language, or language column. Language tags are normalized to lowercase. Native numeric columns expose their stored value; a text column can preserve an original lexical spelling.

Map relationships

An orders table can expose a composite foreign key directly:

mapping.map_rdf(RdfMapping::table(
    "orders", Term::template("urn:customer:", ["tenant", "customer_id"]),
    "https://example.com/hasOrder", Term::template("urn:order:", ["tenant", "id"]),
).writable(["tenant", "id"]));
mapping.map_rdf(RdfMapping::table(
    "orders", Term::template("urn:order:", ["tenant", "id"]),
    "https://example.com/total", Term::literal("total"),
));

Register the orders schema and these rules before constructing the engine. Partial-null foreign keys emit no relationship. Independent edge tables work the same way: map their source and destination key columns. Registered SQL views can provide joins or computed read projections.

PREFIX ex: <https://example.com/>
SELECT ?name ?total WHERE {
  ?customer ex:name ?name ; ex:hasOrder ?order .
  ?order ex:total ?total .
  FILTER(?total > 100)
}

No type assertion is required. Constant predicates prune unrelated mappings. Compatible composite identity mappings retain native key columns for joins. Variable predicates enumerate applicable rules. Overlapping statements are removed at graph boundaries; query solution multiplicity follows SPARQL.

Default and named graphs

Rules without a graph belong to the default graph. Add .graph(Term::constant("urn:team")) or .graph(Term::iri("graph_iri")) for named graphs. A null mapped graph expression emits no statement. GRAPH ?g, FROM, and FROM NAMED use the same rules. An explicit named-graph registry preserves empty graphs; configure it through RdfDatasetMapping::map_writable_named_graphs and attach it with GraphMapping::with_rdf_mapping before adding further RDF rules.

Updates and transactions

Call graph.sparql_update(update, "default", None).await?. Writable rules declare a complete physical row key. Subject and object identities determine key/FK values; property inserts for a new subject combine into one row insertion. Deleting a property clears its column, deleting an FK relationship unlinks its non-key columns, and deleting an all-key edge or class assertion deletes its row. DELETE/INSERT replaces values in a single row transition, including required columns. Defaults and database constraints still apply.

A scalar column cannot represent two different simultaneous RDF values. To replace a value, delete the old statement and insert the new one in the same request. Ambiguous targets, missing key components, and constraint failures abort the request. No fallback storage is created.

begin, commit, and rollback cover all three query languages. SPARQL writes are immediately visible to Cypher and Gremlin on that connection. A failed explicit transaction must be rolled back. Automatic transactions roll back on errors or cancellation.

Inspect execution and results

sparql_query returns typed SparqlResults: SELECT solutions, an ASK boolean, or CONSTRUCT/DESCRIBE graph triples. sparql_dataset returns QueryResult, with Arrow data in returned.batch and execution statistics in stats. sparql_sql shows generated SQL. into_executor returns the connection for further relational work.

Application-owned quad tables remain a supported source shape. Existing RdfDatasetMapping configurations attach through GraphMapping::new().with_rdf_mapping(mapping). The old RdfGraphEngine API is a compatibility facade delegating to GraphEngine; it owns no separate executor.

Migration from RDF-specific engine construction

New code constructs GraphEngine::mapped(connection, Arc::new(mapping)) once. GraphEngine re-exports SparqlResults and RdfTermValue; callers do not need an RDF-specific engine to decode terms. Existing facades retain their signatures:

Existing RDF facadeShared GraphEngine API
query(query)sparql_query(query, dataset)
sparql(query)sparql_dataset(query, dataset); access returned for batches
sql(query)sparql_sql(query, dataset)
update(update, base)sparql_update(update, dataset, base)
last_query_stats()sparql_dataset(...).await?.stats
into_executor()into_executor()

The dataset argument selects a declared dataset (usually "default"); it is separate from named graphs selected by GRAPH. sparql_query returns typed terms without the statistics wrapper. sparql_dataset exposes the logical/physical plans, generated SQL, and physical table choices. The ontology convenience method sparql(query, ontology) continues to return legacy scalar columns.

Logical sources and collections

RDF rules can reference the same partition-layout logical sources and expanded collection tables as node and edge mappings. Register physical providers first, then layout definitions, then collection sources. RDF filters participate in physical layout selection; collection parent filters may push beneath Unnest, while element filters remain after expansion. Null mapped terms emit no statement; duplicate statements are removed at RDF graph boundaries.

Use these derived sources for reads. Writable RDF rules should reference the physical row owner and supply a complete key; declaring a derived mapping writable does not make its expansion or layout alternatives writable. See collection mappings and physical layouts.

Execution boundary

SPARQL uses the shared relational DAG and DuckDB transaction owner. It does not create another connection for RDF queries or updates. Remote SERVICE, including SERVICE SILENT, is unsupported. Native maps/JSON are not implicitly RDF sources; use explicit columns, term rules, or supported collection mappings.

Equivalent source representations

RDF rules can use a representation source as their table. The shared relational optimizer can then choose a collection expansion, a SQL derivation, or an equivalent materialized table. sparql_dataset exposes the choice in stats.representation_selections; RDF term construction and graph semantics remain above that choice. Representation sources are read-only even if an RDF rule declares itself writable.