fix(reasoning): refuse SPARQL query execution instead of returning empty results (#1087)

* fix(reasoning): refuse SPARQL query execution instead of returning empty results (#1083)

SPARQLReasoner.execute_query() never executed the query: both branches
returned an empty SPARQLQueryResult, with or without a triplet store, so
callers that trust an empty result as "no matches" silently drew wrong
conclusions. Until a real triplet-store execution path lands, the method
raises NotImplementedError with an explanation, per the issue's
suggestion. The dead cache/inference scaffolding after the execution
point is removed along with it.

Co-Authored-By: Claude <noreply@anthropic.com>

* docs(reasoning): align execute_query() docs with the NotImplementedError contract (#1087)

Review feedback: the docstring still carried a "Returns" section and the
reasoning guide showed execute_query() returning bindings, both of which
now mislead. The docstring documents Raises only, the guide demonstrates
expand_query() and points to rdflib for execution until the triplet-store
path lands, and query_cache/clear_cache() are marked as reserved for that
future execution path.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Aldrin Joseph
2026-08-22 14:00:28 +05:00
committed by GitHub
co-authored by Claude
parent 8e9f7c5526
commit 394ce5fe61
3 changed files with 50 additions and 80 deletions
+6 -15
View File
@@ -269,7 +269,7 @@ print("Loaded {} facts from graph".format(count))
## Step 5 — SPARQL queries over enriched working memory
After forward chaining has derived new facts, `SPARQLReasoner` lets you query the enriched working memory using SPARQL triple-pattern matching with optional inference expansion:
After forward chaining has derived new facts, `SPARQLReasoner` prepares SPARQL queries over the enriched working memory with optional inference expansion:
```python
from semantica.reasoning import SPARQLReasoner
@@ -288,22 +288,13 @@ query = """
}
"""
# execute_query() runs: expansion → inference → deduplication
result = sparql.execute_query(query)
for binding in result.bindings:
print("Actor: {:15s} CVE: {}".format(
binding.get("actor", "?"),
binding.get("cve", "?"),
))
# metadata shows how many results came from inference vs ground facts
print("Original: {} Inferred: {}".format(
result.metadata.get("original_count", 0),
result.metadata.get("inferred_count", 0),
))
# expand_query() applies inference rules to the query text:
expanded = sparql.expand_query(query)
print(expanded)
```
`execute_query()` is not implemented yet: no triplet-store execution path exists, so it raises `NotImplementedError` rather than returning an empty result set that callers would misread as "no matches". Until execution lands, run the expanded query against your RDF store directly (for example with `rdflib`).
Inspect the expanded query before running it:
```python
+21 -65
View File
@@ -84,6 +84,9 @@ class SPARQLReasoner:
self.triplet_store = self.config.get("triplet_store")
self.enable_inference = self.config.get("enable_inference", True)
# Reserved for query caching once a triplet-store execution path
# lands. execute_query() raises NotImplementedError until then, so
# the cache cannot be populated through any public path yet.
self.query_cache: Dict[str, Any] = {}
def expand_query(self, query: str, **options) -> str:
@@ -330,79 +333,32 @@ class SPARQLReasoner:
"""
Execute SPARQL query with reasoning.
Not implemented: no triplet-store execution path exists yet, so the
query is refused loudly instead of returning an empty result set
that callers would read as "no matches" (issue #1083).
Args:
query: SPARQL query string
**options: Additional options
Returns:
Query results
Raises:
NotImplementedError: always, until a triplet-store execution
path lands.
"""
tracking_id = self.progress_tracker.start_tracking(
module="reasoning",
submodule="SPARQLReasoner",
message="Executing SPARQL query with reasoning",
raise NotImplementedError(
"SPARQLReasoner.execute_query() is not implemented: no "
"triplet-store execution path exists yet. Returning an empty "
"result set would be misread as 'no matches', so the query "
"is refused instead."
)
try:
# Check cache
self.progress_tracker.update_tracking(
tracking_id, message="Checking query cache..."
)
if query in self.query_cache:
self.logger.debug("Returning cached query result")
self.progress_tracker.stop_tracking(
tracking_id,
status="completed",
message="Returned cached query result",
)
return self.query_cache[query]
# Expand query
self.progress_tracker.update_tracking(
tracking_id, message="Expanding query with inference rules..."
)
expanded_query = self.expand_query(query, **options)
# Execute query (if triplet store available)
self.progress_tracker.update_tracking(
tracking_id, message="Executing query..."
)
if self.triplet_store:
# This would call the triplet store's query method
# For now, return empty result
result = SPARQLQueryResult(bindings=[], variables=[])
else:
# Mock result for testing
result = SPARQLQueryResult(bindings=[], variables=[])
# Infer additional results
if self.enable_inference:
self.progress_tracker.update_tracking(
tracking_id, message="Inferring additional results..."
)
result = self.infer_results(result, **options)
# Cache result
self.progress_tracker.update_tracking(
tracking_id, message="Caching query result..."
)
self.query_cache[query] = result
self.progress_tracker.stop_tracking(
tracking_id,
status="completed",
message=f"Query executed: {len(result.bindings)} results",
)
return result
except Exception as e:
self.progress_tracker.stop_tracking(
tracking_id, status="failed", message=str(e)
)
raise
def clear_cache(self) -> None:
"""Clear query cache."""
"""Clear query cache.
Reserved for when a triplet-store execution path lands: until then,
``execute_query()`` raises ``NotImplementedError`` and nothing can
populate the cache.
"""
self.query_cache.clear()
def add_inference_rule(self, rule_definition: str, **options) -> Rule:
@@ -30,6 +30,29 @@ class TestSpecializedReasoners(unittest.TestCase):
binding_types = [b.get("x_type") for b in inferred.bindings]
self.assertIn("Human", binding_types)
def test_execute_query_raises_not_implemented(self):
"""Empty results must not pass as a valid answer (issue #1083).
Both branches returned ``SPARQLQueryResult(bindings=[], variables=[])``
-- with or without a triplet store -- so callers that trust an empty
result as "no matches" silently drew wrong conclusions. Until a real
execution path lands, refusing loudly is safer.
"""
reasoner = SPARQLReasoner()
with self.assertRaises(NotImplementedError):
reasoner.execute_query("SELECT ?s ?p ?o WHERE { ?s ?p ?o }")
def test_execute_query_with_triplet_store_raises_not_implemented(self):
reasoner = SPARQLReasoner(triplet_store=object())
with self.assertRaises(NotImplementedError):
reasoner.execute_query("SELECT ?s ?p ?o WHERE { ?s ?p ?o }")
def test_execute_query_error_explains_why_the_query_is_refused(self):
reasoner = SPARQLReasoner()
with self.assertRaises(NotImplementedError) as ctx:
reasoner.execute_query("SELECT ?s WHERE { ?s ?p ?o }")
self.assertIn("not implemented", str(ctx.exception))
def test_abductive_reasoner_generate_hypotheses(self):
reasoner = AbductiveReasoner()
reasoner.reasoner.add_rule("IF Disease(Flu) THEN Symptom(Fever)")