akar_processor/physical/write_ops/
optionalextend.rs1use crate::physical::common::store_value_in_vector;
2use crate::physical::scan_filter::PhysicalScan;
3use akar_common::error::ProcessorError;
4use akar_common::types::{PhysicalTypeID, Value};
5use akar_common::vector::{DataChunk, ValueVector};
6use akar_parser::ast::EdgeDirection;
7use akar_storage::table::TableCatalog;
8use std::collections::HashMap;
9use std::sync::Arc;
10
11pub struct PhysicalOptionalExtend {
32 pub rel_table_name: String,
34 pub rel_table_id: u64,
36 pub rel_var: String,
39 pub src_node_var: String,
41 pub dst_node_var: String,
44 pub direction: EdgeDirection,
46 pub table_catalog: Arc<TableCatalog>,
48}
49
50fn find_node_id_col(chunk: &DataChunk, var: &str) -> Result<usize, ProcessorError> {
53 let name_id = format!("{}.{}", var, "_id");
54 let name_pk = format!("{}.{}", var, "id");
55 let idx = chunk
56 .field_names
57 .iter()
58 .position(|n| n == &name_id)
59 .or_else(|| chunk.field_names.iter().position(|n| n == var))
60 .or_else(|| chunk.field_names.iter().position(|n| n == &name_pk));
61 idx.ok_or_else(|| {
62 format!(
63 "Node variable {} not found in OptionalExtend input. Available fields: {:?}",
64 var, chunk.field_names
65 )
66 .into()
67 })
68}
69
70fn probe_edge(
73 fwd_adj: &HashMap<u64, Vec<(u64, usize)>>,
74 rev_adj: &HashMap<u64, Vec<(u64, usize)>>,
75 src: u64,
76 dst: u64,
77 direction: &EdgeDirection,
78) -> Option<usize> {
79 match direction {
80 EdgeDirection::LeftToRight => fwd_adj
81 .get(&src)
82 .and_then(|e| e.iter().find(|(o, _)| *o == dst).map(|(_, i)| *i)),
83 EdgeDirection::RightToLeft => rev_adj
84 .get(&src)
85 .and_then(|e| e.iter().find(|(o, _)| *o == dst).map(|(_, i)| *i)),
86 EdgeDirection::Both => fwd_adj
87 .get(&src)
88 .and_then(|e| e.iter().find(|(o, _)| *o == dst).map(|(_, i)| *i))
89 .or_else(|| {
90 rev_adj
91 .get(&src)
92 .and_then(|e| e.iter().find(|(o, _)| *o == dst).map(|(_, i)| *i))
93 }),
94 }
95}
96
97fn probe_incident_edges(
101 fwd_adj: &HashMap<u64, Vec<(u64, usize)>>,
102 rev_adj: &HashMap<u64, Vec<(u64, usize)>>,
103 src: u64,
104 direction: &EdgeDirection,
105) -> Vec<usize> {
106 let mut idxs: Vec<usize> = Vec::new();
107 match direction {
108 EdgeDirection::LeftToRight => {
109 if let Some(entries) = fwd_adj.get(&src) {
110 idxs.extend(entries.iter().map(|(_, i)| *i));
111 }
112 }
113 EdgeDirection::RightToLeft => {
114 if let Some(entries) = rev_adj.get(&src) {
115 idxs.extend(entries.iter().map(|(_, i)| *i));
116 }
117 }
118 EdgeDirection::Both => {
119 for list in [fwd_adj.get(&src), rev_adj.get(&src)].into_iter().flatten() {
120 for (_, i) in list {
121 if !idxs.contains(i) {
122 idxs.push(*i);
123 }
124 }
125 }
126 }
127 }
128 idxs
129}
130
131impl PhysicalOptionalExtend {
132 pub fn execute(&self, input: Vec<DataChunk>) -> Result<Vec<DataChunk>, ProcessorError> {
133 if input.is_empty() {
134 return Ok(input);
135 }
136
137 let (fwd_adj, rev_adj, rel_props, rel_cols) = {
139 let rel_table = self
140 .table_catalog
141 .get_rel_table_by_name(&self.rel_table_name)
142 .ok_or_else(|| format!("Rel table {} not found", self.rel_table_name))?;
143 (
144 rel_table.fwd_adj.clone(),
145 rel_table.rev_adj.clone(),
146 rel_table.properties.clone(),
147 rel_table.columns.clone(),
148 )
149 };
150
151 let num_rel_cols = rel_cols.len();
152 let rel_prefix = if self.rel_var.is_empty() {
153 self.rel_table_name.clone()
154 } else {
155 self.rel_var.clone()
156 };
157 let rel_field_names: Vec<String> = rel_cols.iter().map(|c| format!("{}.{}", rel_prefix, c.name)).collect();
158
159 let mut output = Vec::with_capacity(input.len());
160 let fan_out = self.dst_node_var.is_empty();
161
162 for chunk in input {
163 if chunk.size == 0 {
164 output.push(chunk);
165 continue;
166 }
167
168 let src_idx = find_node_id_col(&chunk, &self.src_node_var)?;
169 let dst_idx = if fan_out {
171 0
172 } else {
173 find_node_id_col(&chunk, &self.dst_node_var)?
174 };
175
176 let num_input_fields = chunk.fields.len();
177 let num_out_cols = num_input_fields + num_rel_cols + 1;
178 let mut out_data: Vec<Vec<Value>> = vec![Vec::new(); num_out_cols];
179
180 for i in 0..chunk.size {
181 let input_vals: Vec<Value> = (0..num_input_fields)
182 .map(|col| chunk.get_value(col, i).unwrap_or(Value::Null))
183 .collect();
184
185 let matches: Vec<Option<usize>> = if fan_out {
189 match input_vals.get(src_idx) {
190 Some(Value::Int64(s)) => {
191 let found = probe_incident_edges(&fwd_adj, &rev_adj, *s as u64, &self.direction);
192 if found.is_empty() {
193 vec![None]
194 } else {
195 found.into_iter().map(Some).collect()
196 }
197 }
198 _ => vec![None],
199 }
200 } else {
201 let edge_idx = match (chunk.get_value(src_idx, i), chunk.get_value(dst_idx, i)) {
202 (Some(Value::Int64(s)), Some(Value::Int64(d))) => {
203 probe_edge(&fwd_adj, &rev_adj, s as u64, d as u64, &self.direction)
204 }
205 _ => None,
206 };
207 vec![edge_idx]
208 };
209
210 for edge_idx in matches {
211 for col in 0..num_input_fields {
212 out_data[col].push(input_vals[col].clone());
213 }
214 for col in 0..num_rel_cols {
215 let val = match edge_idx {
216 Some(ei) => rel_props
217 .get(col)
218 .and_then(|c| c.get(ei))
219 .cloned()
220 .unwrap_or(Value::Null),
221 None => Value::Null,
222 };
223 out_data[num_input_fields + col].push(val);
224 }
225 out_data[num_input_fields + num_rel_cols].push(match edge_idx {
228 Some(ei) => Value::Int64(ei as i64),
229 None => Value::Null,
230 });
231 }
232 }
233
234 let out_size = out_data.first().map(Vec::len).unwrap_or(chunk.size);
235
236 let mut fields: Vec<arrow::array::ArrayRef> = Vec::with_capacity(num_out_cols);
240 let mut field_types = Vec::with_capacity(num_out_cols);
241 let mut field_names = Vec::with_capacity(num_out_cols);
242
243 for col in 0..num_input_fields {
244 let phys_type = chunk.field_types[col];
245 if matches!(
247 phys_type,
248 PhysicalTypeID::String | PhysicalTypeID::List | PhysicalTypeID::Array | PhysicalTypeID::Struct
249 ) {
250 let arr = crate::expression_evaluator::build_arrow_from_values(&out_data[col], phys_type, out_size)
251 .map_err(|e| e.to_string())?;
252 fields.push(arr.array);
253 } else {
254 let mut v = ValueVector::new(phys_type, out_size);
255 v.resize(out_size);
256 for row in 0..out_size {
257 store_value_in_vector(&mut v, row, &out_data[col][row])?;
258 }
259 fields.push(akar_common::arrow_vector::ArrowVector::from_legacy(&v).array);
260 }
261 field_types.push(phys_type);
262 field_names.push(if col < chunk.field_names.len() {
263 chunk.field_names[col].clone()
264 } else {
265 format!("field_{}", col)
266 });
267 }
268 for col in 0..num_rel_cols {
269 let phys_type = if col < rel_cols.len() {
270 PhysicalScan::logical_to_physical(&rel_cols[col].logical_type)
271 } else {
272 PhysicalTypeID::Int64
273 };
274 if matches!(
276 phys_type,
277 PhysicalTypeID::String | PhysicalTypeID::List | PhysicalTypeID::Array | PhysicalTypeID::Struct
278 ) {
279 let arr = crate::expression_evaluator::build_arrow_from_values(
280 &out_data[num_input_fields + col],
281 phys_type,
282 out_size,
283 )
284 .map_err(|e| e.to_string())?;
285 fields.push(arr.array);
286 } else {
287 let mut v = ValueVector::new(phys_type, out_size);
288 v.resize(out_size);
289 for row in 0..out_size {
290 store_value_in_vector(&mut v, row, &out_data[num_input_fields + col][row])?;
291 }
292 fields.push(akar_common::arrow_vector::ArrowVector::from_legacy(&v).array);
293 }
294 field_types.push(phys_type);
295 field_names.push(rel_field_names[col].clone());
296 }
297 let mut id_v = ValueVector::new(PhysicalTypeID::Int64, out_size);
299 id_v.resize(out_size);
300 for row in 0..out_size {
301 store_value_in_vector(&mut id_v, row, &out_data[num_input_fields + num_rel_cols][row])?;
302 }
303 fields.push(akar_common::arrow_vector::ArrowVector::from_legacy(&id_v).array);
304 field_types.push(PhysicalTypeID::Int64);
305 field_names.push(format!("{}.{}", rel_prefix, "_id"));
306
307 output.push(DataChunk {
308 fields,
309 field_types,
310 size: out_size,
311 field_names,
312 sel_vector: None,
313 });
314 }
315
316 Ok(output)
317 }
318}