The previous post queried exactly
what was asserted. This post asks a graph what it implies. RDFS and
OWL 2 RL are both entailment regimes: given a set of asserted triples
plus some schema-level axioms (subclass relationships, equivalences),
a closure computation derives additional triples that were never
written down. Factoidal's SPARQL evaluator can materialize either
closure on demand via the entail option — the two cells below run
that closure live, side by side with the plain (no-entailment) result.
@prefix schema: <https://schema.org/> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix ex: <http://example.org/> .
schema:Person rdfs:subClassOf schema:Thing .
ex:alice a schema:Person .
One instance (ex:alice, a schema:Person), one subclass axiom. The
query itself — "what type does ex:alice have" — is the same across
every cell in this post, so it's named once and referenced everywhere
below:
q = `# What type or types does ex:alice have.
SELECT ?type WHERE { <http://example.org/alice> a ?type }`
Ask it twice — with and without RDFS entailment:
const ttl = `
@prefix schema: <https://schema.org/> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix ex: <http://example.org/> .
schema:Person rdfs:subClassOf schema:Thing .
ex:alice a schema:Person .
`;
const dataset = await fn.parse(ttl);
const plain = await fn.query(dataset, q);
const withRdfs = await fn.query(dataset, q, { entail: "RDFS" });
return {
withoutEntailment: plain.map((r) => r.get("type").value),
withRDFS: withRdfs.map((r) => r.get("type").value).sort(),
};
Plain SELECT sees exactly what's written: ex:alice a schema:Person.
With entail: 'RDFS', the closure applies rdfs:subClassOf
transitively (RDF.Graph.Executable.fst's rdfs_closure, the same
rule set rdf-mt's 14 RDFS-closure tests exercise) and schema:Thing
appears as a second type — derived, not asserted. Since the 2026-07-31
rule-completeness work the closure also derives rdfs:Resource for
every subject (rules rdfs4a/rdfs4b — in RDFS, everything is a
resource), so entailed type lists carry it alongside the
vocabulary-specific types below.
To map two classes from different vocabularies onto each other, OWL
gives you a single, self-documenting triple. Map schema:Person onto
foaf:Person:
@prefix schema: <https://schema.org/> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix ex: <http://example.org/> .
schema:Person owl:equivalentClass foaf:Person .
ex:alice a schema:Person .
const ttl2 = `
@prefix schema: <https://schema.org/> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix ex: <http://example.org/> .
schema:Person owl:equivalentClass foaf:Person .
ex:alice a schema:Person .
`;
const dataset2 = await fn.parse(ttl2);
const withRdfs = await fn.query(dataset2, q, { entail: "RDFS" });
const withOwlRl = await fn.query(dataset2, q, { entail: "OWL-RL" });
return {
withRDFS: withRdfs.map((r) => r.get("type").value),
withOWLRL: withOwlRl.map((r) => r.get("type").value).sort(),
};
entail: 'RDFS' leaves the result unchanged — owl:equivalentClass
isn't an RDFS-vocabulary construct, so the RDFS closure has nothing to
do with it. entail: 'OWL-RL' derives two more types: foaf:Person
(via the equivalence, both directions) and owl:Thing (every OWL
individual belongs to it). Same input graph, same query — the only
thing that changed is which closure ran first.
Plain RDFS can express the same equivalence, though — assert
rdfs:subClassOf in both directions and the subclass rule fires
both ways. (That is exactly how OWL defines EquivalentClasses: a
subclass axiom in each direction.) What owl:equivalentClass buys is
the single self-documenting triple rather than a pair:
const bothWays = `
@prefix schema: <https://schema.org/> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix ex: <http://example.org/> .
schema:Person rdfs:subClassOf foaf:Person .
foaf:Person rdfs:subClassOf schema:Person .
ex:alice a schema:Person .
`;
const ds3 = await fn.parse(bothWays);
const rows = await fn.query(ds3, q, { entail: "RDFS" });
return rows.map((r) => r.get("type").value).sort();
Both types, pure RDFS, no OWL vocabulary in sight.
This is measured against the real W3C OWL 2 RL test catalog, not asserted: positive entailment 28 pass, 2 fail (of 30 — the two fails are a documented-impossible comprehension pair, not a silent gap), negative entailment 6 pass 0 fail, consistency 76 pass 0 fail, inconsistency 14 pass 0 fail.
Real-world graphs mix vocabularies constantly — the same "person" idea
gets asserted as schema:Person on one site, foaf:Person on
another, and a Wikidata query might expect neither directly. Whichever
of those three vocabularies your query is written against, an
owl:equivalentClass bridge plus entail: 'OWL-RL' means you don't
have to rewrite the query for every source's vocabulary choice — the
closure does the translation once, over the data, before the query
ever runs.
Concept schemes: SKOS is next, followed by Shapes that validate: SHACL. Other posts in the series cover the RDF serializations (N-Triples, N-Quads, TriG, RDF/XML), SPARQL Update and the HTTP protocol, and ShEx — see the series plan for the full map.
Every live cell above is pinned in
tests/hub/post03_test.mjs —
the exact same source, executed against the real npm/factoidal
typed API instead of the in-browser fn adapter.