Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 31 additions & 13 deletions crates/ruvector-postgres/src/graph/operators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -332,19 +332,37 @@ fn ruvector_sparql(store_name: &str, query: &str, format: &str) -> Result<String
let store = get_store(store_name)
.ok_or_else(|| format!("Triple store '{}' does not exist", store_name))?;

let parsed = parse_sparql(query).map_err(|e| format!("Parse error: {}", e))?;

let result = execute_sparql(&store, &parsed).map_err(|e| format!("Execution error: {}", e))?;

let result_format = match format.to_lowercase().as_str() {
"json" => ResultFormat::Json,
"xml" => ResultFormat::Xml,
"csv" => ResultFormat::Csv,
"tsv" => ResultFormat::Tsv,
_ => ResultFormat::Json,
};

Ok(format_results(&result, result_format))
let format_lower = format.to_lowercase();

// Wrap parse/execute/format in catch_unwind to convert any remaining
// panics into PostgreSQL errors instead of crashing the backend
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let parsed = parse_sparql(query).map_err(|e| format!("Parse error: {}", e))?;
let result =
execute_sparql(&store, &parsed).map_err(|e| format!("Execution error: {}", e))?;
let result_format = match format_lower.as_str() {
"json" => ResultFormat::Json,
"xml" => ResultFormat::Xml,
"csv" => ResultFormat::Csv,
"tsv" => ResultFormat::Tsv,
_ => ResultFormat::Json,
};
Ok(format_results(&result, result_format))
}));

match result {
Ok(inner) => inner,
Err(panic_info) => {
let msg = if let Some(s) = panic_info.downcast_ref::<String>() {
s.clone()
} else if let Some(s) = panic_info.downcast_ref::<&str>() {
s.to_string()
} else {
"Unknown internal error".to_string()
};
Err(format!("Internal error: {}", msg))
}
}
}

/// Execute a SPARQL query and return results as JSONB
Expand Down
11 changes: 5 additions & 6 deletions crates/ruvector-postgres/src/graph/sparql/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ static EMPTY_PREFIXES: Lazy<HashMap<String, Iri>> = Lazy::new(HashMap::new);
/// Execution context for SPARQL queries
pub struct SparqlContext<'a> {
pub store: &'a TripleStore,
pub default_graph: Option<&'a str>,
pub default_graph: Option<String>,
pub named_graphs: Vec<&'a str>,
pub base: Option<&'a Iri>,
pub prefixes: &'a HashMap<String, Iri>,
Expand Down Expand Up @@ -260,23 +260,22 @@ fn evaluate_graph_pattern(

if let Some(graph) = graph_iri {
// Temporarily set the graph context
let old_default = ctx.default_graph;
ctx.default_graph = Some(Box::leak(graph.into_boxed_str()));
let old_default = ctx.default_graph.clone();
ctx.default_graph = Some(graph);
let result = evaluate_graph_pattern(ctx, inner);
ctx.default_graph = old_default;
result
} else {
// Union over all named graphs
let mut all_solutions = Vec::new();
for graph in ctx.store.list_graphs() {
let graph_str: &'static str = Box::leak(graph.into_boxed_str());
ctx.default_graph = Some(graph_str);
ctx.default_graph = Some(graph.clone());
let solutions = evaluate_graph_pattern(ctx, inner)?;

// Add graph variable binding
if let VarOrIri::Variable(var) = graph_name {
for mut sol in solutions {
sol.insert(var.clone(), RdfTerm::Iri(Iri::new(graph_str)));
sol.insert(var.clone(), RdfTerm::Iri(Iri::new(&graph)));
all_solutions.push(sol);
}
} else {
Expand Down
3 changes: 2 additions & 1 deletion crates/ruvector-postgres/src/graph/sparql/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ impl<'a> SparqlParser<'a> {

fn parse_query_body(&mut self) -> Result<QueryBody, SparqlError> {
self.skip_whitespace();
let saved_pos = self.pos;

if self.match_keyword("SELECT") {
Ok(QueryBody::Select(self.parse_select_query()?))
Expand All @@ -87,7 +88,7 @@ impl<'a> SparqlParser<'a> {
|| self.match_keyword("CREATE")
|| self.match_keyword("DROP")
{
self.pos = self.pos.saturating_sub(6); // Backtrack
self.pos = saved_pos; // Backtrack to before the matched keyword
Ok(QueryBody::Update(self.parse_update()?))
} else {
Err(SparqlError::ParseError(format!(
Expand Down