From 9d0744e20e17c69808b4c7d42d053162a56f8776 Mon Sep 17 00:00:00 2001 From: KaifAhmad1 Date: Sat, 11 Apr 2026 16:51:39 +0530 Subject: [PATCH] fix(triplet-store): coerce non-string IDs and guard known vocabulary prefixes in _resolve_iri MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- semantica/triplet_store/triplet_store.py | 35 +++++++++-- tests/triplet_store/test_triplet_store.py | 75 +++++++++++++++++++++++ 2 files changed, 105 insertions(+), 5 deletions(-) diff --git a/semantica/triplet_store/triplet_store.py b/semantica/triplet_store/triplet_store.py index 026d22dc..2fd573d5 100644 --- a/semantica/triplet_store/triplet_store.py +++ b/semantica/triplet_store/triplet_store.py @@ -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::`` 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::``. """ - 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}" diff --git a/tests/triplet_store/test_triplet_store.py b/tests/triplet_store/test_triplet_store.py index a320511e..56401a85 100644 --- a/tests/triplet_store/test_triplet_store.py +++ b/tests/triplet_store/test_triplet_store.py @@ -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)