nodedb_lite/query/query_ops/
recursive_scan.rs1use std::collections::{HashMap, HashSet};
10
11use nodedb_types::result::QueryResult;
12use nodedb_types::value::Value;
13
14use crate::error::LiteError;
15use crate::query::engine::LiteQueryEngine;
16use crate::query::query_ops::joins::common::{
17 apply_filters, decode_filters, maps_to_result, scan_collection,
18};
19use crate::storage::engine::StorageEngine;
20
21pub struct RecursiveScanSpec<'a> {
26 pub collection: &'a str,
27 pub base_filters: &'a [u8],
28 pub recursive_filters: &'a [u8],
29 pub join_link: Option<&'a (String, String)>,
30 pub max_iterations: usize,
31 pub distinct: bool,
32 pub limit: usize,
33}
34
35pub async fn execute_recursive_scan<S: StorageEngine>(
41 engine: &LiteQueryEngine<S>,
42 spec: RecursiveScanSpec<'_>,
43) -> Result<QueryResult, LiteError> {
44 let RecursiveScanSpec {
45 collection,
46 base_filters,
47 recursive_filters,
48 join_link,
49 max_iterations,
50 distinct,
51 limit,
52 } = spec;
53 let base_parsed = decode_filters(base_filters)?;
54 let rec_parsed = decode_filters(recursive_filters)?;
55
56 let full_collection = scan_collection(engine, collection).await?;
58
59 let anchor = apply_filters(full_collection.clone(), &base_parsed)?;
61
62 let effective_limit = if limit == 0 { usize::MAX } else { limit };
63 let max_iter = if max_iterations == 0 {
64 100
65 } else {
66 max_iterations
67 };
68
69 let mut accumulator: Vec<HashMap<String, Value>> = Vec::new();
70 let mut seen: HashSet<String> = HashSet::new();
71
72 let append_rows = |acc: &mut Vec<HashMap<String, Value>>,
73 seen: &mut HashSet<String>,
74 rows: Vec<HashMap<String, Value>>,
75 dedup: bool,
76 cap: usize|
77 -> Vec<HashMap<String, Value>> {
78 let mut new_working: Vec<HashMap<String, Value>> = Vec::new();
79 for row in rows {
80 if acc.len() >= cap {
81 break;
82 }
83 if dedup {
84 let key = row_key(&row);
85 if seen.contains(&key) {
86 continue;
87 }
88 seen.insert(key);
89 }
90 new_working.push(row.clone());
91 acc.push(row);
92 }
93 new_working
94 };
95
96 let mut working_set = append_rows(
97 &mut accumulator,
98 &mut seen,
99 anchor,
100 distinct,
101 effective_limit,
102 );
103
104 for _ in 0..max_iter {
105 if working_set.is_empty() || accumulator.len() >= effective_limit {
106 break;
107 }
108
109 let candidates = apply_filters(full_collection.clone(), &rec_parsed)?;
111
112 let next_rows = match join_link {
114 Some((coll_field, working_field)) => {
115 let working_vals: HashSet<String> = working_set
117 .iter()
118 .map(|r| value_key(r.get(working_field).unwrap_or(&Value::Null)))
119 .collect();
120
121 candidates
122 .into_iter()
123 .filter(|row| {
124 let v = value_key(row.get(coll_field).unwrap_or(&Value::Null));
125 working_vals.contains(&v)
126 })
127 .collect()
128 }
129 None => candidates,
130 };
131
132 working_set = append_rows(
133 &mut accumulator,
134 &mut seen,
135 next_rows,
136 distinct,
137 effective_limit,
138 );
139 }
140
141 Ok(maps_to_result(accumulator))
142}
143
144fn row_key(row: &HashMap<String, Value>) -> String {
146 let mut pairs: Vec<(&String, &Value)> = row.iter().collect();
147 pairs.sort_by_key(|(k, _)| k.as_str());
148 pairs
149 .into_iter()
150 .map(|(k, v)| format!("{k}={}", value_key(v)))
151 .collect::<Vec<_>>()
152 .join(";")
153}
154
155fn value_key(v: &Value) -> String {
156 match v {
157 Value::Null => "null".to_string(),
158 Value::Bool(b) => format!("b:{b}"),
159 Value::Integer(n) => format!("i:{n}"),
160 Value::Float(f) => format!("f:{f}"),
161 Value::String(s) => format!("s:{s}"),
162 Value::Uuid(s) | Value::Ulid(s) => format!("id:{s}"),
163 Value::Bytes(b) => format!("by:{}", b.len()),
164 Value::Array(a) | Value::Set(a) => format!("a:{}", a.len()),
165 Value::Object(m) => format!("o:{}", m.len()),
166 _ => "other".to_string(),
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173
174 fn node(id: i64, parent_id: i64) -> HashMap<String, Value> {
175 let mut m = HashMap::new();
176 m.insert("id".to_string(), Value::Integer(id));
177 m.insert("parent_id".to_string(), Value::Integer(parent_id));
178 m
179 }
180
181 #[test]
185 fn test_recursive_scan_tree_logic() {
186 let all_nodes = [node(1, 0), node(2, 1), node(3, 1), node(4, 2)];
187
188 let anchor: Vec<_> = all_nodes
190 .iter()
191 .filter(|r| r.get("parent_id") == Some(&Value::Integer(0)))
192 .cloned()
193 .collect();
194 assert_eq!(anchor.len(), 1); let join_link = ("parent_id".to_string(), "id".to_string());
197 let mut accumulator: Vec<HashMap<String, Value>> = anchor.clone();
198 let mut working_set: Vec<HashMap<String, Value>> = anchor;
199
200 for _ in 0..10 {
201 if working_set.is_empty() {
202 break;
203 }
204 let working_vals: std::collections::HashSet<String> = working_set
205 .iter()
206 .map(|r| value_key(r.get(&join_link.1).unwrap_or(&Value::Null)))
207 .collect();
208 let next: Vec<_> = all_nodes
209 .iter()
210 .filter(|r| {
211 let v = value_key(r.get(&join_link.0).unwrap_or(&Value::Null));
212 working_vals.contains(&v)
213 })
214 .cloned()
215 .collect();
216 let already: std::collections::HashSet<String> =
218 accumulator.iter().map(row_key).collect();
219 let new_rows: Vec<_> = next
220 .into_iter()
221 .filter(|r| !already.contains(&row_key(r)))
222 .collect();
223 working_set = new_rows.clone();
224 accumulator.extend(new_rows);
225 }
226
227 assert_eq!(accumulator.len(), 4, "all 4 tree nodes reachable from root");
228 }
229}