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:
Ash.Resource
│
▼
AshPostgres
│
▼
PostgreSQL
│
├── SQL
└── generated R2RML → OBDA → SPARQLThere is one persisted subject, not a relational database plus a synchronized graph database.
Define an Ash domain
defmodule Demo.Domain do
use Ash.Domain
resources do
resource Demo.Organization
resource Demo.Person
end
endMap an organization
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
endMap a person and relationship
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
endInspect normalized mappings
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
{: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:
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:
ontology/application profile
↓
SHACL operational shapes
↓
ggen
↓
generated Ash resources
↓
AshR2RML.Mapping
↓
R2RMLThe 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:
- Ash persists the fixture in PostgreSQL;
- generated R2RML parses independently;
- a real OBDA engine loads the mapping;
- SPARQL returns the expected subject IRIs, scalar values, and relationships;
- those results match the same records visible through Ash.
That is the finished AshR2RML contract.