AshR2RML
Ontology-first semantic mappings for Ash. Keep PostgreSQL relational; expose the same admitted subject as RDF.
AshR2RML is an Ash extension that compiles Ash resources and relationships into standards-valid W3C R2RML mappings. It lets an Ash application persist through its normal data layer—typically AshPostgres—while exposing the same relational state as a virtual RDF graph through any compatible OBDA/R2RML engine.
AshR2RML is not a graph database, not an Ash.DataLayer, and not a SPARQL engine.
Its job is narrower and more useful:
Ash.Resource + semantic mapping
│
▼
AshR2RML.Mapping
╱ ╲
▼ ▼
AshPostgres R2RML
│ │
▼ ▼
PostgreSQL virtual RDF graph
│ │
└─────┬─────┘
▼
ONE SUBJECT
SQL / Ash / SPARQL
The same facts are not synchronized between two databases. PostgreSQL remains the operational store; R2RML defines a semantic projection over it.
Why AshR2RML
A conventional semantic integration often starts too late:
application schema → database tables → retrofit RDF mapping
AshR2RML supports both that pragmatic direction and an ontology-first direction:
RDF/OWL + SHACL
│
▼
ggen
│
▼
generated Ash resources
│
▼
AshR2RML.Mapping
╱ ╲
▼ ▼
AshPostgres R2RML
Both routes converge on the same semantic mapping intermediate representation.
That gives an application three lawful query surfaces over one admitted subject:
- Ash queries and actions for application behavior.
- SQL for relational analytics and operations.
- SPARQL over the virtual RDF graph for semantic interoperability.
Installation
Add AshR2RML beside the Ash data layer you already use:
def deps do
[
{:ash, "~> 3.0"},
{:ash_postgres, "~> 2.0"},
{:ash_r2rml, "~> 1.0"}
]
end
AshR2RML does not replace AshPostgres. A resource continues to use its normal data layer:
defmodule MyApp.Person do
use Ash.Resource,
domain: MyApp.Domain,
data_layer: AshPostgres.DataLayer,
extensions: [AshR2RML.Resource]
end
Ash-first quick start
Define the relational resource normally, then add only the semantic information Ash cannot infer.
defmodule MyApp.Person do
use Ash.Resource,
domain: MyApp.Domain,
data_layer: AshPostgres.DataLayer,
extensions: [AshR2RML.Resource]
postgres do
table "people"
repo MyApp.Repo
end
r2rml do
class "http://xmlns.com/foaf/0.1/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 "http://xmlns.com/foaf/0.1/name"
end
end
end
end
AshR2RML introspects the Ash resource, its data layer metadata, attributes, identities, and relationships and compiles them into a normalized mapping.
mapping = AshR2RML.Resource.Info.mapping(MyApp.Person)
Generate R2RML:
{:ok, turtle} = AshR2RML.R2RML.render([MyApp.Person])
File.write!("priv/r2rml/application.ttl", turtle)
The resulting triples map is equivalent in shape to:
<#Person>
a rr:TriplesMap ;
rr:logicalTable [ rr:tableName "people" ] ;
rr:subjectMap [
rr:template "https://example.org/people/{id}" ;
rr:class foaf:Person ;
rr:termType rr:IRI
] ;
rr:predicateObjectMap [
rr:predicate foaf:name ;
rr:objectMap [
rr:column "name" ;
rr:datatype xsd:string
]
] .
Relationships are semantic edges
Ash relationships are first-class inputs to the mapping compiler.
relationships do
belongs_to :organization, MyApp.Organization do
allow_nil? false
rdf do
predicate "https://schema.org/memberOf"
end
end
end
For a relational data layer, AshR2RML derives the join from the relationship metadata and emits an R2RML reference object map:
rr:predicateObjectMap [
rr:predicate schema:memberOf ;
rr:objectMap [
rr:parentTriplesMap <#Organization> ;
rr:joinCondition [
rr:child "organization_id" ;
rr:parent "id"
]
]
] .
One admitted relationship therefore has three corresponding projections:
Ash relationship
│
├── PostgreSQL FK / join structure
└── R2RML RefObjectMap / RDF object property
AshR2RML refuses mappings that cannot be derived without inventing semantics.
Semantic identity
Database identity and RDF identity are separate concerns.
AshR2RML supports subject maps based on:
- IRI templates,
- a column,
- a constant IRI,
- blank nodes where explicitly configured.
Example:
r2rml do
class "https://schema.org/Organization"
subject do
template "https://example.org/org/{tenant_id}/{id}"
term_type :iri
end
end
Every template field must resolve to an admitted Ash attribute. Subject construction is validated at compile time; invalid or ambiguous identity mappings are refused rather than guessed.
Datatypes
AshR2RML maps Ash types to RDF datatypes through an explicit datatype registry.
Typical built-ins include:
| Ash type | RDF datatype |
|---|---|
:string | xsd:string |
:integer | xsd:integer |
:boolean | xsd:boolean |
:decimal | xsd:decimal |
:date | xsd:date |
| UTC datetime types | xsd:dateTime |
:uuid | xsd:string unless overridden |
:duration (Ash.Type.Duration, Ash >= 3.23) | xsd:duration |
A type with no lawful mapping is UNSUPPORTED; it is never silently coerced to a string.
Custom Ash types can implement the AshR2RML datatype contract to define their RDF lexical form and datatype IRI.
The mapping intermediate representation
Every public entry path compiles to the same IR:
AshR2RML.Mapping.Resource
AshR2RML.Mapping.SubjectMap
AshR2RML.Mapping.PredicateObjectMap
AshR2RML.Mapping.ReferenceObjectMap
AshR2RML.Mapping.JoinCondition
AshR2RML.Mapping.Datatype
AshR2RML.Mapping.GraphMap
This representation is deliberately close to R2RML terminology. It is inspectable, deterministic, and independent of any one application ontology.
%AshR2RML.Mapping.Resource{
resource: MyApp.Person,
class_iris: ["http://xmlns.com/foaf/0.1/Person"],
logical_table: "people",
subject_map: %AshR2RML.Mapping.SubjectMap{...},
properties: [...],
relationships: [...]
}
Ontology-first Ash with ggen
AshR2RML ships a ggen pack for generating ordinary Ash resources from an admitted RDF/SHACL application profile.
The source of truth is the ontology/profile, not the generated Elixir:
public ontology / application ontology
│
▼
SHACL shapes
│
▼
ggen
│
▼
generated Ash.Resource
│
▼
AshR2RML.Mapping
│
▼
R2RML
Example shape:
ex:PersonShape
a sh:NodeShape ;
sh:targetClass foaf:Person ;
r2ml:ashModule "MyApp.Person" ;
r2ml:table "people" ;
r2ml:subjectTemplate "https://example.org/people/{id}" ;
sh:property [
sh:path foaf:name ;
sh:datatype xsd:string ;
sh:minCount 1 ;
sh:maxCount 1
] .
The pack deterministically manufactures the corresponding Ash resource and semantic annotations. Generated resources are projections and should not be hand-edited.
See Ontology-first generation.
SHACL as the operational closure boundary
AshR2RML does not claim that arbitrary OWL can be deterministically compiled into a relational schema.
OWL describes open-world semantics. Ash resources and relational schemas need operationally closed decisions about cardinality, datatype, identity, and storage.
For ontology-first generation, SHACL supplies that closure:
OWL/RDFS vocabulary
↓
application profile
↓
SHACL operational shapes
↓
ggen compilation
If the shape does not provide enough information to choose one lawful Ash/relational projection, generation fails with a typed refusal.
Typed refusals
AshR2RML fails closed at semantic boundaries. Representative failures include:
REFUSED_INVALID_CLASS_IRI
REFUSED_MISSING_SUBJECT_MAP
REFUSED_NON_UNIQUE_SEMANTIC_IDENTITY
REFUSED_UNMAPPED_DATATYPE
REFUSED_AMBIGUOUS_RELATIONSHIP
REFUSED_INVALID_JOIN_CONDITION
REFUSED_RELATIONSHIP_WITHOUT_PREDICATE
REFUSED_R2RML_JOIN_WITHOUT_IDENTITY
REFUSED_UNPROVEN_EQUIVALENCE
UNSUPPORTED_TERM_TYPE
UNSUPPORTED_ASH_TYPE
The exact Elixir error is a typed AshR2RML/Spark error. No mapping path silently drops a resource, attribute, relationship, or identity.
Knowledge hooks
AshR2RML.KnowledgeHooks admits a read-only predicate over the graph, evaluates it against
real data, and constructs a downstream Intent — hooks manufacture intents, they never
actuate. Six predicate types are supported:
:ask — SPARQL ASK query truth value
:result_delta — change between successive SELECT result sets
:external_trigger — an externally-supplied receipt (no in-repo query)
:shacl — SHACL shape conformance for one or more focus nodes
:threshold — a bound SPARQL variable compared against a numeric bound
:count — row count of a SELECT query compared against a numeric bound
:shacl, :threshold, and :count each fail closed at admission time on malformed input
(an unparseable shapes graph or empty focus set, an unsupported comparator atom, or a
non-SELECT query form, respectively) — see AGENTS.md's "Knowledge hooks" section for the
exact refusal codes.
Two predicate types are explicitly open, not-yet-designed extensions: :temporal_window
(windowed evaluation over a time range) and :datalog (a predicate expressed as Datalog
rules). Neither has admission rules, evaluation semantics, or a receipt shape defined yet —
this is a named gap, not an oversight.
Virtual RDF, not RDF synchronization
AshR2RML generates mappings; an OBDA engine executes SPARQL against the relational database.
SPARQL
│
▼
OBDA / R2RML engine
│ rewrites
▼
SQL
│
▼
PostgreSQL
There is no RDF replication requirement and no dual-write protocol. The RDF graph is virtual unless the application deliberately materializes it elsewhere.
This eliminates an entire class of synchronization drift:
Postgres row/FK state == source of truth
RDF triples == semantic projection of that state
Architecture invariant
AshR2RML follows this correspondence:
| Semantic construct | Ash | Relational projection | R2RML |
|---|---|---|---|
| RDF/OWL class | Resource | table/view | rr:class |
| datatype property | attribute | column/expression | predicate-object map |
| object property | relationship | FK/join | reference object map |
| semantic identity | subject DSL / Ash identity | unique key(s) | subject map |
| datatype | Ash type | storage type | rr:datatype |
| required property | allow_nil? false | NOT NULL where applicable | shape constraint |
| one-to-one / many-to-one | relationship | FK | reference object map |
| many-to-many | relationship/join resource | join table | chained reference maps |
R2RML should name and expose relationships the relational model already preserves. It should not repair a semantically impoverished schema.
Deterministic generation
The ontology-first pack follows the ggen model:
ontology + shapes + queries + templates
│
▼
ggen sync
│
▼
deterministic artifacts
A semantic change should require one authoritative edit and zero manual synchronization across generated Ash and R2RML projections.
Verification model
AshR2RML treats compile success as a checkpoint, not the crown.
The integration contract is:
Ash resources
↓
PostgreSQL fixture
↓
AshR2RML-generated R2RML
↓
real R2RML/OBDA engine
↓
SPARQL
The semantic identity and required relationships returned through SPARQL must match the subject visible through Ash.
Ontology-first generation adds the upstream leg:
RDF/SHACL
↓
ggen
↓
Ash
↓
Postgres
↓
R2RML
↓
SPARQL
Federation
AshR2RML.Federation (lib/ash_r2rml/federation.ex) is real, mechanically
checkable determinism substrate: admit_environment/1 admits a named
environment identity (name, compiler_version, admitted_ontology_sha256)
or refuses a malformed one; compile_for_environments/2 compiles the SAME
admitted semantic profile independently, once per admitted environment, and
returns a FederationReceipt asserting whether the generated artifact
identity (a sha256 over the compiled Ash/Ecto/DDL/R2RML/SHACL output) is
byte-identical across every environment that shares compiler+ontology
identity — "same input, same compiler identity, same artifact identity,"
checked by hash equality, not narrative.
What this proves: deterministic artifact identity is verifiable across
independently-configured environments — compile the same profile N times, in
N differently-named environment configs, and get back N byte-identical
generated artifacts (or, when an environment's admitted ontology identity
diverges, a receipt that names exactly which environment diverged instead of
silently averaging it away). test/federation_test.exs compiles a real
profile across 3 named environments and asserts this for real, then mutates
one environment's admitted_ontology_sha256 and asserts the receipt reports
all_identical?: false and names the diverging environment.
What this does NOT prove: no real multi-tenant deployment, no network federation protocol, no actual Fortune 500 customer environment. Every "environment" here is an in-process identity tuple compiled sequentially in one BEAM node — there is no runtime, no cluster, no customer infrastructure behind it. This module is the determinism substrate a federation claim would need, not the claim itself.
Knowledge hooks
AshR2RML.KnowledgeHooks (documentation/how_to/knowledge_hooks.md) admits a read-only
predicate over the RDF graph, evaluates it, and constructs a downstream Intent with
authority: :UNAUTHORIZED — it never actuates. PARTIAL_ALIVE: 8 predicate types are real
and tested end-to-end (:ask, :result_delta, :external_trigger, :shacl, :threshold,
:count, :temporal_window, :datalog), each with typed admission refusals and zero-mock
Chicago-style tests over real RDF.Graph/RDF.Turtle fixtures. :datalog is a deliberately
scoped hand-written single-rule evaluator (no recursion/negation/aggregation/stratification),
not a general Datalog engine — a real, named gap if broader Datalog semantics are ever needed.
GitVan v4 and KNHK Turtle vocabularies are recognized on import.
Status
This repository is PARTIAL_ALIVE, not a finished product. Real, verified capability:
R2RML compilation from Ash resources (relational + join-derived reference object maps),
two in-repo OBDA execution backends (AshR2RML.OBDA.InMemory over Ash.DataLayer.Ets,
AshR2RML.OBDA.Ontop over AshPostgres+Ontop+JDBC), a typed refusal vocabulary (14 mapping
refusal codes plus 4 knowledge-hook refusal codes), auto-projected GraphQL over the same
admitted subject, 8 knowledge-hook predicate types, and an in-process AshR2RML.Federation
determinism substrate (byte-identical artifact hashing across N named environment configs
compiled sequentially in one BEAM node).
Named gaps, stated honestly rather than glossed over:
AshR2RML.Federationproves deterministic artifact identity across in-process environment configs — it does not implement network federation, multi-tenant deployment, or run against any real Fortune 500 or other customer environment.Ash.Type.Rangehas no R2RML datatype mapping yet (parameterized-type rendering path not built;UNSUPPORTED_ASH_TYPE).:datalogknowledge hooks support one non-recursive rule only, by design.- Live-Postgres/Ontop adversarial tests depend on a running local Postgres+Ontop stack and are
not run in every environment; see
test/adversarial/.
Never treat this README, AGENTS.md, or CHANGELOG.md as proof a capability is wired end to
end — the real evidence is the cited test file and its actual passing run.
What AshR2RML does not do
AshR2RML deliberately does not:
- replace AshPostgres, AshSql, Ecto, or another Ash data layer;
- store RDF triples itself;
- require Neo4j or another graph database;
- implement a SPARQL optimizer;
- infer arbitrary OWL semantics;
- invent missing relationship or identity information;
- make generated ontology facts executable with ambient authority.
Documentation
- Architecture
- Ash-first mapping
- Ontology-first generation
- Managing relational schema and R2RML
- Knowledge hooks
- Palantir/Kudzu migration demonstration
- Usage rules
Development
AshR2RML uses ggen to manufacture generated semantic compiler surfaces. Generated artifacts are projections; change their owning ontology/query/template and regenerate rather than hand-editing them.
The repository's AGENTS.md is the authoritative contributor contract.
License
MIT. See LICENSES/MIT.md.