Skip to content

Ontologies

An ontology is a formal model of a domain: the classes of things that exist, the properties that describe and relate them, and (optionally) the individuals — the actual instances. The web standard for ontologies is OWL, built on RDF; an ontology is just a set of RDF triples and can be serialized as RDF/XML (.owl), Turtle (.ttl), N-Triples, JSON-LD, and more.

Ontologies map almost one-to-one onto Synalog's knowledge-graph conventions: a class is a node, a relationship is an edge, an individual is a fact. synalog import does that translation for you, so an ontology someone already modeled — yours, a domain standard, or one downloaded from the web — becomes a runnable Synalog program with no hand-writing.

Importing an ontology

synalog import reads any RDF serialization rdflib understands (.owl, .ttl, .rdf, .n3, JSON-LD), from a local file or an http(s)/ftp URL, and writes a Synalog program to stdout — redirect it into a .l file:

synalog import ontology.ttl > ontology.l                  # local file
synalog import https://example.org/onto.owl > onto.l      # downloaded

The output has two layers, so the command works whether an ontology is mostly a schema (lots of classes, few or no individuals — a taxonomy like the OBO biomedical ontologies) or mostly data (rich in individuals — FOAF, the HR model below):

Schema layer (always emitted — the TBox):

Ontology construct Synalog output
Class a row of Class (keyed by the class URI, with a label from rdfs:label)
rdfs:subClassOf a recursive SubClassOf — the transitive closure of the hierarchy

Instance layer (emitted for each class that actually has individuals — the ABox):

Ontology construct Synalog output
Class with individuals a concept (named after the class) keyed by the individual's URI (preserved verbatim)
Datatype property a column on the concept of every individual that uses it
Object property a concept (named after the property) joining two entities through their URIs
Individual a *Raw fact the concepts build on, so the program runs as-is

OWL property axioms and characteristics are translated to the matching rule patterns — so a transitive property is actually closed, an inverse is actually derived, and a functional property is actually checked. Rather than tabulate them, Every OWL axiom, by example below shows each construct end-to-end: the OWL declaration and the exact Synalog import generates from it.

Characteristics propagate across owl:inverseOf and owl:equivalentProperty (the inverse of a transitive property is transitive, of a functional one is inverse-functional; equivalents share everything), so the closures are complete on both sides. Every generated program is validated by the verifier. owl:complementOf and owl:propertyChainAxiom are not translated — they need open-world / non-stratifiable reasoning that does not map to a finite SQL rule.

Emitting per-class entity concepts only for classes that have individuals is what keeps a class-centric ontology usable: a 15,000-class taxonomy with no individuals becomes a single Class plus its SubClassOf hierarchy, not 15,000 empty one-row predicates.

Datatype columns are data-driven: a property becomes a column of a concept when an individual of that class actually uses it. That means inheritance works without reasoning over the class hierarchy — an individual typed as a subclass still gets the superclass's properties — and properties with no declared rdfs:domain are picked up too. Values keep their natural type (numbers, booleans) where rdflib can infer it; strings are emitted as single-quoted literals, and a property an individual lacks is filled with null so every row of a concept has the same shape.

Building an ontology with Protégé

Protégé is the standard free, open-source ontology editor from Stanford. It's the easiest way to author an ontology by hand and export a file synalog import can read.

  1. Install and create. Download the desktop app from protege.stanford.edu, then File → New. On the Active Ontology tab, set the Ontology IRI to a base IRI for your domain (e.g. http://example.org/hr#); every term you create lives under it, and these IRIs become the uri values in the generated program.

    Protégé Active Ontology tab, showing the ontology IRI bar and metrics

  2. Classes (the Classes / Entities tab) — add a class per kind of entity (Person, Company, …). Nest a class under another to create an rdfs:subClassOf relationship (Employee under Person); Synalog turns the hierarchy into SubClassOf. In the screenshot below the class hierarchy is on the left and the selected class's superclasses are in Class Description on the right.

    Protégé Entities tab, showing the class hierarchy on the left and the class description on the right

  3. Datatype properties (Data properties tab) — add the attributes (name, email, salary). Set each property's Domain to the class it describes and its Range to a datatype (xsd:string, xsd:integer, …). These become node columns.

  4. Object properties (Object properties tab) — add the relationships (worksAt, reportsTo, knows). Set Domain and Range to the connected classes; these become relationship concepts that join through the two classes.
  5. Individuals (Individuals tab, optional) — add instances, assign each a Type (its class), and fill in property values. These become the *Raw facts, so the imported program runs immediately. Skip this step to import a schema-only ontology (the concepts are still generated, just without data).
  6. Export. File → Save as… and choose RDF/XML (.owl) or Turtle (.ttl) — synalog import reads either. Then:

    synalog import my-ontology.owl > my-ontology.l
    synalog my-ontology.l check        # validate
    synalog my-ontology.l run Person
    

Don't build from scratch when a standard exists

Many domains already have a public ontology (schema.org, FOAF, SKOS, the OBO biomedical ontologies, Wikidata, …). Point synalog import straight at its URL, or open it in Protégé to adapt it before importing.

Screenshots from the Protégé "Getting Started" guide (Stanford Center for Biomedical Informatics Research), using the example Pizza ontology.

Worked example

This small HR ontology (Turtle) has three levels of classes, a few datatype and object properties, and four individuals:

# A small self-contained ontology (Turtle): a TBox (classes + properties) and
# an ABox (individuals). `synalog import ontology.ttl > ontology.l` converts it
# to the Synalog program in ontology.l.
@prefix :     <http://example.org/hr#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd:  <http://www.w3.org/2001/XMLSchema#> .

# Classes
:Person   a owl:Class .
:Employee a owl:Class ; rdfs:subClassOf :Person .
:Manager  a owl:Class ; rdfs:subClassOf :Employee .
:Company  a owl:Class .

# Datatype properties (become node columns)
:name  a owl:DatatypeProperty ; rdfs:domain :Person  ; rdfs:range xsd:string .
:email a owl:DatatypeProperty ; rdfs:domain :Person  ; rdfs:range xsd:string .
:salary a owl:DatatypeProperty ; rdfs:domain :Employee ; rdfs:range xsd:integer .

# Object properties (become edges)
:worksAt   a owl:ObjectProperty ; rdfs:domain :Employee ; rdfs:range :Company .
:reportsTo a owl:ObjectProperty ; rdfs:domain :Employee ; rdfs:range :Manager .
:knows     a owl:ObjectProperty ; rdfs:domain :Person   ; rdfs:range :Person .

# Individuals (the ABox)
:acme a :Company ; :name "Acme \"Robotics\"" .

:carol a :Manager  ; :name "Carol"      ; :email "carol@acme.example" ; :salary 150000 ;
                     :worksAt :acme .
:alice a :Employee ; :name "Alice O'Hara" ; :email "alice@acme.example" ; :salary 90000 ;
                     :worksAt :acme ; :reportsTo :carol ; :knows :bob .
:bob   a :Person   ; :name "Bob" ; :knows :alice .
The same ontology as RDF/XML (ontology.owl)

The serialization doesn't matter — synalog import ontology.owl converts this RDF/XML form (the default Protégé export) to exactly the same program.

<?xml version="1.0" encoding="utf-8"?>
<!-- The same ontology as ontology.ttl, serialized as RDF/XML (.owl).
     `synalog import ontology.owl` converts it identically. -->
<rdf:RDF
  xmlns:hr="http://example.org/hr#"
  xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
  xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#"
  xmlns:owl="http://www.w3.org/2002/07/owl#"
>
  <owl:DatatypeProperty rdf:about="http://example.org/hr#email">
    <rdfs:domain rdf:resource="http://example.org/hr#Person"/>
    <rdfs:range rdf:resource="http://www.w3.org/2001/XMLSchema#string"/>
  </owl:DatatypeProperty>
  <owl:ObjectProperty rdf:about="http://example.org/hr#reportsTo">
    <rdfs:domain rdf:resource="http://example.org/hr#Employee"/>
    <rdfs:range rdf:resource="http://example.org/hr#Manager"/>
  </owl:ObjectProperty>
  <owl:DatatypeProperty rdf:about="http://example.org/hr#name">
    <rdfs:domain rdf:resource="http://example.org/hr#Person"/>
    <rdfs:range rdf:resource="http://www.w3.org/2001/XMLSchema#string"/>
  </owl:DatatypeProperty>
  <owl:ObjectProperty rdf:about="http://example.org/hr#knows">
    <rdfs:domain rdf:resource="http://example.org/hr#Person"/>
    <rdfs:range rdf:resource="http://example.org/hr#Person"/>
  </owl:ObjectProperty>
  <owl:ObjectProperty rdf:about="http://example.org/hr#worksAt">
    <rdfs:domain rdf:resource="http://example.org/hr#Employee"/>
    <rdfs:range rdf:resource="http://example.org/hr#Company"/>
  </owl:ObjectProperty>
  <owl:DatatypeProperty rdf:about="http://example.org/hr#salary">
    <rdfs:domain rdf:resource="http://example.org/hr#Employee"/>
    <rdfs:range rdf:resource="http://www.w3.org/2001/XMLSchema#integer"/>
  </owl:DatatypeProperty>
  <owl:Class rdf:about="http://example.org/hr#Company"/>
  <hr:Employee rdf:about="http://example.org/hr#alice">
    <hr:name>Alice O'Hara</hr:name>
    <hr:email>alice@acme.example</hr:email>
    <hr:salary rdf:datatype="http://www.w3.org/2001/XMLSchema#integer">90000</hr:salary>
    <hr:worksAt>
      <hr:Company rdf:about="http://example.org/hr#acme">
        <hr:name>Acme "Robotics"</hr:name>
      </hr:Company>
    </hr:worksAt>
    <hr:reportsTo>
      <hr:Manager rdf:about="http://example.org/hr#carol">
        <hr:name>Carol</hr:name>
        <hr:email>carol@acme.example</hr:email>
        <hr:salary rdf:datatype="http://www.w3.org/2001/XMLSchema#integer">150000</hr:salary>
        <hr:worksAt rdf:resource="http://example.org/hr#acme"/>
      </hr:Manager>
    </hr:reportsTo>
    <hr:knows>
      <hr:Person rdf:about="http://example.org/hr#bob">
        <hr:name>Bob</hr:name>
        <hr:knows rdf:resource="http://example.org/hr#alice"/>
      </hr:Person>
    </hr:knows>
  </hr:Employee>
  <owl:Class rdf:about="http://example.org/hr#Person"/>
  <owl:Class rdf:about="http://example.org/hr#Employee">
    <rdfs:subClassOf rdf:resource="http://example.org/hr#Person"/>
  </owl:Class>
  <owl:Class rdf:about="http://example.org/hr#Manager">
    <rdfs:subClassOf rdf:resource="http://example.org/hr#Employee"/>
  </owl:Class>
</rdf:RDF>

synalog import ontology.ttl produces the program below. Note how Employee and Manager carry name/email even though those properties are declared on Person — the columns follow the data:

# run: Class, SubClassOf, Person, Employee, Manager, Company, WorksAt, Knows, ReportsTo
# Generated by `synalog import` from an OWL/RDF ontology.
# 4 classes, 3 object properties, 4 individuals.

# Schema — every class as a concept (the TBox)
@OrderBy(Class, "uri");
Class(uri:) distinct :- ClassRaw(uri:);
ClassRaw(uri: 'http://example.org/hr#Company');
ClassRaw(uri: 'http://example.org/hr#Employee');
ClassRaw(uri: 'http://example.org/hr#Manager');
ClassRaw(uri: 'http://example.org/hr#Person');

# Class hierarchy (transitive subClassOf / equivalentClass), joined through Class
@Recursive(SubClassOf, 100);
SubClassOf(child_uri:, parent_uri:) distinct :- SubClassOfRaw(child_uri:, parent_uri:), Class(uri: child_uri), Class(uri: parent_uri);
SubClassOf(child_uri:, parent_uri:) distinct :- SubClassOf(child_uri:, parent_uri: mid), SubClassOfRaw(child_uri: mid, parent_uri:);
SubClassOfRaw(child_uri: 'http://example.org/hr#Employee', parent_uri: 'http://example.org/hr#Person');
SubClassOfRaw(child_uri: 'http://example.org/hr#Manager', parent_uri: 'http://example.org/hr#Employee');

# Concepts — classes that have individuals

@OrderBy(Company, "uri");
Company(uri:, name:) distinct :- CompanyRaw(uri:, name:);
CompanyRaw(uri: 'http://example.org/hr#acme', name: 'Acme "Robotics"');

@OrderBy(Employee, "uri");
Employee(uri:, email:, name:, salary:) distinct :- EmployeeRaw(uri:, email:, name:, salary:);
EmployeeRaw(uri: 'http://example.org/hr#alice', email: 'alice@acme.example', name: 'Alice O\'Hara', salary: 90000);

@OrderBy(Manager, "uri");
Manager(uri:, email:, name:, salary:) distinct :- ManagerRaw(uri:, email:, name:, salary:);
ManagerRaw(uri: 'http://example.org/hr#carol', email: 'carol@acme.example', name: 'Carol', salary: 150000);

@OrderBy(Person, "uri");
Person(uri:, email:, name:) distinct :- PersonRaw(uri:, email:, name:);
PersonRaw(uri: 'http://example.org/hr#bob', email: null, name: 'Bob');

# Concepts — object properties (relationships)

@OrderBy(Knows, "subject_uri");
Knows(subject_uri:, object_uri:) distinct :- KnowsRaw(subject_uri:, object_uri:), Person(uri: subject_uri), Person(uri: object_uri);
KnowsRaw(subject_uri: 'http://example.org/hr#alice', object_uri: 'http://example.org/hr#bob');
KnowsRaw(subject_uri: 'http://example.org/hr#bob', object_uri: 'http://example.org/hr#alice');

@OrderBy(ReportsTo, "subject_uri");
ReportsTo(subject_uri:, object_uri:) distinct :- ReportsToRaw(subject_uri:, object_uri:), Employee(uri: subject_uri), Manager(uri: object_uri);
ReportsToRaw(subject_uri: 'http://example.org/hr#alice', object_uri: 'http://example.org/hr#carol');

@OrderBy(WorksAt, "subject_uri");
WorksAt(subject_uri:, object_uri:) distinct :- WorksAtRaw(subject_uri:, object_uri:), Employee(uri: subject_uri), Company(uri: object_uri);
WorksAtRaw(subject_uri: 'http://example.org/hr#alice', object_uri: 'http://example.org/hr#acme');
WorksAtRaw(subject_uri: 'http://example.org/hr#carol', object_uri: 'http://example.org/hr#acme');
Generated SQL and execution results
$ synalog.check('ontology.l')
No errors found.

$ synalog.compile('ontology.l', 'Class')
-- Initializing DuckDB environment.
create schema if not exists logica_home;
-- Empty record, has to have a field by DuckDB syntax.
drop type if exists logicarecord893574736 cascade; create type logicarecord893574736 as struct(nirvana numeric);
create sequence if not exists eternal_logical_sequence;

WITH t_0_ClassRaw AS (SELECT * FROM (

    SELECT
      E'http://example.org/hr#Company' AS uri
   UNION ALL

    SELECT
      E'http://example.org/hr#Employee' AS uri
   UNION ALL

    SELECT
      E'http://example.org/hr#Manager' AS uri
   UNION ALL

    SELECT
      E'http://example.org/hr#Person' AS uri

) AS UNUSED_TABLE_NAME  )
SELECT
  ClassRaw.uri AS uri
FROM
  t_0_ClassRaw AS ClassRaw
GROUP BY ClassRaw.uri ORDER BY uri;

-- Executed on DuckDB:
| uri                            |
|--------------------------------|
| http://example.org/hr#Company  |
| http://example.org/hr#Employee |
| http://example.org/hr#Manager  |
| http://example.org/hr#Person   |
(4 rows)

$ synalog.compile('ontology.l', 'SubClassOf')
-- Initializing DuckDB environment.
create schema if not exists logica_home;
-- Empty record, has to have a field by DuckDB syntax.
drop type if exists logicarecord893574736 cascade; create type logicarecord893574736 as struct(nirvana numeric);
create sequence if not exists eternal_logical_sequence;

CREATE OR REPLACE TABLE logica_home.SubClassOf_ifr0 AS WITH t_2_SubClassOfRaw AS (SELECT * FROM (

    SELECT
      E'http://example.org/hr#Employee' AS child_uri,
      E'http://example.org/hr#Person' AS parent_uri
   UNION ALL

    SELECT
      E'http://example.org/hr#Manager' AS child_uri,
      E'http://example.org/hr#Employee' AS parent_uri

) AS UNUSED_TABLE_NAME  ),
t_4_ClassRaw AS (SELECT * FROM (

    SELECT
      E'http://example.org/hr#Company' AS uri
   UNION ALL

    SELECT
      E'http://example.org/hr#Employee' AS uri
   UNION ALL

    SELECT
      E'http://example.org/hr#Manager' AS uri
   UNION ALL

    SELECT
      E'http://example.org/hr#Person' AS uri

) AS UNUSED_TABLE_NAME  ),
t_3_Class AS (SELECT
  ClassRaw.uri AS uri
FROM
  t_4_ClassRaw AS ClassRaw
GROUP BY ClassRaw.uri ORDER BY uri),
t_0_SubClassOf_MultBodyAggAux_f1 AS (SELECT * FROM (

    SELECT
      SubClassOfRaw.child_uri AS child_uri,
      SubClassOfRaw.parent_uri AS parent_uri
    FROM
      t_2_SubClassOfRaw AS SubClassOfRaw, t_3_Class AS Class, t_3_Class AS t_1_Class
    WHERE
      (Class.uri = SubClassOfRaw.child_uri) AND
      (t_1_Class.uri = SubClassOfRaw.parent_uri)

) AS UNUSED_TABLE_NAME  )
SELECT
  SubClassOf_MultBodyAggAux_f1.child_uri AS child_uri,
  SubClassOf_MultBodyAggAux_f1.parent_uri AS parent_uri
FROM
  t_0_SubClassOf_MultBodyAggAux_f1 AS SubClassOf_MultBodyAggAux_f1
GROUP BY SubClassOf_MultBodyAggAux_f1.child_uri, SubClassOf_MultBodyAggAux_f1.parent_uri;

-- Interacting with table logica_home.SubClassOf_ifr0

CREATE OR REPLACE TABLE logica_home.SubClassOf_ifr1 AS WITH t_2_SubClassOfRaw AS (SELECT * FROM (

    SELECT
      E'http://example.org/hr#Employee' AS child_uri,
      E'http://example.org/hr#Person' AS parent_uri
   UNION ALL

    SELECT
      E'http://example.org/hr#Manager' AS child_uri,
      E'http://example.org/hr#Employee' AS parent_uri

) AS UNUSED_TABLE_NAME  ),
t_4_ClassRaw AS (SELECT * FROM (

    SELECT
      E'http://example.org/hr#Company' AS uri
   UNION ALL

    SELECT
      E'http://example.org/hr#Employee' AS uri
   UNION ALL

    SELECT
      E'http://example.org/hr#Manager' AS uri
   UNION ALL

    SELECT
      E'http://example.org/hr#Person' AS uri

) AS UNUSED_TABLE_NAME  ),
t_3_Class AS (SELECT
  ClassRaw.uri AS uri
FROM
  t_4_ClassRaw AS ClassRaw
GROUP BY ClassRaw.uri ORDER BY uri),
t_0_SubClassOf_MultBodyAggAux_f2 AS (SELECT * FROM (

    SELECT
      SubClassOfRaw.child_uri AS child_uri,
      SubClassOfRaw.parent_uri AS parent_uri
    FROM
      t_2_SubClassOfRaw AS SubClassOfRaw, t_3_Class AS Class, t_3_Class AS t_1_Class
    WHERE
      (Class.uri = SubClassOfRaw.child_uri) AND
      (t_1_Class.uri = SubClassOfRaw.parent_uri)
   UNION ALL

    SELECT
      SubClassOf_ifr0.child_uri AS child_uri,
      t_2_SubClassOfRaw.parent_uri AS parent_uri
    FROM
      logica_home.SubClassOf_ifr0 AS SubClassOf_ifr0, t_2_SubClassOfRaw
    WHERE
      (t_2_SubClassOfRaw.child_uri = SubClassOf_ifr0.parent_uri)

) AS UNUSED_TABLE_NAME  )
SELECT
  SubClassOf_MultBodyAggAux_f2.child_uri AS child_uri,
  SubClassOf_MultBodyAggAux_f2.parent_uri AS parent_uri
FROM
  t_0_SubClassOf_MultBodyAggAux_f2 AS SubClassOf_MultBodyAggAux_f2
GROUP BY SubClassOf_MultBodyAggAux_f2.child_uri, SubClassOf_MultBodyAggAux_f2.parent_uri;

-- Interacting with table logica_home.SubClassOf_ifr1

CREATE OR REPLACE TABLE logica_home.SubClassOf_ifr2 AS WITH t_2_SubClassOfRaw AS (SELECT * FROM (

    SELECT
      E'http://example.org/hr#Employee' AS child_uri,
      E'http://example.org/hr#Person' AS parent_uri
   UNION ALL

    SELECT
      E'http://example.org/hr#Manager' AS child_uri,
      E'http://example.org/hr#Employee' AS parent_uri

) AS UNUSED_TABLE_NAME  ),
t_4_ClassRaw AS (SELECT * FROM (

    SELECT
      E'http://example.org/hr#Company' AS uri
   UNION ALL

    SELECT
      E'http://example.org/hr#Employee' AS uri
   UNION ALL

    SELECT
      E'http://example.org/hr#Manager' AS uri
   UNION ALL

    SELECT
      E'http://example.org/hr#Person' AS uri

) AS UNUSED_TABLE_NAME  ),
t_3_Class AS (SELECT
  ClassRaw.uri AS uri
FROM
  t_4_ClassRaw AS ClassRaw
GROUP BY ClassRaw.uri ORDER BY uri),
t_0_SubClassOf_MultBodyAggAux_f3 AS (SELECT * FROM (

    SELECT
      SubClassOfRaw.child_uri AS child_uri,
      SubClassOfRaw.parent_uri AS parent_uri
    FROM
      t_2_SubClassOfRaw AS SubClassOfRaw, t_3_Class AS Class, t_3_Class AS t_1_Class
    WHERE
      (Class.uri = SubClassOfRaw.child_uri) AND
      (t_1_Class.uri = SubClassOfRaw.parent_uri)
   UNION ALL

    SELECT
      SubClassOf_ifr1.child_uri AS child_uri,
      t_2_SubClassOfRaw.parent_uri AS parent_uri
    FROM
      logica_home.SubClassOf_ifr1 AS SubClassOf_ifr1, t_2_SubClassOfRaw
    WHERE
      (t_2_SubClassOfRaw.child_uri = SubClassOf_ifr1.parent_uri)

) AS UNUSED_TABLE_NAME  )
SELECT
  SubClassOf_MultBodyAggAux_f3.child_uri AS child_uri,
  SubClassOf_MultBodyAggAux_f3.parent_uri AS parent_uri
FROM
  t_0_SubClassOf_MultBodyAggAux_f3 AS SubClassOf_MultBodyAggAux_f3
GROUP BY SubClassOf_MultBodyAggAux_f3.child_uri, SubClassOf_MultBodyAggAux_f3.parent_uri;

-- Interacting with table logica_home.SubClassOf_ifr2

CREATE OR REPLACE TABLE logica_home.SubClassOf_ifr1 AS WITH t_2_SubClassOfRaw AS (SELECT * FROM (

    SELECT
      E'http://example.org/hr#Employee' AS child_uri,
      E'http://example.org/hr#Person' AS parent_uri
   UNION ALL

    SELECT
      E'http://example.org/hr#Manager' AS child_uri,
      E'http://example.org/hr#Employee' AS parent_uri

) AS UNUSED_TABLE_NAME  ),
t_4_ClassRaw AS (SELECT * FROM (

    SELECT
      E'http://example.org/hr#Company' AS uri
   UNION ALL

    SELECT
      E'http://example.org/hr#Employee' AS uri
   UNION ALL

    SELECT
      E'http://example.org/hr#Manager' AS uri
   UNION ALL

    SELECT
      E'http://example.org/hr#Person' AS uri

) AS UNUSED_TABLE_NAME  ),
t_3_Class AS (SELECT
  ClassRaw.uri AS uri
FROM
  t_4_ClassRaw AS ClassRaw
GROUP BY ClassRaw.uri ORDER BY uri),
t_0_SubClassOf_MultBodyAggAux_f4 AS (SELECT * FROM (

    SELECT
      SubClassOfRaw.child_uri AS child_uri,
      SubClassOfRaw.parent_uri AS parent_uri
    FROM
      t_2_SubClassOfRaw AS SubClassOfRaw, t_3_Class AS Class, t_3_Class AS t_1_Class
    WHERE
      (Class.uri = SubClassOfRaw.child_uri) AND
      (t_1_Class.uri = SubClassOfRaw.parent_uri)
   UNION ALL

    SELECT
      SubClassOf_ifr2.child_uri AS child_uri,
      t_2_SubClassOfRaw.parent_uri AS parent_uri
    FROM
      logica_home.SubClassOf_ifr2 AS SubClassOf_ifr2, t_2_SubClassOfRaw
    WHERE
      (t_2_SubClassOfRaw.child_uri = SubClassOf_ifr2.parent_uri)

) AS UNUSED_TABLE_NAME  )
SELECT
  SubClassOf_MultBodyAggAux_f4.child_uri AS child_uri,
  SubClassOf_MultBodyAggAux_f4.parent_uri AS parent_uri
FROM
  t_0_SubClassOf_MultBodyAggAux_f4 AS SubClassOf_MultBodyAggAux_f4
GROUP BY SubClassOf_MultBodyAggAux_f4.child_uri, SubClassOf_MultBodyAggAux_f4.parent_uri;

-- Interacting with table logica_home.SubClassOf_ifr1

WITH t_2_SubClassOfRaw AS (SELECT * FROM (

    SELECT
      E'http://example.org/hr#Employee' AS child_uri,
      E'http://example.org/hr#Person' AS parent_uri
   UNION ALL

    SELECT
      E'http://example.org/hr#Manager' AS child_uri,
      E'http://example.org/hr#Employee' AS parent_uri

) AS UNUSED_TABLE_NAME  ),
t_4_ClassRaw AS (SELECT * FROM (

    SELECT
      E'http://example.org/hr#Company' AS uri
   UNION ALL

    SELECT
      E'http://example.org/hr#Employee' AS uri
   UNION ALL

    SELECT
      E'http://example.org/hr#Manager' AS uri
   UNION ALL

    SELECT
      E'http://example.org/hr#Person' AS uri

) AS UNUSED_TABLE_NAME  ),
t_3_Class AS (SELECT
  ClassRaw.uri AS uri
FROM
  t_4_ClassRaw AS ClassRaw
GROUP BY ClassRaw.uri ORDER BY uri),
t_0_SubClassOf_MultBodyAggAux_f5 AS (SELECT * FROM (

    SELECT
      SubClassOfRaw.child_uri AS child_uri,
      SubClassOfRaw.parent_uri AS parent_uri
    FROM
      t_2_SubClassOfRaw AS SubClassOfRaw, t_3_Class AS Class, t_3_Class AS t_1_Class
    WHERE
      (Class.uri = SubClassOfRaw.child_uri) AND
      (t_1_Class.uri = SubClassOfRaw.parent_uri)
   UNION ALL

    SELECT
      SubClassOf_ifr3.child_uri AS child_uri,
      t_6_SubClassOfRaw.parent_uri AS parent_uri
    FROM
      logica_home.SubClassOf_ifr1 AS SubClassOf_ifr3, t_2_SubClassOfRaw AS t_6_SubClassOfRaw
    WHERE
      (t_6_SubClassOfRaw.child_uri = SubClassOf_ifr3.parent_uri)

) AS UNUSED_TABLE_NAME  )
SELECT
  SubClassOf_MultBodyAggAux_f5.child_uri AS child_uri,
  SubClassOf_MultBodyAggAux_f5.parent_uri AS parent_uri
FROM
  t_0_SubClassOf_MultBodyAggAux_f5 AS SubClassOf_MultBodyAggAux_f5
GROUP BY SubClassOf_MultBodyAggAux_f5.child_uri, SubClassOf_MultBodyAggAux_f5.parent_uri;

-- Executed on DuckDB:
| child_uri                      | parent_uri                     |
|--------------------------------|--------------------------------|
| http://example.org/hr#Manager  | http://example.org/hr#Person   |
| http://example.org/hr#Manager  | http://example.org/hr#Employee |
| http://example.org/hr#Employee | http://example.org/hr#Person   |
(3 rows)

$ synalog.compile('ontology.l', 'Person')
-- Initializing DuckDB environment.
create schema if not exists logica_home;
-- Empty record, has to have a field by DuckDB syntax.
drop type if exists logicarecord893574736 cascade; create type logicarecord893574736 as struct(nirvana numeric);
create sequence if not exists eternal_logical_sequence;

SELECT
  E'http://example.org/hr#bob' AS uri,
  null AS email,
  E'Bob' AS name
FROM
  (SELECT 'singleton' as s) as unused_singleton
GROUP BY (E'http://example.org/hr#bob' || ''), null, (E'Bob' || '') ORDER BY uri;

-- Executed on DuckDB:
| uri                       | email | name |
|---------------------------|-------|------|
| http://example.org/hr#bob |       | Bob  |
(1 row)

$ synalog.compile('ontology.l', 'Employee')
-- Initializing DuckDB environment.
create schema if not exists logica_home;
-- Empty record, has to have a field by DuckDB syntax.
drop type if exists logicarecord893574736 cascade; create type logicarecord893574736 as struct(nirvana numeric);
create sequence if not exists eternal_logical_sequence;

SELECT
  E'http://example.org/hr#alice' AS uri,
  E'alice@acme.example' AS email,
  E'Alice O''Hara' AS name,
  90000 AS salary
FROM
  (SELECT 'singleton' as s) as unused_singleton
GROUP BY (E'http://example.org/hr#alice' || ''), (E'alice@acme.example' || ''), (E'Alice O''Hara' || ''), 90000 + 0 ORDER BY uri;

-- Executed on DuckDB:
| uri                         | email              | name         | salary |
|-----------------------------|--------------------|--------------|--------|
| http://example.org/hr#alice | alice@acme.example | Alice O'Hara | 90000  |
(1 row)

$ synalog.compile('ontology.l', 'Manager')
-- Initializing DuckDB environment.
create schema if not exists logica_home;
-- Empty record, has to have a field by DuckDB syntax.
drop type if exists logicarecord893574736 cascade; create type logicarecord893574736 as struct(nirvana numeric);
create sequence if not exists eternal_logical_sequence;

SELECT
  E'http://example.org/hr#carol' AS uri,
  E'carol@acme.example' AS email,
  E'Carol' AS name,
  150000 AS salary
FROM
  (SELECT 'singleton' as s) as unused_singleton
GROUP BY (E'http://example.org/hr#carol' || ''), (E'carol@acme.example' || ''), (E'Carol' || ''), 150000 + 0 ORDER BY uri;

-- Executed on DuckDB:
| uri                         | email              | name  | salary |
|-----------------------------|--------------------|-------|--------|
| http://example.org/hr#carol | carol@acme.example | Carol | 150000 |
(1 row)

$ synalog.compile('ontology.l', 'Company')
-- Initializing DuckDB environment.
create schema if not exists logica_home;
-- Empty record, has to have a field by DuckDB syntax.
drop type if exists logicarecord893574736 cascade; create type logicarecord893574736 as struct(nirvana numeric);
create sequence if not exists eternal_logical_sequence;

SELECT
  E'http://example.org/hr#acme' AS uri,
  E'Acme "Robotics"' AS name
FROM
  (SELECT 'singleton' as s) as unused_singleton
GROUP BY (E'http://example.org/hr#acme' || ''), (E'Acme "Robotics"' || '') ORDER BY uri;

-- Executed on DuckDB:
| uri                        | name            |
|----------------------------|-----------------|
| http://example.org/hr#acme | Acme "Robotics" |
(1 row)

$ synalog.compile('ontology.l', 'WorksAt')
-- Initializing DuckDB environment.
create schema if not exists logica_home;
-- Empty record, has to have a field by DuckDB syntax.
drop type if exists logicarecord893574736 cascade; create type logicarecord893574736 as struct(nirvana numeric);
create sequence if not exists eternal_logical_sequence;

WITH t_0_WorksAtRaw AS (SELECT * FROM (

    SELECT
      E'http://example.org/hr#alice' AS subject_uri,
      E'http://example.org/hr#acme' AS object_uri
   UNION ALL

    SELECT
      E'http://example.org/hr#carol' AS subject_uri,
      E'http://example.org/hr#acme' AS object_uri

) AS UNUSED_TABLE_NAME  ),
t_1_Employee AS (SELECT
  E'http://example.org/hr#alice' AS uri,
  E'alice@acme.example' AS email,
  E'Alice O''Hara' AS name,
  90000 AS salary
FROM
  (SELECT 'singleton' as s) as unused_singleton
GROUP BY (E'http://example.org/hr#alice' || ''), (E'alice@acme.example' || ''), (E'Alice O''Hara' || ''), 90000 + 0 ORDER BY uri),
t_2_Company AS (SELECT
  E'http://example.org/hr#acme' AS uri,
  E'Acme "Robotics"' AS name
FROM
  (SELECT 'singleton' as s) as unused_singleton
GROUP BY (E'http://example.org/hr#acme' || ''), (E'Acme "Robotics"' || '') ORDER BY uri)
SELECT
  WorksAtRaw.subject_uri AS subject_uri,
  WorksAtRaw.object_uri AS object_uri
FROM
  t_0_WorksAtRaw AS WorksAtRaw, t_1_Employee AS Employee, t_2_Company AS Company
WHERE
  (Employee.uri = WorksAtRaw.subject_uri) AND
  (Company.uri = WorksAtRaw.object_uri)
GROUP BY WorksAtRaw.subject_uri, WorksAtRaw.object_uri ORDER BY subject_uri;

-- Executed on DuckDB:
| subject_uri                 | object_uri                 |
|-----------------------------|----------------------------|
| http://example.org/hr#alice | http://example.org/hr#acme |
(1 row)

$ synalog.compile('ontology.l', 'Knows')
-- Initializing DuckDB environment.
create schema if not exists logica_home;
-- Empty record, has to have a field by DuckDB syntax.
drop type if exists logicarecord893574736 cascade; create type logicarecord893574736 as struct(nirvana numeric);
create sequence if not exists eternal_logical_sequence;

WITH t_1_KnowsRaw AS (SELECT * FROM (

    SELECT
      E'http://example.org/hr#alice' AS subject_uri,
      E'http://example.org/hr#bob' AS object_uri
   UNION ALL

    SELECT
      E'http://example.org/hr#bob' AS subject_uri,
      E'http://example.org/hr#alice' AS object_uri

) AS UNUSED_TABLE_NAME  ),
t_2_Person AS (SELECT
  E'http://example.org/hr#bob' AS uri,
  null AS email,
  E'Bob' AS name
FROM
  (SELECT 'singleton' as s) as unused_singleton
GROUP BY (E'http://example.org/hr#bob' || ''), null, (E'Bob' || '') ORDER BY uri)
SELECT
  KnowsRaw.subject_uri AS subject_uri,
  KnowsRaw.object_uri AS object_uri
FROM
  t_1_KnowsRaw AS KnowsRaw, t_2_Person AS Person, t_2_Person AS t_0_Person
WHERE
  (Person.uri = KnowsRaw.subject_uri) AND
  (t_0_Person.uri = KnowsRaw.object_uri)
GROUP BY KnowsRaw.subject_uri, KnowsRaw.object_uri ORDER BY subject_uri;

-- Executed on DuckDB:
| subject_uri | object_uri |
|-------------|------------|
(0 rows)

$ synalog.compile('ontology.l', 'ReportsTo')
-- Initializing DuckDB environment.
create schema if not exists logica_home;
-- Empty record, has to have a field by DuckDB syntax.
drop type if exists logicarecord893574736 cascade; create type logicarecord893574736 as struct(nirvana numeric);
create sequence if not exists eternal_logical_sequence;

WITH t_0_Employee AS (SELECT
  E'http://example.org/hr#alice' AS uri,
  E'alice@acme.example' AS email,
  E'Alice O''Hara' AS name,
  90000 AS salary
FROM
  (SELECT 'singleton' as s) as unused_singleton
GROUP BY (E'http://example.org/hr#alice' || ''), (E'alice@acme.example' || ''), (E'Alice O''Hara' || ''), 90000 + 0 ORDER BY uri),
t_1_Manager AS (SELECT
  E'http://example.org/hr#carol' AS uri,
  E'carol@acme.example' AS email,
  E'Carol' AS name,
  150000 AS salary
FROM
  (SELECT 'singleton' as s) as unused_singleton
GROUP BY (E'http://example.org/hr#carol' || ''), (E'carol@acme.example' || ''), (E'Carol' || ''), 150000 + 0 ORDER BY uri)
SELECT
  Employee.uri AS subject_uri,
  Manager.uri AS object_uri
FROM
  t_0_Employee AS Employee, t_1_Manager AS Manager
WHERE
  (E'http://example.org/hr#alice' = Employee.uri) AND
  (E'http://example.org/hr#carol' = Manager.uri)
GROUP BY Employee.uri, Manager.uri ORDER BY subject_uri;

-- Executed on DuckDB:
| subject_uri                 | object_uri                  |
|-----------------------------|-----------------------------|
| http://example.org/hr#alice | http://example.org/hr#carol |
(1 row)

Every OWL axiom, by example

The HR ontology above stays deliberately simple. This second one is the opposite: it packs every OWL axiom and property characteristic import translates into a single file, so you can read the exact Synalog each produces. A few assertions are intentionally inconsistent (flagged inline) so the generated *Violation concepts return rows:

# An ontology that exercises EVERY OWL axiom and property characteristic
# `synalog import` translates. `synalog import axioms.ttl > axioms.l` turns each
# construct below into the matching Synalog rule pattern (closures, derived
# inverses, constraint checks). A few assertions are deliberately inconsistent
# (flagged inline) so the generated *Violation concepts return rows.
@prefix :     <http://example.org/co#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd:  <http://www.w3.org/2001/XMLSchema#> .

# --- Classes ---------------------------------------------------------------
:Agent        a owl:Class .
:Person       a owl:Class ; rdfs:subClassOf :Agent .          # rdfs:subClassOf
:Organization a owl:Class ; rdfs:subClassOf :Agent .
:Human        a owl:Class ; owl:equivalentClass :Person .     # owl:equivalentClass
:Desk         a owl:Class .

:Employee   a owl:Class ; rdfs:subClassOf :Person .
:Contractor a owl:Class ; rdfs:subClassOf :Person .
:Employee   owl:disjointWith :Contractor .                    # owl:disjointWith

# --- Datatype properties (become node columns) -----------------------------
:name  a owl:DatatypeProperty ; rdfs:domain :Agent  ; rdfs:range xsd:string .
:email a owl:DatatypeProperty ; rdfs:domain :Person ; rdfs:range xsd:string .

# --- Object properties + characteristics -----------------------------------
:ancestorOf     a owl:ObjectProperty, owl:TransitiveProperty ;          # transitive closure
                rdfs:domain :Person ; rdfs:range :Person .
:knows          a owl:ObjectProperty, owl:SymmetricProperty ;           # reverse unioned in
                rdfs:domain :Person ; rdfs:range :Person .
:acquaintedWith a owl:ObjectProperty ; owl:equivalentProperty :knows .  # shares knows' facts
:worksAt        a owl:ObjectProperty ; rdfs:domain :Person ; rdfs:range :Organization .
:employs        a owl:ObjectProperty ; owl:inverseOf :worksAt .         # derived opposite direction
:parentOf       a owl:ObjectProperty ; rdfs:domain :Person ; rdfs:range :Person .
:fatherOf       a owl:ObjectProperty ; rdfs:subPropertyOf :parentOf .   # facts flow up to parentOf
:sameTeamAs     a owl:ObjectProperty, owl:ReflexiveProperty ;           # (x, x) over the domain
                rdfs:domain :Person ; rdfs:range :Person .
:hasDesk        a owl:ObjectProperty,
                  owl:FunctionalProperty, owl:InverseFunctionalProperty ;  # one desk/person & vice-versa
                rdfs:domain :Person ; rdfs:range :Desk .
:supervises     a owl:ObjectProperty,
                  owl:AsymmetricProperty, owl:IrreflexiveProperty ;     # both-ways & self-loops are violations
                rdfs:domain :Person ; rdfs:range :Person .

# --- Individuals (the ABox) ------------------------------------------------
# A concept is keyed by the type an individual is asserted with (membership is
# data-driven, not inferred through subClassOf), so the people who take part in
# the Person→Person properties are typed :Person directly. :Employee / :Contractor
# carry a single individual each, only to demonstrate the owl:disjointWith check.
:acme a :Organization ; :name "Acme" .

:alice a :Person ; :name "Alice" ; :email "alice@acme.example" ;
       :worksAt :acme ; :knows :bob ;
       :ancestorOf :bob ; :fatherOf :carol ;
       :hasDesk :d1 , :d2 ;             # two desks — owl:FunctionalProperty violation
       :supervises :bob .
:bob   a :Person ; :name "Bob" ;
       :ancestorOf :carol ;            # alice→bob→carol entails alice ancestorOf carol
       :supervises :alice .            # alice⇄bob — owl:AsymmetricProperty violation
:carol a :Person ; :name "Carol" ;
       :supervises :carol .            # self-loop — owl:IrreflexiveProperty violation
:dave  a :Employee, :Contractor ;      # both disjoint classes — owl:disjointWith violation
       :name "Dave" .

:d1 a :Desk . :d2 a :Desk .

:alice owl:sameAs :alice2 .            # symmetric + transitive SameAs
:alice owl:differentFrom :bob .        # asserted distinct (consistent here)

synalog import axioms.ttl turns it into the program below; the rest of this section walks through it construct by construct.

# run: AncestorOf, Knows, Employs, ParentOf, SameTeamAs, HasDeskFunctionalViolation, SupervisesAsymmetricViolation, SupervisesIrreflexiveViolation, DisjointWithViolation, SameAs
# Generated by `synalog import` from an OWL/RDF ontology.
# 7 classes, 10 object properties, 7 individuals.

# Schema — every class as a concept (the TBox)
@OrderBy(Class, "uri");
Class(uri:) distinct :- ClassRaw(uri:);
ClassRaw(uri: 'http://example.org/co#Agent');
ClassRaw(uri: 'http://example.org/co#Contractor');
ClassRaw(uri: 'http://example.org/co#Desk');
ClassRaw(uri: 'http://example.org/co#Employee');
ClassRaw(uri: 'http://example.org/co#Human');
ClassRaw(uri: 'http://example.org/co#Organization');
ClassRaw(uri: 'http://example.org/co#Person');

# Class hierarchy (transitive subClassOf / equivalentClass), joined through Class
@Recursive(SubClassOf, 100);
SubClassOf(child_uri:, parent_uri:) distinct :- SubClassOfRaw(child_uri:, parent_uri:), Class(uri: child_uri), Class(uri: parent_uri);
SubClassOf(child_uri:, parent_uri:) distinct :- SubClassOf(child_uri:, parent_uri: mid), SubClassOfRaw(child_uri: mid, parent_uri:);
SubClassOfRaw(child_uri: 'http://example.org/co#Contractor', parent_uri: 'http://example.org/co#Person');
SubClassOfRaw(child_uri: 'http://example.org/co#Employee', parent_uri: 'http://example.org/co#Person');
SubClassOfRaw(child_uri: 'http://example.org/co#Human', parent_uri: 'http://example.org/co#Person');
SubClassOfRaw(child_uri: 'http://example.org/co#Organization', parent_uri: 'http://example.org/co#Agent');
SubClassOfRaw(child_uri: 'http://example.org/co#Person', parent_uri: 'http://example.org/co#Agent');
SubClassOfRaw(child_uri: 'http://example.org/co#Person', parent_uri: 'http://example.org/co#Human');

# Concepts — classes that have individuals

@OrderBy(Contractor, "uri");
Contractor(uri:, name:) distinct :- ContractorRaw(uri:, name:);
ContractorRaw(uri: 'http://example.org/co#dave', name: 'Dave');

@OrderBy(Desk, "uri");
Desk(uri:) distinct :- DeskRaw(uri:);
DeskRaw(uri: 'http://example.org/co#d1');
DeskRaw(uri: 'http://example.org/co#d2');

@OrderBy(Employee, "uri");
Employee(uri:, name:) distinct :- EmployeeRaw(uri:, name:);
EmployeeRaw(uri: 'http://example.org/co#dave', name: 'Dave');

@OrderBy(Organization, "uri");
Organization(uri:, name:) distinct :- OrganizationRaw(uri:, name:);
OrganizationRaw(uri: 'http://example.org/co#acme', name: 'Acme');

@OrderBy(Person, "uri");
Person(uri:, email:, name:) distinct :- PersonRaw(uri:, email:, name:);
PersonRaw(uri: 'http://example.org/co#alice', email: 'alice@acme.example', name: 'Alice');
PersonRaw(uri: 'http://example.org/co#bob', email: null, name: 'Bob');
PersonRaw(uri: 'http://example.org/co#carol', email: null, name: 'Carol');

# Concepts — object properties (relationships)

@OrderBy(AcquaintedWith, "subject_uri");
AcquaintedWith(subject_uri:, object_uri:) distinct :- KnowsRaw(subject_uri:, object_uri:);

@OrderBy(AncestorOfStep, "subject_uri");
AncestorOfStep(subject_uri:, object_uri:) distinct :-
  AncestorOfRaw(subject_uri:, object_uri:), Person(uri: subject_uri), Person(uri: object_uri);
@OrderBy(AncestorOf, "subject_uri");
@Recursive(AncestorOf, 100);
AncestorOf(subject_uri:, object_uri:) distinct :- AncestorOfStep(subject_uri:, object_uri:);
AncestorOf(subject_uri:, object_uri:) distinct :- AncestorOf(subject_uri:, object_uri: mid), AncestorOfStep(subject_uri: mid, object_uri:);
AncestorOfRaw(subject_uri: 'http://example.org/co#alice', object_uri: 'http://example.org/co#bob');
AncestorOfRaw(subject_uri: 'http://example.org/co#bob', object_uri: 'http://example.org/co#carol');

@OrderBy(Employs, "subject_uri");
Employs(subject_uri:, object_uri:) distinct :- WorksAtRaw(subject_uri: object_uri, object_uri: subject_uri);

@OrderBy(FatherOf, "subject_uri");
FatherOf(subject_uri:, object_uri:) distinct :- FatherOfRaw(subject_uri:, object_uri:);
FatherOfRaw(subject_uri: 'http://example.org/co#alice', object_uri: 'http://example.org/co#carol');

@OrderBy(HasDesk, "subject_uri");
HasDesk(subject_uri:, object_uri:) distinct :- HasDeskRaw(subject_uri:, object_uri:), Person(uri: subject_uri), Desk(uri: object_uri);
HasDeskRaw(subject_uri: 'http://example.org/co#alice', object_uri: 'http://example.org/co#d1');
HasDeskRaw(subject_uri: 'http://example.org/co#alice', object_uri: 'http://example.org/co#d2');
@OrderBy(HasDeskFunctionalViolation, "subject_uri");
HasDeskFunctionalViolation(subject_uri:) distinct :- HasDesk(subject_uri:, object_uri: value_a), HasDesk(subject_uri:, object_uri: value_b), value_a != value_b;
@OrderBy(HasDeskInverseFunctionalViolation, "object_uri");
HasDeskInverseFunctionalViolation(object_uri:) distinct :- HasDesk(subject_uri: subject_a, object_uri:), HasDesk(subject_uri: subject_b, object_uri:), subject_a != subject_b;

@OrderBy(Knows, "subject_uri");
Knows(subject_uri:, object_uri:) distinct :-
  KnowsRaw(subject_uri:, object_uri:), Person(uri: subject_uri), Person(uri: object_uri) |
  KnowsRaw(subject_uri: object_uri, object_uri: subject_uri), Person(uri: subject_uri), Person(uri: object_uri);
KnowsRaw(subject_uri: 'http://example.org/co#alice', object_uri: 'http://example.org/co#bob');

@OrderBy(ParentOf, "subject_uri");
ParentOf(subject_uri:, object_uri:) distinct :- FatherOfRaw(subject_uri:, object_uri:), Person(uri: subject_uri), Person(uri: object_uri);

@OrderBy(SameTeamAs, "subject_uri");
SameTeamAs(subject_uri:, object_uri:) distinct :- Person(uri: subject_uri), object_uri == subject_uri;

@OrderBy(Supervises, "subject_uri");
Supervises(subject_uri:, object_uri:) distinct :- SupervisesRaw(subject_uri:, object_uri:), Person(uri: subject_uri), Person(uri: object_uri);
SupervisesRaw(subject_uri: 'http://example.org/co#alice', object_uri: 'http://example.org/co#bob');
SupervisesRaw(subject_uri: 'http://example.org/co#bob', object_uri: 'http://example.org/co#alice');
SupervisesRaw(subject_uri: 'http://example.org/co#carol', object_uri: 'http://example.org/co#carol');
@OrderBy(SupervisesAsymmetricViolation, "subject_uri");
SupervisesAsymmetricViolation(subject_uri:, object_uri:) distinct :- Supervises(subject_uri:, object_uri:), Supervises(subject_uri: object_uri, object_uri: subject_uri);
@OrderBy(SupervisesIrreflexiveViolation, "uri");
SupervisesIrreflexiveViolation(uri:) distinct :- Supervises(subject_uri: uri, object_uri: uri);

@OrderBy(WorksAt, "subject_uri");
WorksAt(subject_uri:, object_uri:) distinct :- WorksAtRaw(subject_uri:, object_uri:), Person(uri: subject_uri), Organization(uri: object_uri);
WorksAtRaw(subject_uri: 'http://example.org/co#alice', object_uri: 'http://example.org/co#acme');

# owl:sameAs — symmetric, transitive individual equivalence
@OrderBy(SameAs, "left_uri");
@Recursive(SameAs, 100);
SameAs(left_uri:, right_uri:) distinct :-
  SameAsRaw(left_uri:, right_uri:) |
  SameAsRaw(left_uri: right_uri, right_uri: left_uri);
SameAs(left_uri:, right_uri:) distinct :- SameAs(left_uri:, right_uri: mid), SameAs(left_uri: mid, right_uri:);
SameAsRaw(left_uri: 'http://example.org/co#alice', right_uri: 'http://example.org/co#alice2');
@OrderBy(DifferentFromViolation, "left_uri");
DifferentFromViolation(left_uri:, right_uri:) distinct :- SameAs(left_uri:, right_uri:), DifferentFromRaw(left_uri:, right_uri:);
DifferentFromRaw(left_uri: 'http://example.org/co#alice', right_uri: 'http://example.org/co#bob');

# owl:disjointWith — an individual typed by two disjoint classes
@OrderBy(DisjointWithViolation, "uri");
DisjointWithViolation(uri:, class_a:, class_b:) distinct :-
  Contractor(uri:), Employee(uri:), class_a == 'http://example.org/co#Contractor', class_b == 'http://example.org/co#Employee';

Class axioms

rdfs:subClassOf and owl:equivalentClass both feed one recursive SubClassOf — an equivalence is just subclassing both ways (HumanPerson and PersonHuman):

:Person a owl:Class ; rdfs:subClassOf :Agent .
:Human  a owl:Class ; owl:equivalentClass :Person .
@Recursive(SubClassOf, 100);
SubClassOf(child_uri:, parent_uri:) distinct :- SubClassOfRaw(child_uri:, parent_uri:), Class(uri: child_uri), Class(uri: parent_uri);
SubClassOf(child_uri:, parent_uri:) distinct :- SubClassOf(child_uri:, parent_uri: mid), SubClassOfRaw(child_uri: mid, parent_uri:);
SubClassOfRaw(child_uri: 'http://example.org/co#Person', parent_uri: 'http://example.org/co#Agent');
SubClassOfRaw(child_uri: 'http://example.org/co#Human', parent_uri: 'http://example.org/co#Person');
SubClassOfRaw(child_uri: 'http://example.org/co#Person', parent_uri: 'http://example.org/co#Human');

owl:disjointWith becomes a check — an individual that lands in both classes is a contradiction. :dave is typed as both, so the concept returns him:

:Employee owl:disjointWith :Contractor .
:dave a :Employee, :Contractor .
@OrderBy(DisjointWithViolation, "uri");
DisjointWithViolation(uri:, class_a:, class_b:) distinct :-
  Contractor(uri:), Employee(uri:), class_a == 'http://example.org/co#Contractor', class_b == 'http://example.org/co#Employee';

Property characteristics

owl:TransitiveProperty — a one-step …Step predicate plus its @Recursive closure, so alice ancestorOf bob and bob ancestorOf carol entail alice ancestorOf carol:

:ancestorOf a owl:ObjectProperty, owl:TransitiveProperty ; rdfs:domain :Person ; rdfs:range :Person .
AncestorOfStep(subject_uri:, object_uri:) distinct :-
  AncestorOfRaw(subject_uri:, object_uri:), Person(uri: subject_uri), Person(uri: object_uri);
@Recursive(AncestorOf, 100);
AncestorOf(subject_uri:, object_uri:) distinct :- AncestorOfStep(subject_uri:, object_uri:);
AncestorOf(subject_uri:, object_uri:) distinct :- AncestorOf(subject_uri:, object_uri: mid), AncestorOfStep(subject_uri: mid, object_uri:);

owl:SymmetricProperty — the reverse direction is unioned in, so the single asserted alice knows bob yields both directions:

Knows(subject_uri:, object_uri:) distinct :-
  KnowsRaw(subject_uri:, object_uri:), Person(uri: subject_uri), Person(uri: object_uri) |
  KnowsRaw(subject_uri: object_uri, object_uri: subject_uri), Person(uri: subject_uri), Person(uri: object_uri);

owl:ReflexiveProperty(x, x) for every individual in the domain:

SameTeamAs(subject_uri:, object_uri:) distinct :- Person(uri: subject_uri), object_uri == subject_uri;

owl:FunctionalProperty / owl:InverseFunctionalProperty — a check for a subject (resp. object) that appears with two different values. :alice has two desks, so the functional check flags her:

:hasDesk a owl:ObjectProperty, owl:FunctionalProperty, owl:InverseFunctionalProperty ;
         rdfs:domain :Person ; rdfs:range :Desk .
HasDeskFunctionalViolation(subject_uri:) distinct :- HasDesk(subject_uri:, object_uri: value_a), HasDesk(subject_uri:, object_uri: value_b), value_a != value_b;
HasDeskInverseFunctionalViolation(object_uri:) distinct :- HasDesk(subject_uri: subject_a, object_uri:), HasDesk(subject_uri: subject_b, object_uri:), subject_a != subject_b;

owl:AsymmetricProperty / owl:IrreflexiveProperty — a check for a pair that holds both ways (resp. a self-loop). :alice and :bob supervise each other and :carol supervises herself, so both fire:

:supervises a owl:ObjectProperty, owl:AsymmetricProperty, owl:IrreflexiveProperty ;
            rdfs:domain :Person ; rdfs:range :Person .
SupervisesAsymmetricViolation(subject_uri:, object_uri:) distinct :- Supervises(subject_uri:, object_uri:), Supervises(subject_uri: object_uri, object_uri: subject_uri);
SupervisesIrreflexiveViolation(uri:) distinct :- Supervises(subject_uri: uri, object_uri: uri);

Relations between properties

owl:inverseOfemploys derives its rows by swapping the subject and object of worksAt's facts:

:employs a owl:ObjectProperty ; owl:inverseOf :worksAt .
Employs(subject_uri:, object_uri:) distinct :- WorksAtRaw(subject_uri: object_uri, object_uri: subject_uri);

rdfs:subPropertyOffatherOf's facts flow up into parentOf:

:fatherOf a owl:ObjectProperty ; rdfs:subPropertyOf :parentOf .
ParentOf(subject_uri:, object_uri:) distinct :- FatherOfRaw(subject_uri:, object_uri:), Person(uri: subject_uri), Person(uri: object_uri);

owl:equivalentPropertyacquaintedWith includes knows's facts:

:acquaintedWith a owl:ObjectProperty ; owl:equivalentProperty :knows .
AcquaintedWith(subject_uri:, object_uri:) distinct :- KnowsRaw(subject_uri:, object_uri:);

Individual axioms

owl:sameAs — a symmetric, transitive SameAs concept (the same closure shape as a transitive property):

@Recursive(SameAs, 100);
SameAs(left_uri:, right_uri:) distinct :-
  SameAsRaw(left_uri:, right_uri:) |
  SameAsRaw(left_uri: right_uri, right_uri: left_uri);
SameAs(left_uri:, right_uri:) distinct :- SameAs(left_uri:, right_uri: mid), SameAs(left_uri: mid, right_uri:);

owl:differentFrom — a check: two individuals asserted different yet inferred SameAs are inconsistent (none are, here):

DifferentFromViolation(left_uri:, right_uri:) distinct :- SameAs(left_uri:, right_uri:), DifferentFromRaw(left_uri:, right_uri:);

Running the headline predicates shows the closures filled in and the *Violation checks firing on the inconsistent rows:

Generated SQL and execution results
$ synalog.check('axioms.l')
No errors found.

$ synalog.compile('axioms.l', 'AncestorOf')
-- Initializing DuckDB environment.
create schema if not exists logica_home;
-- Empty record, has to have a field by DuckDB syntax.
drop type if exists logicarecord893574736 cascade; create type logicarecord893574736 as struct(nirvana numeric);
create sequence if not exists eternal_logical_sequence;

CREATE OR REPLACE TABLE logica_home.AncestorOf_ifr0 AS WITH t_3_AncestorOfRaw AS (SELECT * FROM (

    SELECT
      E'http://example.org/co#alice' AS subject_uri,
      E'http://example.org/co#bob' AS object_uri
   UNION ALL

    SELECT
      E'http://example.org/co#bob' AS subject_uri,
      E'http://example.org/co#carol' AS object_uri

) AS UNUSED_TABLE_NAME  ),
t_5_PersonRaw AS (SELECT * FROM (

    SELECT
      E'http://example.org/co#alice' AS uri,
      E'alice@acme.example' AS email,
      E'Alice' AS name
   UNION ALL

    SELECT
      E'http://example.org/co#bob' AS uri,
      null AS email,
      E'Bob' AS name
   UNION ALL

    SELECT
      E'http://example.org/co#carol' AS uri,
      null AS email,
      E'Carol' AS name

) AS UNUSED_TABLE_NAME  ),
t_4_Person AS (SELECT
  PersonRaw.uri AS uri,
  PersonRaw.email AS email,
  PersonRaw.name AS name
FROM
  t_5_PersonRaw AS PersonRaw
GROUP BY PersonRaw.uri, PersonRaw.email, PersonRaw.name ORDER BY uri),
t_1_AncestorOfStep AS (SELECT
  AncestorOfRaw.subject_uri AS subject_uri,
  AncestorOfRaw.object_uri AS object_uri
FROM
  t_3_AncestorOfRaw AS AncestorOfRaw, t_4_Person AS Person, t_4_Person AS t_2_Person
WHERE
  (Person.uri = AncestorOfRaw.subject_uri) AND
  (t_2_Person.uri = AncestorOfRaw.object_uri)
GROUP BY AncestorOfRaw.subject_uri, AncestorOfRaw.object_uri ORDER BY subject_uri),
t_0_AncestorOf_MultBodyAggAux_f1 AS (SELECT * FROM (

    SELECT
      AncestorOfStep.subject_uri AS subject_uri,
      AncestorOfStep.object_uri AS object_uri
    FROM
      t_1_AncestorOfStep AS AncestorOfStep

) AS UNUSED_TABLE_NAME  )
SELECT
  AncestorOf_MultBodyAggAux_f1.subject_uri AS subject_uri,
  AncestorOf_MultBodyAggAux_f1.object_uri AS object_uri
FROM
  t_0_AncestorOf_MultBodyAggAux_f1 AS AncestorOf_MultBodyAggAux_f1
GROUP BY AncestorOf_MultBodyAggAux_f1.subject_uri, AncestorOf_MultBodyAggAux_f1.object_uri ORDER BY subject_uri;

-- Interacting with table logica_home.AncestorOf_ifr0

CREATE OR REPLACE TABLE logica_home.AncestorOf_ifr1 AS WITH t_3_AncestorOfRaw AS (SELECT * FROM (

    SELECT
      E'http://example.org/co#alice' AS subject_uri,
      E'http://example.org/co#bob' AS object_uri
   UNION ALL

    SELECT
      E'http://example.org/co#bob' AS subject_uri,
      E'http://example.org/co#carol' AS object_uri

) AS UNUSED_TABLE_NAME  ),
t_5_PersonRaw AS (SELECT * FROM (

    SELECT
      E'http://example.org/co#alice' AS uri,
      E'alice@acme.example' AS email,
      E'Alice' AS name
   UNION ALL

    SELECT
      E'http://example.org/co#bob' AS uri,
      null AS email,
      E'Bob' AS name
   UNION ALL

    SELECT
      E'http://example.org/co#carol' AS uri,
      null AS email,
      E'Carol' AS name

) AS UNUSED_TABLE_NAME  ),
t_4_Person AS (SELECT
  PersonRaw.uri AS uri,
  PersonRaw.email AS email,
  PersonRaw.name AS name
FROM
  t_5_PersonRaw AS PersonRaw
GROUP BY PersonRaw.uri, PersonRaw.email, PersonRaw.name ORDER BY uri),
t_1_AncestorOfStep AS (SELECT
  AncestorOfRaw.subject_uri AS subject_uri,
  AncestorOfRaw.object_uri AS object_uri
FROM
  t_3_AncestorOfRaw AS AncestorOfRaw, t_4_Person AS Person, t_4_Person AS t_2_Person
WHERE
  (Person.uri = AncestorOfRaw.subject_uri) AND
  (t_2_Person.uri = AncestorOfRaw.object_uri)
GROUP BY AncestorOfRaw.subject_uri, AncestorOfRaw.object_uri ORDER BY subject_uri),
t_0_AncestorOf_MultBodyAggAux_f2 AS (SELECT * FROM (

    SELECT
      AncestorOfStep.subject_uri AS subject_uri,
      AncestorOfStep.object_uri AS object_uri
    FROM
      t_1_AncestorOfStep AS AncestorOfStep
   UNION ALL

    SELECT
      AncestorOf_ifr0.subject_uri AS subject_uri,
      t_2_AncestorOfStep.object_uri AS object_uri
    FROM
      logica_home.AncestorOf_ifr0 AS AncestorOf_ifr0, t_1_AncestorOfStep AS t_2_AncestorOfStep
    WHERE
      (t_2_AncestorOfStep.subject_uri = AncestorOf_ifr0.object_uri)

) AS UNUSED_TABLE_NAME  )
SELECT
  AncestorOf_MultBodyAggAux_f2.subject_uri AS subject_uri,
  AncestorOf_MultBodyAggAux_f2.object_uri AS object_uri
FROM
  t_0_AncestorOf_MultBodyAggAux_f2 AS AncestorOf_MultBodyAggAux_f2
GROUP BY AncestorOf_MultBodyAggAux_f2.subject_uri, AncestorOf_MultBodyAggAux_f2.object_uri ORDER BY subject_uri;

-- Interacting with table logica_home.AncestorOf_ifr1

CREATE OR REPLACE TABLE logica_home.AncestorOf_ifr2 AS WITH t_3_AncestorOfRaw AS (SELECT * FROM (

    SELECT
      E'http://example.org/co#alice' AS subject_uri,
      E'http://example.org/co#bob' AS object_uri
   UNION ALL

    SELECT
      E'http://example.org/co#bob' AS subject_uri,
      E'http://example.org/co#carol' AS object_uri

) AS UNUSED_TABLE_NAME  ),
t_5_PersonRaw AS (SELECT * FROM (

    SELECT
      E'http://example.org/co#alice' AS uri,
      E'alice@acme.example' AS email,
      E'Alice' AS name
   UNION ALL

    SELECT
      E'http://example.org/co#bob' AS uri,
      null AS email,
      E'Bob' AS name
   UNION ALL

    SELECT
      E'http://example.org/co#carol' AS uri,
      null AS email,
      E'Carol' AS name

) AS UNUSED_TABLE_NAME  ),
t_4_Person AS (SELECT
  PersonRaw.uri AS uri,
  PersonRaw.email AS email,
  PersonRaw.name AS name
FROM
  t_5_PersonRaw AS PersonRaw
GROUP BY PersonRaw.uri, PersonRaw.email, PersonRaw.name ORDER BY uri),
t_1_AncestorOfStep AS (SELECT
  AncestorOfRaw.subject_uri AS subject_uri,
  AncestorOfRaw.object_uri AS object_uri
FROM
  t_3_AncestorOfRaw AS AncestorOfRaw, t_4_Person AS Person, t_4_Person AS t_2_Person
WHERE
  (Person.uri = AncestorOfRaw.subject_uri) AND
  (t_2_Person.uri = AncestorOfRaw.object_uri)
GROUP BY AncestorOfRaw.subject_uri, AncestorOfRaw.object_uri ORDER BY subject_uri),
t_0_AncestorOf_MultBodyAggAux_f3 AS (SELECT * FROM (

    SELECT
      AncestorOfStep.subject_uri AS subject_uri,
      AncestorOfStep.object_uri AS object_uri
    FROM
      t_1_AncestorOfStep AS AncestorOfStep
   UNION ALL

    SELECT
      AncestorOf_ifr1.subject_uri AS subject_uri,
      t_2_AncestorOfStep.object_uri AS object_uri
    FROM
      logica_home.AncestorOf_ifr1 AS AncestorOf_ifr1, t_1_AncestorOfStep AS t_2_AncestorOfStep
    WHERE
      (t_2_AncestorOfStep.subject_uri = AncestorOf_ifr1.object_uri)

) AS UNUSED_TABLE_NAME  )
SELECT
  AncestorOf_MultBodyAggAux_f3.subject_uri AS subject_uri,
  AncestorOf_MultBodyAggAux_f3.object_uri AS object_uri
FROM
  t_0_AncestorOf_MultBodyAggAux_f3 AS AncestorOf_MultBodyAggAux_f3
GROUP BY AncestorOf_MultBodyAggAux_f3.subject_uri, AncestorOf_MultBodyAggAux_f3.object_uri ORDER BY subject_uri;

-- Interacting with table logica_home.AncestorOf_ifr2

CREATE OR REPLACE TABLE logica_home.AncestorOf_ifr1 AS WITH t_3_AncestorOfRaw AS (SELECT * FROM (

    SELECT
      E'http://example.org/co#alice' AS subject_uri,
      E'http://example.org/co#bob' AS object_uri
   UNION ALL

    SELECT
      E'http://example.org/co#bob' AS subject_uri,
      E'http://example.org/co#carol' AS object_uri

) AS UNUSED_TABLE_NAME  ),
t_5_PersonRaw AS (SELECT * FROM (

    SELECT
      E'http://example.org/co#alice' AS uri,
      E'alice@acme.example' AS email,
      E'Alice' AS name
   UNION ALL

    SELECT
      E'http://example.org/co#bob' AS uri,
      null AS email,
      E'Bob' AS name
   UNION ALL

    SELECT
      E'http://example.org/co#carol' AS uri,
      null AS email,
      E'Carol' AS name

) AS UNUSED_TABLE_NAME  ),
t_4_Person AS (SELECT
  PersonRaw.uri AS uri,
  PersonRaw.email AS email,
  PersonRaw.name AS name
FROM
  t_5_PersonRaw AS PersonRaw
GROUP BY PersonRaw.uri, PersonRaw.email, PersonRaw.name ORDER BY uri),
t_1_AncestorOfStep AS (SELECT
  AncestorOfRaw.subject_uri AS subject_uri,
  AncestorOfRaw.object_uri AS object_uri
FROM
  t_3_AncestorOfRaw AS AncestorOfRaw, t_4_Person AS Person, t_4_Person AS t_2_Person
WHERE
  (Person.uri = AncestorOfRaw.subject_uri) AND
  (t_2_Person.uri = AncestorOfRaw.object_uri)
GROUP BY AncestorOfRaw.subject_uri, AncestorOfRaw.object_uri ORDER BY subject_uri),
t_0_AncestorOf_MultBodyAggAux_f4 AS (SELECT * FROM (

    SELECT
      AncestorOfStep.subject_uri AS subject_uri,
      AncestorOfStep.object_uri AS object_uri
    FROM
      t_1_AncestorOfStep AS AncestorOfStep
   UNION ALL

    SELECT
      AncestorOf_ifr2.subject_uri AS subject_uri,
      t_2_AncestorOfStep.object_uri AS object_uri
    FROM
      logica_home.AncestorOf_ifr2 AS AncestorOf_ifr2, t_1_AncestorOfStep AS t_2_AncestorOfStep
    WHERE
      (t_2_AncestorOfStep.subject_uri = AncestorOf_ifr2.object_uri)

) AS UNUSED_TABLE_NAME  )
SELECT
  AncestorOf_MultBodyAggAux_f4.subject_uri AS subject_uri,
  AncestorOf_MultBodyAggAux_f4.object_uri AS object_uri
FROM
  t_0_AncestorOf_MultBodyAggAux_f4 AS AncestorOf_MultBodyAggAux_f4
GROUP BY AncestorOf_MultBodyAggAux_f4.subject_uri, AncestorOf_MultBodyAggAux_f4.object_uri ORDER BY subject_uri;

-- Interacting with table logica_home.AncestorOf_ifr1

WITH t_3_AncestorOfRaw AS (SELECT * FROM (

    SELECT
      E'http://example.org/co#alice' AS subject_uri,
      E'http://example.org/co#bob' AS object_uri
   UNION ALL

    SELECT
      E'http://example.org/co#bob' AS subject_uri,
      E'http://example.org/co#carol' AS object_uri

) AS UNUSED_TABLE_NAME  ),
t_5_PersonRaw AS (SELECT * FROM (

    SELECT
      E'http://example.org/co#alice' AS uri,
      E'alice@acme.example' AS email,
      E'Alice' AS name
   UNION ALL

    SELECT
      E'http://example.org/co#bob' AS uri,
      null AS email,
      E'Bob' AS name
   UNION ALL

    SELECT
      E'http://example.org/co#carol' AS uri,
      null AS email,
      E'Carol' AS name

) AS UNUSED_TABLE_NAME  ),
t_4_Person AS (SELECT
  PersonRaw.uri AS uri,
  PersonRaw.email AS email,
  PersonRaw.name AS name
FROM
  t_5_PersonRaw AS PersonRaw
GROUP BY PersonRaw.uri, PersonRaw.email, PersonRaw.name ORDER BY uri),
t_1_AncestorOfStep AS (SELECT
  AncestorOfRaw.subject_uri AS subject_uri,
  AncestorOfRaw.object_uri AS object_uri
FROM
  t_3_AncestorOfRaw AS AncestorOfRaw, t_4_Person AS Person, t_4_Person AS t_2_Person
WHERE
  (Person.uri = AncestorOfRaw.subject_uri) AND
  (t_2_Person.uri = AncestorOfRaw.object_uri)
GROUP BY AncestorOfRaw.subject_uri, AncestorOfRaw.object_uri ORDER BY subject_uri),
t_0_AncestorOf_MultBodyAggAux_f5 AS (SELECT * FROM (

    SELECT
      AncestorOfStep.subject_uri AS subject_uri,
      AncestorOfStep.object_uri AS object_uri
    FROM
      t_1_AncestorOfStep AS AncestorOfStep
   UNION ALL

    SELECT
      AncestorOf_ifr3.subject_uri AS subject_uri,
      t_7_AncestorOfStep.object_uri AS object_uri
    FROM
      logica_home.AncestorOf_ifr1 AS AncestorOf_ifr3, t_1_AncestorOfStep AS t_7_AncestorOfStep
    WHERE
      (t_7_AncestorOfStep.subject_uri = AncestorOf_ifr3.object_uri)

) AS UNUSED_TABLE_NAME  )
SELECT
  AncestorOf_MultBodyAggAux_f5.subject_uri AS subject_uri,
  AncestorOf_MultBodyAggAux_f5.object_uri AS object_uri
FROM
  t_0_AncestorOf_MultBodyAggAux_f5 AS AncestorOf_MultBodyAggAux_f5
GROUP BY AncestorOf_MultBodyAggAux_f5.subject_uri, AncestorOf_MultBodyAggAux_f5.object_uri ORDER BY subject_uri;

-- Executed on DuckDB:
| subject_uri                 | object_uri                  |
|-----------------------------|-----------------------------|
| http://example.org/co#alice | http://example.org/co#carol |
| http://example.org/co#alice | http://example.org/co#bob   |
| http://example.org/co#bob   | http://example.org/co#carol |
(3 rows)

$ synalog.compile('axioms.l', 'Knows')
-- Initializing DuckDB environment.
create schema if not exists logica_home;
-- Empty record, has to have a field by DuckDB syntax.
drop type if exists logicarecord893574736 cascade; create type logicarecord893574736 as struct(nirvana numeric);
create sequence if not exists eternal_logical_sequence;

WITH t_3_PersonRaw AS (SELECT * FROM (

    SELECT
      E'http://example.org/co#alice' AS uri,
      E'alice@acme.example' AS email,
      E'Alice' AS name
   UNION ALL

    SELECT
      E'http://example.org/co#bob' AS uri,
      null AS email,
      E'Bob' AS name
   UNION ALL

    SELECT
      E'http://example.org/co#carol' AS uri,
      null AS email,
      E'Carol' AS name

) AS UNUSED_TABLE_NAME  ),
t_2_Person AS (SELECT
  PersonRaw.uri AS uri,
  PersonRaw.email AS email,
  PersonRaw.name AS name
FROM
  t_3_PersonRaw AS PersonRaw
GROUP BY PersonRaw.uri, PersonRaw.email, PersonRaw.name ORDER BY uri),
t_0_Knows_MultBodyAggAux AS (SELECT * FROM (

    SELECT
      Person.uri AS subject_uri,
      t_1_Person.uri AS object_uri
    FROM
      t_2_Person AS Person, t_2_Person AS t_1_Person
    WHERE
      (E'http://example.org/co#alice' = Person.uri) AND
      (E'http://example.org/co#bob' = t_1_Person.uri)
   UNION ALL

    SELECT
      t_6_Person.uri AS subject_uri,
      t_7_Person.uri AS object_uri
    FROM
      t_2_Person AS t_6_Person, t_2_Person AS t_7_Person
    WHERE
      (E'http://example.org/co#alice' = t_7_Person.uri) AND
      (E'http://example.org/co#bob' = t_6_Person.uri)

) AS UNUSED_TABLE_NAME  )
SELECT
  Knows_MultBodyAggAux.subject_uri AS subject_uri,
  Knows_MultBodyAggAux.object_uri AS object_uri
FROM
  t_0_Knows_MultBodyAggAux AS Knows_MultBodyAggAux
GROUP BY Knows_MultBodyAggAux.subject_uri, Knows_MultBodyAggAux.object_uri ORDER BY subject_uri;

-- Executed on DuckDB:
| subject_uri                 | object_uri                  |
|-----------------------------|-----------------------------|
| http://example.org/co#alice | http://example.org/co#bob   |
| http://example.org/co#bob   | http://example.org/co#alice |
(2 rows)

$ synalog.compile('axioms.l', 'Employs')
-- Initializing DuckDB environment.
create schema if not exists logica_home;
-- Empty record, has to have a field by DuckDB syntax.
drop type if exists logicarecord893574736 cascade; create type logicarecord893574736 as struct(nirvana numeric);
create sequence if not exists eternal_logical_sequence;

SELECT
  E'http://example.org/co#acme' AS subject_uri,
  E'http://example.org/co#alice' AS object_uri
FROM
  (SELECT 'singleton' as s) as unused_singleton
GROUP BY (E'http://example.org/co#acme' || ''), (E'http://example.org/co#alice' || '') ORDER BY subject_uri;

-- Executed on DuckDB:
| subject_uri                | object_uri                  |
|----------------------------|-----------------------------|
| http://example.org/co#acme | http://example.org/co#alice |
(1 row)

$ synalog.compile('axioms.l', 'ParentOf')
-- Initializing DuckDB environment.
create schema if not exists logica_home;
-- Empty record, has to have a field by DuckDB syntax.
drop type if exists logicarecord893574736 cascade; create type logicarecord893574736 as struct(nirvana numeric);
create sequence if not exists eternal_logical_sequence;

WITH t_2_PersonRaw AS (SELECT * FROM (

    SELECT
      E'http://example.org/co#alice' AS uri,
      E'alice@acme.example' AS email,
      E'Alice' AS name
   UNION ALL

    SELECT
      E'http://example.org/co#bob' AS uri,
      null AS email,
      E'Bob' AS name
   UNION ALL

    SELECT
      E'http://example.org/co#carol' AS uri,
      null AS email,
      E'Carol' AS name

) AS UNUSED_TABLE_NAME  ),
t_1_Person AS (SELECT
  PersonRaw.uri AS uri,
  PersonRaw.email AS email,
  PersonRaw.name AS name
FROM
  t_2_PersonRaw AS PersonRaw
GROUP BY PersonRaw.uri, PersonRaw.email, PersonRaw.name ORDER BY uri)
SELECT
  Person.uri AS subject_uri,
  t_0_Person.uri AS object_uri
FROM
  t_1_Person AS Person, t_1_Person AS t_0_Person
WHERE
  (E'http://example.org/co#alice' = Person.uri) AND
  (E'http://example.org/co#carol' = t_0_Person.uri)
GROUP BY Person.uri, t_0_Person.uri ORDER BY subject_uri;

-- Executed on DuckDB:
| subject_uri                 | object_uri                  |
|-----------------------------|-----------------------------|
| http://example.org/co#alice | http://example.org/co#carol |
(1 row)

$ synalog.compile('axioms.l', 'SameTeamAs')
-- Initializing DuckDB environment.
create schema if not exists logica_home;
-- Empty record, has to have a field by DuckDB syntax.
drop type if exists logicarecord893574736 cascade; create type logicarecord893574736 as struct(nirvana numeric);
create sequence if not exists eternal_logical_sequence;

WITH t_1_PersonRaw AS (SELECT * FROM (

    SELECT
      E'http://example.org/co#alice' AS uri,
      E'alice@acme.example' AS email,
      E'Alice' AS name
   UNION ALL

    SELECT
      E'http://example.org/co#bob' AS uri,
      null AS email,
      E'Bob' AS name
   UNION ALL

    SELECT
      E'http://example.org/co#carol' AS uri,
      null AS email,
      E'Carol' AS name

) AS UNUSED_TABLE_NAME  ),
t_0_Person AS (SELECT
  PersonRaw.uri AS uri,
  PersonRaw.email AS email,
  PersonRaw.name AS name
FROM
  t_1_PersonRaw AS PersonRaw
GROUP BY PersonRaw.uri, PersonRaw.email, PersonRaw.name ORDER BY uri)
SELECT
  Person.uri AS subject_uri,
  Person.uri AS object_uri
FROM
  t_0_Person AS Person
GROUP BY Person.uri, Person.uri ORDER BY subject_uri;

-- Executed on DuckDB:
| subject_uri                 | object_uri                  |
|-----------------------------|-----------------------------|
| http://example.org/co#alice | http://example.org/co#alice |
| http://example.org/co#bob   | http://example.org/co#bob   |
| http://example.org/co#carol | http://example.org/co#carol |
(3 rows)

$ synalog.compile('axioms.l', 'HasDeskFunctionalViolation')
-- Initializing DuckDB environment.
create schema if not exists logica_home;
-- Empty record, has to have a field by DuckDB syntax.
drop type if exists logicarecord893574736 cascade; create type logicarecord893574736 as struct(nirvana numeric);
create sequence if not exists eternal_logical_sequence;

WITH t_2_HasDeskRaw AS (SELECT * FROM (

    SELECT
      E'http://example.org/co#alice' AS subject_uri,
      E'http://example.org/co#d1' AS object_uri
   UNION ALL

    SELECT
      E'http://example.org/co#alice' AS subject_uri,
      E'http://example.org/co#d2' AS object_uri

) AS UNUSED_TABLE_NAME  ),
t_4_PersonRaw AS (SELECT * FROM (

    SELECT
      E'http://example.org/co#alice' AS uri,
      E'alice@acme.example' AS email,
      E'Alice' AS name
   UNION ALL

    SELECT
      E'http://example.org/co#bob' AS uri,
      null AS email,
      E'Bob' AS name
   UNION ALL

    SELECT
      E'http://example.org/co#carol' AS uri,
      null AS email,
      E'Carol' AS name

) AS UNUSED_TABLE_NAME  ),
t_3_Person AS (SELECT
  PersonRaw.uri AS uri,
  PersonRaw.email AS email,
  PersonRaw.name AS name
FROM
  t_4_PersonRaw AS PersonRaw
GROUP BY PersonRaw.uri, PersonRaw.email, PersonRaw.name ORDER BY uri),
t_6_DeskRaw AS (SELECT * FROM (

    SELECT
      E'http://example.org/co#d1' AS uri
   UNION ALL

    SELECT
      E'http://example.org/co#d2' AS uri

) AS UNUSED_TABLE_NAME  ),
t_5_Desk AS (SELECT
  DeskRaw.uri AS uri
FROM
  t_6_DeskRaw AS DeskRaw
GROUP BY DeskRaw.uri ORDER BY uri),
t_1_HasDesk AS (SELECT
  HasDeskRaw.subject_uri AS subject_uri,
  HasDeskRaw.object_uri AS object_uri
FROM
  t_2_HasDeskRaw AS HasDeskRaw, t_3_Person AS Person, t_5_Desk AS Desk
WHERE
  (Person.uri = HasDeskRaw.subject_uri) AND
  (Desk.uri = HasDeskRaw.object_uri)
GROUP BY HasDeskRaw.subject_uri, HasDeskRaw.object_uri ORDER BY subject_uri)
SELECT
  HasDesk.subject_uri AS subject_uri
FROM
  t_1_HasDesk AS HasDesk, t_1_HasDesk AS t_0_HasDesk
WHERE
  (HasDesk.object_uri != t_0_HasDesk.object_uri) AND
  (t_0_HasDesk.subject_uri = HasDesk.subject_uri)
GROUP BY HasDesk.subject_uri ORDER BY subject_uri;

-- Executed on DuckDB:
| subject_uri                 |
|-----------------------------|
| http://example.org/co#alice |
(1 row)

$ synalog.compile('axioms.l', 'SupervisesAsymmetricViolation')
-- Initializing DuckDB environment.
create schema if not exists logica_home;
-- Empty record, has to have a field by DuckDB syntax.
drop type if exists logicarecord893574736 cascade; create type logicarecord893574736 as struct(nirvana numeric);
create sequence if not exists eternal_logical_sequence;

WITH t_3_SupervisesRaw AS (SELECT * FROM (

    SELECT
      E'http://example.org/co#alice' AS subject_uri,
      E'http://example.org/co#bob' AS object_uri
   UNION ALL

    SELECT
      E'http://example.org/co#bob' AS subject_uri,
      E'http://example.org/co#alice' AS object_uri
   UNION ALL

    SELECT
      E'http://example.org/co#carol' AS subject_uri,
      E'http://example.org/co#carol' AS object_uri

) AS UNUSED_TABLE_NAME  ),
t_5_PersonRaw AS (SELECT * FROM (

    SELECT
      E'http://example.org/co#alice' AS uri,
      E'alice@acme.example' AS email,
      E'Alice' AS name
   UNION ALL

    SELECT
      E'http://example.org/co#bob' AS uri,
      null AS email,
      E'Bob' AS name
   UNION ALL

    SELECT
      E'http://example.org/co#carol' AS uri,
      null AS email,
      E'Carol' AS name

) AS UNUSED_TABLE_NAME  ),
t_4_Person AS (SELECT
  PersonRaw.uri AS uri,
  PersonRaw.email AS email,
  PersonRaw.name AS name
FROM
  t_5_PersonRaw AS PersonRaw
GROUP BY PersonRaw.uri, PersonRaw.email, PersonRaw.name ORDER BY uri),
t_1_Supervises AS (SELECT
  SupervisesRaw.subject_uri AS subject_uri,
  SupervisesRaw.object_uri AS object_uri
FROM
  t_3_SupervisesRaw AS SupervisesRaw, t_4_Person AS Person, t_4_Person AS t_2_Person
WHERE
  (Person.uri = SupervisesRaw.subject_uri) AND
  (t_2_Person.uri = SupervisesRaw.object_uri)
GROUP BY SupervisesRaw.subject_uri, SupervisesRaw.object_uri ORDER BY subject_uri)
SELECT
  Supervises.subject_uri AS subject_uri,
  Supervises.object_uri AS object_uri
FROM
  t_1_Supervises AS Supervises, t_1_Supervises AS t_0_Supervises
WHERE
  (t_0_Supervises.subject_uri = Supervises.object_uri) AND
  (t_0_Supervises.object_uri = Supervises.subject_uri)
GROUP BY Supervises.subject_uri, Supervises.object_uri ORDER BY subject_uri;

-- Executed on DuckDB:
| subject_uri                 | object_uri                  |
|-----------------------------|-----------------------------|
| http://example.org/co#alice | http://example.org/co#bob   |
| http://example.org/co#bob   | http://example.org/co#alice |
| http://example.org/co#carol | http://example.org/co#carol |
(3 rows)

$ synalog.compile('axioms.l', 'SupervisesIrreflexiveViolation')
-- Initializing DuckDB environment.
create schema if not exists logica_home;
-- Empty record, has to have a field by DuckDB syntax.
drop type if exists logicarecord893574736 cascade; create type logicarecord893574736 as struct(nirvana numeric);
create sequence if not exists eternal_logical_sequence;

WITH t_2_SupervisesRaw AS (SELECT * FROM (

    SELECT
      E'http://example.org/co#alice' AS subject_uri,
      E'http://example.org/co#bob' AS object_uri
   UNION ALL

    SELECT
      E'http://example.org/co#bob' AS subject_uri,
      E'http://example.org/co#alice' AS object_uri
   UNION ALL

    SELECT
      E'http://example.org/co#carol' AS subject_uri,
      E'http://example.org/co#carol' AS object_uri

) AS UNUSED_TABLE_NAME  ),
t_4_PersonRaw AS (SELECT * FROM (

    SELECT
      E'http://example.org/co#alice' AS uri,
      E'alice@acme.example' AS email,
      E'Alice' AS name
   UNION ALL

    SELECT
      E'http://example.org/co#bob' AS uri,
      null AS email,
      E'Bob' AS name
   UNION ALL

    SELECT
      E'http://example.org/co#carol' AS uri,
      null AS email,
      E'Carol' AS name

) AS UNUSED_TABLE_NAME  ),
t_3_Person AS (SELECT
  PersonRaw.uri AS uri,
  PersonRaw.email AS email,
  PersonRaw.name AS name
FROM
  t_4_PersonRaw AS PersonRaw
GROUP BY PersonRaw.uri, PersonRaw.email, PersonRaw.name ORDER BY uri),
t_0_Supervises AS (SELECT
  SupervisesRaw.subject_uri AS subject_uri,
  SupervisesRaw.object_uri AS object_uri
FROM
  t_2_SupervisesRaw AS SupervisesRaw, t_3_Person AS Person, t_3_Person AS t_1_Person
WHERE
  (Person.uri = SupervisesRaw.subject_uri) AND
  (t_1_Person.uri = SupervisesRaw.object_uri)
GROUP BY SupervisesRaw.subject_uri, SupervisesRaw.object_uri ORDER BY subject_uri)
SELECT
  Supervises.subject_uri AS uri
FROM
  t_0_Supervises AS Supervises
WHERE
  (Supervises.object_uri = Supervises.subject_uri)
GROUP BY Supervises.subject_uri ORDER BY uri;

-- Executed on DuckDB:
| uri                         |
|-----------------------------|
| http://example.org/co#carol |
(1 row)

$ synalog.compile('axioms.l', 'DisjointWithViolation')
-- Initializing DuckDB environment.
create schema if not exists logica_home;
-- Empty record, has to have a field by DuckDB syntax.
drop type if exists logicarecord893574736 cascade; create type logicarecord893574736 as struct(nirvana numeric);
create sequence if not exists eternal_logical_sequence;

WITH t_0_Contractor AS (SELECT
  E'http://example.org/co#dave' AS uri,
  E'Dave' AS name
FROM
  (SELECT 'singleton' as s) as unused_singleton
GROUP BY (E'http://example.org/co#dave' || ''), (E'Dave' || '') ORDER BY uri),
t_1_Employee AS (SELECT
  E'http://example.org/co#dave' AS uri,
  E'Dave' AS name
FROM
  (SELECT 'singleton' as s) as unused_singleton
GROUP BY (E'http://example.org/co#dave' || ''), (E'Dave' || '') ORDER BY uri)
SELECT
  Contractor.uri AS uri,
  E'http://example.org/co#Contractor' AS class_a,
  E'http://example.org/co#Employee' AS class_b
FROM
  t_0_Contractor AS Contractor, t_1_Employee AS Employee
WHERE
  (Employee.uri = Contractor.uri)
GROUP BY Contractor.uri, (E'http://example.org/co#Contractor' || ''), (E'http://example.org/co#Employee' || '') ORDER BY uri;

-- Executed on DuckDB:
| uri                        | class_a                          | class_b                        |
|----------------------------|----------------------------------|--------------------------------|
| http://example.org/co#dave | http://example.org/co#Contractor | http://example.org/co#Employee |
(1 row)

$ synalog.compile('axioms.l', 'SameAs')
-- Initializing DuckDB environment.
create schema if not exists logica_home;
-- Empty record, has to have a field by DuckDB syntax.
drop type if exists logicarecord893574736 cascade; create type logicarecord893574736 as struct(nirvana numeric);
create sequence if not exists eternal_logical_sequence;

CREATE OR REPLACE TABLE logica_home.SameAs_ifr0 AS WITH t_0_SameAs_MultBodyAggAux_f6 AS (SELECT * FROM (

    SELECT
      E'http://example.org/co#alice' AS left_uri,
      E'http://example.org/co#alice2' AS right_uri
   UNION ALL

    SELECT
      E'http://example.org/co#alice2' AS left_uri,
      E'http://example.org/co#alice' AS right_uri

) AS UNUSED_TABLE_NAME  )
SELECT
  SameAs_MultBodyAggAux_f6.left_uri AS left_uri,
  SameAs_MultBodyAggAux_f6.right_uri AS right_uri
FROM
  t_0_SameAs_MultBodyAggAux_f6 AS SameAs_MultBodyAggAux_f6
GROUP BY SameAs_MultBodyAggAux_f6.left_uri, SameAs_MultBodyAggAux_f6.right_uri ORDER BY left_uri;

-- Interacting with table logica_home.SameAs_ifr0

CREATE OR REPLACE TABLE logica_home.SameAs_ifr1 AS WITH t_0_SameAs_MultBodyAggAux_f7 AS (SELECT * FROM (

    SELECT
      E'http://example.org/co#alice' AS left_uri,
      E'http://example.org/co#alice2' AS right_uri
   UNION ALL

    SELECT
      E'http://example.org/co#alice2' AS left_uri,
      E'http://example.org/co#alice' AS right_uri
   UNION ALL

    SELECT
      SameAs_ifr0.left_uri AS left_uri,
      t_2_SameAs_ifr0.right_uri AS right_uri
    FROM
      logica_home.SameAs_ifr0 AS SameAs_ifr0, logica_home.SameAs_ifr0 AS t_2_SameAs_ifr0
    WHERE
      (t_2_SameAs_ifr0.left_uri = SameAs_ifr0.right_uri)

) AS UNUSED_TABLE_NAME  )
SELECT
  SameAs_MultBodyAggAux_f7.left_uri AS left_uri,
  SameAs_MultBodyAggAux_f7.right_uri AS right_uri
FROM
  t_0_SameAs_MultBodyAggAux_f7 AS SameAs_MultBodyAggAux_f7
GROUP BY SameAs_MultBodyAggAux_f7.left_uri, SameAs_MultBodyAggAux_f7.right_uri ORDER BY left_uri;

-- Interacting with table logica_home.SameAs_ifr1

CREATE OR REPLACE TABLE logica_home.SameAs_ifr2 AS WITH t_0_SameAs_MultBodyAggAux_f8 AS (SELECT * FROM (

    SELECT
      E'http://example.org/co#alice' AS left_uri,
      E'http://example.org/co#alice2' AS right_uri
   UNION ALL

    SELECT
      E'http://example.org/co#alice2' AS left_uri,
      E'http://example.org/co#alice' AS right_uri
   UNION ALL

    SELECT
      SameAs_ifr1.left_uri AS left_uri,
      t_2_SameAs_ifr1.right_uri AS right_uri
    FROM
      logica_home.SameAs_ifr1 AS SameAs_ifr1, logica_home.SameAs_ifr1 AS t_2_SameAs_ifr1
    WHERE
      (t_2_SameAs_ifr1.left_uri = SameAs_ifr1.right_uri)

) AS UNUSED_TABLE_NAME  )
SELECT
  SameAs_MultBodyAggAux_f8.left_uri AS left_uri,
  SameAs_MultBodyAggAux_f8.right_uri AS right_uri
FROM
  t_0_SameAs_MultBodyAggAux_f8 AS SameAs_MultBodyAggAux_f8
GROUP BY SameAs_MultBodyAggAux_f8.left_uri, SameAs_MultBodyAggAux_f8.right_uri ORDER BY left_uri;

-- Interacting with table logica_home.SameAs_ifr2

CREATE OR REPLACE TABLE logica_home.SameAs_ifr1 AS WITH t_0_SameAs_MultBodyAggAux_f9 AS (SELECT * FROM (

    SELECT
      E'http://example.org/co#alice' AS left_uri,
      E'http://example.org/co#alice2' AS right_uri
   UNION ALL

    SELECT
      E'http://example.org/co#alice2' AS left_uri,
      E'http://example.org/co#alice' AS right_uri
   UNION ALL

    SELECT
      SameAs_ifr2.left_uri AS left_uri,
      t_2_SameAs_ifr2.right_uri AS right_uri
    FROM
      logica_home.SameAs_ifr2 AS SameAs_ifr2, logica_home.SameAs_ifr2 AS t_2_SameAs_ifr2
    WHERE
      (t_2_SameAs_ifr2.left_uri = SameAs_ifr2.right_uri)

) AS UNUSED_TABLE_NAME  )
SELECT
  SameAs_MultBodyAggAux_f9.left_uri AS left_uri,
  SameAs_MultBodyAggAux_f9.right_uri AS right_uri
FROM
  t_0_SameAs_MultBodyAggAux_f9 AS SameAs_MultBodyAggAux_f9
GROUP BY SameAs_MultBodyAggAux_f9.left_uri, SameAs_MultBodyAggAux_f9.right_uri ORDER BY left_uri;

-- Interacting with table logica_home.SameAs_ifr1

WITH t_0_SameAs_MultBodyAggAux_f10 AS (SELECT * FROM (

    SELECT
      E'http://example.org/co#alice' AS left_uri,
      E'http://example.org/co#alice2' AS right_uri
   UNION ALL

    SELECT
      E'http://example.org/co#alice2' AS left_uri,
      E'http://example.org/co#alice' AS right_uri
   UNION ALL

    SELECT
      SameAs_ifr3.left_uri AS left_uri,
      t_2_SameAs_ifr3.right_uri AS right_uri
    FROM
      logica_home.SameAs_ifr1 AS SameAs_ifr3, logica_home.SameAs_ifr1 AS t_2_SameAs_ifr3
    WHERE
      (t_2_SameAs_ifr3.left_uri = SameAs_ifr3.right_uri)

) AS UNUSED_TABLE_NAME  )
SELECT
  SameAs_MultBodyAggAux_f10.left_uri AS left_uri,
  SameAs_MultBodyAggAux_f10.right_uri AS right_uri
FROM
  t_0_SameAs_MultBodyAggAux_f10 AS SameAs_MultBodyAggAux_f10
GROUP BY SameAs_MultBodyAggAux_f10.left_uri, SameAs_MultBodyAggAux_f10.right_uri ORDER BY left_uri;

-- Executed on DuckDB:
| left_uri                     | right_uri                    |
|------------------------------|------------------------------|
| http://example.org/co#alice  | http://example.org/co#alice  |
| http://example.org/co#alice  | http://example.org/co#alice2 |
| http://example.org/co#alice2 | http://example.org/co#alice  |
| http://example.org/co#alice2 | http://example.org/co#alice2 |
(4 rows)

Large, real-world ontologies

The two-layer mapping shines on the big public ontologies, which are almost entirely schema. The Human Disease Ontology (DOID) is a good stress test — point import straight at its URL:

synalog import https://purl.obolibrary.org/obo/doid.owl > doid.l

That 28 MB ontology has 14,703 classes, a 17,224-edge subClassOf hierarchy and no individuals — so the output is the schema layer only: one Class (every disease, with its rdfs:label) and the recursive SubClassOf, instead of 14,703 empty per-class predicates. An excerpt:

# Generated by `synalog import` from an OWL/RDF ontology.
# 14703 classes, 2 object properties, 0 individuals.

# Schema — every class as a node (the TBox)
@OrderBy(Class, "uri");
Class(uri:, label:) distinct :- ClassRaw(uri:, label:);
ClassRaw(uri: 'http://purl.obolibrary.org/obo/DOID_0001816', label: 'angiosarcoma');
ClassRaw(uri: 'http://purl.obolibrary.org/obo/DOID_0002116', label: 'pterygium');
ClassRaw(uri: 'http://purl.obolibrary.org/obo/DOID_0014667', label: 'disease of metabolism');
ClassRaw(uri: 'http://purl.obolibrary.org/obo/DOID_0040001', label: 'shrimp allergy');
# … 14,699 more classes …

# Class hierarchy (transitive subClassOf), joined through Class
@Recursive(SubClassOf, 100);
SubClassOf(child_uri:, parent_uri:) distinct :- SubClassOfRaw(child_uri:, parent_uri:), Class(uri: child_uri), Class(uri: parent_uri);
SubClassOf(child_uri:, parent_uri:) distinct :- SubClassOf(child_uri:, parent_uri: mid), SubClassOfRaw(child_uri: mid, parent_uri:);
SubClassOfRaw(child_uri: 'http://purl.obolibrary.org/obo/DOID_0001816', parent_uri: 'http://purl.obolibrary.org/obo/DOID_175');
SubClassOfRaw(child_uri: 'http://purl.obolibrary.org/obo/DOID_0002116', parent_uri: 'http://purl.obolibrary.org/obo/DOID_10124');
SubClassOfRaw(child_uri: 'http://purl.obolibrary.org/obo/DOID_0014667', parent_uri: 'http://purl.obolibrary.org/obo/DOID_4');
# … 17,221 more subClassOf edges …

# Concepts (edges)  —  DOID has no individuals, so these carry no facts
@OrderBy(IsA, "subject_uri");
IsA(subject_uri:, object_uri:) distinct :- IsARaw(subject_uri:, object_uri:);

The whole thing converts in a few seconds; from there a recursive query over SubClassOf gives you, say, every subtype of a disease by a single ancestor lookup.

Next steps

From here it's an ordinary Synalog program: add rules that traverse the edges, compose them, or join them with your other tables — see Knowledge graphs.