use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use std::time::Instant;
use crate::datatypes::Value;
use crate::error::KgError;
use crate::graph::dir_graph::DirGraph;
use crate::graph::embedder::Embedder;
use crate::graph::languages::cypher;
use crate::graph::languages::cypher::ast::{CypherQuery, OutputFormat};
use crate::graph::languages::cypher::result::CypherResult;
use crate::graph::languages::cypher::value_codec::ValueCodec;
pub struct ExecuteOptions<'a> {
pub params: &'a HashMap<String, Value>,
pub deadline: Option<Instant>,
pub max_rows: Option<usize>,
pub lazy_eligible: bool,
pub disabled_passes: Option<&'a HashSet<String>>,
pub embedder: Option<Arc<dyn Embedder>>,
pub value_codecs: Option<&'a [ValueCodec]>,
pub cancel: Option<&'static AtomicBool>,
}
impl<'a> ExecuteOptions<'a> {
pub fn new(params: &'a HashMap<String, Value>) -> Self {
Self::eager(params)
}
pub fn eager(params: &'a HashMap<String, Value>) -> Self {
Self {
params,
deadline: None,
max_rows: None,
lazy_eligible: false,
disabled_passes: None,
embedder: None,
value_codecs: None,
cancel: None,
}
}
}
#[inline]
fn exec_err(opts: &ExecuteOptions<'_>, message: String) -> KgError {
if opts
.cancel
.is_some_and(|c| c.load(std::sync::atomic::Ordering::Relaxed))
{
KgError::Cancelled
} else {
KgError::CypherExecution {
message,
position: None,
}
}
}
pub struct ExecuteOutcome {
pub result: CypherResult,
pub is_mutation: bool,
pub output_format: OutputFormat,
pub explain: bool,
}
pub fn execute_read(
graph: &DirGraph,
query: &str,
opts: &ExecuteOptions<'_>,
) -> Result<ExecuteOutcome, KgError> {
let (parsed, params, encode_plan) = prepare(graph, query, opts)?;
let is_mutation = cypher::is_mutation_query(&parsed);
if parsed.explain {
let result = cypher::generate_explain_result(&parsed, graph);
return Ok(ExecuteOutcome {
result,
is_mutation,
output_format: parsed.output_format,
explain: true,
});
}
if is_mutation {
return Err(KgError::Argument(
"execute_read called with a mutation query (CREATE/SET/DELETE/REMOVE/MERGE) \
— use execute_mut against a mutable graph view"
.to_string(),
));
}
let mut result = cypher::CypherExecutor::with_params(graph, ¶ms, opts.deadline)
.with_max_rows(opts.max_rows)
.with_streaming(opts.lazy_eligible)
.with_cancel(opts.cancel)
.execute(&parsed)
.map_err(|message| exec_err(opts, message))?;
cypher::value_codec::apply_encode(&mut result, &encode_plan);
Ok(ExecuteOutcome {
result,
is_mutation: false,
output_format: parsed.output_format,
explain: false,
})
}
pub fn execute_mut(
graph: &mut DirGraph,
query: &str,
opts: &ExecuteOptions<'_>,
) -> Result<ExecuteOutcome, KgError> {
let (parsed, params, encode_plan) = prepare(graph, query, opts)?;
let is_mutation = cypher::is_mutation_query(&parsed);
if parsed.explain {
let result = cypher::generate_explain_result(&parsed, graph);
return Ok(ExecuteOutcome {
result,
is_mutation,
output_format: parsed.output_format,
explain: true,
});
}
let mut result = if is_mutation {
let interrupt = crate::graph::algorithms::Interrupt {
deadline: opts.deadline,
cancel: opts.cancel,
};
let r = cypher::execute_mutable(graph, &parsed, params, interrupt)
.map_err(|message| exec_err(opts, message))?;
graph.bump_version();
r
} else {
cypher::CypherExecutor::with_params(graph, ¶ms, opts.deadline)
.with_max_rows(opts.max_rows)
.with_streaming(opts.lazy_eligible)
.with_cancel(opts.cancel)
.execute(&parsed)
.map_err(|message| exec_err(opts, message))?
};
cypher::value_codec::apply_encode(&mut result, &encode_plan);
Ok(ExecuteOutcome {
result,
is_mutation,
output_format: parsed.output_format,
explain: false,
})
}
type PreparedQuery = (
Arc<CypherQuery>,
HashMap<String, Value>,
Vec<Option<ValueCodec>>,
);
fn prepare(
graph: &DirGraph,
query: &str,
opts: &ExecuteOptions<'_>,
) -> Result<PreparedQuery, KgError> {
let cacheable = opts.params.is_empty()
&& opts.disabled_passes.is_none_or(|s| s.is_empty())
&& opts.value_codecs.is_none_or(|c| c.is_empty());
if cacheable {
if let Some(plan) =
cypher::plan_cache::get(graph.graph_id(), graph.version(), opts.lazy_eligible, query)
{
return Ok((plan, HashMap::new(), Vec::new()));
}
}
let mut parsed = cypher::parse_cypher(query)?;
let codecs = opts.value_codecs.unwrap_or(&[]);
cypher::value_codec::apply_decode(&mut parsed, codecs);
let encode_plan = cypher::value_codec::build_encode_plan(&parsed, codecs);
cypher::validate_schema(&parsed, graph).map_err(KgError::from)?;
cypher::warn_unknown_pattern_refs(&parsed, graph);
let rewrite = cypher::rewrite_text_score(&mut parsed, opts.params).map_err(|message| {
KgError::CypherExecution {
message,
position: None,
}
})?;
let params: Cow<'_, HashMap<String, Value>> =
if !rewrite.texts_to_embed.is_empty() && !parsed.explain {
Cow::Owned(embed_into_params(opts, &rewrite)?)
} else {
Cow::Borrowed(opts.params)
};
let disabled_default = cypher::planner::empty_disabled_set();
let disabled_ref = opts.disabled_passes.unwrap_or(disabled_default);
cypher::planner::optimize_with_disabled(&mut parsed, graph, ¶ms, disabled_ref);
if opts.lazy_eligible {
cypher::mark_lazy_eligibility(&mut parsed);
}
let plan = Arc::new(parsed);
if cacheable && params.is_empty() {
cypher::plan_cache::insert(
graph.graph_id(),
graph.version(),
opts.lazy_eligible,
query,
plan.clone(),
);
}
Ok((plan, params.into_owned(), encode_plan))
}
fn embed_into_params(
opts: &ExecuteOptions<'_>,
rewrite: &cypher::planner::simplification::TextScoreRewrite,
) -> Result<HashMap<String, Value>, KgError> {
let model = opts
.embedder
.as_ref()
.ok_or_else(|| KgError::CypherExecution {
message: "text_score() requires a registered embedding model. \
Call g.set_embedder(model) first (Python) or pass an embedder \
via ExecuteOptions::embedder (downstream Rust consumers)."
.to_string(),
position: None,
})?;
model.load().map_err(|message| KgError::CypherExecution {
message,
position: None,
})?;
let texts: Vec<String> = rewrite
.texts_to_embed
.iter()
.map(|(_, t)| t.clone())
.collect();
let embed_result = model.embed(&texts);
model.unload();
let embeddings: Vec<Vec<f32>> = embed_result.map_err(|message| KgError::CypherExecution {
message,
position: None,
})?;
if embeddings.len() != texts.len() {
return Err(KgError::CypherExecution {
message: format!(
"text_score: model.embed() returned {} vectors for {} texts",
embeddings.len(),
texts.len()
),
position: None,
});
}
let mut params = opts.params.clone();
for (i, (param_name, _)) in rewrite.texts_to_embed.iter().enumerate() {
let json = format!(
"[{}]",
embeddings[i]
.iter()
.map(|f| f.to_string())
.collect::<Vec<_>>()
.join(", ")
);
params.insert(param_name.clone(), Value::String(json));
}
Ok(params)
}
#[cfg(test)]
mod version_soundness_tests {
use super::*;
use crate::graph::dir_graph::DirGraph;
#[test]
fn execute_mut_write_bumps_version() {
let mut g = DirGraph::new();
let params = HashMap::new();
let opts = ExecuteOptions::eager(¶ms);
let before = g.version();
execute_mut(&mut g, "CREATE (:Item {id: 1})", &opts).expect("create");
assert!(
g.version() > before,
"a Cypher write must bump version (was {before}, now {})",
g.version()
);
}
#[test]
fn execute_read_does_not_bump_version() {
let mut g = DirGraph::new();
let params = HashMap::new();
let opts = ExecuteOptions::eager(¶ms);
execute_mut(&mut g, "CREATE (:Item {id: 1})", &opts).expect("create");
let after_write = g.version();
let _ = execute_read(&g, "MATCH (n:Item) RETURN n.id", &opts).expect("read");
assert_eq!(g.version(), after_write, "a read must not bump version");
}
}