use std::collections::{HashMap, HashSet};
use nodedb_query::expr_parse::parser::parse_generated_expr;
use nodedb_query::value_ops::is_truthy;
use nodedb_types::result::QueryResult;
use nodedb_types::value::Value;
use crate::error::LiteError;
use crate::query::query_ops::joins::common::maps_to_result;
pub async fn execute_recursive_value(
cte_name: &str,
columns: &[String],
init_exprs: &[String],
step_exprs: &[String],
condition: Option<&str>,
max_depth: usize,
distinct: bool,
) -> Result<QueryResult, LiteError> {
if columns.len() != init_exprs.len() || columns.len() != step_exprs.len() {
return Err(LiteError::Storage {
detail: format!(
"recursive CTE '{cte_name}': columns/init_exprs/step_exprs length mismatch \
(columns={}, init={}, step={})",
columns.len(),
init_exprs.len(),
step_exprs.len(),
),
});
}
let init_parsed: Vec<_> = init_exprs
.iter()
.map(|e| parse_expr_text(cte_name, e))
.collect::<Result<_, _>>()?;
let step_parsed: Vec<_> = step_exprs
.iter()
.map(|e| parse_expr_text(cte_name, e))
.collect::<Result<_, _>>()?;
let cond_parsed = condition
.map(|c| parse_expr_text(cte_name, c))
.transpose()?;
let effective_max = if max_depth == 0 { 100 } else { max_depth };
let mut accumulator: Vec<HashMap<String, Value>> = Vec::new();
let mut seen: HashSet<String> = HashSet::new();
let empty_doc = Value::Object(HashMap::new());
let anchor_row = eval_exprs(&init_parsed, &empty_doc, columns)?;
maybe_push(&mut accumulator, &mut seen, anchor_row.clone(), distinct);
let mut current_row = anchor_row;
for iter in 0..usize::MAX {
if iter >= effective_max {
return Err(LiteError::Storage {
detail: format!("recursive CTE '{cte_name}' exceeded max_depth={effective_max}"),
});
}
let doc = Value::Object(current_row.clone());
if let Some(cond) = &cond_parsed {
let result = cond.eval(&doc)?;
if !is_truthy(&result) {
break;
}
} else {
if !accumulator.is_empty() {
break;
}
}
let next_row = eval_exprs(&step_parsed, &doc, columns)?;
if distinct {
let key = row_dedup_key(&next_row);
if seen.contains(&key) {
break;
}
}
maybe_push(&mut accumulator, &mut seen, next_row.clone(), distinct);
current_row = next_row;
}
Ok(maps_to_result(accumulator))
}
fn parse_expr_text(
cte_name: &str,
text: &str,
) -> Result<nodedb_query::expr::types::SqlExpr, LiteError> {
parse_generated_expr(text)
.map(|(expr, _)| expr)
.map_err(|e| LiteError::Storage {
detail: format!("recursive CTE '{cte_name}': failed to parse expr '{text}': {e}"),
})
}
fn eval_exprs(
exprs: &[nodedb_query::expr::types::SqlExpr],
doc: &Value,
columns: &[String],
) -> Result<HashMap<String, Value>, LiteError> {
columns
.iter()
.zip(exprs.iter())
.map(|(col, expr)| Ok((col.clone(), expr.eval(doc)?)))
.collect()
}
fn maybe_push(
acc: &mut Vec<HashMap<String, Value>>,
seen: &mut HashSet<String>,
row: HashMap<String, Value>,
distinct: bool,
) {
if distinct {
let key = row_dedup_key(&row);
if seen.contains(&key) {
return;
}
seen.insert(key);
}
acc.push(row);
}
fn row_dedup_key(row: &HashMap<String, Value>) -> String {
let mut pairs: Vec<(&String, &Value)> = row.iter().collect();
pairs.sort_by_key(|(k, _)| k.as_str());
pairs
.into_iter()
.map(|(k, v)| format!("{k}={v:?}"))
.collect::<Vec<_>>()
.join(";")
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_recursive_value_counter_1_to_10() {
let columns = vec!["n".to_string()];
let init = vec!["1".to_string()];
let step = vec!["n + 1".to_string()];
let condition = Some("n < 10");
let result =
execute_recursive_value("counter", &columns, &init, &step, condition, 50, false)
.await
.unwrap();
assert_eq!(result.rows.len(), 10, "expected 10 rows (1 through 10)");
let n_col = result.columns.iter().position(|c| c == "n").unwrap();
let first = &result.rows[0][n_col];
let last = &result.rows[9][n_col];
assert_eq!(*first, Value::Integer(1));
assert_eq!(*last, Value::Integer(10));
}
#[tokio::test]
async fn test_recursive_value_distinct_deduplication() {
let columns = vec!["n".to_string()];
let init = vec!["5".to_string()];
let step = vec!["5".to_string()]; let condition = Some("n < 10");
let result =
execute_recursive_value("dup_cte", &columns, &init, &step, condition, 20, true)
.await
.unwrap();
assert_eq!(result.rows.len(), 1);
}
}