AshR2RML — relational state as virtual RDF

Copy Markdown View Source
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 → SPARQL

There 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
end

Map 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
end

Map 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
end

Inspect 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
        ↓
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.