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#[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
215pub(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
231pub(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
247fn 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
283pub(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 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 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 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}