diff --git a/semantica/triplet_store/jena_store.py b/semantica/triplet_store/jena_store.py index 11bfd7be..b994343d 100644 --- a/semantica/triplet_store/jena_store.py +++ b/semantica/triplet_store/jena_store.py @@ -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}") diff --git a/tests/triplet_store/test_jena_store.py b/tests/triplet_store/test_jena_store.py new file mode 100644 index 00000000..c8021a2e --- /dev/null +++ b/tests/triplet_store/test_jena_store.py @@ -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=' .', 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 = ' "42"^^ .' + 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 = ' "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=' .', 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=' "Alice" .', format='nt') + + def test_end_to_end_with_jena_backend(self): + template = ConstructTemplate( + name="test", + description="test", + construct_query=""" + CONSTRUCT { + ?s "true"^^ . + } WHERE { + ?s ?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()