1use crate::physical::common::store_value_in_vector;
2use crate::physical::scan_filter::PhysicalScan;
3use crate::physical::types::{OperatorResult, PhysicalOperatorExec};
4use crate::physical::write_ops::{PhysicalFtsScan, evaluate_expression_for_row};
5use akar_common::error::ProcessorError;
6use akar_common::types::{PhysicalTypeID, Value};
7use akar_common::vector::{DataChunk, ValueVector};
8use akar_storage::table::TableCatalog;
9use std::collections::HashSet;
10use std::sync::Arc;
11
12pub struct PhysicalRecursiveExtend {
30 pub source_table_id: u64,
31 pub rel_table_ids: Vec<u64>,
32 pub lower_bound: u64,
33 pub upper_bound: u64,
34 pub direction: akar_common::enums::ExtendDirection,
35 pub semantic: akar_common::enums::PathSemantic,
36 pub table_catalog: Option<Arc<TableCatalog>>,
37 pub weight_property: Option<String>,
40 pub cost_output_name: Option<String>,
42}
43
44impl PhysicalOperatorExec for PhysicalRecursiveExtend {
45 fn operator_type(&self) -> &str {
46 "recursive_extend"
47 }
48
49 fn execute(&self, input: Vec<DataChunk>) -> OperatorResult {
50 use akar_common::enums::ExtendDirection;
51 use akar_common::enums::PathSemantic;
52 use akar_common::types::Value;
53 use akar_common::vector::ValueVector;
54 use std::collections::{HashMap, VecDeque};
55
56 let catalog = self
57 .table_catalog
58 .as_ref()
59 .ok_or_else(|| "No table catalog available for RecursiveExtend".to_string())?;
60
61 let mut fwd_adj: HashMap<u64, Vec<(u64, u64)>> = HashMap::new();
63 let mut rev_adj: HashMap<u64, Vec<(u64, u64)>> = HashMap::new();
64 let mut edge_weights: HashMap<u64, f64> = HashMap::new();
66 let is_weighted = self.weight_property.is_some();
68 let mut weight_col_idx: HashMap<u64, Option<usize>> = HashMap::new();
70
71 for &rel_table_id in &self.rel_table_ids {
72 if let Some(rel_table) = catalog.get_rel_table(rel_table_id) {
73 if let Some(ref wp) = self.weight_property {
75 let idx = rel_table.columns.iter().position(|c| c.name == *wp);
76 weight_col_idx.insert(rel_table_id, idx);
77 }
78
79 for (&src, neighbors) in rel_table.fwd_adj.iter() {
80 fwd_adj
81 .entry(src)
82 .or_default()
83 .extend(neighbors.iter().map(|(dst, edge_idx)| (*dst, *edge_idx as u64)));
84 if is_weighted && let Some(col_idx) = weight_col_idx.get(&rel_table_id).and_then(|&c| c) {
86 for &(_dst, edge_idx) in neighbors {
87 if let Some(weight_val) =
88 rel_table.properties.get(col_idx).and_then(|col| col.get(edge_idx))
89 {
90 let w = match weight_val {
91 Value::Int64(i) => *i as f64,
92 Value::Double(d) => *d,
93 Value::Float(f) => *f as f64,
94 Value::Int32(i) => *i as f64,
95 _ => 1.0, };
97 edge_weights.insert(edge_idx as u64, w);
98 }
99 }
100 }
101 }
102 for (&dst, neighbors) in rel_table.rev_adj.iter() {
103 rev_adj
104 .entry(dst)
105 .or_default()
106 .extend(neighbors.iter().map(|(src, edge_idx)| (*src, *edge_idx as u64)));
107 }
108 }
109 }
110
111 let source_offsets: Vec<i64> = if input.is_empty() || input[0].fields.is_empty() {
113 let mut all: Vec<i64> = fwd_adj
114 .keys()
115 .chain(rev_adj.keys())
116 .copied()
117 .map(|k| k as i64)
118 .collect();
119 all.sort();
120 all.dedup();
121 all
122 } else {
123 let field = &input[0].fields[0];
124 let num_rows = input[0].size;
125 let mut offsets = Vec::with_capacity(num_rows);
126 for i in 0..num_rows {
127 if !field.is_null(i) {
128 let offset = if let Some(Value::Int64(val)) = input[0].get_value(0, i) {
129 val
130 } else {
131 0
132 };
133 offsets.push(offset);
134 }
135 }
136 offsets
137 };
138
139 if source_offsets.is_empty() {
140 return Ok(vec![DataChunk::new(vec![], vec![])]);
141 }
142
143 let mut result_src: Vec<i64> = Vec::new();
145 let mut result_dst: Vec<i64> = Vec::new();
146 let mut result_len: Vec<i64> = Vec::new();
147 let mut result_cost: Vec<f64> = Vec::new(); let mut result_path_nodes: Vec<Vec<i64>> = Vec::new();
150 let mut result_path_edges: Vec<Vec<i64>> = Vec::new();
151
152 for &src in &source_offsets {
153 let src_u = src as u64;
154
155 if is_weighted {
156 use std::cmp::Reverse;
158 use std::collections::BinaryHeap;
159
160 const COST_PRECISION: i64 = 1000;
163
164 let cost_to_i64 = |c: f64| -> i64 { (c * COST_PRECISION as f64).round() as i64 };
166
167 let mut parents: HashMap<u64, (u64, u64, u64, f64)> = HashMap::new();
169 let mut pq: BinaryHeap<Reverse<(i64, u64)>> = BinaryHeap::new();
170
171 pq.push(Reverse((cost_to_i64(0.0), src_u)));
172 parents.insert(src_u, (u64::MAX, u64::MAX, 0, 0.0));
173
174 while let Some(Reverse((cur_cost_i64, node))) = pq.pop() {
175 let cur_cost = cur_cost_i64 as f64 / COST_PRECISION as f64;
176 let cur_depth = parents.get(&node).map(|&(_, _, d, _)| d).unwrap_or(0);
177
178 if let Some(&(_, _, _, best_cost)) = parents.get(&node)
180 && cur_cost > best_cost + 1e-9
181 {
182 continue;
183 }
184
185 if cur_depth >= self.upper_bound {
186 continue;
187 }
188
189 let neighbors: Vec<(u64, u64)> = match self.direction {
191 ExtendDirection::Fwd => fwd_adj.get(&node).cloned().unwrap_or_default(),
192 ExtendDirection::Bwd => rev_adj.get(&node).cloned().unwrap_or_default(),
193 ExtendDirection::Both => {
194 let mut nbrs = fwd_adj.get(&node).cloned().unwrap_or_default();
195 if let Some(bwd) = rev_adj.get(&node) {
196 nbrs.extend(bwd.iter().copied());
197 }
198 nbrs
199 }
200 };
201
202 for (nbr, edge_id) in neighbors {
203 let edge_w = edge_weights.get(&edge_id).copied().unwrap_or(1.0);
204 let new_cost = cur_cost + edge_w;
205 let new_depth = cur_depth + 1;
206
207 let should_visit = match parents.get(&nbr) {
208 Some(&(_, _, _, existing_cost)) => new_cost < existing_cost - 1e-9,
209 None => true,
210 };
211
212 if should_visit {
213 parents.insert(nbr, (node, edge_id, new_depth, new_cost));
214 pq.push(Reverse((cost_to_i64(new_cost), nbr)));
215 }
216 }
217 }
218
219 for (&node, &(_parent, _eid, depth, cost)) in &parents {
221 if depth < self.lower_bound || depth > self.upper_bound {
222 continue;
223 }
224 if depth == 0 && self.lower_bound > 0 {
225 continue;
226 }
227
228 result_src.push(src);
229 result_dst.push(node as i64);
230 result_len.push(depth as i64);
231 result_cost.push(cost);
232
233 let mut cur = node;
235 let mut temp_nodes = vec![node as i64];
236 let mut temp_edges = Vec::new();
237
238 while cur != src_u {
239 if let Some(&(parent, eid, _, _)) = parents.get(&cur) {
240 if parent == u64::MAX {
241 break;
242 }
243 temp_edges.push(eid as i64);
244 temp_nodes.push(parent as i64);
245 cur = parent;
246 } else {
247 break;
248 }
249 }
250
251 temp_nodes.reverse();
252 temp_edges.reverse();
253 let mut path_nodes = vec![src];
254 path_nodes.extend(temp_nodes);
255 result_path_nodes.push(path_nodes);
256 result_path_edges.push(temp_edges);
257 }
258 } else {
259 let mut queue = VecDeque::new();
261 let mut parents: HashMap<u64, (u64, u64, u64)> = HashMap::new();
263 queue.push_back((src_u, 0u64));
264 parents.insert(src_u, (u64::MAX, u64::MAX, 0));
265
266 let semantic = self.semantic;
267
268 while let Some((node, depth)) = queue.pop_front() {
269 if depth >= self.upper_bound {
270 continue;
271 }
272
273 let neighbors: Vec<(u64, u64)> = match self.direction {
274 ExtendDirection::Fwd => fwd_adj.get(&node).cloned().unwrap_or_default(),
275 ExtendDirection::Bwd => rev_adj.get(&node).cloned().unwrap_or_default(),
276 ExtendDirection::Both => {
277 let mut nbrs = fwd_adj.get(&node).cloned().unwrap_or_default();
278 if let Some(bwd) = rev_adj.get(&node) {
279 nbrs.extend(bwd.iter().copied());
280 }
281 nbrs
282 }
283 };
284
285 'neighbors: for (nbr, edge_id) in neighbors {
286 if parents.contains_key(&nbr) {
287 match semantic {
288 PathSemantic::Walk | PathSemantic::Acyclic => continue 'neighbors,
289 PathSemantic::Trail => {
290 let mut cur = node;
291 while let Some(&(p, eid, _)) = parents.get(&cur) {
292 if eid == edge_id {
293 continue 'neighbors;
294 }
295 if p == u64::MAX {
296 break;
297 }
298 cur = p;
299 }
300 }
301 }
302 }
303
304 let new_depth = depth + 1;
305 parents.insert(nbr, (node, edge_id, new_depth));
306 queue.push_back((nbr, new_depth));
307 }
308 }
309
310 for (&node, &(_parent_node, _edge_id, depth)) in &parents {
312 if depth < self.lower_bound || depth > self.upper_bound {
313 continue;
314 }
315 if depth == 0 && self.lower_bound > 0 {
316 continue;
317 }
318
319 result_src.push(src);
320 result_dst.push(node as i64);
321 result_len.push(depth as i64);
322
323 let mut cur = node;
325 let mut temp_nodes = vec![node as i64];
326 let mut temp_edges = Vec::new();
327
328 while cur != src_u {
329 if let Some(&(parent, eid, _)) = parents.get(&cur) {
330 if parent == u64::MAX {
331 break;
332 }
333 temp_edges.push(eid as i64);
334 temp_nodes.push(parent as i64);
335 cur = parent;
336 } else {
337 break;
338 }
339 }
340
341 temp_nodes.reverse();
342 temp_edges.reverse();
343 let mut path_nodes = vec![src];
344 path_nodes.extend(temp_nodes);
345 result_path_nodes.push(path_nodes);
346 result_path_edges.push(temp_edges);
347 }
348 }
349 }
350
351 let num_results = result_src.len();
353 if num_results == 0 {
354 return Ok(vec![DataChunk::new(vec![], vec![])]);
355 }
356
357 let mut src_v = ValueVector::new(akar_common::types::PhysicalTypeID::Int64, num_results);
359 let mut dst_v = ValueVector::new(akar_common::types::PhysicalTypeID::Int64, num_results);
360 let mut len_v = ValueVector::new(akar_common::types::PhysicalTypeID::Int64, num_results);
361
362 for i in 0..num_results {
363 let offset = i * 8;
364 src_v.data_mut()[offset..offset + 8].copy_from_slice(&result_src[i].to_le_bytes());
365 src_v.set_null(i, false);
366 dst_v.data_mut()[offset..offset + 8].copy_from_slice(&result_dst[i].to_le_bytes());
367 dst_v.set_null(i, false);
368 len_v.data_mut()[offset..offset + 8].copy_from_slice(&result_len[i].to_le_bytes());
369 len_v.set_null(i, false);
370 }
371 src_v.resize(num_results);
372 dst_v.resize(num_results);
373 len_v.resize(num_results);
374
375 let mut path_nodes_col: Vec<Value> = Vec::with_capacity(num_results);
378 let mut path_edges_col: Vec<Value> = Vec::with_capacity(num_results);
379
380 for i in 0..num_results {
381 let node_vals: Vec<Value> = result_path_nodes[i].iter().map(|&n| Value::Int64(n)).collect();
383 path_nodes_col.push(Value::List(node_vals));
384 let edge_vals: Vec<Value> = result_path_edges[i].iter().map(|&e| Value::Int64(e)).collect();
386 path_edges_col.push(Value::List(edge_vals));
387 }
388
389 let mut path_nodes_v = ValueVector::new(akar_common::types::PhysicalTypeID::List, num_results);
391 let mut path_edges_v = ValueVector::new(akar_common::types::PhysicalTypeID::List, num_results);
392
393 for (i, val) in path_nodes_col.iter().enumerate() {
394 path_nodes_v.set_value(i, val)?;
395 }
396 for (i, val) in path_edges_col.iter().enumerate() {
397 path_edges_v.set_value(i, val)?;
398 }
399
400 let has_cost = is_weighted;
402
403 if has_cost {
404 let mut cost_v = ValueVector::new(akar_common::types::PhysicalTypeID::Double, num_results);
405 for (i, cost) in result_cost.iter().enumerate().take(num_results) {
406 let offset = i * 8;
407 cost_v.data_mut()[offset..offset + 8].copy_from_slice(&cost.to_le_bytes());
408 cost_v.set_null(i, false);
409 }
410 cost_v.resize(num_results);
411
412 Ok(vec![DataChunk {
413 fields: vec![
414 akar_common::arrow_vector::ArrowVector::from_legacy(&src_v).array,
415 akar_common::arrow_vector::ArrowVector::from_legacy(&dst_v).array,
416 akar_common::arrow_vector::ArrowVector::from_legacy(&len_v).array,
417 akar_common::arrow_vector::ArrowVector::from_legacy(&path_nodes_v).array,
418 akar_common::arrow_vector::ArrowVector::from_legacy(&path_edges_v).array,
419 akar_common::arrow_vector::ArrowVector::from_legacy(&cost_v).array,
420 ],
421 field_types: vec![
422 src_v.physical_type(),
423 dst_v.physical_type(),
424 len_v.physical_type(),
425 path_nodes_v.physical_type(),
426 path_edges_v.physical_type(),
427 cost_v.physical_type(),
428 ],
429 size: num_results,
430 field_names: vec![],
431 sel_vector: None,
432 }])
433 } else {
434 Ok(vec![DataChunk {
435 fields: vec![
436 akar_common::arrow_vector::ArrowVector::from_legacy(&src_v).array,
437 akar_common::arrow_vector::ArrowVector::from_legacy(&dst_v).array,
438 akar_common::arrow_vector::ArrowVector::from_legacy(&len_v).array,
439 akar_common::arrow_vector::ArrowVector::from_legacy(&path_nodes_v).array,
440 akar_common::arrow_vector::ArrowVector::from_legacy(&path_edges_v).array,
441 ],
442 field_types: vec![
443 src_v.physical_type(),
444 dst_v.physical_type(),
445 len_v.physical_type(),
446 path_nodes_v.physical_type(),
447 path_edges_v.physical_type(),
448 ],
449 size: num_results,
450 field_names: vec![],
451 sel_vector: None,
452 }])
453 }
454 }
455}
456
457pub struct PhysicalCreateNode {
458 pub table_name: String,
459 pub table_id: u64,
460 pub out_var_name: String,
461 pub properties: Vec<(String, akar_parser::ast::Expression)>,
462 pub table_catalog: Arc<TableCatalog>,
463}
464
465impl PhysicalCreateNode {
466 pub fn execute(&self, input: Vec<DataChunk>) -> Result<Vec<DataChunk>, ProcessorError> {
467 if input.is_empty() {
468 return Ok(input);
469 }
470
471 let mut table = self
472 .table_catalog
473 .get_node_table_by_name_mut(&self.table_name)
474 .ok_or_else(|| format!("Node table {} not found", self.table_name))?;
475
476 let mut output = Vec::with_capacity(input.len());
478
479 for mut chunk in input {
480 let mut node_ids = ValueVector::new(akar_common::types::PhysicalTypeID::Int64, chunk.size);
481
482 for i in 0..chunk.size {
483 let mut values = vec![akar_common::types::Value::Null; table.columns.len()];
484 for (prop_name, prop_expr) in &self.properties {
485 if let Some(col_idx) = table.columns.iter().position(|c| c.name == *prop_name) {
486 values[col_idx] = evaluate_expression_for_row(prop_expr, &chunk, i);
487 }
488 }
489
490 let row_offset = table.insert_row(values)?;
491 node_ids.data_mut()[i * 8..(i + 1) * 8].copy_from_slice(&(row_offset as i64).to_le_bytes());
492 node_ids.set_null(i, false);
493 }
494 node_ids.resize(chunk.size);
495
496 chunk
497 .fields
498 .push(akar_common::arrow_vector::ArrowVector::from_legacy(&node_ids).array);
499 chunk.field_types.push(akar_common::types::PhysicalTypeID::List);
500 chunk.field_names.push(self.out_var_name.clone());
501 output.push(chunk);
502 }
503
504 Ok(output)
505 }
506}
507
508pub struct PhysicalCreateRel {
509 pub table_name: String,
510 pub table_id: u64,
511 pub src_node_name: String,
512 pub dst_node_name: String,
513 pub properties: Vec<(String, akar_parser::ast::Expression)>,
514 pub table_catalog: Arc<TableCatalog>,
515}
516
517impl PhysicalCreateRel {
518 pub fn execute(&self, input: Vec<DataChunk>) -> Result<Vec<DataChunk>, ProcessorError> {
519 if input.is_empty() {
520 return Ok(input);
521 }
522
523 let mut table = self
524 .table_catalog
525 .get_rel_table_by_name_mut(&self.table_name)
526 .ok_or_else(|| format!("Rel table {} not found", self.table_name))?;
527
528 let mut output = Vec::with_capacity(input.len());
529
530 for chunk in input {
531 let src_name_id = format!("{}.{}", self.src_node_name, "_id");
532 let src_name_pk = format!("{}.{}", self.src_node_name, "id");
533 let src_idx = chunk
534 .field_names
535 .iter()
536 .position(|name| name == &src_name_id)
537 .or_else(|| chunk.field_names.iter().position(|name| name == &self.src_node_name))
538 .or_else(|| chunk.field_names.iter().position(|name| name == &src_name_pk))
539 .ok_or_else(|| format!("Source node variable {} not found", self.src_node_name))?;
540
541 let dst_name_id = format!("{}.{}", self.dst_node_name, "_id");
542 let dst_name_pk = format!("{}.{}", self.dst_node_name, "id");
543 let dst_idx = chunk
544 .field_names
545 .iter()
546 .position(|name| name == &dst_name_id)
547 .or_else(|| chunk.field_names.iter().position(|name| name == &self.dst_node_name))
548 .or_else(|| chunk.field_names.iter().position(|name| name == &dst_name_pk))
549 .ok_or_else(|| format!("Destination node variable {} not found", self.dst_node_name))?;
550
551 let src_vec = &chunk.fields[src_idx];
552 let dst_vec = &chunk.fields[dst_idx];
553
554 let mut inserted = 0;
555 for i in 0..chunk.size {
556 if src_vec.is_null(i) || dst_vec.is_null(i) {
557 continue; }
559
560 let mut src_bytes = [0u8; 8];
561 src_bytes.copy_from_slice(&src_vec.to_data().buffers()[0].as_slice()[i * 8..(i + 1) * 8]);
562 let src_id = i64::from_le_bytes(src_bytes) as u64;
563
564 let mut dst_bytes = [0u8; 8];
565 dst_bytes.copy_from_slice(&dst_vec.to_data().buffers()[0].as_slice()[i * 8..(i + 1) * 8]);
566 let dst_id = i64::from_le_bytes(dst_bytes) as u64;
567
568 let mut values = vec![akar_common::types::Value::Null; table.columns.len()];
569 for (prop_name, prop_expr) in &self.properties {
570 if let Some(col_idx) = table.columns.iter().position(|c| c.name == *prop_name) {
571 values[col_idx] = evaluate_expression_for_row(prop_expr, &chunk, i);
572 }
573 }
574
575 table.insert_rel(src_id, dst_id, values)?;
576 inserted += 1;
577 }
578 tracing::debug!(
579 "PhysicalCreateRel inserted {} relationships from chunk of size {}",
580 inserted,
581 chunk.size
582 );
583
584 output.push(chunk);
585 }
586
587 Ok(output)
588 }
589}
590
591pub struct PhysicalExtend {
600 pub rel_table_name: String,
602 pub rel_table_id: u64,
604 pub rel_var: String,
607 pub bound_node_var: String,
609 pub direction: akar_parser::ast::EdgeDirection,
611 pub dst_node_var: String,
613 pub dst_table_name: String,
615 pub dst_table_id: u64,
617 pub fts_query: Option<PhysicalFtsScan>,
621 pub table_catalog: Arc<TableCatalog>,
623}
624
625impl PhysicalExtend {
626 pub fn execute(&self, input: Vec<DataChunk>) -> Result<Vec<DataChunk>, ProcessorError> {
627 if input.is_empty() || input.iter().all(|c| c.size == 0) {
628 return Ok(input);
629 }
630
631 let fts_doc_ids: Option<HashSet<u64>> = if let Some(ref fts) = self.fts_query {
635 let fts_chunks = fts.execute(vec![])?;
636 let mut ids = HashSet::new();
637 if let Some(chunk) = fts_chunks.first() {
638 for row in 0..chunk.size {
639 if let Some(doc_id) = chunk.get_i64(0, row) {
640 ids.insert(doc_id as u64);
641 }
642 }
643 }
644 Some(ids)
645 } else {
646 None
647 };
648
649 let (fwd_adj, rev_adj, rel_props, rel_cols) = {
651 let rel_table = self
652 .table_catalog
653 .get_rel_table_by_name(&self.rel_table_name)
654 .ok_or_else(|| format!("Rel table {} not found", self.rel_table_name))?;
655 let fwd = rel_table.fwd_adj.clone();
656 let rev = rel_table.rev_adj.clone();
657 let props = rel_table.properties.clone();
658 let cols = rel_table.columns.clone();
659 (fwd, rev, props, cols)
660 };
661
662 let (dest_data, dest_cols) = {
664 let dest_table = self
665 .table_catalog
666 .get_node_table_by_name(&self.dst_table_name)
667 .ok_or_else(|| format!("Node table {} not found", self.dst_table_name))?;
668 let data = dest_table.to_column_major_data();
669 let cols = dest_table.columns.clone();
670 (data, cols)
671 };
672 let dest_num_rows = dest_data.first().map(|c| c.len()).unwrap_or(0);
676
677 let mut output = Vec::with_capacity(input.len());
678
679 for chunk in input {
680 let bound_name_id = format!("{}.{}", self.bound_node_var, "_id");
682 let bound_name_pk = format!("{}.{}", self.bound_node_var, "id");
683 let bound_idx = chunk
684 .field_names
685 .iter()
686 .position(|name| name == &bound_name_id)
687 .or_else(|| chunk.field_names.iter().position(|name| name == &self.bound_node_var))
688 .or_else(|| chunk.field_names.iter().position(|name| name == &bound_name_pk))
689 .ok_or_else(|| {
690 format!(
691 "Bound node variable {} not found in Extend input. Available fields: {:?}",
692 self.bound_node_var, chunk.field_names
693 )
694 })?;
695
696 let mut total_rows = 0;
698 let mut row_mappings: Vec<(usize, u64, usize)> = Vec::new(); for i in 0..chunk.size {
701 if chunk.fields[bound_idx].is_null(i) {
702 continue;
703 }
704 let src_id = if let Some(akar_common::types::Value::Int64(val)) = chunk.get_value(bound_idx, i) {
705 val as u64
706 } else {
707 continue;
708 };
709
710 let edges: Vec<(u64, usize)> = match self.direction {
711 akar_parser::ast::EdgeDirection::LeftToRight => fwd_adj.get(&src_id).cloned().unwrap_or_default(),
712 akar_parser::ast::EdgeDirection::RightToLeft => rev_adj.get(&src_id).cloned().unwrap_or_default(),
713 akar_parser::ast::EdgeDirection::Both => {
714 let mut all = fwd_adj.get(&src_id).cloned().unwrap_or_default();
715 if let Some(rev) = rev_adj.get(&src_id) {
716 all.extend(rev.iter().cloned());
717 }
718 all
719 }
720 };
721
722 for &(dst_offset, edge_idx) in &edges {
723 if dst_offset as usize >= dest_num_rows {
724 continue;
725 }
726 if let Some(ref ids) = fts_doc_ids {
728 if !ids.contains(&dst_offset) {
729 continue;
730 }
731 }
732 total_rows += 1;
733 row_mappings.push((i, dst_offset, edge_idx));
734 }
735 }
736
737 if total_rows == 0 {
738 output.push(DataChunk::new(vec![], vec![]));
739 continue;
740 }
741
742 let num_input_fields = chunk.fields.len();
745 let num_rel_cols = rel_cols.len();
746 let num_dest_cols = dest_cols.len();
747 let num_out_cols = num_input_fields + num_rel_cols + num_dest_cols + 1;
748
749 let mut out_data: Vec<Vec<Value>> = vec![Vec::with_capacity(total_rows); num_out_cols];
751 let mut out_dst_ids: Vec<Value> = Vec::with_capacity(total_rows);
752
753 for &(input_row, dst_offset, edge_idx) in &row_mappings {
754 for col in 0..num_input_fields {
756 let val = chunk.get_value(col, input_row).unwrap_or(Value::Null);
757 out_data[col].push(val);
758 }
759 for col in 0..num_rel_cols {
761 let val = rel_props
762 .get(col)
763 .and_then(|c| c.get(edge_idx))
764 .cloned()
765 .unwrap_or(Value::Null);
766 out_data[num_input_fields + col].push(val);
767 }
768 let dest_row = dst_offset as usize;
771 for col in 0..num_dest_cols {
772 let val = dest_data
773 .get(col)
774 .and_then(|c| c.get(dest_row))
775 .cloned()
776 .unwrap_or(Value::Null);
777 out_data[num_input_fields + num_rel_cols + col].push(val);
778 }
779 out_dst_ids.push(Value::Int64(dst_offset as i64));
781 }
782
783 let mut fields: Vec<arrow::array::ArrayRef> = Vec::with_capacity(num_out_cols);
788 let mut field_type_ids: Vec<PhysicalTypeID> = Vec::with_capacity(num_out_cols);
789 let mut field_names = Vec::with_capacity(num_out_cols);
790
791 for col in 0..num_input_fields {
793 let phys_type = chunk.field_types[col];
794 let needs_arrow_builder = matches!(
799 phys_type,
800 PhysicalTypeID::List | PhysicalTypeID::Array | PhysicalTypeID::Struct | PhysicalTypeID::String
801 );
802 if needs_arrow_builder {
803 fields.push(
804 crate::expression_evaluator::build_arrow_from_values(&out_data[col], phys_type, total_rows)
805 .map_err(|e| e.to_string())?
806 .array,
807 );
808 field_type_ids.push(phys_type);
809 } else {
810 let mut v = ValueVector::new(phys_type, total_rows);
811 v.resize(total_rows);
812 for row in 0..total_rows {
813 store_value_in_vector(&mut v, row, &out_data[col][row])?;
814 }
815 fields.push(akar_common::arrow_vector::ArrowVector::from_legacy(&v).array);
816 field_type_ids.push(v.physical_type());
817 }
818 if col < chunk.field_names.len() {
819 field_names.push(chunk.field_names[col].clone());
820 } else {
821 field_names.push(format!("field_{}", col));
822 }
823 }
824
825 for col in 0..num_rel_cols {
827 let phys_type = if col < rel_cols.len() {
828 PhysicalScan::logical_to_physical(&rel_cols[col].logical_type)
829 } else {
830 PhysicalTypeID::Int64
831 };
832 if matches!(
834 phys_type,
835 PhysicalTypeID::String | PhysicalTypeID::List | PhysicalTypeID::Array | PhysicalTypeID::Struct
836 ) {
837 fields.push(
838 crate::expression_evaluator::build_arrow_from_values(
839 &out_data[num_input_fields + col],
840 phys_type,
841 total_rows,
842 )
843 .map_err(|e| e.to_string())?
844 .array,
845 );
846 field_type_ids.push(phys_type);
847 } else {
848 let mut v = ValueVector::new(phys_type, total_rows);
849 v.resize(total_rows);
850 for row in 0..total_rows {
851 store_value_in_vector(&mut v, row, &out_data[num_input_fields + col][row])?;
852 }
853 fields.push(akar_common::arrow_vector::ArrowVector::from_legacy(&v).array);
854 field_type_ids.push(v.physical_type());
855 }
856 let rel_prefix = if self.rel_var.is_empty() {
857 &self.rel_table_name
858 } else {
859 &self.rel_var
860 };
861 let col_name = rel_cols.get(col).map(|c| c.name.as_str()).unwrap_or("");
862 field_names.push(format!("{}.{}", rel_prefix, col_name));
863 }
864
865 for col in 0..num_dest_cols {
867 let phys_type = if col < dest_cols.len() {
868 PhysicalScan::logical_to_physical(&dest_cols[col].logical_type)
869 } else {
870 PhysicalTypeID::Int64
871 };
872 let needs_arrow_builder = matches!(
874 phys_type,
875 PhysicalTypeID::List | PhysicalTypeID::Array | PhysicalTypeID::Struct | PhysicalTypeID::String
876 );
877 if needs_arrow_builder {
878 fields.push(
879 crate::expression_evaluator::build_arrow_from_values(
880 &out_data[num_input_fields + num_rel_cols + col],
881 phys_type,
882 total_rows,
883 )
884 .map_err(|e| e.to_string())?
885 .array,
886 );
887 field_type_ids.push(phys_type);
888 } else {
889 let mut v = ValueVector::new(phys_type, total_rows);
890 v.resize(total_rows);
891 for row in 0..total_rows {
892 store_value_in_vector(&mut v, row, &out_data[num_input_fields + num_rel_cols + col][row])?;
893 }
894 fields.push(akar_common::arrow_vector::ArrowVector::from_legacy(&v).array);
895 field_type_ids.push(v.physical_type());
896 }
897 let prefix = &self.dst_node_var;
898 let col_name = dest_cols.get(col).map(|c| c.name.as_str()).unwrap_or("");
899 field_names.push(format!("{}.{}", prefix, col_name));
900 }
901
902 let mut id_v = ValueVector::new(PhysicalTypeID::Int64, total_rows);
905 id_v.resize(total_rows);
906 for row in 0..total_rows {
907 store_value_in_vector(&mut id_v, row, &out_dst_ids[row])?;
908 }
909 fields.push(akar_common::arrow_vector::ArrowVector::from_legacy(&id_v).array);
910 field_type_ids.push(PhysicalTypeID::Int64);
911 field_names.push(format!("{}.{}", self.dst_node_var, "_id"));
912
913 output.push(DataChunk {
914 fields,
915 field_types: field_type_ids,
916 size: total_rows,
917 field_names,
918 sel_vector: None,
919 });
920 }
921
922 Ok(output)
923 }
924}