Skip to main content

akar_processor/physical/write_ops/
packedextend.rs

1//! Auto-extracted from physical_operator.rs
2use crate::physical::types::{OperatorResult, PhysicalOperatorExec};
3use akar_common::types::Value;
4use akar_common::vector::{DataChunk, ValueVector};
5use akar_storage::table::TableCatalog;
6use std::sync::Arc;
7
8// ==================== PackedExtend ====================
9
10/// Physical operator for multi-rel extend, producing flattened output rows.
11///
12/// Extends from a source node using a `CsrIndex` or adjacency list and
13/// produces one output row per relationship, duplicating the source node
14/// properties for each destination neighbor. This normalized format is
15/// what downstream operators (HashJoin, Filter, etc.) expect.
16pub struct PhysicalPackedExtend {
17    pub rel_table_name: String,
18    pub rel_table_id: u64,
19    pub bound_node_var: String,
20    pub direction: akar_parser::ast::EdgeDirection,
21    pub dst_node_var: String,
22    pub table_catalog: Arc<TableCatalog>,
23}
24
25impl PhysicalOperatorExec for PhysicalPackedExtend {
26    fn operator_type(&self) -> &str {
27        "packed_extend"
28    }
29
30    fn execute(&self, input: Vec<DataChunk>) -> OperatorResult {
31        if input.is_empty() || input.iter().all(|c| c.size == 0) {
32            return Ok(input);
33        }
34
35        let rel_table = self
36            .table_catalog
37            .get_rel_table_by_name(&self.rel_table_name)
38            .ok_or_else(|| format!("Rel table {} not found", self.rel_table_name))?;
39
40        let mut output_chunks = Vec::new();
41
42        for chunk in input {
43            if chunk.size == 0 {
44                continue;
45            }
46
47            // Find bound node column index
48            let bound_idx = chunk
49                .field_names
50                .iter()
51                .position(|name| name == &self.bound_node_var)
52                .unwrap_or(0);
53
54            // --- Pass 1: collect neighbor lists and estimate total output size ---
55            let mut per_src_neighbors: Vec<Vec<u64>> = Vec::with_capacity(chunk.size);
56            let mut total_output_rows: usize = 0;
57
58            for i in 0..chunk.size {
59                if chunk.fields[bound_idx].is_null(i) {
60                    per_src_neighbors.push(Vec::new());
61                    continue;
62                }
63
64                let src_id = match chunk.get_value(bound_idx, i) {
65                    Some(Value::Int64(id)) => id as u64,
66                    Some(Value::UInt64(id)) => id,
67                    _ => {
68                        per_src_neighbors.push(Vec::new());
69                        continue;
70                    }
71                };
72
73                let neighbors = self.fetch_neighbors(&rel_table, src_id);
74                total_output_rows += neighbors.len();
75                per_src_neighbors.push(neighbors);
76            }
77
78            if total_output_rows == 0 {
79                continue;
80            }
81
82            // --- Pass 2: build flat output vectors with pre-allocated capacity ---
83            let num_input_cols = chunk.fields.len();
84            let mut out_fields: Vec<arrow::array::ArrayRef> = Vec::with_capacity(num_input_cols + 1);
85            let mut out_field_types: Vec<akar_common::types::PhysicalTypeID> = Vec::with_capacity(num_input_cols + 1);
86
87            // Duplicate each input column for every output row
88            for col_idx in 0..num_input_cols {
89                let phys_type = chunk.field_types[col_idx];
90                let mut v = ValueVector::new(phys_type, total_output_rows);
91                v.resize(total_output_rows);
92
93                let mut out_pos = 0;
94                for (src_row, neighbors) in per_src_neighbors.iter().enumerate() {
95                    if neighbors.is_empty() {
96                        continue;
97                    }
98                    // Copy source row value once, then duplicate for each neighbor
99                    let is_null = chunk.fields[col_idx].is_null(src_row);
100                    let val = chunk.get_value(col_idx, src_row);
101                    for _ in neighbors {
102                        if is_null {
103                            v.set_null(out_pos, true);
104                        } else if let Some(ref val) = val {
105                            crate::physical::common::store_value_in_vector(&mut v, out_pos, val)?;
106                        }
107                        out_pos += 1;
108                    }
109                }
110                out_fields.push(akar_common::arrow_vector::ArrowVector::from_legacy(&v).array);
111                out_field_types.push(phys_type);
112            }
113
114            // Destination column: flat Int64 of neighbor node IDs
115            let mut dst_field = ValueVector::new(akar_common::types::PhysicalTypeID::Int64, total_output_rows);
116            dst_field.resize(total_output_rows);
117
118            let mut out_pos = 0;
119            for neighbors in &per_src_neighbors {
120                for &dst_id in neighbors {
121                    crate::physical::common::store_value_in_vector(
122                        &mut dst_field,
123                        out_pos,
124                        &Value::Int64(dst_id as i64),
125                    )?;
126                    out_pos += 1;
127                }
128            }
129            out_fields.push(akar_common::arrow_vector::ArrowVector::from_legacy(&dst_field).array);
130            out_field_types.push(akar_common::types::PhysicalTypeID::Int64);
131
132            let mut new_names = chunk.field_names.clone();
133            new_names.push(self.dst_node_var.clone());
134
135            output_chunks.push(DataChunk {
136                fields: out_fields,
137                field_types: out_field_types,
138                size: total_output_rows,
139                field_names: new_names,
140                sel_vector: None,
141            });
142        }
143
144        if output_chunks.is_empty() {
145            Ok(vec![DataChunk {
146                fields: vec![],
147                field_types: vec![],
148                size: 0,
149                field_names: vec![],
150                sel_vector: None,
151            }])
152        } else {
153            Ok(output_chunks)
154        }
155    }
156}
157
158impl PhysicalPackedExtend {
159    fn fetch_neighbors(&self, rel_table: &akar_storage::table::RelTable, src_id: u64) -> Vec<u64> {
160        if let Some(csr) = &rel_table.csr_index {
161            let is_fwd = matches!(self.direction, akar_parser::ast::EdgeDirection::LeftToRight);
162            csr.get_neighbors(src_id, is_fwd).unwrap_or_default()
163        } else {
164            match self.direction {
165                akar_parser::ast::EdgeDirection::LeftToRight => rel_table
166                    .get_outgoing_edges(src_id)
167                    .into_iter()
168                    .map(|(dst, _)| dst)
169                    .collect(),
170                akar_parser::ast::EdgeDirection::RightToLeft => rel_table
171                    .get_incoming_edges(src_id)
172                    .into_iter()
173                    .map(|(dst, _)| dst)
174                    .collect(),
175                akar_parser::ast::EdgeDirection::Both => {
176                    let mut n: Vec<u64> = rel_table
177                        .get_outgoing_edges(src_id)
178                        .into_iter()
179                        .map(|(dst, _)| dst)
180                        .collect();
181                    n.extend(rel_table.get_incoming_edges(src_id).into_iter().map(|(dst, _)| dst));
182                    n
183                }
184            }
185        }
186    }
187}