"""SPARQL tool for incremental graph updates. This module provides functionality for executing SPARQL operations on RDF graphs, enabling incremental updates instead of full graph replacement. """ import logging from rdflib import BNode, Literal, URIRef from rdflib.plugins.sparql import prepareQuery from ontocast.onto.enum import SPARQLOperationType from ontocast.onto.rdfgraph import RDFGraph from ontocast.onto.sparql_models import SPARQLOperationModel from ontocast.tool.triple_manager.core import TripleStoreManager logger = logging.getLogger(__name__) class SPARQLTool: """Tool for executing SPARQL operations on RDF graphs.""" def __init__(self, triple_store_manager: TripleStoreManager | None = None): """Initialize SPARQL tool. Args: triple_store_manager: Optional triple store manager for persistent storage. """ self.triple_store_manager = triple_store_manager self.operation_history = [] def execute_operations( self, graph: RDFGraph, operations: list[SPARQLOperationModel] ) -> RDFGraph: """Execute a list of SPARQL operations on a graph. Args: graph: The RDF graph to operate on. operations: List of SPARQL operations to execute. Returns: RDFGraph: Updated graph after applying operations. """ logger.info(f"Executing {len(operations)} SPARQL operations") for operation in operations: try: self._execute_single_operation(graph, operation) self.operation_history.append(operation) logger.debug( f"Executed {operation.operation_type} operation: {operation.description}" ) except Exception as e: logger.error( f"Failed to execute {operation.operation_type} operation: {str(e)}" ) raise return graph def execute_operation(self, operation: SPARQLOperationModel) -> None: """Execute a single SPARQL operation. Args: operation: The SPARQL operation to execute. """ # For now, we'll use a simple approach - in a real implementation, # you might want to track which graph this operation should be applied to logger.info( f"Executing {operation.operation_type} operation: {operation.description}" ) # This is a placeholder - in practice, you'd need to specify which graph to operate on # or maintain a default graph in the tool def _execute_single_operation( self, graph: RDFGraph, operation: SPARQLOperationModel ): """Execute a single SPARQL operation. Args: graph: The RDF graph to operate on. operation: The SPARQL operation to execute. """ if operation.operation_type == SPARQLOperationType.INSERT: self._execute_insert(graph, operation) elif operation.operation_type == SPARQLOperationType.DELETE: self._execute_delete(graph, operation) elif operation.operation_type == SPARQLOperationType.UPDATE: self._execute_update(graph, operation) else: raise ValueError(f"Unknown operation type: {operation.operation_type}") def _execute_insert(self, graph: RDFGraph, operation: SPARQLOperationModel): """Execute INSERT operation. Args: graph: The RDF graph to operate on. operation: The INSERT operation to execute. """ # Parse the INSERT query query = prepareQuery(operation.query) # For INSERT DATA, we need to parse the triples and add them to the graph if "INSERT DATA" in operation.query.upper(): # Extract triples from INSERT DATA query triples = self._parse_insert_data_triples(operation.query) for triple in triples: graph.add(triple) else: # For other INSERT queries, execute against the graph graph.query(query) # INSERT queries typically don't return results, but we execute them def _execute_delete(self, graph: RDFGraph, operation: SPARQLOperationModel): """Execute DELETE operation. Args: graph: The RDF graph to operate on. operation: The DELETE operation to execute. """ # Parse the DELETE query query = prepareQuery(operation.query) # For DELETE DATA, we need to parse the triples and remove them from the graph if "DELETE DATA" in operation.query.upper(): # Extract triples from DELETE DATA query triples = self._parse_delete_data_triples(operation.query) for triple in triples: graph.remove(triple) else: # For other DELETE queries, execute against the graph graph.query(query) # DELETE queries typically don't return results, but we execute them def _execute_update(self, graph: RDFGraph, operation: SPARQLOperationModel): """Execute UPDATE operation. Args: graph: The RDF graph to operate on. operation: The UPDATE operation to execute. """ # Parse the UPDATE query query = prepareQuery(operation.query) # Execute the UPDATE query graph.query(query) # UPDATE queries typically don't return results, but we execute them def _parse_insert_data_triples(self, query: str) -> list[tuple]: """Parse triples from INSERT DATA query. Args: query: The INSERT DATA query string. Returns: List of triples to insert. """ # This is a simplified parser - in practice, you'd want a more robust parser triples = [] # Extract the content between INSERT DATA { ... } start = query.upper().find("INSERT DATA {") if start == -1: return triples start += len("INSERT DATA {") end = query.rfind("}") if end == -1: return triples data_content = query[start:end].strip() # Split by lines and parse each triple lines = [line.strip() for line in data_content.split("\n") if line.strip()] for line in lines: if line.endswith("."): line = line[:-1] # Remove trailing period # Parse the triple (simplified - assumes standard N3 format) parts = line.split() if len(parts) >= 3: subject = self._parse_term(parts[0]) predicate = self._parse_term(parts[1]) object_part = self._parse_term(" ".join(parts[2:])) if subject and predicate and object_part: triples.append((subject, predicate, object_part)) return triples def _parse_delete_data_triples(self, query: str) -> list[tuple]: """Parse triples from DELETE DATA query. Args: query: The DELETE DATA query string. Returns: List of triples to delete. """ # Similar to INSERT DATA parsing return self._parse_insert_data_triples( query.replace("DELETE DATA", "INSERT DATA") ) def _parse_term(self, term: str): """Parse a SPARQL term (subject, predicate, or object). Args: term: The term string to parse. Returns: Parsed RDF term (URIRef, Literal, or BNode). """ term = term.strip() if term.startswith("<") and term.endswith(">"): # URI return URIRef(term[1:-1]) elif term.startswith('"') and term.endswith('"'): # Literal return Literal(term[1:-1]) elif term.startswith("_:"): # Blank node return BNode(term[2:]) elif term.startswith('"') and '"^^' in term: # Typed literal value, datatype = term.split('"^^') return Literal(value[1:], datatype=URIRef(datatype)) else: # Assume it's a URI without angle brackets return URIRef(term) def validate_operation(self, operation: SPARQLOperationModel) -> bool: """Validate a SPARQL operation. Args: operation: The operation to validate. Returns: bool: True if valid, False otherwise. """ try: prepareQuery(operation.query) return True except Exception as e: logger.error(f"Invalid SPARQL operation: {str(e)}") return False def get_operation_history(self) -> list[SPARQLOperationModel]: """Get the history of executed operations. Returns: List of executed operations. """ return self.operation_history.copy() def clear_history(self): """Clear the operation history.""" self.operation_history.clear() def create_insert_operation( self, query: str, description: str = "" ) -> SPARQLOperationModel: """Create an INSERT operation. Args: query: The SPARQL INSERT query. description: Optional description of the operation. Returns: SPARQLOperationModel: The created operation. """ return SPARQLOperationModel( operation_type=SPARQLOperationType.INSERT, query=query, description=description, ) def create_delete_operation( self, query: str, description: str = "" ) -> SPARQLOperationModel: """Create a DELETE operation. Args: query: The SPARQL DELETE query. description: Optional description of the operation. Returns: SPARQLOperationModel: The created operation. """ return SPARQLOperationModel( operation_type=SPARQLOperationType.DELETE, query=query, description=description, ) def create_update_operation( self, query: str, description: str = "" ) -> SPARQLOperationModel: """Create an UPDATE operation. Args: query: The SPARQL UPDATE query. description: Optional description of the operation. Returns: SPARQLOperationModel: The created operation. """ return SPARQLOperationModel( operation_type=SPARQLOperationType.UPDATE, query=query, description=description, )