use ggen_core::Graph;
use ggen_utils::error::{Context, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Debug, Clone)]
pub struct QueryOptions {
pub query: String,
pub graph_file: Option<String>,
pub output_format: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QueryResult {
pub bindings: Vec<HashMap<String, String>>,
pub variables: Vec<String>,
pub result_count: usize,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct QueryInput {
pub query: String,
pub graph_file: Option<PathBuf>,
pub format: String,
}
impl QueryInput {
pub fn new(query: String) -> Self {
Self {
query,
format: "table".to_string(),
..Default::default()
}
}
}
impl QueryResult {
pub fn empty() -> Self {
Self {
bindings: Vec::new(),
variables: Vec::new(),
result_count: 0,
}
}
pub fn from_bindings(bindings: Vec<HashMap<String, String>>, variables: Vec<String>) -> Self {
let result_count = bindings.len();
Self {
bindings,
variables,
result_count,
}
}
}
pub fn execute_sparql(options: QueryOptions) -> Result<QueryResult> {
let graph = if let Some(graph_file) = &options.graph_file {
Graph::load_from_file(graph_file)
.context(format!("Failed to load graph from file: {}", graph_file))?
} else {
Graph::new().context("Failed to create empty graph")?
};
let query_results = graph
.query(&options.query)
.context("Failed to execute SPARQL query")?;
match query_results {
oxigraph::sparql::QueryResults::Solutions(solutions) => {
let variables: Vec<String> = solutions
.variables()
.iter()
.map(|v| v.to_string())
.collect();
let mut bindings = Vec::new();
for solution in solutions {
let solution = solution
.map_err(|e| {
ggen_utils::error::Error::new(&format!(
"Failed to process SPARQL solution: {}",
e
))
})
.context("Failed to process SPARQL solution")?;
let mut binding = HashMap::new();
for variable in &variables {
let var_name = variable.strip_prefix('?').unwrap_or(variable);
if let Some(value) = solution.get(var_name) {
binding.insert(variable.clone(), value.to_string());
}
}
bindings.push(binding);
}
Ok(QueryResult::from_bindings(bindings, variables))
}
oxigraph::sparql::QueryResults::Boolean(result) => {
let mut binding = HashMap::new();
binding.insert("result".to_string(), result.to_string());
Ok(QueryResult::from_bindings(
vec![binding],
vec!["result".to_string()],
))
}
oxigraph::sparql::QueryResults::Graph(_) => {
Ok(QueryResult::from_bindings(
vec![],
vec!["graph".to_string()],
))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_execute_sparql_with_real_graph() -> Result<()> {
let graph = Graph::new()?;
let turtle = r#"
@prefix ex: <http://example.org/> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
ex:alice a foaf:Person ;
foaf:name "Alice" ;
foaf:age "30" .
ex:bob a foaf:Person ;
foaf:name "Bob" ;
foaf:age "25" .
"#;
graph.insert_turtle(turtle)?;
let temp_file = tempfile::Builder::new().suffix(".ttl").tempfile()?;
std::fs::write(temp_file.path(), turtle.as_bytes())?;
let temp_path = temp_file.path().to_string_lossy().to_string();
let options = QueryOptions {
query: r#"
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
SELECT ?name ?age
WHERE {
?person foaf:name ?name ;
foaf:age ?age .
}
ORDER BY ?name
"#
.to_string(),
graph_file: Some(temp_path),
output_format: "json".to_string(),
};
let result = execute_sparql(options)?;
assert_eq!(result.variables, vec!["?name", "?age"]);
assert_eq!(result.result_count, 2);
assert_eq!(result.bindings.len(), 2);
assert!(result.bindings[0].get("?name").unwrap().contains("Alice"));
assert!(result.bindings[1].get("?name").unwrap().contains("Bob"));
Ok(())
}
#[test]
fn test_execute_ask_query_with_real_graph() -> Result<()> {
let graph = Graph::new()?;
let turtle = r#"
@prefix ex: <http://example.org/> .
ex:subject ex:predicate ex:object .
"#;
graph.insert_turtle(turtle)?;
let temp_file = tempfile::Builder::new().suffix(".ttl").tempfile()?;
std::fs::write(temp_file.path(), turtle.as_bytes())?;
let temp_path = temp_file.path().to_string_lossy().to_string();
let options = QueryOptions {
query: "ASK { ?s ?p ?o }".to_string(),
graph_file: Some(temp_path),
output_format: "json".to_string(),
};
let result = execute_sparql(options)?;
assert_eq!(result.variables, vec!["result"]);
assert!(result.bindings[0].get("result").unwrap().contains("true"));
Ok(())
}
#[test]
fn test_execute_sparql_empty_graph() -> Result<()> {
let options = QueryOptions {
query: "SELECT ?s ?p ?o WHERE { ?s ?p ?o }".to_string(),
graph_file: None,
output_format: "json".to_string(),
};
let result = execute_sparql(options)?;
assert_eq!(result.result_count, 0);
assert_eq!(result.bindings.len(), 0);
Ok(())
}
#[test]
fn test_execute_sparql_with_filter() -> Result<()> {
let graph = Graph::new()?;
let turtle = r#"
@prefix ex: <http://example.org/> .
ex:alice ex:age "30"^^<http://www.w3.org/2001/XMLSchema#integer> .
ex:bob ex:age "25"^^<http://www.w3.org/2001/XMLSchema#integer> .
ex:charlie ex:age "35"^^<http://www.w3.org/2001/XMLSchema#integer> .
"#;
graph.insert_turtle(turtle)?;
let temp_file = tempfile::Builder::new().suffix(".ttl").tempfile()?;
std::fs::write(temp_file.path(), turtle.as_bytes())?;
let temp_path = temp_file.path().to_string_lossy().to_string();
let options = QueryOptions {
query: r#"
PREFIX ex: <http://example.org/>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
SELECT ?person ?age
WHERE {
?person ex:age ?age .
FILTER(?age > "28"^^xsd:integer)
}
"#
.to_string(),
graph_file: Some(temp_path),
output_format: "json".to_string(),
};
let result = execute_sparql(options)?;
assert_eq!(result.result_count, 2); assert_eq!(result.bindings.len(), 2);
Ok(())
}
}
pub async fn execute_query(input: QueryInput) -> Result<QueryResult> {
let options = QueryOptions {
query: input.query,
graph_file: input
.graph_file
.as_ref()
.map(|p| p.to_string_lossy().to_string()),
output_format: input.format,
};
execute_sparql(options)
.map_err(|e| ggen_utils::error::Error::new(&format!("SPARQL query failed: {}", e)))
}