mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-08-29 04:26:20 +00:00
fix(triplet-store): coerce non-string IDs and guard known vocabulary prefixes in _resolve_iri
Bug 1 — non-string IDs crash store(): _resolve_iri() called .startswith() directly on local, causing AttributeError when upstream graph builders emit integer entity/relationship IDs. Fixed by coercing local to str() at entry; None/empty returns a safe urn: sentinel. Bug 2 — prefixed W3C terms mis-resolved under base_uri: Values like 'owl:Thing' and 'xsd:date' were not recognised as absolute IRIs and with base_uri set were rewritten to e.g. https://example.com/owl:Thing, corrupting standard OWL/XSD IRIs in stored triples. Fixed by adding a known- prefix expansion table (xsd/rdf/rdfs/owl/skos/semantica) that is checked before base_uri is applied, matching the same prefix map already used in blazegraph_store. Added 5 regression tests covering both bugs: integer IDs with/without base_uri, owl:Thing domain/range, xsd:date range, and rdfs:/skos: parent class expansion.
This commit is contained in:
@@ -181,15 +181,40 @@ class TripletStore:
|
||||
if base_uri and not base_uri.endswith(("/", "#")):
|
||||
base_uri = base_uri + "/"
|
||||
|
||||
def _resolve_iri(local: str, kind: str) -> str:
|
||||
# Known W3C vocabulary prefixes — expanded before base_uri is applied so
|
||||
# values like "owl:Thing" or "xsd:date" are never re-namespaced under
|
||||
# the ontology's own base URI.
|
||||
_KNOWN_PREFIXES = {
|
||||
"xsd": "http://www.w3.org/2001/XMLSchema#",
|
||||
"rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
|
||||
"rdfs": "http://www.w3.org/2000/01/rdf-schema#",
|
||||
"owl": "http://www.w3.org/2002/07/owl#",
|
||||
"skos": "http://www.w3.org/2004/02/skos/core#",
|
||||
"semantica": "https://semantica.dev/ontology/",
|
||||
}
|
||||
|
||||
def _resolve_iri(local: object, kind: str) -> str:
|
||||
"""Expand a bare local name to a full IRI.
|
||||
|
||||
If *base_uri* is available the local name is appended to it so the
|
||||
resulting IRI sits in the same namespace as the ontology classes.
|
||||
Falls back to ``urn:<kind>:<local>`` only when no base URI is known.
|
||||
Accepts any type for *local* — non-strings are coerced via str()
|
||||
so integer/numeric IDs passed from graph builders do not crash.
|
||||
|
||||
Resolution order:
|
||||
1. Already an absolute IRI (http / https / urn:) → return as-is.
|
||||
2. Known vocabulary prefix (xsd:, rdf:, rdfs:, owl:, skos:,
|
||||
semantica:) → expand to the canonical W3C IRI.
|
||||
3. base_uri is set → append local name to base_uri.
|
||||
4. Fallback → ``urn:<kind>:<local>``.
|
||||
"""
|
||||
if local.startswith("http") or local.startswith("urn:"):
|
||||
local = str(local) if local is not None else ""
|
||||
if not local:
|
||||
return f"urn:{kind}:unknown"
|
||||
if local.startswith(("http://", "https://", "urn:")):
|
||||
return local
|
||||
if ":" in local:
|
||||
prefix, name = local.split(":", 1)
|
||||
if prefix in _KNOWN_PREFIXES:
|
||||
return f"{_KNOWN_PREFIXES[prefix]}{name}"
|
||||
if base_uri:
|
||||
return f"{base_uri}{local}"
|
||||
return f"urn:{kind}:{local}"
|
||||
|
||||
@@ -601,3 +601,78 @@ class TestTripletStoreOntologyNamespace(unittest.TestCase):
|
||||
subjects = {t.subject for t in captured}
|
||||
self.assertIn("https://example.com/alice", subjects)
|
||||
self.assertNotIn("https://example.com//alice", subjects)
|
||||
|
||||
# --- Bug: non-string IDs crash store ---
|
||||
|
||||
def test_integer_entity_id_does_not_crash(self, mock_bg):
|
||||
"""Integer entity IDs must be coerced to str, not crash with AttributeError."""
|
||||
store, captured = self._make_store(mock_bg)
|
||||
kg = {
|
||||
"entities": [
|
||||
{"id": 1, "type": "Person"},
|
||||
{"id": 2, "type": "Person"},
|
||||
],
|
||||
"relationships": [{"source": 1, "target": 2, "type": "knows"}],
|
||||
}
|
||||
store.store(kg, self._ontology())
|
||||
subjects = {t.subject for t in captured}
|
||||
self.assertIn(f"{self.BASE}1", subjects)
|
||||
self.assertIn(f"{self.BASE}2", subjects)
|
||||
predicates = {t.predicate for t in captured}
|
||||
self.assertIn(f"{self.BASE}knows", predicates)
|
||||
|
||||
def test_integer_entity_id_fallback_to_urn(self, mock_bg):
|
||||
"""Integer IDs fall back to urn: when no base_uri is set."""
|
||||
store, captured = self._make_store(mock_bg)
|
||||
kg = {"entities": [{"id": 42, "type": "Person"}], "relationships": []}
|
||||
ontology = {"classes": [], "properties": []}
|
||||
store.store(kg, ontology)
|
||||
subjects = {t.subject for t in captured}
|
||||
self.assertIn("urn:entity:42", subjects)
|
||||
|
||||
# --- Bug: prefixed W3C terms mis-resolved under base_uri ---
|
||||
|
||||
def test_owl_thing_domain_not_rewritten_under_base_uri(self, mock_bg):
|
||||
"""owl:Thing in domain/range must expand to the W3C OWL IRI, not base_uri + 'owl:Thing'."""
|
||||
store, captured = self._make_store(mock_bg)
|
||||
ontology = {
|
||||
"namespace": {"base_uri": self.BASE},
|
||||
"classes": [],
|
||||
"properties": [{"name": "hasThing", "domain": ["owl:Thing"], "range": ["owl:Thing"]}],
|
||||
}
|
||||
store.store({"entities": [], "relationships": []}, ontology)
|
||||
RDFS_DOMAIN = "http://www.w3.org/2000/01/rdf-schema#domain"
|
||||
RDFS_RANGE = "http://www.w3.org/2000/01/rdf-schema#range"
|
||||
domain_objects = {t.object for t in captured if t.predicate == RDFS_DOMAIN}
|
||||
range_objects = {t.object for t in captured if t.predicate == RDFS_RANGE}
|
||||
self.assertIn("http://www.w3.org/2002/07/owl#Thing", domain_objects)
|
||||
self.assertNotIn(f"{self.BASE}owl:Thing", domain_objects)
|
||||
self.assertIn("http://www.w3.org/2002/07/owl#Thing", range_objects)
|
||||
|
||||
def test_xsd_date_range_not_rewritten_under_base_uri(self, mock_bg):
|
||||
"""xsd:date in range must expand to the W3C XSD IRI, not base_uri + 'xsd:date'."""
|
||||
store, captured = self._make_store(mock_bg)
|
||||
ontology = {
|
||||
"namespace": {"base_uri": self.BASE},
|
||||
"classes": [],
|
||||
"properties": [{"name": "birthDate", "range": ["xsd:date"]}],
|
||||
}
|
||||
store.store({"entities": [], "relationships": []}, ontology)
|
||||
RDFS_RANGE = "http://www.w3.org/2000/01/rdf-schema#range"
|
||||
range_objects = {t.object for t in captured if t.predicate == RDFS_RANGE}
|
||||
self.assertIn("http://www.w3.org/2001/XMLSchema#date", range_objects)
|
||||
self.assertNotIn(f"{self.BASE}xsd:date", range_objects)
|
||||
|
||||
def test_rdfs_and_skos_prefixes_expanded_correctly(self, mock_bg):
|
||||
"""rdfs: and skos: prefixes in class URIs and parent links expand to W3C IRIs."""
|
||||
store, captured = self._make_store(mock_bg)
|
||||
ontology = {
|
||||
"namespace": {"base_uri": self.BASE},
|
||||
"classes": [{"name": "Concept", "parent": "skos:Concept"}],
|
||||
"properties": [],
|
||||
}
|
||||
store.store({"entities": [], "relationships": []}, ontology)
|
||||
RDFS_SUBCLASS = "http://www.w3.org/2000/01/rdf-schema#subClassOf"
|
||||
parent_objects = {t.object for t in captured if t.predicate == RDFS_SUBCLASS}
|
||||
self.assertIn("http://www.w3.org/2004/02/skos/core#Concept", parent_objects)
|
||||
self.assertNotIn(f"{self.BASE}skos:Concept", parent_objects)
|
||||
|
||||
Reference in New Issue
Block a user