Current section
Files
Jump to
Current section
Files
ash_r2rml.livemd
<!--
SPDX-FileCopyrightText: 2026 ash_r2rml contributors
SPDX-License-Identifier: MIT
-->
# AshR2RML — relational state as virtual RDF
```elixir
Mix.install(
[
{:ash, "~> 3.0"},
{:ash_postgres, "~> 2.0"},
{:ash_r2rml, "~> 1.0"}
],
consolidate_protocols: false
)
```
## The idea
AshR2RML keeps relational persistence relational and adds a standards-based semantic projection:
```text
Ash.Resource
│
▼
AshPostgres
│
▼
PostgreSQL
│
├── SQL
└── generated R2RML → OBDA → SPARQL
```
There is one persisted subject, not a relational database plus a synchronized graph database.
## Define an Ash domain
```elixir
defmodule Demo.Domain do
use Ash.Domain
resources do
resource Demo.Organization
resource Demo.Person
end
end
```
## Map an organization
```elixir
defmodule Demo.Organization do
use Ash.Resource,
domain: Demo.Domain,
data_layer: AshPostgres.DataLayer,
extensions: [AshR2RML.Resource]
postgres do
table "organizations"
repo Demo.Repo
end
r2rml do
class "https://schema.org/Organization"
subject do
template "https://example.org/organizations/{id}"
term_type :iri
end
end
attributes do
uuid_primary_key :id
attribute :name, :string do
allow_nil? false
public? true
rdf do
predicate "https://schema.org/name"
end
end
end
end
```
## Map a person and relationship
```elixir
defmodule Demo.Person do
use Ash.Resource,
domain: Demo.Domain,
data_layer: AshPostgres.DataLayer,
extensions: [AshR2RML.Resource]
postgres do
table "people"
repo Demo.Repo
end
r2rml do
class "https://schema.org/Person"
subject do
template "https://example.org/people/{id}"
term_type :iri
end
end
attributes do
uuid_primary_key :id
attribute :name, :string do
allow_nil? false
public? true
rdf do
predicate "https://schema.org/name"
end
end
end
relationships do
belongs_to :organization, Demo.Organization do
allow_nil? false
public? true
rdf do
predicate "https://schema.org/memberOf"
end
end
end
end
```
## Inspect normalized mappings
```elixir
person_mapping = AshR2RML.Resource.Info.mapping(Demo.Person)
organization_mapping = AshR2RML.Resource.Info.mapping(Demo.Organization)
{person_mapping, organization_mapping}
```
The mapping IR makes class IRIs, subject maps, scalar predicate-object maps, relationship reference maps, joins, datatypes, and logical tables explicit before serialization.
## Render R2RML
```elixir
{:ok, ttl} = AshR2RML.R2RML.render([Demo.Organization, Demo.Person])
IO.puts(ttl)
```
A conforming rendering contains a triples map for each resource and a reference object map for `Person.organization`.
## Semantic identity
The database UUID and RDF IRI are related by the subject template, but they are not the same layer of identity. Changing an internal table name must not change the public subject IRI.
## Virtual RDF
Run the generated mapping with a compatible OBDA engine against the same PostgreSQL database.
A query such as:
```sparql
SELECT ?person ?organization
WHERE {
?person <https://schema.org/memberOf> ?organization .
}
```
is rewritten by the OBDA engine into SQL over `people` and `organizations` using the join derived from the Ash relationship.
## Ontology-first generation
The same finished mapping model can be reached from RDF/OWL and SHACL:
```text
ontology/application profile
↓
SHACL operational shapes
↓
ggen
↓
generated Ash resources
↓
AshR2RML.Mapping
↓
R2RML
```
The generated Elixir is a projection. Change the ontology/profile/shape or generator and regenerate instead of hand-maintaining semantic duplicates.
## What to verify
The crown is not `mix compile` or a Turtle string. A complete integration proves:
1. Ash persists the fixture in PostgreSQL;
2. generated R2RML parses independently;
3. a real OBDA engine loads the mapping;
4. SPARQL returns the expected subject IRIs, scalar values, and relationships;
5. those results match the same records visible through Ash.
That is the finished AshR2RML contract.