use std::collections::{HashMap, HashSet};
use nodedb_types::result::QueryResult;
use nodedb_types::value::Value;
use crate::error::LiteError;
use crate::query::engine::LiteQueryEngine;
use crate::query::query_ops::joins::common::{
apply_filters, decode_filters, maps_to_result, scan_collection,
};
use crate::storage::engine::StorageEngine;
pub struct RecursiveScanSpec<'a> {
pub collection: &'a str,
pub base_filters: &'a [u8],
pub recursive_filters: &'a [u8],
pub join_link: Option<&'a (String, String)>,
pub max_iterations: usize,
pub distinct: bool,
pub limit: usize,
}
pub async fn execute_recursive_scan<S: StorageEngine>(
engine: &LiteQueryEngine<S>,
spec: RecursiveScanSpec<'_>,
) -> Result<QueryResult, LiteError> {
let RecursiveScanSpec {
collection,
base_filters,
recursive_filters,
join_link,
max_iterations,
distinct,
limit,
} = spec;
let base_parsed = decode_filters(base_filters)?;
let rec_parsed = decode_filters(recursive_filters)?;
let full_collection = scan_collection(engine, collection).await?;
let anchor = apply_filters(full_collection.clone(), &base_parsed)?;
let effective_limit = if limit == 0 { usize::MAX } else { limit };
let max_iter = if max_iterations == 0 {
100
} else {
max_iterations
};
let mut accumulator: Vec<HashMap<String, Value>> = Vec::new();
let mut seen: HashSet<String> = HashSet::new();
let append_rows = |acc: &mut Vec<HashMap<String, Value>>,
seen: &mut HashSet<String>,
rows: Vec<HashMap<String, Value>>,
dedup: bool,
cap: usize|
-> Vec<HashMap<String, Value>> {
let mut new_working: Vec<HashMap<String, Value>> = Vec::new();
for row in rows {
if acc.len() >= cap {
break;
}
if dedup {
let key = row_key(&row);
if seen.contains(&key) {
continue;
}
seen.insert(key);
}
new_working.push(row.clone());
acc.push(row);
}
new_working
};
let mut working_set = append_rows(
&mut accumulator,
&mut seen,
anchor,
distinct,
effective_limit,
);
for _ in 0..max_iter {
if working_set.is_empty() || accumulator.len() >= effective_limit {
break;
}
let candidates = apply_filters(full_collection.clone(), &rec_parsed)?;
let next_rows = match join_link {
Some((coll_field, working_field)) => {
let working_vals: HashSet<String> = working_set
.iter()
.map(|r| value_key(r.get(working_field).unwrap_or(&Value::Null)))
.collect();
candidates
.into_iter()
.filter(|row| {
let v = value_key(row.get(coll_field).unwrap_or(&Value::Null));
working_vals.contains(&v)
})
.collect()
}
None => candidates,
};
working_set = append_rows(
&mut accumulator,
&mut seen,
next_rows,
distinct,
effective_limit,
);
}
Ok(maps_to_result(accumulator))
}
fn row_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}={}", value_key(v)))
.collect::<Vec<_>>()
.join(";")
}
fn value_key(v: &Value) -> String {
match v {
Value::Null => "null".to_string(),
Value::Bool(b) => format!("b:{b}"),
Value::Integer(n) => format!("i:{n}"),
Value::Float(f) => format!("f:{f}"),
Value::String(s) => format!("s:{s}"),
Value::Uuid(s) | Value::Ulid(s) => format!("id:{s}"),
Value::Bytes(b) => format!("by:{}", b.len()),
Value::Array(a) | Value::Set(a) => format!("a:{}", a.len()),
Value::Object(m) => format!("o:{}", m.len()),
_ => "other".to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn node(id: i64, parent_id: i64) -> HashMap<String, Value> {
let mut m = HashMap::new();
m.insert("id".to_string(), Value::Integer(id));
m.insert("parent_id".to_string(), Value::Integer(parent_id));
m
}
#[test]
fn test_recursive_scan_tree_logic() {
let all_nodes = [node(1, 0), node(2, 1), node(3, 1), node(4, 2)];
let anchor: Vec<_> = all_nodes
.iter()
.filter(|r| r.get("parent_id") == Some(&Value::Integer(0)))
.cloned()
.collect();
assert_eq!(anchor.len(), 1);
let join_link = ("parent_id".to_string(), "id".to_string());
let mut accumulator: Vec<HashMap<String, Value>> = anchor.clone();
let mut working_set: Vec<HashMap<String, Value>> = anchor;
for _ in 0..10 {
if working_set.is_empty() {
break;
}
let working_vals: std::collections::HashSet<String> = working_set
.iter()
.map(|r| value_key(r.get(&join_link.1).unwrap_or(&Value::Null)))
.collect();
let next: Vec<_> = all_nodes
.iter()
.filter(|r| {
let v = value_key(r.get(&join_link.0).unwrap_or(&Value::Null));
working_vals.contains(&v)
})
.cloned()
.collect();
let already: std::collections::HashSet<String> =
accumulator.iter().map(row_key).collect();
let new_rows: Vec<_> = next
.into_iter()
.filter(|r| !already.contains(&row_key(r)))
.collect();
working_set = new_rows.clone();
accumulator.extend(new_rows);
}
assert_eq!(accumulator.len(), 4, "all 4 tree nodes reachable from root");
}
}