nodedb_lite/query/query_ops/joins/
nested_loop.rs1use std::collections::HashMap;
8
9use nodedb_query::scan_filter::ScanFilter;
10use nodedb_types::result::QueryResult;
11use nodedb_types::value::Value;
12
13use crate::error::LiteError;
14use crate::query::engine::LiteQueryEngine;
15use crate::storage::engine::StorageEngine;
16
17use super::common::{maps_to_result, merge_rows, scan_collection};
18
19pub async fn execute_nested_loop_join<S: StorageEngine>(
20 engine: &LiteQueryEngine<S>,
21 left_collection: &str,
22 right_collection: &str,
23 condition: &[u8],
24 join_type: &str,
25 limit: usize,
26) -> Result<QueryResult, LiteError> {
27 let left_rows = scan_collection(engine, left_collection).await?;
28 let right_rows = scan_collection(engine, right_collection).await?;
29
30 let filters: Vec<ScanFilter> = if condition.is_empty() {
31 Vec::new()
32 } else {
33 zerompk::from_msgpack(condition).map_err(|e| LiteError::Serialization {
34 detail: format!("decode nested loop condition: {e}"),
35 })?
36 };
37
38 let effective_limit = if limit == 0 { usize::MAX } else { limit };
39 let mut output: Vec<HashMap<String, Value>> = Vec::new();
40
41 'outer: for left_row in &left_rows {
42 let mut matched = false;
43 for right_row in &right_rows {
44 let merged = merge_rows(left_row, right_row, None);
45 let doc = Value::Object(merged.clone());
46 let passes = ScanFilter::all_match_value(&filters, &doc)?;
47 if passes {
48 matched = true;
49 if join_type != "semi" && join_type != "anti" {
50 output.push(merged);
51 if output.len() >= effective_limit {
52 break 'outer;
53 }
54 } else if join_type == "semi" {
55 output.push(left_row.clone());
56 if output.len() >= effective_limit {
57 break 'outer;
58 }
59 break; }
61 }
62 }
63 if !matched {
64 match join_type {
65 "left" | "full" => {
66 output.push(left_row.clone());
67 if output.len() >= effective_limit {
68 break;
69 }
70 }
71 "anti" => {
72 output.push(left_row.clone());
73 if output.len() >= effective_limit {
74 break;
75 }
76 }
77 _ => {}
78 }
79 }
80 }
81
82 if join_type == "right" || join_type == "full" {
84 'right: for right_row in &right_rows {
85 let mut any_match = false;
86 for left_row in &left_rows {
87 let merged = merge_rows(left_row, right_row, None);
88 let doc = Value::Object(merged);
89 if ScanFilter::all_match_value(&filters, &doc)? {
90 any_match = true;
91 break;
92 }
93 }
94 if !any_match {
95 output.push(right_row.clone());
96 if output.len() >= effective_limit {
97 break 'right;
98 }
99 }
100 }
101 }
102
103 Ok(maps_to_result(output))
104}
105
106#[cfg(test)]
107mod tests {
108 use super::super::common::merge_rows;
109 use nodedb_query::scan_filter::{FilterOp, ScanFilter};
110 use nodedb_types::value::Value;
111 use std::collections::HashMap;
112
113 fn row(fields: &[(&str, Value)]) -> HashMap<String, Value> {
114 fields
115 .iter()
116 .map(|(k, v)| (k.to_string(), v.clone()))
117 .collect()
118 }
119
120 #[test]
121 fn non_equi_condition_evaluates() {
122 let left = row(&[
125 ("item", Value::String("apple".into())),
126 ("price", Value::Integer(5)),
127 ]);
128 let right = row(&[("min_price", Value::Integer(3))]);
129 let merged = merge_rows(&left, &right, None);
130
131 let filter = ScanFilter {
132 field: "price".into(),
133 op: FilterOp::Gt,
134 value: Value::Integer(3),
135 clauses: Vec::new(),
136 expr: None,
137 };
138
139 let doc = Value::Object(merged);
140 assert!(
141 filter
142 .matches_value(&doc)
143 .expect("filter evaluation failed")
144 );
145 }
146}