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:

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:

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 typeRDF datatype
:stringxsd:string
:integerxsd:integer
:booleanxsd:boolean
:decimalxsd:decimal
:datexsd:date
UTC datetime typesxsd:dateTime
:uuidxsd: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 constructAshRelational projectionR2RML
RDF/OWL classResourcetable/viewrr:class
datatype propertyattributecolumn/expressionpredicate-object map
object propertyrelationshipFK/joinreference object map
semantic identitysubject DSL / Ash identityunique key(s)subject map
datatypeAsh typestorage typerr:datatype
required propertyallow_nil? falseNOT NULL where applicableshape constraint
one-to-one / many-to-onerelationshipFKreference object map
many-to-manyrelationship/join resourcejoin tablechained 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:

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:

Documentation

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.