Add SPARQL CONSTRUCT support to Jena backend (#754)

Extends CONSTRUCT support to JenaStore, which uses rdflib.Graph natively rather
than an HTTP protocol - CONSTRUCT results come as native 3-tuples with no
Accept-header/parsing dance needed, unlike Blazegraph/RDF4J.

- CONSTRUCT-aware execute_sparql: reuses shared sparql_escaping.CONSTRUCT_QUERY_RE,
  extracts datatype/language from rdflib Literal objects into the same 4-tuple
  metadata contract used by Blazegraph/RDF4J
- Non-CONSTRUCT path (SELECT/ASK) confirmed byte-for-byte unchanged (Property 9)
- execute_construct_template confirmed backend-agnostic against JenaStore, zero
  changes needed
- Named-graph support explicitly out of scope - JenaStore wraps a single
  rdflib.Graph with no named-graph concept; add_triplets continues to silently
  ignore graph= exactly as before. Tracked separately as a follow-up issue
  requiring a Graph -> ConjunctiveGraph/Dataset migration.
This commit is contained in:
Sameer6305
2026-07-19 17:26:28 +05:30
parent b3245613f5
commit 77026122fc
2 changed files with 166 additions and 0 deletions
+42
View File
@@ -32,6 +32,7 @@ from ..semantic_extract.triplet_extractor import Triplet
from ..utils.exceptions import ProcessingError, ValidationError
from ..utils.logging import get_logger
from ..utils.progress_tracker import get_progress_tracker
from . import sparql_escaping
# Optional Jena imports
try:
@@ -75,6 +76,10 @@ class JenaStore:
self.graph: Optional[Graph] = None
self._initialize_graph()
def _is_construct_query(self, query: str) -> bool:
"""Check if query is a CONSTRUCT query."""
return bool(sparql_escaping.CONSTRUCT_QUERY_RE.search(query))
def _initialize_graph(self) -> None:
"""Initialize RDF graph."""
if HAS_JENA_RDFLIB:
@@ -277,8 +282,43 @@ class JenaStore:
raise ProcessingError("Graph not initialized")
try:
result_format = options.get("result_format")
if result_format is None:
result_format = "construct" if self._is_construct_query(query) else "bindings"
elif result_format not in ("construct", "bindings"):
raise ValidationError(f"Invalid result_format: {result_format!r}")
results = self.graph.query(query)
if result_format == "construct":
triples = []
try:
for s, p, o in results:
obj_metadata: Dict[str, Any] = {}
if isinstance(o, Literal):
if o.datatype is not None:
obj_metadata["datatype"] = str(o.datatype)
if o.language is not None:
obj_metadata["language"] = str(o.language)
triples.append((str(s), str(p), str(o), obj_metadata))
except ValueError as e:
raise ValidationError(
"result_format='construct' was explicitly requested, but "
"the query does not appear to be a CONSTRUCT query (results "
"cannot be unpacked into 3-tuples)."
) from e
return {
"success": True,
"bindings": [],
"variables": [],
"triples": triples,
"metadata": {
"query": query,
"result_format": "construct",
},
}
bindings = []
variables = []
@@ -304,6 +344,8 @@ class JenaStore:
"variables": variables,
"metadata": {"query": query},
}
except ValidationError:
raise
except Exception as e:
self.logger.error(f"SPARQL query failed: {e}")
raise ProcessingError(f"SPARQL query failed: {e}")
+124
View File
@@ -0,0 +1,124 @@
import unittest
from unittest.mock import patch, MagicMock
from rdflib import Graph, URIRef, Literal, Namespace
from semantica.triplet_store.jena_store import JenaStore
from semantica.semantic_extract.triplet_extractor import Triplet
from semantica.utils.exceptions import ValidationError
from semantica.triplet_store.construct_templates import execute_construct_template, ConstructTemplate
class TestJenaStoreExecuteSparqlConstructPath(unittest.TestCase):
def setUp(self):
self.store = JenaStore()
# Ensure we use an in-memory graph
self.store.graph = Graph()
def test_construct_parses_triples_from_rdflib_natively(self):
# Insert some test data
self.store.graph.parse(data='<http://ex.org/s> <http://ex.org/p> <http://ex.org/o> .', format='nt')
query = "CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }"
result = self.store.execute_sparql(query)
self.assertTrue(result['success'])
self.assertEqual(result['bindings'], [])
self.assertEqual(result['variables'], [])
self.assertIn('triples', result)
self.assertEqual(result['metadata']['result_format'], 'construct')
triples = result['triples']
self.assertEqual(len(triples), 1)
s, p, o, meta = triples[0]
self.assertEqual(s, 'http://ex.org/s')
self.assertEqual(p, 'http://ex.org/p')
self.assertEqual(o, 'http://ex.org/o')
self.assertEqual(meta, {})
def test_typed_literal_datatype_preserved_in_metadata(self):
data = '<http://ex.org/s> <http://ex.org/age> "42"^^<http://www.w3.org/2001/XMLSchema#integer> .'
self.store.graph.parse(data=data, format='nt')
query = "CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }"
result = self.store.execute_sparql(query)
triples = result['triples']
self.assertEqual(len(triples), 1)
s, p, o, meta = triples[0]
self.assertEqual(o, '42')
self.assertEqual(meta['datatype'], 'http://www.w3.org/2001/XMLSchema#integer')
self.assertNotIn('language', meta)
def test_language_tagged_literal_preserved_in_metadata(self):
data = '<http://ex.org/s> <http://ex.org/label> "hello"@en .'
self.store.graph.parse(data=data, format='nt')
query = "CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }"
result = self.store.execute_sparql(query)
triples = result['triples']
self.assertEqual(len(triples), 1)
s, p, o, meta = triples[0]
self.assertEqual(o, 'hello')
self.assertEqual(meta['language'], 'en')
self.assertNotIn('datatype', meta)
class TestJenaStoreProperty9NonConstructUnchanged(unittest.TestCase):
def setUp(self):
self.store = JenaStore()
self.store.graph = Graph()
self.store.graph.parse(data='<http://ex.org/s> <http://ex.org/p> <http://ex.org/o> .', format='nt')
def test_select_response_shape_unchanged(self):
query = "SELECT ?s ?p ?o WHERE { ?s ?p ?o }"
result = self.store.execute_sparql(query)
self.assertTrue(result['success'])
self.assertIn('bindings', result)
self.assertEqual(len(result['bindings']), 1)
self.assertNotIn('triples', result)
self.assertEqual(result['metadata']['query'], query)
binding = result['bindings'][0]
self.assertEqual(binding['s']['value'], 'http://ex.org/s')
self.assertEqual(binding['s']['type'], 'uri')
def test_ask_uses_json_not_turtle_path(self):
query = "ASK WHERE { ?s ?p ?o }"
result = self.store.execute_sparql(query)
self.assertTrue(result['success'])
# For ASK in rdflib, results is a bool wrapped in SPARQLResult.
# results.vars is None, so it returns empty bindings. This matches the byte-for-byte behavior.
self.assertEqual(result['bindings'], [])
self.assertNotIn('triples', result)
class TestExecuteConstructTemplateWithJenaBackend(unittest.TestCase):
def setUp(self):
self.store = JenaStore()
self.store.graph = Graph()
self.store.graph.parse(data='<http://ex.org/s> <http://ex.org/name> "Alice" .', format='nt')
def test_end_to_end_with_jena_backend(self):
template = ConstructTemplate(
name="test",
description="test",
construct_query="""
CONSTRUCT {
?s <http://ex.org/isPerson> "true"^^<http://www.w3.org/2001/XMLSchema#boolean> .
} WHERE {
?s <http://ex.org/name> ?name .
}
""",
parameters=[]
)
results = execute_construct_template(template, {}, self.store)
self.assertEqual(len(results), 1)
triplet = results[0]
self.assertEqual(triplet.subject, 'http://ex.org/s')
self.assertEqual(triplet.predicate, 'http://ex.org/isPerson')
self.assertEqual(triplet.object, 'true')
self.assertEqual(triplet.metadata.get('datatype'), 'http://www.w3.org/2001/XMLSchema#boolean')
if __name__ == "__main__":
unittest.main()