Files
semantica/tests/ingest/test_ontology_named_graph.py
T
13g4d0 241ff8e481 fix(ingest): read JSON-LD named graphs in OntologyIngestor (#1129)
A JSON-LD document with a top-level `@id` *and* `@graph` places its terms in a named
graph. `rdflib.Graph.parse()` loads only the default graph and discards the rest
without raising, so every class and property in such a document was dropped while
the load reported success.

`OntologyIngestor.ingest_ontology` now parses into a `Dataset` and flattens the
quads into the working `Graph`, keeping both the default and the named graphs. This
is the same `Graph` -> `Dataset` migration #757 made for `JenaStore` (#756); the
ingest path was not covered by it.

Measured on the 12-line reproduction from the issue:

    before   classes=0  properties=0
    after    classes=2  properties=0

On a real ontology the gap is larger: 25 triples / 1 subject against 719 / 88 for
the document that surfaced this.

Tests: `tests/ingest/test_ontology_named_graph.py` covers the named-graph document,
keeps a canary on the default-graph document so the fix cannot trade one blind spot
for another, and asserts that the reported result matches the terms returned.
Reverting `Dataset()` to `Graph()` turns all four red.

`tests/ontology`, `tests/export` and `tests/ingest` pass apart from six failures in
web/feed/database/API ingestion, unrelated to this change and failing the same way
on an unmodified checkout.

Not included, and happy to add here or as a follow-up: making a load that yields
zero classes stop returning `status: "success"`. That value is what made this take
an afternoon to find, but it is a behaviour change on a different layer and seemed
worth reviewing on its own.
2026-08-20 12:55:45 -04:00

93 lines
3.3 KiB
Python

"""A JSON-LD ontology whose terms live in a named graph must not be silently dropped.
A JSON-LD document with a top-level ``@id`` *and* ``@graph`` places its terms in a NAMED
graph. ``rdflib.Graph.parse()`` loads only the default graph and discards the rest without
raising, so every class and property in such a document disappeared while the load reported
success — see issue #1129 for the reproduction through the public API.
This is the same ``Graph`` -> ``Dataset`` migration #757 made for ``JenaStore`` (#756); the
ingest path was not covered by it.
"""
from __future__ import annotations
import json
import pytest
from semantica.ingest.ontology_ingestor import OntologyIngestor
NAMED_GRAPH_ONTOLOGY = {
"@context": {
"ex": "https://example.org/ns#",
"owl": "http://www.w3.org/2002/07/owl#",
"rdfs": "http://www.w3.org/2000/01/rdf-schema#",
},
"@id": "https://example.org/ns",
"@type": "owl:Ontology",
"@graph": [
{"@id": "ex:Thing", "@type": "owl:Class", "rdfs:label": "Thing"},
{"@id": "ex:Other", "@type": "owl:Class", "rdfs:label": "Other"},
{
"@id": "ex:relatesTo",
"@type": "owl:ObjectProperty",
"rdfs:domain": {"@id": "ex:Thing"},
"rdfs:range": {"@id": "ex:Other"},
},
],
}
DEFAULT_GRAPH_ONTOLOGY = {
"@context": NAMED_GRAPH_ONTOLOGY["@context"],
"@graph": [
{"@id": "ex:Thing", "@type": "owl:Class", "rdfs:label": "Thing"},
{"@id": "ex:Other", "@type": "owl:Class", "rdfs:label": "Other"},
],
}
def _write(tmp_path, name, document):
path = tmp_path / name
path.write_text(json.dumps(document), encoding="utf-8")
return path
def test_terms_in_a_named_graph_are_ingested(tmp_path):
"""The regression: two classes and one object property, all inside the named graph."""
path = _write(tmp_path, "named.jsonld", NAMED_GRAPH_ONTOLOGY)
data = OntologyIngestor().ingest_ontology(path).data
assert len(data["classes"]) == 2, (
"classes inside a JSON-LD named graph were dropped; the ingestor is reading only "
"the default graph"
)
assert len(data["properties"]) == 1
assert {c["uri"] for c in data["classes"]} == {
"https://example.org/ns#Thing",
"https://example.org/ns#Other",
}
def test_terms_in_the_default_graph_still_work(tmp_path):
"""Canary for the test above: a document *without* a top-level ``@id`` keeps its terms
in the default graph and always parsed correctly. If this stopped passing, the fix would
have traded one blind spot for another."""
path = _write(tmp_path, "default.jsonld", DEFAULT_GRAPH_ONTOLOGY)
data = OntologyIngestor().ingest_ontology(path).data
assert len(data["classes"]) == 2
@pytest.mark.parametrize("document", [NAMED_GRAPH_ONTOLOGY, DEFAULT_GRAPH_ONTOLOGY])
def test_metadata_reports_what_was_actually_read(tmp_path, document):
"""Whatever the shape of the document, the counts reported have to match the terms
returned — a load that says it succeeded while returning nothing is what made #1129
cost an afternoon to find."""
path = _write(tmp_path, "any.jsonld", document)
result = OntologyIngestor().ingest_ontology(path)
assert result.data["classes"], "reported success with zero classes"