Skip to main content

linkrs_query/executor/streaming/operators/
sink_operator.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3
4use parking_lot::RwLock;
5
6use crate::executor::expression::evaluator::traits::ExpressionContext;
7use crate::executor::expression::evaluator::ExpressionEvaluator;
8use crate::executor::streaming::chunk::DataChunk;
9use crate::executor::streaming::context::ValueRowContext;
10use crate::executor::streaming::executor::StreamingExecutor;
11use crate::executor::streaming::operators::source_operator::OperatorConfig;
12use crate::executor::streaming::runtime::ExecutionRuntime;
13use crate::executor::streaming::slot::SlotLayout;
14use crate::storage::{QueryStorage, StorageWriter};
15use linkrs_core::error::QueryError;
16use linkrs_core::types::expr::Expression;
17use linkrs_core::types::storage_ids::VertexId;
18use linkrs_core::vertex_edge_path::{Edge, Tag, Vertex};
19use linkrs_core::Value;
20
21mod copy;
22mod delete;
23mod insert;
24mod update;
25
26#[derive(Debug)]
27pub enum SinkOperatorKind {
28    CopyFrom {
29        storage: Option<Arc<RwLock<dyn QueryStorage>>>,
30        space_name: String,
31        target: crate::executor::streaming::operators::spec::CopyTarget,
32        file_paths: Vec<String>,
33        by_column: bool,
34        header: bool,
35        delimiter: u8,
36        batch_size: usize,
37        rows_inserted: u64,
38        summary_returned: bool,
39    },
40    CopyTo {
41        storage: Option<Arc<RwLock<dyn QueryStorage>>>,
42        space_name: String,
43        target: crate::executor::streaming::operators::spec::CopyTarget,
44        file_path: String,
45        header: bool,
46        delimiter: u8,
47        rows_exported: u64,
48        summary_returned: bool,
49    },
50    InsertVertices {
51        storage: Option<Arc<RwLock<dyn QueryStorage>>>,
52        space_name: String,
53        vertex_properties: Vec<(String, Expression)>,
54        tag: String,
55        tag_property_names: Vec<String>,
56        if_not_exists: bool,
57        rows_inserted: u64,
58        summary_returned: bool,
59    },
60    InsertEdges {
61        storage: Option<Arc<RwLock<dyn QueryStorage>>>,
62        space_name: String,
63        src_col: String,
64        dst_col: String,
65        rank_col: String,
66        edge_type: String,
67        edge_properties: Vec<(String, Expression)>,
68        if_not_exists: bool,
69        rows_inserted: u64,
70        summary_returned: bool,
71    },
72    UpdateVertices {
73        storage: Option<Arc<RwLock<dyn QueryStorage>>>,
74        space_name: String,
75        tag_name: String,
76        updates: Vec<(String, Expression)>,
77        condition: Option<Expression>,
78        is_upsert: bool,
79        replace_properties: bool,
80        rows_updated: u64,
81        summary_returned: bool,
82    },
83    UpdateEdges {
84        storage: Option<Arc<RwLock<dyn QueryStorage>>>,
85        space_name: String,
86        src_col: String,
87        dst_col: String,
88        rank_col: String,
89        edge_type: String,
90        updates: Vec<(String, Expression)>,
91        condition: Option<Expression>,
92        is_upsert: bool,
93        replace_properties: bool,
94        rows_updated: u64,
95        summary_returned: bool,
96    },
97    DeleteVertices {
98        storage: Option<Arc<RwLock<dyn QueryStorage>>>,
99        space_name: String,
100        tag: String,
101        vertex_id_col: String,
102        cascade: bool,
103        rows_deleted: u64,
104        summary_returned: bool,
105    },
106    DeleteEdges {
107        storage: Option<Arc<RwLock<dyn QueryStorage>>>,
108        space_name: String,
109        src_col: String,
110        dst_col: String,
111        rank_col: String,
112        edge_type: String,
113        rows_deleted: u64,
114        summary_returned: bool,
115    },
116    PipeDeleteVertices {
117        storage: Option<Arc<RwLock<dyn QueryStorage>>>,
118        space_name: String,
119        vertex_id_col: String,
120        cascade: bool,
121        vertex_id_is_literal: bool,
122        rows_deleted: u64,
123        summary_returned: bool,
124    },
125    PipeDeleteEdges {
126        storage: Option<Arc<RwLock<dyn QueryStorage>>>,
127        space_name: String,
128        src_col: String,
129        dst_col: String,
130        rank_col: String,
131        edge_type: String,
132        rows_deleted: u64,
133        summary_returned: bool,
134    },
135}
136
137/// Sink operator.
138///
139/// Wraps [`SinkOperatorKind`] with the runtime context injected at `open()`.
140/// Lifecycle state is owned exclusively by the executor; operators never
141/// write it.
142#[derive(Debug)]
143pub struct SinkOperator {
144    pub kind: SinkOperatorKind,
145    pub runtime: Option<Arc<ExecutionRuntime>>,
146    pub output_layout: Arc<SlotLayout>,
147    pub config: OperatorConfig,
148}
149
150pub(super) fn make_modify_result(
151    output_layout: Arc<SlotLayout>,
152    op: &str,
153    count: u64,
154) -> DataChunk {
155    let row = vec![Value::string(op), Value::BigInt(count as i64)];
156    DataChunk::new_with_layout(vec![row], output_layout)
157}
158
159pub(super) fn eval_expr(
160    expr: &Expression,
161    context: &mut ValueRowContext,
162) -> Result<Value, QueryError> {
163    ExpressionEvaluator::evaluate(expr, context).map_err(|e| QueryError::execution(e.to_string()))
164}
165
166pub(super) fn eval_update_props(
167    updates: &[(String, Expression)],
168    replace_properties: bool,
169    context: &mut ValueRowContext,
170) -> Result<HashMap<Arc<str>, Value>, QueryError> {
171    if !replace_properties {
172        let mut props = HashMap::new();
173        for (prop_name, expr) in updates.iter() {
174            let val = eval_expr(expr, context)?;
175            props.insert(Arc::from(prop_name.as_str()), val);
176        }
177        return Ok(props);
178    }
179    let mut props = HashMap::new();
180    let mut saw_map = false;
181    for (prop_name, expr) in updates.iter() {
182        let val = eval_expr(expr, context)?;
183        match val {
184            Value::Map(entries) => {
185                saw_map = true;
186                for (key, item) in entries.iter() {
187                    match key {
188                        Value::String(name) => {
189                            props.insert(Arc::from(name.as_str()), item.clone());
190                        }
191                        Value::FixedString(name) => {
192                            props.insert(Arc::from(name.as_str()), item.clone());
193                        }
194                        _ => {
195                            return Err(QueryError::execution(
196                                "Map overwrite keys must be strings".to_string(),
197                            ));
198                        }
199                    }
200                }
201            }
202            other => {
203                props.insert(Arc::from(prop_name.as_str()), other);
204            }
205        }
206    }
207    if !saw_map {
208        return Err(QueryError::execution(
209            "Map overwrite value must be a map".to_string(),
210        ));
211    }
212    Ok(props)
213}
214
215/// Build a row context that resolves `$name` parameter references.
216///
217/// Sink operators evaluate shape-normalized DML expressions (`$__dml_N`
218/// placeholders), so the context must carry the runtime parameter values —
219/// otherwise parameter resolution fails with "Undefined parameter".
220pub(super) fn row_context(
221    row: Vec<Value>,
222    layout: Arc<SlotLayout>,
223    params: Option<Arc<HashMap<String, Value>>>,
224) -> ValueRowContext {
225    match params {
226        Some(parameters) => ValueRowContext::with_parameters(row, layout, parameters),
227        None => ValueRowContext::new(row, layout),
228    }
229}
230
231/// Row predicate semantics for update conditions (`WHEN`/`WHERE`), matching
232/// the filter operator: false/null/zero/empty reject the row.
233pub(super) fn condition_matches(value: &Value) -> bool {
234    match value {
235        Value::Bool(b) => *b,
236        Value::Null(_) => false,
237        Value::Int(i) => *i != 0,
238        Value::SmallInt(i) => *i != 0,
239        Value::BigInt(i) => *i != 0,
240        Value::Float(f) => *f != 0.0,
241        Value::Double(f) => *f != 0.0,
242        Value::String(s) => !s.is_empty(),
243        _ => true,
244    }
245}
246
247/// Resolve edge endpoints from row values.
248///
249/// When the row carries an `Edge` value (e.g. `MATCH ... DELETE EDGE e`), the
250/// endpoints are taken from the edge itself; otherwise both values are
251/// converted to vertex ids directly.
252///
253/// Whether a write-path error message indicates a transaction conflict
254/// (write-write conflict, rollback-only transaction).
255///
256/// Storage errors are flattened to strings at the operator boundary; the
257/// storage layer's typed `StorageErrorKind::Conflict` renders as "conflict"
258/// / "Write-write conflict", which this classifier recognizes.
259fn is_transaction_conflict_message(message: &str) -> bool {
260    let lowered = message.to_ascii_lowercase();
261    lowered.contains("conflict")
262        || lowered.contains("rollback-only")
263        || lowered.contains("rollback_only")
264}
265
266pub(super) fn resolve_edge_endpoints(
267    src_val: &Value,
268    dst_val: &Value,
269) -> Option<(VertexId, VertexId)> {
270    match (src_val, dst_val) {
271        (Value::Edge(edge), _) => Some((edge.src, edge.dst)),
272        (_, Value::Edge(edge)) => Some((edge.src, edge.dst)),
273        (Value::EdgeHeader(header), _) => Some((header.src, header.dst)),
274        (_, Value::EdgeHeader(header)) => Some((header.src, header.dst)),
275        _ => {
276            let src = VertexId::try_from(src_val).ok()?;
277            let dst = VertexId::try_from(dst_val).ok()?;
278            Some((src, dst))
279        }
280    }
281}
282
283/// Resolve the edge rank for a write row.
284///
285/// A materialized `rank` column wins; otherwise the rank carried by an edge
286/// value in the row is used; otherwise the rank defaults to 0.
287pub(super) fn resolve_edge_rank(
288    context: &ValueRowContext,
289    rank_col: &str,
290    src_val: &Value,
291    dst_val: &Value,
292) -> Result<i64, QueryError> {
293    if let Some(rank_val) = context.get_variable(rank_col) {
294        if !matches!(rank_val, Value::Null(_)) {
295            return match &rank_val {
296                Value::SmallInt(i) => Ok(*i as i64),
297                Value::Int(i) => Ok(*i as i64),
298                Value::BigInt(i) => Ok(*i),
299                _ => Err(QueryError::execution(format!(
300                    "Invalid edge rank: {:?}",
301                    rank_val
302                ))),
303            };
304        }
305    }
306    match (src_val, dst_val) {
307        (Value::Edge(edge), _) => Ok(edge.ranking),
308        (_, Value::Edge(edge)) => Ok(edge.ranking),
309        (Value::EdgeHeader(header), _) => Ok(header.ranking),
310        (_, Value::EdgeHeader(header)) => Ok(header.ranking),
311        _ => Ok(0),
312    }
313}
314
315impl SinkOperator {
316    pub fn from_spec(
317        spec: &super::spec::SinkSpec,
318        storage: Option<Arc<RwLock<dyn QueryStorage>>>,
319        output_layout: Arc<SlotLayout>,
320    ) -> Self {
321        let kind = match spec {
322            super::spec::SinkSpec::CopyFrom {
323                space_name,
324                target,
325                file_paths,
326                by_column,
327                header,
328                delimiter,
329                batch_size,
330            } => SinkOperatorKind::CopyFrom {
331                storage,
332                space_name: space_name.clone(),
333                target: target.clone(),
334                file_paths: file_paths.clone(),
335                by_column: *by_column,
336                header: *header,
337                delimiter: *delimiter,
338                batch_size: *batch_size,
339                rows_inserted: 0,
340                summary_returned: false,
341            },
342            super::spec::SinkSpec::CopyTo {
343                space_name,
344                target,
345                file_path,
346                header,
347                delimiter,
348            } => SinkOperatorKind::CopyTo {
349                storage,
350                space_name: space_name.clone(),
351                target: target.clone(),
352                file_path: file_path.clone(),
353                header: *header,
354                delimiter: *delimiter,
355                rows_exported: 0,
356                summary_returned: false,
357            },
358            super::spec::SinkSpec::InsertVertices {
359                space_name,
360                vertex_properties,
361                tag,
362                tag_property_names,
363                if_not_exists,
364            } => SinkOperatorKind::InsertVertices {
365                storage,
366                space_name: space_name.clone(),
367                vertex_properties: vertex_properties.clone(),
368                tag: tag.clone(),
369                tag_property_names: tag_property_names.clone(),
370                if_not_exists: *if_not_exists,
371                rows_inserted: 0,
372                summary_returned: false,
373            },
374            super::spec::SinkSpec::InsertEdges {
375                space_name,
376                src_col,
377                dst_col,
378                rank_col,
379                edge_type,
380                edge_properties,
381                if_not_exists,
382            } => SinkOperatorKind::InsertEdges {
383                storage,
384                space_name: space_name.clone(),
385                src_col: src_col.clone(),
386                dst_col: dst_col.clone(),
387                rank_col: rank_col.clone(),
388                edge_type: edge_type.clone(),
389                edge_properties: edge_properties.clone(),
390                if_not_exists: *if_not_exists,
391                rows_inserted: 0,
392                summary_returned: false,
393            },
394            super::spec::SinkSpec::UpdateVertices {
395                space_name,
396                tag_name,
397                updates,
398                condition,
399                is_upsert,
400                replace_properties,
401            } => SinkOperatorKind::UpdateVertices {
402                storage,
403                space_name: space_name.clone(),
404                tag_name: tag_name.clone(),
405                updates: updates.clone(),
406                condition: condition.clone(),
407                is_upsert: *is_upsert,
408                replace_properties: *replace_properties,
409                rows_updated: 0,
410                summary_returned: false,
411            },
412            super::spec::SinkSpec::UpdateEdges {
413                space_name,
414                src_col,
415                dst_col,
416                rank_col,
417                edge_type,
418                updates,
419                condition,
420                is_upsert,
421                replace_properties,
422            } => SinkOperatorKind::UpdateEdges {
423                storage,
424                space_name: space_name.clone(),
425                src_col: src_col.clone(),
426                dst_col: dst_col.clone(),
427                rank_col: rank_col.clone(),
428                edge_type: edge_type.clone(),
429                updates: updates.clone(),
430                condition: condition.clone(),
431                is_upsert: *is_upsert,
432                replace_properties: *replace_properties,
433                rows_updated: 0,
434                summary_returned: false,
435            },
436            super::spec::SinkSpec::DeleteVertices {
437                space_name,
438                tag,
439                vertex_id_col,
440                cascade,
441            } => SinkOperatorKind::DeleteVertices {
442                storage,
443                space_name: space_name.clone(),
444                tag: tag.clone(),
445                vertex_id_col: vertex_id_col.clone(),
446                cascade: *cascade,
447                rows_deleted: 0,
448                summary_returned: false,
449            },
450            super::spec::SinkSpec::DeleteEdges {
451                space_name,
452                src_col,
453                dst_col,
454                rank_col,
455                edge_type,
456            } => SinkOperatorKind::DeleteEdges {
457                storage,
458                space_name: space_name.clone(),
459                src_col: src_col.clone(),
460                dst_col: dst_col.clone(),
461                rank_col: rank_col.clone(),
462                edge_type: edge_type.clone(),
463                rows_deleted: 0,
464                summary_returned: false,
465            },
466            super::spec::SinkSpec::PipeDeleteVertices {
467                space_name,
468                vertex_id_col,
469                cascade,
470                vertex_id_is_literal,
471            } => SinkOperatorKind::PipeDeleteVertices {
472                storage,
473                space_name: space_name.clone(),
474                vertex_id_col: vertex_id_col.clone(),
475                cascade: *cascade,
476                vertex_id_is_literal: *vertex_id_is_literal,
477                rows_deleted: 0,
478                summary_returned: false,
479            },
480            super::spec::SinkSpec::PipeDeleteEdges {
481                space_name,
482                src_col,
483                dst_col,
484                rank_col,
485                edge_type,
486            } => SinkOperatorKind::PipeDeleteEdges {
487                storage,
488                space_name: space_name.clone(),
489                src_col: src_col.clone(),
490                dst_col: dst_col.clone(),
491                rank_col: rank_col.clone(),
492                edge_type: edge_type.clone(),
493                rows_deleted: 0,
494                summary_returned: false,
495            },
496        };
497        Self::new(kind, output_layout)
498    }
499
500    pub fn new(kind: SinkOperatorKind, output_layout: Arc<SlotLayout>) -> Self {
501        Self {
502            kind,
503            runtime: None,
504            output_layout,
505            config: OperatorConfig::default(),
506        }
507    }
508
509    /// Inject the runtime and execution config (called once by the executor
510    /// before this operator produces any data).
511    pub fn inject_context(
512        &mut self,
513        runtime: Option<&Arc<ExecutionRuntime>>,
514        config: OperatorConfig,
515    ) {
516        if let Some(rt) = runtime {
517            self.runtime = Some(rt.clone());
518        }
519        self.config = config;
520    }
521
522    /// Check that the transaction scope allows writes.
523    ///
524    /// requires a transaction scope for DML operations.  Absent scope
525    /// is rejected to prevent unbounded writes outside any transaction.
526    fn check_write_permission(&self) -> Result<(), QueryError> {
527        let rt = self.runtime.as_ref().ok_or_else(|| {
528            QueryError::execution(
529                "DML requires an execution runtime with transaction scope".to_string(),
530            )
531        })?;
532        let scope = rt.transaction_scope().ok_or_else(|| {
533            QueryError::execution(
534                "DML requires a transaction scope — no transaction is active".to_string(),
535            )
536        })?;
537        if !scope.allows_write() {
538            return Err(QueryError::execution(
539                "Write operation not allowed in current transaction scope".to_string(),
540            ));
541        }
542        Ok(())
543    }
544
545    pub fn open(&mut self, input: &mut StreamingExecutor) -> Result<(), QueryError> {
546        self.check_write_permission()?;
547        match &mut self.kind {
548            SinkOperatorKind::CopyFrom { .. }
549            | SinkOperatorKind::CopyTo { .. }
550            | SinkOperatorKind::InsertVertices { .. }
551            | SinkOperatorKind::InsertEdges { .. }
552            | SinkOperatorKind::UpdateVertices { .. }
553            | SinkOperatorKind::UpdateEdges { .. }
554            | SinkOperatorKind::DeleteVertices { .. }
555            | SinkOperatorKind::DeleteEdges { .. }
556            | SinkOperatorKind::PipeDeleteVertices { .. }
557            | SinkOperatorKind::PipeDeleteEdges { .. } => {
558                input.open()?;
559                Ok(())
560            }
561        }
562    }
563
564    pub fn next(&mut self, input: &mut StreamingExecutor) -> Result<Option<DataChunk>, QueryError> {
565        let result = self.next_inner(input);
566        if let Err(error) = &result {
567            // Write-conflict linkage: a conflict-classified failure (write-write
568            // conflict, rollback-only transaction) cancels the remaining pipeline
569            // stages of the same transaction instead of letting them compute on.
570            if is_transaction_conflict_message(&error.to_string()) {
571                if let Some(rt) = self.runtime.as_ref() {
572                    rt.note_transaction_conflict();
573                }
574            }
575        }
576        result
577    }
578
579    fn next_inner(
580        &mut self,
581        input: &mut StreamingExecutor,
582    ) -> Result<Option<DataChunk>, QueryError> {
583        if matches!(&self.kind, SinkOperatorKind::CopyFrom { .. }) {
584            return copy::handle_copy_from(self, input);
585        }
586        if matches!(&self.kind, SinkOperatorKind::CopyTo { .. }) {
587            return copy::handle_copy_to(self, input);
588        }
589        if matches!(&self.kind, SinkOperatorKind::InsertVertices { .. }) {
590            return insert::handle_insert_vertices(self, input);
591        }
592        if matches!(&self.kind, SinkOperatorKind::InsertEdges { .. }) {
593            return insert::handle_insert_edges(self, input);
594        }
595        if matches!(&self.kind, SinkOperatorKind::UpdateVertices { .. }) {
596            return update::handle_update_vertices(self, input);
597        }
598        if matches!(&self.kind, SinkOperatorKind::UpdateEdges { .. }) {
599            return update::handle_update_edges(self, input);
600        }
601        if matches!(&self.kind, SinkOperatorKind::DeleteVertices { .. }) {
602            return delete::handle_delete_vertices(self, input);
603        }
604        if matches!(&self.kind, SinkOperatorKind::DeleteEdges { .. }) {
605            return delete::handle_delete_edges(self, input);
606        }
607        if matches!(&self.kind, SinkOperatorKind::PipeDeleteVertices { .. }) {
608            return delete::handle_pipe_delete_vertices(self, input);
609        }
610        if matches!(&self.kind, SinkOperatorKind::PipeDeleteEdges { .. }) {
611            return delete::handle_pipe_delete_edges(self, input);
612        }
613        unreachable!("sink_operator::next_inner called for an unknown kind")
614    }
615
616    pub fn stop(&mut self) -> Result<(), QueryError> {
617        Ok(())
618    }
619
620    pub fn close(&mut self) -> Result<(), QueryError> {
621        Ok(())
622    }
623}