akar_processor/physical/write_ops/
merge_rel.rs1use crate::physical::types::{OperatorResult, PhysicalOperatorExec};
4use crate::physical::write_ops::set::{PhysicalSet, evaluate_expression_for_row};
5use akar_common::error::ProcessorError;
6use akar_common::types::{PhysicalTypeID, Value};
7use akar_common::vector::{DataChunk, ValueVector};
8use akar_parser::ast::{EdgeDirection, Expression};
9use akar_storage::table::TableCatalog;
10use akar_storage::wal::{WalSink, log_rel_insert_record};
11use akar_transaction::UndoRecord;
12use std::sync::{Arc, Mutex};
13
14pub struct PhysicalMergeRel {
23 pub rel_table_name: String,
24 pub rel_table_id: u64,
25 pub edge_var: String,
26 pub src_node_var: String,
27 pub dst_node_var: String,
28 pub direction: EdgeDirection,
29 pub properties: Vec<(String, Expression)>,
30 pub on_match: Vec<PhysicalSet>,
31 pub on_create: Vec<PhysicalSet>,
32 pub table_catalog: Arc<TableCatalog>,
33 pub txn_id: Option<u64>,
35 pub undo_sink: Option<Arc<Mutex<Vec<UndoRecord>>>>,
37 pub wal_sink: Option<WalSink>,
39}
40
41impl PhysicalOperatorExec for PhysicalMergeRel {
42 fn operator_type(&self) -> &str {
43 "merge_rel"
44 }
45
46 fn execute(&self, input: Vec<DataChunk>) -> OperatorResult {
47 if input.is_empty() || input.iter().all(|c| c.size == 0) {
48 return Ok(input);
49 }
50
51 let (fwd_adj, rev_adj, cols) = {
53 let rel_table = self
54 .table_catalog
55 .get_rel_table_by_name(&self.rel_table_name)
56 .ok_or_else(|| format!("Rel table '{}' not found", self.rel_table_name))?;
57 (
58 rel_table.fwd_adj.clone(),
59 rel_table.rev_adj.clone(),
60 rel_table.columns.clone(),
61 )
62 };
63
64 let mut output = Vec::with_capacity(input.len());
65
66 for chunk in input {
67 let src_col = chunk
68 .field_names
69 .iter()
70 .position(|n| n == &format!("{}.{}", self.src_node_var, "_id"))
71 .ok_or_else(|| {
72 format!(
73 "Bound node '{}' not found in MERGE input. Available fields: {:?}",
74 self.src_node_var, chunk.field_names
75 )
76 })?;
77 let dst_col = chunk
78 .field_names
79 .iter()
80 .position(|n| n == &format!("{}.{}", self.dst_node_var, "_id"))
81 .ok_or_else(|| {
82 format!(
83 "Bound node '{}' not found in MERGE input. Available fields: {:?}",
84 self.dst_node_var, chunk.field_names
85 )
86 })?;
87
88 let mut matched_idx: Vec<Option<u64>> = Vec::with_capacity(chunk.size);
89 let mut matched: Vec<u64> = Vec::new();
90 let mut created: Vec<u64> = Vec::new();
91
92 for row in 0..chunk.size {
93 let src_id = match chunk.get_value(src_col, row) {
94 Some(Value::Int64(v)) => v as u64,
95 _ => continue,
96 };
97 let dst_id = match chunk.get_value(dst_col, row) {
98 Some(Value::Int64(v)) => v as u64,
99 _ => continue,
100 };
101
102 let candidates: Vec<(u64, usize)> = match self.direction {
103 EdgeDirection::LeftToRight => fwd_adj.get(&src_id).cloned().unwrap_or_default(),
104 EdgeDirection::RightToLeft => rev_adj.get(&src_id).cloned().unwrap_or_default(),
105 EdgeDirection::Both => {
106 let mut all = fwd_adj.get(&src_id).cloned().unwrap_or_default();
107 if let Some(rev) = rev_adj.get(&src_id) {
108 all.extend(rev.iter().cloned());
109 }
110 all
111 }
112 };
113
114 let mut found: Option<usize> = None;
115 for &(dst_offset, edge_idx) in &candidates {
116 if dst_offset != dst_id {
117 continue;
118 }
119 if self.props_match(edge_idx, &cols, &chunk, row)? {
120 found = Some(edge_idx);
121 break;
122 }
123 }
124
125 match found {
126 Some(edge_idx) => {
127 matched.push(edge_idx as u64);
128 matched_idx.push(Some(edge_idx as u64));
129 }
130 None => {
131 let mut values: Vec<Value> = vec![Value::Null; cols.len()];
132 for (prop_name, prop_expr) in &self.properties {
133 if let Some(col_idx) = cols.iter().position(|c| c.name == *prop_name) {
134 values[col_idx] = evaluate_expression_for_row(prop_expr, &chunk, row);
135 }
136 }
137 let edge_idx = self.insert_rel(src_id, dst_id, values)?;
138 created.push(edge_idx);
139 matched_idx.push(Some(edge_idx));
140 }
141 }
142 }
143
144 let edge_count = matched_idx.len();
146 let mut v = ValueVector::new(PhysicalTypeID::Int64, edge_count);
147 v.resize(edge_count);
148 for (i, e) in matched_idx.iter().enumerate() {
149 if let Some(idx) = e {
150 v.set_i64(i, *idx as i64);
151 } else {
152 v.set_null(i, true);
153 }
154 }
155 let arr = akar_common::arrow_vector::ArrowVector::from_legacy(&v).array;
156 let out = DataChunk::new(vec![arr], vec![PhysicalTypeID::Int64])
157 .with_names(vec![format!("{}.{}", self.edge_var, "_id")]);
158 output.push(out);
159
160 if !matched.is_empty() {
161 self.apply_on_clause(&self.on_match, &matched)?;
162 }
163 if !created.is_empty() {
164 self.apply_on_clause(&self.on_create, &created)?;
165 }
166 }
167
168 Ok(output)
169 }
170}
171
172impl PhysicalMergeRel {
173 fn props_match(
176 &self,
177 edge_idx: usize,
178 cols: &[akar_storage::table::ColumnDefinition],
179 chunk: &DataChunk,
180 row: usize,
181 ) -> Result<bool, ProcessorError> {
182 let rel_table = self
183 .table_catalog
184 .get_rel_table_by_name(&self.rel_table_name)
185 .ok_or_else(|| format!("Rel table '{}' not found", self.rel_table_name))?;
186 let props = rel_table.get_edge_properties(edge_idx);
187 for (prop_name, prop_expr) in &self.properties {
188 let col_idx = cols
189 .iter()
190 .position(|c| c.name == *prop_name)
191 .ok_or_else(|| format!("Rel column '{prop_name}' not found"))?;
192 let expected = evaluate_expression_for_row(prop_expr, chunk, row);
193 let actual = props.get(col_idx).cloned().unwrap_or(Value::Null);
194 if expected != actual {
195 return Ok(false);
196 }
197 }
198 Ok(true)
199 }
200
201 fn insert_rel(&self, src_id: u64, dst_id: u64, values: Vec<Value>) -> Result<u64, ProcessorError> {
203 let logged_values = self.wal_sink.is_some().then(|| values.clone());
204 let (edge_idx, table_id) = {
205 let mut rel_table = self
206 .table_catalog
207 .get_rel_table_by_name_mut(&self.rel_table_name)
208 .ok_or_else(|| format!("Rel table '{}' not found", self.rel_table_name))?;
209 let edge_idx = rel_table.edges.len() as u64;
210 rel_table
211 .insert_rel(src_id, dst_id, values)
212 .map_err(|e| format!("MERGE CREATE edge failed: {e}"))?;
213 (edge_idx, rel_table.table_id)
214 };
215 log_rel_insert_record(
216 &self.wal_sink,
217 table_id,
218 src_id,
219 dst_id,
220 logged_values.as_deref().unwrap_or(&[]),
221 );
222 if let Some(sink) = self.undo_sink.as_ref()
223 && let Ok(mut u) = sink.lock()
224 {
225 u.push(UndoRecord::insert(table_id, edge_idx));
226 }
227 Ok(edge_idx)
228 }
229
230 fn apply_on_clause(&self, set_ops: &[PhysicalSet], edge_ids: &[u64]) -> Result<(), ProcessorError> {
234 if set_ops.is_empty() || edge_ids.is_empty() {
235 return Ok(());
236 }
237 let n = edge_ids.len();
238 let mut v = ValueVector::new(PhysicalTypeID::Int64, n);
239 v.resize(n);
240 for (i, e) in edge_ids.iter().enumerate() {
241 v.set_i64(i, *e as i64);
242 }
243 let arr = akar_common::arrow_vector::ArrowVector::from_legacy(&v).array;
244 let chunk = DataChunk::new(vec![arr], vec![PhysicalTypeID::Int64])
245 .with_names(vec![format!("{}.{}", self.edge_var, "_id")]);
246 for set_op in set_ops {
247 let _ = set_op.execute(vec![chunk.clone()])?;
248 }
249 Ok(())
250 }
251}