Skip to main content

akar_processor/processor/mapper/
map_update.rs

1use super::ExecutionContext;
2use crate::physical_operator::*;
3use akar_common::error::ProcessorError;
4use akar_common::vector::DataChunk;
5use akar_planner::logical_operator::LogicalOperator;
6
7pub fn map_and_execute_update(
8    op: &LogicalOperator,
9    current_input: Vec<DataChunk>,
10    ctx: &mut ExecutionContext,
11) -> Result<Vec<DataChunk>, ProcessorError> {
12    match op {
13        LogicalOperator::Set(sl) => {
14            let table_catalog = ctx
15                .table_catalog
16                .clone()
17                .ok_or_else(|| "No table catalog available for SET".to_string())?;
18
19            let set_op = PhysicalSet {
20                table_name: sl.table_name.clone(),
21                table_id: sl.table_id,
22                column_name: sl.column_name.clone(),
23                column_idx: sl.column_idx,
24                value: sl.value.clone(),
25                is_node: sl.is_node,
26                table_catalog,
27            };
28            let result = set_op.execute(current_input)?;
29            // Record written rows for OCC conflict detection
30            record_set_writes(sl.table_id, &result, ctx);
31            Ok(result)
32        }
33        LogicalOperator::Delete(dl) => {
34            let table_catalog = ctx
35                .table_catalog
36                .clone()
37                .ok_or_else(|| "No table catalog available for DELETE".to_string())?;
38
39            let delete_op = PhysicalDelete {
40                table_name: dl.table_name.clone(),
41                table_id: dl.table_id,
42                primary_key_column: dl.primary_key_column.clone(),
43                is_node: dl.is_node,
44                detach: dl.detach,
45                row_indices: Vec::new(),
46                table_catalog,
47            };
48            let result = delete_op.execute(current_input)?;
49            // Record written rows for OCC conflict detection
50            record_delete_writes(dl.table_id, &result, ctx);
51            Ok(result)
52        }
53        LogicalOperator::CreateNode(cn) => {
54            let table_catalog = ctx
55                .table_catalog
56                .clone()
57                .ok_or_else(|| "No table catalog available for CREATE".to_string())?;
58
59            let create_node_op = PhysicalInsertNode {
60                table_name: cn.table_name.clone(),
61                table_id: cn.table_id,
62                out_var_name: cn.out_var_name.clone(),
63                properties: cn.properties.clone(),
64                table_catalog,
65            };
66            let result = create_node_op.execute(current_input)?;
67            // Record written rows for OCC conflict detection
68            record_insert_writes(cn.table_id, &result, ctx);
69            Ok(result)
70        }
71        LogicalOperator::CreateRel(cr) => {
72            let table_catalog = ctx
73                .table_catalog
74                .clone()
75                .ok_or_else(|| "No table catalog available for CREATE".to_string())?;
76
77            let create_rel_op = PhysicalInsertRel {
78                table_name: cr.table_name.clone(),
79                table_id: cr.table_id,
80                src_node_name: cr.src_node_name.clone(),
81                dst_node_name: cr.dst_node_name.clone(),
82                properties: cr.properties.clone(),
83                table_catalog,
84            };
85            let result = create_rel_op.execute(current_input)?;
86            // Record written rows for OCC conflict detection
87            record_insert_writes(cr.table_id, &result, ctx);
88            Ok(result)
89        }
90        LogicalOperator::Extend(ex) => {
91            let table_catalog = ctx
92                .table_catalog
93                .clone()
94                .ok_or_else(|| "No table catalog available for Extend".to_string())?;
95
96            let extend_op = PhysicalExtend {
97                rel_table_name: ex.rel_table_name.clone(),
98                rel_table_id: ex.rel_table_id,
99                bound_node_var: ex.bound_node_var.clone(),
100                direction: ex.direction.clone(),
101                dst_node_var: ex.dst_node_var.clone(),
102                dst_table_name: ex.dst_table_name.clone(),
103                dst_table_id: ex.dst_table_id,
104                table_catalog,
105            };
106            let result = extend_op.execute(current_input)?;
107            // Record written rows for OCC conflict detection
108            record_insert_writes(ex.rel_table_id, &result, ctx);
109            Ok(result)
110        }
111        LogicalOperator::Merge(m) => {
112            let table_catalog = ctx
113                .table_catalog
114                .clone()
115                .ok_or_else(|| "No table catalog available for MERGE".to_string())?;
116
117            let mut on_match_ops = Vec::new();
118            for set_item in &m.on_match {
119                on_match_ops.push(PhysicalSet {
120                    table_name: set_item.table_name.clone(),
121                    table_id: set_item.table_id,
122                    column_name: set_item.column_name.clone(),
123                    column_idx: set_item.column_idx,
124                    value: set_item.value.clone(),
125                    is_node: set_item.is_node,
126                    table_catalog: table_catalog.clone(),
127                });
128            }
129
130            let mut on_create_ops = Vec::new();
131            for set_item in &m.on_create {
132                on_create_ops.push(PhysicalSet {
133                    table_name: set_item.table_name.clone(),
134                    table_id: set_item.table_id,
135                    column_name: set_item.column_name.clone(),
136                    column_idx: set_item.column_idx,
137                    value: set_item.value.clone(),
138                    is_node: set_item.is_node,
139                    table_catalog: table_catalog.clone(),
140                });
141            }
142
143            let merge_op = PhysicalMerge {
144                table_name: m.table_name.clone(),
145                table_id: m.table_id,
146                properties: m.properties.clone(),
147                on_match: on_match_ops,
148                on_create: on_create_ops,
149                table_catalog,
150            };
151            let result = merge_op.execute(current_input)?;
152            // Record written rows for OCC conflict detection
153            record_insert_writes(m.table_id, &result, ctx);
154            Ok(result)
155        }
156        LogicalOperator::CopyFrom(cf) => {
157            let table_catalog = ctx
158                .table_catalog
159                .clone()
160                .ok_or_else(|| "No table catalog available for COPY FROM".to_string())?;
161
162            // Get column definitions from the table catalog
163            let columns = if let Some(node_table) = table_catalog.get_node_table_by_name(&cf.table_name) {
164                node_table.columns.clone()
165            } else if let Some(rel_table) = table_catalog.get_rel_table_by_name(&cf.table_name) {
166                rel_table.columns.clone()
167            } else {
168                return Err(format!("Table '{}' not found in storage catalog", cf.table_name).into());
169            };
170
171            let copy_op = PhysicalCopyFrom {
172                table_name: cf.table_name.clone(),
173                table_id: cf.table_id,
174                file_path: cf.file_path.clone(),
175                columns,
176                options: cf.options.clone(),
177                table_catalog,
178                vfs: ctx
179                    .vfs
180                    .clone()
181                    .ok_or_else(|| "VFS not initialized in processor".to_string())?,
182            };
183            let result = copy_op.execute(current_input)?;
184            // Record written rows for OCC conflict detection
185            record_insert_writes(cf.table_id, &result, ctx);
186            Ok(result)
187        }
188        LogicalOperator::BatchInsert(bi) => {
189            let table_catalog = ctx
190                .table_catalog
191                .clone()
192                .ok_or_else(|| "No table catalog available for BATCH INSERT".to_string())?;
193
194            let batch_op = PhysicalBatchInsert {
195                table_name: bi.table_name.clone(),
196                table_id: bi.table_id,
197                rows: bi.rows.clone(),
198                table_catalog,
199            };
200            let result = batch_op.execute(current_input)?;
201            // Record written rows for OCC conflict detection
202            record_insert_writes(bi.table_id, &result, ctx);
203            Ok(result)
204        }
205        LogicalOperator::Insert(i) => {
206            let exec = crate::physical::misc::PhysicalInsert {
207                table_name: i.table_name.clone(),
208                table_id: i.table_id,
209                columns: i.columns.clone(),
210                values: i.values.clone(),
211                table_catalog: ctx.table_catalog.clone().unwrap(),
212            };
213            let result = exec.execute(current_input)?;
214            // Record written rows for OCC conflict detection
215            record_insert_writes(i.table_id, &result, ctx);
216            Ok(result)
217        }
218        _ => Err(format!("Not an update operator: {:?}", op).into()),
219    }
220}
221
222/// Record rows written by a SET operation for OCC conflict detection.
223/// The result DataChunk contains the row indices that were updated (first column).
224fn record_set_writes(table_id: u64, result: &[DataChunk], ctx: &mut ExecutionContext) {
225    if let Some(chunk) = result.first() {
226        for row in 0..chunk.size {
227            if !chunk.fields.is_empty() {
228                if let Some(akar_common::types::Value::Int64(row_idx)) = chunk.get_value(0, row) {
229                    ctx.written_rows.push((table_id, row_idx as u64));
230                }
231            }
232        }
233    }
234}
235
236/// Record rows written by a DELETE operation for OCC conflict detection.
237/// The result DataChunk contains the row indices that were deleted (first column).
238fn record_delete_writes(table_id: u64, result: &[DataChunk], ctx: &mut ExecutionContext) {
239    if let Some(chunk) = result.first() {
240        for row in 0..chunk.size {
241            if !chunk.fields.is_empty() {
242                if let Some(akar_common::types::Value::Int64(row_idx)) = chunk.get_value(0, row) {
243                    ctx.written_rows.push((table_id, row_idx as u64));
244                }
245            }
246        }
247    }
248}
249
250/// Record rows written by an INSERT operation for OCC conflict detection.
251/// When the result chunk contains a second column with assigned row IDs,
252/// tracks at row level. Otherwise, row-level inserts are not tracked
253/// (PK uniqueness is enforced by the storage layer's hash index).
254fn record_insert_writes(table_id: u64, result: &[DataChunk], ctx: &mut ExecutionContext) {
255    if let Some(chunk) = result.first() {
256        // Column 0 = inserted_count, Column 1 = assigned row IDs (optional)
257        if chunk.fields.len() > 1 {
258            for row in 0..chunk.fields[1].len() {
259                if let Some(akar_common::types::Value::Int64(row_id)) = chunk.get_value(1, row) {
260                    ctx.written_rows.push((table_id, row_id as u64));
261                }
262            }
263        }
264    }
265}