akar_processor/processor/
union_helpers.rs1use crate::physical::common::value_hash;
2use crate::processor::chunk_helpers::{extract_all_rows_from_chunks, rows_to_columns};
3use akar_common::error::ProcessorError;
4use akar_common::types::Value;
5use akar_common::vector::DataChunk;
6use akar_planner::logical_operator::LogicalOperator;
7use std::collections::HashMap;
8
9pub fn flatten_union_child(op: &LogicalOperator) -> Vec<LogicalOperator> {
10 match op {
11 LogicalOperator::Projection(p) if p.expressions.is_empty() => p.children.clone(),
12 other => vec![other.clone()],
13 }
14}
15
16pub fn merge_union_chunks(
17 left: Vec<DataChunk>,
18 right: Vec<DataChunk>,
19 all: bool,
20) -> Result<Vec<DataChunk>, ProcessorError> {
21 if left.is_empty() {
22 return Ok(right);
23 }
24 if right.is_empty() {
25 return Ok(left);
26 }
27
28 let num_fields = left[0].num_fields();
29 for chunk in &right {
30 if chunk.num_fields() != num_fields {
31 return Err(format!(
32 "UNION column count mismatch: left has {num_fields} columns, right has {} columns",
33 chunk.num_fields()
34 )
35 .into());
36 }
37 }
38
39 let mut left_rows = extract_all_rows_from_chunks(&left);
40 let right_rows = extract_all_rows_from_chunks(&right);
41 left_rows.extend(right_rows);
42
43 let mut deduped: Vec<Vec<Value>> = Vec::with_capacity(left_rows.len());
44 if !all {
45 for row in &left_rows {
46 if !deduped.contains(row) {
47 deduped.push(row.clone());
48 }
49 }
50 } else {
51 deduped = left_rows;
52 }
53
54 if deduped.is_empty() {
55 return Ok(vec![DataChunk::new(vec![], vec![])]);
56 }
57
58 let (fields, field_types) = rows_to_columns(&deduped);
59 let final_size = deduped.len();
60 let field_names = left.first().map(|c| c.field_names.clone()).unwrap_or_default();
61
62 Ok(vec![DataChunk {
63 fields,
64 field_types,
65 size: final_size,
66 field_names,
67 sel_vector: None,
68 }])
69}
70
71pub fn merge_optional_chunks(left: Vec<DataChunk>, right: Vec<DataChunk>) -> Result<Vec<DataChunk>, ProcessorError> {
72 if left.is_empty() {
73 return Ok(left);
74 }
75 if right.is_empty() {
76 return Ok(left);
77 }
78
79 let left_rows = extract_all_rows_from_chunks(&left);
80 let right_rows = extract_all_rows_from_chunks(&right);
81 if left_rows.is_empty() {
82 return Ok(left);
83 }
84
85 let left_names: Vec<String> = left.first().map(|c| c.field_names.clone()).unwrap_or_default();
86 let right_names: Vec<String> = right.first().map(|c| c.field_names.clone()).unwrap_or_default();
87 let num_right_cols = right_rows.first().map(|r| r.len()).unwrap_or(right_names.len());
88
89 if right_rows.is_empty() {
90 if num_right_cols == 0 {
94 return Ok(left);
95 }
96 let mut combined: Vec<Vec<Value>> = Vec::with_capacity(left_rows.len());
97 for lrow in &left_rows {
98 let mut row = Vec::with_capacity(left_names.len() + num_right_cols);
99 row.extend_from_slice(lrow);
100 row.extend(std::iter::repeat_n(Value::Null, num_right_cols));
101 combined.push(row);
102 }
103 let (fields, field_types) = rows_to_columns(&combined);
104 let mut field_names = left_names;
105 field_names.extend(right_names);
106 return Ok(vec![DataChunk {
107 fields,
108 field_types,
109 size: combined.len(),
110 field_names,
111 sel_vector: None,
112 }]);
113 }
114
115 let num_left_cols = left_rows.first().map(|r| r.len()).unwrap_or(0);
116
117 let join_col_left = left_names.iter().position(|n| right_names.contains(n));
124 let join_col_right = join_col_left.and_then(|li| right_names.iter().position(|n| *n == left_names[li]));
125
126 let mut combined: Vec<Vec<Value>> = Vec::new();
127 match (join_col_left, join_col_right) {
128 (Some(li), Some(ri)) => {
129 let mut hash: HashMap<u64, Vec<usize>> = HashMap::new();
131 for (i, row) in right_rows.iter().enumerate() {
132 if let Some(val) = row.get(ri) {
133 if !matches!(val, Value::Null) {
134 hash.entry(value_hash(val)).or_default().push(i);
135 }
136 }
137 }
138 for lrow in &left_rows {
139 let key = lrow.get(li);
140 let mut emitted = false;
141 if let Some(key) = key {
142 if !matches!(key, Value::Null) {
143 if let Some(bucket) = hash.get(&value_hash(key)) {
144 for &i in bucket {
145 if &right_rows[i][ri] == key {
146 let mut row = Vec::with_capacity(num_left_cols + num_right_cols);
147 row.extend_from_slice(lrow);
148 row.extend_from_slice(&right_rows[i]);
149 combined.push(row);
150 emitted = true;
151 }
152 }
153 }
154 }
155 }
156 if !emitted {
157 let mut row = Vec::with_capacity(num_left_cols + num_right_cols);
158 row.extend_from_slice(lrow);
159 row.extend(std::iter::repeat_n(Value::Null, num_right_cols));
160 combined.push(row);
161 }
162 }
163 }
164 _ => {
165 for lrow in &left_rows {
168 for rrow in &right_rows {
169 let mut row = Vec::with_capacity(num_left_cols + num_right_cols);
170 row.extend_from_slice(lrow);
171 row.extend_from_slice(rrow);
172 combined.push(row);
173 }
174 }
175 }
176 }
177
178 if combined.is_empty() {
179 return Ok(vec![]);
180 }
181
182 let (fields, field_types) = rows_to_columns(&combined);
183 let size = combined.len();
184 let mut field_names = left_names;
185 field_names.extend(right_names);
186 Ok(vec![DataChunk {
187 fields,
188 field_types,
189 size,
190 field_names,
191 sel_vector: None,
192 }])
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198 use akar_common::types::PhysicalTypeID;
199 use akar_common::vector::ValueVector;
200
201 fn make_chunk(cols: &[Vec<Value>], names: &[&str]) -> DataChunk {
202 let mut fields = Vec::with_capacity(cols.len());
203 let mut types = Vec::with_capacity(cols.len());
204 for col in cols {
205 let first = col.iter().find(|v| !matches!(v, Value::Null)).unwrap_or(&Value::Int64(0));
206 let ptype = match first {
207 Value::Int64(_) => PhysicalTypeID::Int64,
208 Value::String(_) => PhysicalTypeID::String,
209 _ => PhysicalTypeID::Int64,
210 };
211 let mut v = ValueVector::new(ptype, col.len().max(1));
212 for (i, val) in col.iter().enumerate() {
213 let _ = v.set_value(i, val);
214 }
215 v.resize(col.len());
216 fields.push(akar_common::arrow_vector::ArrowVector::from_legacy(&v).array);
217 types.push(ptype);
218 }
219 let mut chunk = DataChunk::new(fields, types);
220 chunk.field_names = names.iter().map(|s| s.to_string()).collect();
221 chunk
222 }
223
224 #[test]
225 fn test_merge_optional_empty_right_returns_left() {
226 let left = make_chunk(&[vec![Value::Int64(1), Value::Int64(2)]], &["a.id"]);
227 let merged = merge_optional_chunks(vec![left.clone()], vec![]).unwrap();
228 assert_eq!(merged.len(), 1);
229 assert_eq!(merged[0].size, 2);
230 }
231
232 #[test]
233 fn test_merge_optional_zero_row_right_pads_nulls() {
234 let left = make_chunk(&[vec![Value::Int64(1), Value::Int64(2)]], &["n.id"]);
238 let right = make_chunk(&[Vec::<Value>::new()], &["m.id"]);
239 let merged = merge_optional_chunks(vec![left], vec![right]).unwrap();
240 assert_eq!(merged[0].size, 2, "every left row survives");
241 assert_eq!(merged[0].field_names, vec!["n.id", "m.id"]);
242 assert!(merged[0].is_null(1, 0), "m.id must be null");
243 assert!(merged[0].is_null(1, 1), "m.id must be null");
244 }
245
246 #[test]
247 fn test_merge_optional_cross_product_no_dropped_rows() {
248 let left = make_chunk(
252 &[vec![Value::Int64(1), Value::Int64(2)], vec![Value::Int64(10), Value::Int64(20)]],
253 &["a.id", "a.x"],
254 );
255 let right = make_chunk(&[vec![Value::Int64(5), Value::Int64(6)]], &["b.id"]);
256 let merged = merge_optional_chunks(vec![left], vec![right]).unwrap();
257 assert_eq!(merged[0].size, 4, "expected 2 left rows x 2 right rows");
258 assert_eq!(merged[0].field_names, vec!["a.id", "a.x", "b.id"]);
259 }
260
261 #[test]
262 fn test_merge_optional_joins_on_shared_column() {
263 let left = make_chunk(
265 &[vec![Value::Int64(1), Value::Int64(2)], vec![Value::String("Alice".into()), Value::String("Bob".into())]],
266 &["a.id", "a.name"],
267 );
268 let right = make_chunk(
269 &[vec![Value::Int64(1), Value::Int64(3)], vec![Value::String("Rex".into()), Value::String("Tom".into())]],
270 &["a.id", "a.pet"],
271 );
272 let merged = merge_optional_chunks(vec![left], vec![right]).unwrap();
273 assert_eq!(merged[0].size, 2, "Alice joins Rex; Bob has no match -> nulls");
274 let pet_col = merged[0].get_string(3, 0).map(str::to_string);
275 assert_eq!(pet_col.as_deref(), Some("Rex"));
276 assert!(merged[0].is_null(3, 1), "Bob's pet must be null");
277 }
278}