1#[cfg(feature = "delta-lake")]
24use std::future::Future;
25use std::sync::Arc;
26use std::time::{Duration, Instant};
27
28use arrow_array::{Array, RecordBatch};
29use arrow_schema::SchemaRef;
30use async_trait::async_trait;
31use tracing::{debug, info, warn};
32
33#[cfg(feature = "delta-lake")]
34use deltalake::DeltaTable;
35
36#[cfg(feature = "delta-lake")]
37use deltalake::protocol::SaveMode;
38
39use crate::config::{ConnectorConfig, ConnectorState};
40#[cfg(feature = "delta-lake")]
41use crate::connector::{ConnectorTaskGuard, MAX_COORDINATED_COMMIT_PAYLOAD_BYTES};
42use crate::connector::{
43 ConnectorTaskOwner, ConnectorTaskTracker, SinkConnector, SinkConsistency, SinkContract,
44 SinkInputMode, SinkTopology, WriteResult,
45};
46use crate::error::ConnectorError;
47
48use super::delta_config::{DeltaLakeSinkConfig, DeltaWriteMode};
49use super::delta_metrics::DeltaLakeSinkMetrics;
50use crate::connector::DeliveryGuarantee;
51
52#[cfg(feature = "delta-lake")]
53async fn run_tracked_delta_task<F>(guard: ConnectorTaskGuard, task: F) -> F::Output
54where
55 F: Future,
56{
57 let _guard = guard;
58 task.await
59}
60
61#[cfg(feature = "delta-lake")]
62fn classify_delta_attempt_error(error: super::delta_io::DeltaWriteAttemptError) -> ConnectorError {
63 use super::delta_io::DeltaWriteAttemptError;
64
65 if error.is_definite_optimistic_conflict() {
66 return ConnectorError::WriteError(format!(
67 "Delta optimistic commit collision did not publish: {error}"
68 ));
69 }
70
71 match error {
72 DeltaWriteAttemptError::Local(error) => error,
73 DeltaWriteAttemptError::Delta(error) => {
74 let retryable = super::delta_io::delta_error_has_retryable_transport(&error);
79 ConnectorError::outcome_unknown(
80 format!(
81 "Delta write was dispatched but its catalog commit outcome is not known: {error}"
82 ),
83 retryable,
84 )
85 }
86 }
87}
88
89#[cfg(feature = "delta-lake")]
90struct DeltaWriteTaskSuccess {
91 table: DeltaTable,
92 merge_result: Option<super::delta_io::MergeResult>,
93}
94
95#[cfg(feature = "delta-lake")]
99fn count_collapsed_ops(batch: &RecordBatch) -> (u64, u64) {
100 let Ok(idx) = batch.schema().index_of("_op") else {
101 return (0, 0);
102 };
103 let Some(ops) = batch
104 .column(idx)
105 .as_any()
106 .downcast_ref::<arrow_array::StringArray>()
107 else {
108 return (0, 0);
109 };
110 let deletes = (0..ops.len())
111 .filter(|&i| !ops.is_null(i) && ops.value(i) == "D")
112 .count() as u64;
113 let upserts = ops.len() as u64 - deletes;
114 (upserts, deletes)
115}
116
117#[cfg(feature = "delta-lake")]
118#[derive(Debug, Clone, PartialEq, Eq)]
119struct UnresolvedDeltaPublication {
120 external_key: String,
121 target: crate::connector::CoordinatedCommitCursor,
122 exact_batch_fingerprint: [u8; 32],
123}
124
125#[cfg(feature = "delta-lake")]
126impl UnresolvedDeltaPublication {
127 fn reconciled_by(&self, observed: Option<crate::connector::CoordinatedCommitCursor>) -> bool {
128 observed == Some(self.target)
129 }
130}
131
132#[cfg(feature = "delta-lake")]
133async fn publish_coordinated_delta_batch(
134 table_path: String,
135 storage_options: std::collections::HashMap<String, String>,
136 unresolved: Arc<parking_lot::Mutex<Option<UnresolvedDeltaPublication>>>,
137 pending: UnresolvedDeltaPublication,
138 batch: crate::connector::CoordinatedCommitBatch,
139 deadline: tokio::time::Instant,
140 publication_budget: Duration,
141) -> Result<(), ConnectorError> {
142 super::delta_io::validate_coordinated_storage_preflight(&table_path, &storage_options)?;
143 let storage_options = super::delta_io::bound_coordinated_storage_options(storage_options);
144 let result = async {
145 let table = tokio::time::timeout_at(
146 deadline,
147 super::delta_io::open_or_create_table(&table_path, storage_options, None),
148 )
149 .await
150 .map_err(|_| {
151 ConnectorError::TransactionError(
152 "Delta coordinated table open exceeded the publication deadline".into(),
153 )
154 })??;
155 let descriptor_count =
156 super::delta_io::commit_batch_coordinated(&table, &batch, deadline).await?;
157 info!(
158 epoch = batch.target.epoch,
159 checkpoint_id = batch.target.checkpoint_id,
160 descriptors = descriptor_count,
161 "delta coordinated commit"
162 );
163 Ok(())
164 }
165 .await;
166
167 if result.is_ok() && tokio::time::Instant::now() < deadline {
168 let mut unresolved = unresolved.lock();
169 if unresolved.as_ref() == Some(&pending) {
170 *unresolved = None;
171 }
172 return Ok(());
173 }
174 if result.is_ok() {
175 return Err(ConnectorError::outcome_unknown(
176 format!(
177 "Delta coordinated publication exceeded its {publication_budget:?} remaining \
178 budget; reconcile the exact cursor before replay"
179 ),
180 true,
181 ));
182 }
183 result
184}
185
186pub struct DeltaLakeSink {
200 #[cfg(feature = "delta-lake")]
202 task_owner: ConnectorTaskOwner,
203 task_tracker: ConnectorTaskTracker,
205 config: DeltaLakeSinkConfig,
207 schema: Option<SchemaRef>,
209 state: ConnectorState,
211 current_epoch: u64,
213 buffer: Vec<RecordBatch>,
215 buffered_rows: usize,
217 buffered_bytes: u64,
219 delta_version: u64,
221 buffer_start_time: Option<Instant>,
223 metrics: DeltaLakeSinkMetrics,
225 #[cfg(feature = "delta-lake")]
227 table: Option<DeltaTable>,
228 staged_batches: Vec<RecordBatch>,
232 staged_rows: usize,
234 staged_bytes: u64,
236 #[cfg(feature = "delta-lake")]
239 coordinated_adds: Vec<deltalake::kernel::Add>,
240 #[cfg(feature = "delta-lake")]
243 coordinated_binding: Option<super::commit_descriptor::DeltaTableBinding>,
244 #[cfg(feature = "delta-lake")]
247 coordinated_descriptor_bytes: usize,
248 #[cfg(feature = "delta-lake")]
252 coordinated_unresolved_publication: Arc<parking_lot::Mutex<Option<UnresolvedDeltaPublication>>>,
253 #[cfg(feature = "delta-lake")]
257 resolved_table_path: String,
258 #[cfg(feature = "delta-lake")]
260 resolved_storage_options: std::collections::HashMap<String, String>,
261 #[cfg(feature = "delta-lake")]
265 needs_deferred_delta_init: bool,
266 #[cfg(feature = "delta-lake")]
271 cached_writer_properties: Option<deltalake::parquet::file::properties::WriterProperties>,
272 #[cfg(feature = "delta-lake")]
277 merge_session: Option<datafusion::prelude::SessionContext>,
278 #[cfg(feature = "delta-lake")]
281 unresolved_delta_write: bool,
282 #[cfg(all(test, feature = "delta-lake"))]
285 stall_descriptor_write: bool,
286}
287
288impl DeltaLakeSink {
289 #[must_use]
291 pub fn new(config: DeltaLakeSinkConfig, registry: Option<&prometheus::Registry>) -> Self {
292 let (task_owner, task_tracker) = ConnectorTaskOwner::new();
293 #[cfg(not(feature = "delta-lake"))]
294 let _ = task_owner;
295 Self {
296 #[cfg(feature = "delta-lake")]
297 task_owner,
298 task_tracker,
299 config,
300 schema: None,
301 state: ConnectorState::Created,
302 current_epoch: 0,
303 buffer: Vec::with_capacity(16),
304 buffered_rows: 0,
305 buffered_bytes: 0,
306 delta_version: 0,
307 buffer_start_time: None,
308 metrics: DeltaLakeSinkMetrics::new(registry),
309 staged_batches: Vec::new(),
310 staged_rows: 0,
311 staged_bytes: 0,
312 #[cfg(feature = "delta-lake")]
313 coordinated_adds: Vec::new(),
314 #[cfg(feature = "delta-lake")]
315 coordinated_binding: None,
316 #[cfg(feature = "delta-lake")]
317 coordinated_descriptor_bytes: 0,
318 #[cfg(feature = "delta-lake")]
319 coordinated_unresolved_publication: Arc::new(parking_lot::Mutex::new(None)),
320 #[cfg(feature = "delta-lake")]
321 table: None,
322 #[cfg(feature = "delta-lake")]
323 resolved_table_path: String::new(),
324 #[cfg(feature = "delta-lake")]
325 resolved_storage_options: std::collections::HashMap::new(),
326 #[cfg(feature = "delta-lake")]
327 needs_deferred_delta_init: false,
328 #[cfg(feature = "delta-lake")]
329 cached_writer_properties: None,
330 #[cfg(feature = "delta-lake")]
331 merge_session: None,
332 #[cfg(feature = "delta-lake")]
333 unresolved_delta_write: false,
334 #[cfg(all(test, feature = "delta-lake"))]
335 stall_descriptor_write: false,
336 }
337 }
338
339 #[must_use]
345 pub fn with_schema(config: DeltaLakeSinkConfig, schema: SchemaRef) -> Self {
346 let write_mode = config.write_mode;
347 let mut sink = Self::new(config, None);
348 sink.schema = Some(if write_mode == DeltaWriteMode::Upsert {
349 Self::target_schema(&schema, write_mode)
350 } else {
351 schema
352 });
353 sink
354 }
355
356 #[cfg(feature = "delta-lake")]
361 async fn init_delta_table(
362 &mut self,
363 deadline: tokio::time::Instant,
364 ) -> Result<(), ConnectorError> {
365 use super::delta_io;
366
367 #[cfg(feature = "delta-lake-unity")]
370 tokio::time::timeout_at(
371 deadline,
372 ensure_uc_table_exists(&self.config, self.schema.as_ref()),
373 )
374 .await
375 .map_err(|_| {
376 ConnectorError::ConnectionFailed(
377 "Delta Unity table initialization exceeded the write deadline".into(),
378 )
379 })??;
380
381 let (resolved_path, mut merged_options) = tokio::time::timeout_at(
384 deadline,
385 delta_io::resolve_catalog_options(
386 &self.config.catalog_type,
387 self.config.catalog_database.as_deref(),
388 self.config.catalog_name.as_deref(),
389 self.config.catalog_schema.as_deref(),
390 &self.config.table_path,
391 &self.config.storage_options,
392 ),
393 )
394 .await
395 .map_err(|_| {
396 ConnectorError::ConnectionFailed(
397 "Delta catalog resolution exceeded the write deadline".into(),
398 )
399 })??;
400
401 if self.config.delivery_guarantee == DeliveryGuarantee::ExactlyOnce {
402 delta_io::validate_coordinated_storage_preflight(&resolved_path, &merged_options)?;
403 }
404
405 merged_options
409 .entry("timeout".to_string())
410 .or_insert_with(|| "120s".to_string());
411 merged_options
412 .entry("connect_timeout".to_string())
413 .or_insert_with(|| "30s".to_string());
414 merged_options
415 .entry("pool_idle_timeout".to_string())
416 .or_insert_with(|| "60s".to_string());
417
418 self.resolved_table_path.clone_from(&resolved_path);
420 self.resolved_storage_options.clone_from(&merged_options);
421
422 let table = tokio::time::timeout_at(
423 deadline,
424 delta_io::open_or_create_table(
425 &resolved_path,
426 merged_options.clone(),
427 self.schema.as_ref(),
428 ),
429 )
430 .await
431 .map_err(|_| {
432 ConnectorError::ConnectionFailed(format!(
433 "Delta table initialization exceeded the {:?} write deadline",
434 self.config.write_timeout
435 ))
436 })??;
437
438 if self.schema.is_none() {
440 if let Ok(schema) = delta_io::get_table_schema(&table) {
441 self.schema = Some(schema);
442 }
443 }
444
445 self.delta_version = table
446 .version()
447 .and_then(|version| u64::try_from(version).ok())
448 .unwrap_or(0);
449 self.table = Some(table);
450
451 self.cached_writer_properties = self.config.parquet.to_writer_properties().ok();
459 if self.config.write_mode == DeltaWriteMode::Upsert {
460 self.merge_session = Some(datafusion::prelude::SessionContext::new());
461 }
462
463 Ok(())
464 }
465
466 #[must_use]
468 pub fn state(&self) -> ConnectorState {
469 self.state
470 }
471
472 #[must_use]
474 pub fn current_epoch(&self) -> u64 {
475 self.current_epoch
476 }
477
478 #[must_use]
480 pub fn buffered_rows(&self) -> usize {
481 self.buffered_rows
482 }
483
484 #[must_use]
486 pub fn buffered_bytes(&self) -> u64 {
487 self.buffered_bytes
488 }
489
490 #[must_use]
492 pub fn delta_version(&self) -> u64 {
493 self.delta_version
494 }
495
496 #[must_use]
498 pub fn sink_metrics(&self) -> &DeltaLakeSinkMetrics {
499 &self.metrics
500 }
501
502 #[must_use]
504 pub fn config(&self) -> &DeltaLakeSinkConfig {
505 &self.config
506 }
507
508 #[must_use]
510 pub fn should_flush(&self) -> bool {
511 if self.buffered_rows >= self.config.max_buffer_records {
512 return true;
513 }
514 if self.buffered_bytes >= self.config.target_file_size as u64 {
515 return true;
516 }
517 if let Some(start) = self.buffer_start_time {
518 if start.elapsed() >= self.config.max_buffer_duration {
519 return true;
520 }
521 }
522 false
523 }
524
525 const CHANGELOG_METADATA_COLUMNS: &'static [&'static str] =
530 &["_op", "_ts_ms", laminar_core::changelog::WEIGHT_COLUMN];
531
532 fn target_schema(batch_schema: &SchemaRef, write_mode: DeltaWriteMode) -> SchemaRef {
533 if write_mode == DeltaWriteMode::Upsert {
534 let fields: Vec<_> = batch_schema
535 .fields()
536 .iter()
537 .filter(|f| !Self::CHANGELOG_METADATA_COLUMNS.contains(&f.name().as_str()))
538 .cloned()
539 .collect();
540 Arc::new(arrow_schema::Schema::new(fields))
541 } else {
542 batch_schema.clone()
543 }
544 }
545
546 #[must_use]
548 pub fn estimate_batch_size(batch: &RecordBatch) -> u64 {
549 batch
550 .columns()
551 .iter()
552 .map(|col| col.get_array_memory_size() as u64)
553 .sum()
554 }
555
556 #[cfg(feature = "delta-lake")]
557 fn operation_deadline(&self) -> tokio::time::Instant {
558 tokio::time::Instant::now() + self.config.write_timeout
559 }
560
561 #[cfg(feature = "delta-lake")]
562 fn ensure_write_generation_usable(&self) -> Result<(), ConnectorError> {
563 if self.unresolved_delta_write {
564 return Err(ConnectorError::InvalidState {
565 expected: "a fresh Delta sink generation after reconciliation".into(),
566 actual: "a prior dispatched Delta operation lost its result".into(),
567 });
568 }
569 Ok(())
570 }
571
572 #[cfg(feature = "delta-lake")]
573 fn spawn_tracked_delta_task<F>(
574 &self,
575 task: F,
576 ) -> Result<tokio::task::JoinHandle<F::Output>, ConnectorError>
577 where
578 F: Future + Send + 'static,
579 F::Output: Send + 'static,
580 {
581 let guard = self.task_owner.track().ok_or(ConnectorError::Closed)?;
582 Ok(tokio::spawn(run_tracked_delta_task(guard, task)))
583 }
584
585 #[cfg(feature = "delta-lake")]
589 async fn reopen_table(&mut self) -> Result<(), ConnectorError> {
590 use super::delta_io;
591
592 let table = delta_io::open_or_create_table(
593 &self.resolved_table_path,
594 self.resolved_storage_options.clone(),
595 self.schema.as_ref(),
596 )
597 .await?;
598
599 self.delta_version = table
600 .version()
601 .and_then(|version| u64::try_from(version).ok())
602 .unwrap_or(0);
603 self.table = Some(table);
604 Ok(())
605 }
606
607 #[cfg(feature = "delta-lake")]
609 async fn attempt_delta_write(
610 table: DeltaTable,
611 batches: Vec<RecordBatch>,
612 write_mode: DeltaWriteMode,
613 merge_key_columns: Vec<String>,
614 partition_columns: Vec<String>,
615 schema_evolution: bool,
616 target_file_size: usize,
617 writer_properties: Option<deltalake::parquet::file::properties::WriterProperties>,
618 merge_session: Option<datafusion::prelude::SessionContext>,
619 ) -> Result<DeltaWriteTaskSuccess, super::delta_io::DeltaWriteAttemptError> {
620 if write_mode == DeltaWriteMode::Upsert {
621 let combined = if batches.len() == 1 {
626 batches.into_iter().next().expect("len == 1 checked")
627 } else {
628 match arrow_select::concat::concat_batches(&batches[0].schema(), &batches) {
629 Ok(c) => c,
630 Err(e) => {
631 return Err(ConnectorError::Internal(format!(
632 "failed to concat batches: {e}"
633 ))
634 .into());
635 }
636 }
637 };
638
639 let merge_session = merge_session
640 .as_ref()
641 .expect("merge_session built in init_delta_table for Upsert mode");
642
643 super::delta_io::merge_changelog(
644 table,
645 combined,
646 &merge_key_columns,
647 schema_evolution,
648 writer_properties,
649 merge_session,
650 )
651 .await
652 .map(|(table, merge_result)| DeltaWriteTaskSuccess {
653 table,
654 merge_result: Some(merge_result),
655 })
656 } else {
657 let save_mode = match write_mode {
659 DeltaWriteMode::Append => SaveMode::Append,
660 DeltaWriteMode::Overwrite => SaveMode::Overwrite,
661 DeltaWriteMode::Upsert => unreachable!("handled by the upsert branch above"),
662 };
663
664 let partition_cols = if partition_columns.is_empty() {
665 None
666 } else {
667 Some(partition_columns.as_slice())
668 };
669
670 super::delta_io::write_batches(
671 table,
672 batches,
673 save_mode,
674 partition_cols,
675 schema_evolution,
676 Some(target_file_size),
677 writer_properties,
678 )
679 .await
680 .map(|(table, _version)| DeltaWriteTaskSuccess {
681 table,
682 merge_result: None,
683 })
684 }
685 }
686
687 fn is_coordinated(&self) -> bool {
691 self.config.write_mode == DeltaWriteMode::Append
692 && self.config.delivery_guarantee == DeliveryGuarantee::ExactlyOnce
693 }
694
695 #[cfg(feature = "delta-lake")]
696 fn ensure_coordinated_reconciled(&self) -> Result<(), ConnectorError> {
697 if self.is_coordinated() && self.coordinated_unresolved_publication.lock().is_some() {
698 return Err(ConnectorError::TransactionError(
699 "Delta coordinated publication outcome is unresolved; reconcile or retry the exact cut before processing later work"
700 .into(),
701 ));
702 }
703 Ok(())
704 }
705
706 #[cfg(feature = "delta-lake")]
709 async fn materialize_staged_adds(
710 table: DeltaTable,
711 batches: Vec<RecordBatch>,
712 writer_properties: Option<deltalake::parquet::file::properties::WriterProperties>,
713 #[cfg(test)] stall: bool,
714 ) -> Result<Vec<deltalake::kernel::Add>, ConnectorError> {
715 use deltalake::writer::{DeltaWriter, RecordBatchWriter};
716
717 #[cfg(test)]
718 if stall {
719 std::future::pending::<()>().await;
720 }
721
722 let mut writer = RecordBatchWriter::for_table(&table)
723 .map_err(|e| ConnectorError::WriteError(format!("delta writer: {e}")))?;
724 if let Some(props) = writer_properties {
728 writer = writer.with_writer_properties(props);
729 }
730 for batch in batches {
731 writer
732 .write(batch)
733 .await
734 .map_err(|e| ConnectorError::WriteError(format!("delta write: {e}")))?;
735 }
736 let adds = writer
737 .flush()
738 .await
739 .map_err(|e| ConnectorError::WriteError(format!("delta flush: {e}")))?;
740 Ok(adds)
741 }
742
743 #[cfg(feature = "delta-lake")]
747 async fn materialize_coordinated_staged(
748 &mut self,
749 deadline: tokio::time::Instant,
750 ) -> Result<(), ConnectorError> {
751 if self.staged_batches.is_empty() {
752 return Ok(());
753 }
754 self.ensure_write_generation_usable()?;
755
756 let binding =
757 super::delta_io::coordinated_table_binding(self.table.as_ref().ok_or_else(|| {
758 ConnectorError::InvalidState {
759 expected: "open".into(),
760 actual: "delta table not loaded".into(),
761 }
762 })?)?;
763 if self
764 .coordinated_binding
765 .as_ref()
766 .is_some_and(|existing| existing != &binding)
767 {
768 return Err(ConnectorError::TransactionError(
769 "Delta table binding changed while materializing one coordinated checkpoint cut"
770 .into(),
771 ));
772 }
773
774 let table = self
775 .table
776 .clone()
777 .ok_or_else(|| ConnectorError::InvalidState {
778 expected: "open".into(),
779 actual: "delta table not loaded".into(),
780 })?;
781 let batches = self.staged_batches.clone();
782 let writer_properties = self.cached_writer_properties.clone();
783 #[cfg(test)]
784 let stall = self.stall_descriptor_write;
785 let task = self.spawn_tracked_delta_task(Self::materialize_staged_adds(
786 table,
787 batches,
788 writer_properties,
789 #[cfg(test)]
790 stall,
791 ))?;
792 let adds = match tokio::time::timeout_at(deadline, task).await {
793 Ok(Ok(result)) => result?,
794 Ok(Err(error)) => {
795 self.unresolved_delta_write = true;
796 return Err(ConnectorError::outcome_unknown(
797 format!("Delta descriptor writer task terminated unexpectedly: {error}"),
798 true,
799 ));
800 }
801 Err(_) => {
802 self.unresolved_delta_write = true;
803 return Err(ConnectorError::outcome_unknown(
804 format!(
805 "Delta descriptor materialization timed out at its {:?} end-to-end deadline",
806 self.config.write_timeout
807 ),
808 true,
809 ));
810 }
811 };
812 if adds.is_empty() {
813 self.staged_batches.clear();
814 self.staged_rows = 0;
815 self.staged_bytes = 0;
816 return Ok(());
817 }
818
819 let projected_add_count = self
820 .coordinated_adds
821 .len()
822 .checked_add(adds.len())
823 .ok_or_else(|| {
824 ConnectorError::WriteError("Delta coordinated Add count overflow".into())
825 })?;
826 if projected_add_count > super::delta_io::MAX_COORDINATED_ADD_ACTIONS {
827 return Err(ConnectorError::WriteError(format!(
828 "Delta coordinated checkpoint would exceed the fixed {} Add-action limit",
829 super::delta_io::MAX_COORDINATED_ADD_ACTIONS
830 )));
831 }
832
833 let chunk_add_array_bytes = super::delta_io::encoded_add_array_len(&adds)?;
836 let projected_descriptor_bytes = if self.coordinated_adds.is_empty() {
837 super::delta_io::encode_commit_descriptor(&binding, &adds)?.len()
838 } else {
839 self.coordinated_descriptor_bytes
840 .checked_add(chunk_add_array_bytes)
841 .and_then(|bytes| bytes.checked_sub(2))
842 .and_then(|bytes| bytes.checked_add(1))
843 .ok_or_else(|| {
844 ConnectorError::WriteError("Delta coordinated descriptor size overflow".into())
845 })?
846 };
847 if projected_descriptor_bytes > MAX_COORDINATED_COMMIT_PAYLOAD_BYTES {
848 return Err(ConnectorError::WriteError(format!(
849 "Delta coordinated descriptor would exceed the fixed {MAX_COORDINATED_COMMIT_PAYLOAD_BYTES} byte control-plane \
850 limit; the checkpoint cut produced too many files or partition values",
851 )));
852 }
853
854 self.coordinated_binding = Some(binding);
855 self.coordinated_adds.extend(adds);
856 self.coordinated_descriptor_bytes = projected_descriptor_bytes;
857 self.staged_batches.clear();
858 self.staged_rows = 0;
859 self.staged_bytes = 0;
860 Ok(())
861 }
862
863 #[cfg(feature = "delta-lake")]
866 async fn stage_coordinated_buffer(
867 &mut self,
868 deadline: tokio::time::Instant,
869 ) -> Result<(), ConnectorError> {
870 self.materialize_coordinated_staged(deadline).await?;
871 if self.buffer.is_empty() {
872 return Ok(());
873 }
874
875 self.staged_batches = std::mem::take(&mut self.buffer);
876 self.staged_rows = self.buffered_rows;
877 self.staged_bytes = self.buffered_bytes;
878 self.buffered_rows = 0;
879 self.buffered_bytes = 0;
880 self.buffer_start_time = None;
881 self.materialize_coordinated_staged(deadline).await
882 }
883
884 #[cfg(feature = "delta-lake")]
887 #[allow(clippy::too_many_lines)]
888 async fn flush_staged_to_delta(
889 &mut self,
890 deadline: tokio::time::Instant,
891 ) -> Result<WriteResult, ConnectorError> {
892 if self.staged_batches.is_empty() {
893 return Ok(WriteResult::new(0, 0));
894 }
895 self.ensure_write_generation_usable()?;
896 let write_timeout = self.config.write_timeout;
897
898 if self.config.write_mode == DeltaWriteMode::Upsert {
907 let combined = if self.staged_batches.len() == 1 {
908 self.staged_batches[0].clone()
909 } else {
910 let schema = self.staged_batches[0].schema();
911 arrow_select::concat::concat_batches(&schema, &self.staged_batches).map_err(
912 |e| ConnectorError::Internal(format!("failed to concat staged batches: {e}")),
913 )?
914 };
915 let rows_in = combined.num_rows() as u64;
916 let collapse_start = Instant::now();
917 let collapsed =
918 crate::changelog::collapse_changelog(&combined, &self.config.merge_key_columns)?;
919 let (upserts_out, deletes_out) = count_collapsed_ops(&collapsed);
920 self.metrics.observe_collapse(
921 rows_in,
922 upserts_out,
923 deletes_out,
924 collapse_start.elapsed().as_secs_f64(),
925 );
926 self.staged_batches.clear();
927 self.staged_batches.push(collapsed);
928 }
929
930 let total_rows = self.staged_rows;
931 let estimated_bytes = self.staged_bytes;
932 let flush_start = Instant::now();
933
934 if self.table.is_none() {
938 tokio::time::timeout_at(deadline, self.reopen_table())
939 .await
940 .map_err(|_| {
941 ConnectorError::ConnectionFailed(format!(
942 "Delta table reopen exceeded the {write_timeout:?} write deadline"
943 ))
944 })??;
945 }
946
947 let table = self
948 .table
949 .take()
950 .ok_or_else(|| ConnectorError::InvalidState {
951 expected: "table initialized".into(),
952 actual: "table not initialized".into(),
953 })?;
954
955 let write_task = self.spawn_tracked_delta_task(Self::attempt_delta_write(
956 table,
957 self.staged_batches.clone(),
958 self.config.write_mode,
959 self.config.merge_key_columns.clone(),
960 self.config.partition_columns.clone(),
961 self.config.schema_evolution,
962 self.config.target_file_size,
963 self.cached_writer_properties.clone(),
964 self.merge_session.clone(),
965 ))?;
966 let write = match tokio::time::timeout_at(deadline, write_task).await {
967 Ok(Ok(Ok(write))) => write,
968 Ok(Ok(Err(error))) => {
969 self.metrics
970 .observe_flush_duration(flush_start.elapsed().as_secs_f64());
971 let error = classify_delta_attempt_error(error);
972 if error.is_outcome_unknown() {
973 self.unresolved_delta_write = true;
974 }
975 return Err(error);
976 }
977 Ok(Err(error)) => {
978 self.unresolved_delta_write = true;
979 self.metrics
980 .observe_flush_duration(flush_start.elapsed().as_secs_f64());
981 return Err(ConnectorError::outcome_unknown(
982 format!("Delta writer task terminated unexpectedly: {error}"),
983 true,
984 ));
985 }
986 Err(_) => {
987 self.unresolved_delta_write = true;
988 self.metrics
989 .observe_flush_duration(flush_start.elapsed().as_secs_f64());
990 return Err(ConnectorError::outcome_unknown(
991 format!("Delta write exceeded its {write_timeout:?} end-to-end deadline"),
992 true,
993 ));
994 }
995 };
996 let table = write.table;
997 if let Some(result) = write.merge_result {
998 self.metrics.record_merge();
999 if result.rows_deleted > 0 {
1000 self.metrics.record_deletes(result.rows_deleted as u64);
1001 }
1002 }
1003
1004 self.delta_version = table
1005 .version()
1006 .and_then(|version| u64::try_from(version).ok())
1007 .unwrap_or(0);
1008 self.table = Some(table);
1009 self.staged_batches.clear();
1010 self.staged_rows = 0;
1011 self.staged_bytes = 0;
1012
1013 self.metrics
1014 .record_flush(total_rows as u64, estimated_bytes);
1015 self.metrics.record_commit(self.delta_version);
1016 self.metrics
1017 .observe_flush_duration(flush_start.elapsed().as_secs_f64());
1018
1019 debug!(
1020 rows = total_rows,
1021 bytes = estimated_bytes,
1022 delta_version = self.delta_version,
1023 "Delta Lake: committed staged data to Delta"
1024 );
1025
1026 Ok(WriteResult::new(total_rows, estimated_bytes))
1027 }
1028
1029 pub fn split_changelog_batch(
1042 batch: &RecordBatch,
1043 ) -> Result<(RecordBatch, RecordBatch), ConnectorError> {
1044 let op_idx = batch.schema().index_of("_op").map_err(|_| {
1045 ConnectorError::ConfigurationError(
1046 "upsert mode requires '_op' column in input schema".into(),
1047 )
1048 })?;
1049
1050 let op_array = batch
1051 .column(op_idx)
1052 .as_any()
1053 .downcast_ref::<arrow_array::StringArray>()
1054 .ok_or_else(|| {
1055 ConnectorError::ConfigurationError("'_op' column must be String (Utf8) type".into())
1056 })?;
1057
1058 let len = op_array.len();
1060 let mut insert_mask = Vec::with_capacity(len);
1061 let mut delete_mask = Vec::with_capacity(len);
1062
1063 for i in 0..len {
1064 if op_array.is_null(i) {
1065 insert_mask.push(false);
1066 delete_mask.push(false);
1067 continue;
1068 }
1069 match op_array.value(i) {
1070 "I" | "U" | "r" => {
1071 insert_mask.push(true);
1072 delete_mask.push(false);
1073 }
1074 "D" => {
1075 insert_mask.push(false);
1076 delete_mask.push(true);
1077 }
1078 _ => {
1079 insert_mask.push(false);
1080 delete_mask.push(false);
1081 }
1082 }
1083 }
1084
1085 let user_col_indices: Vec<usize> = batch
1087 .schema()
1088 .fields()
1089 .iter()
1090 .enumerate()
1091 .filter(|(_, f)| !f.name().starts_with('_'))
1092 .map(|(i, _)| i)
1093 .collect();
1094
1095 let insert_batch = filter_and_project(batch, insert_mask, &user_col_indices)?;
1096 let delete_batch = filter_and_project(batch, delete_mask, &user_col_indices)?;
1097
1098 Ok((insert_batch, delete_batch))
1099 }
1100}
1101
1102#[cfg(all(feature = "delta-lake", feature = "delta-lake-unity"))]
1106async fn ensure_uc_table_exists(
1107 config: &DeltaLakeSinkConfig,
1108 schema: Option<&SchemaRef>,
1109) -> Result<(), ConnectorError> {
1110 let super::delta_config::DeltaCatalogType::Unity {
1111 ref workspace_url,
1112 ref access_token,
1113 } = config.catalog_type
1114 else {
1115 return Ok(());
1116 };
1117
1118 let Some(ref storage_location) = config.catalog_storage_location else {
1119 return Ok(());
1120 };
1121
1122 let Some(arrow_schema) = schema else {
1123 warn!(
1124 "catalog.storage.location is set but no schema available — \
1125 skipping Unity Catalog auto-create"
1126 );
1127 return Ok(());
1128 };
1129
1130 let catalog = config.catalog_name.as_deref().unwrap_or_default();
1131 let schema_name = config.catalog_schema.as_deref().unwrap_or_default();
1132 let table_name = config
1133 .table_path
1134 .strip_prefix("uc://")
1135 .and_then(|s| s.rsplit('.').next())
1136 .unwrap_or(&config.table_path);
1137
1138 let columns = super::unity_catalog::arrow_to_uc_columns(arrow_schema);
1139 super::unity_catalog::create_uc_table(
1140 workspace_url,
1141 access_token,
1142 catalog,
1143 schema_name,
1144 table_name,
1145 storage_location,
1146 &columns,
1147 )
1148 .await
1149}
1150
1151#[async_trait]
1152impl SinkConnector for DeltaLakeSink {
1153 fn terminal_task_tracker(&self) -> Option<ConnectorTaskTracker> {
1154 Some(self.task_tracker.clone())
1155 }
1156
1157 fn contract(&self, config: &ConnectorConfig) -> Result<SinkContract, ConnectorError> {
1158 let cfg = if config.properties().is_empty() {
1159 self.config.clone()
1160 } else {
1161 DeltaLakeSinkConfig::from_config(config)?
1162 };
1163 #[cfg(feature = "delta-lake")]
1164 if cfg.delivery_guarantee == DeliveryGuarantee::ExactlyOnce {
1165 super::delta_io::validate_coordinated_storage_preflight(
1166 &cfg.table_path,
1167 &cfg.storage_options,
1168 )?;
1169 }
1170 if cfg.delivery_guarantee == DeliveryGuarantee::ExactlyOnce
1171 && cfg.write_mode != DeltaWriteMode::Append
1172 {
1173 return Err(ConnectorError::ConfigurationError(
1174 "Delta exactly-once requires coordinated append mode".into(),
1175 ));
1176 }
1177 let consistency = if cfg.delivery_guarantee == DeliveryGuarantee::ExactlyOnce
1178 && cfg.write_mode == DeltaWriteMode::Append
1179 {
1180 SinkConsistency::CheckpointCommittable
1184 } else {
1185 SinkConsistency::DurableAtLeastOnce
1186 };
1187 let target = cfg.table_path.to_ascii_lowercase();
1188 let shared_target = [
1189 "s3://", "s3a://", "gs://", "gcs://", "az://", "abfs://", "uc://",
1190 ]
1191 .iter()
1192 .any(|scheme| target.starts_with(scheme));
1193 let topology = if cfg.write_mode == DeltaWriteMode::Append && shared_target {
1194 SinkTopology::MultiWriter
1195 } else {
1196 SinkTopology::Singleton
1198 };
1199 let input_mode = if cfg.write_mode == DeltaWriteMode::Upsert {
1200 SinkInputMode::FullChangelog
1201 } else {
1202 SinkInputMode::AppendOnly
1203 };
1204 Ok(SinkContract::new(consistency, topology, input_mode))
1205 }
1206
1207 async fn open(&mut self, config: &ConnectorConfig) -> Result<(), ConnectorError> {
1208 self.state = ConnectorState::Initializing;
1209
1210 if !config.properties().is_empty() {
1212 self.config = DeltaLakeSinkConfig::from_config(config)?;
1213 }
1214 #[cfg(feature = "delta-lake")]
1215 if self.config.delivery_guarantee == DeliveryGuarantee::ExactlyOnce {
1216 super::delta_io::validate_coordinated_storage_preflight(
1217 &self.config.table_path,
1218 &self.config.storage_options,
1219 )?;
1220 }
1221 if self.config.delivery_guarantee == DeliveryGuarantee::ExactlyOnce
1222 && !self.is_coordinated()
1223 {
1224 self.state = ConnectorState::Failed;
1225 return Err(ConnectorError::ConfigurationError(
1226 "Delta exactly-once requires coordinated append mode".into(),
1227 ));
1228 }
1229
1230 info!(
1231 table_path = %self.config.table_path,
1232 mode = %self.config.write_mode,
1233 guarantee = %self.config.delivery_guarantee,
1234 "opening Delta Lake sink connector"
1235 );
1236
1237 #[cfg(feature = "delta-lake")]
1241 {
1242 let should_defer = matches!(
1243 self.config.catalog_type,
1244 super::delta_config::DeltaCatalogType::Unity { .. }
1245 ) && self.config.catalog_storage_location.is_some()
1246 && self.schema.is_none();
1247
1248 if should_defer {
1249 info!(
1250 "Unity Catalog auto-create configured but pipeline schema not yet \
1251 available — deferring Delta table init to first begin_epoch"
1252 );
1253 self.needs_deferred_delta_init = true;
1254 self.state = ConnectorState::Initializing;
1255 return Ok(());
1256 }
1257
1258 let deadline = self.operation_deadline();
1259 self.init_delta_table(deadline).await?;
1260
1261 if self.table.as_ref().is_some_and(|t| t.version().is_none()) && self.schema.is_none() {
1264 self.needs_deferred_delta_init = true;
1265 self.state = ConnectorState::Initializing;
1266 return Ok(());
1267 }
1268 }
1269
1270 #[cfg(not(feature = "delta-lake"))]
1271 {
1272 self.state = ConnectorState::Failed;
1273 return Err(ConnectorError::ConfigurationError(
1274 "Delta Lake sink requires the 'delta-lake' feature to be enabled. \
1275 Build with: cargo build --features delta-lake"
1276 .into(),
1277 ));
1278 }
1279
1280 #[cfg(feature = "delta-lake")]
1281 {
1282 self.state = ConnectorState::Running;
1283 info!("Delta Lake sink connector opened successfully");
1284 Ok(())
1285 }
1286 }
1287
1288 async fn write_batch(&mut self, batch: &RecordBatch) -> Result<WriteResult, ConnectorError> {
1289 if self.state != ConnectorState::Running && self.state != ConnectorState::Initializing {
1291 return Err(ConnectorError::InvalidState {
1292 expected: "Running".into(),
1293 actual: self.state.to_string(),
1294 });
1295 }
1296
1297 #[cfg(feature = "delta-lake")]
1298 {
1299 self.ensure_coordinated_reconciled()?;
1300 self.ensure_write_generation_usable()?;
1301 }
1302 #[cfg(feature = "delta-lake")]
1303 let deadline = self.operation_deadline();
1304
1305 if batch.num_rows() == 0 {
1306 return Ok(WriteResult::new(0, 0));
1307 }
1308
1309 if self.schema.is_none() {
1312 self.schema = Some(Self::target_schema(&batch.schema(), self.config.write_mode));
1313 }
1314
1315 #[cfg(feature = "delta-lake")]
1319 if self.needs_deferred_delta_init {
1320 info!("schema now available from first batch — completing deferred Delta table init");
1321 match self.init_delta_table(deadline).await {
1322 Ok(()) => {
1323 self.needs_deferred_delta_init = false;
1324 self.state = ConnectorState::Running;
1325 info!("Delta Lake sink connector opened successfully (deferred)");
1326 }
1327 Err(e) => {
1328 self.state = ConnectorState::Failed;
1329 return Err(e);
1330 }
1331 }
1332 }
1333
1334 let num_rows = batch.num_rows();
1335 let estimated_bytes = Self::estimate_batch_size(batch);
1336
1337 if !self.is_coordinated() {
1341 let pending_rows = self.buffered_rows + self.staged_rows + num_rows;
1342 let pending_bytes = self.buffered_bytes + self.staged_bytes + estimated_bytes;
1343 let row_cap = self
1344 .config
1345 .max_buffer_records
1346 .saturating_mul(4)
1347 .max(num_rows);
1348 let byte_cap = (self.config.target_file_size as u64)
1349 .saturating_mul(4)
1350 .max(estimated_bytes);
1351 if pending_rows > row_cap || pending_bytes > byte_cap {
1352 return Err(ConnectorError::WriteError(format!(
1353 "delta sink buffer full ({pending_rows} rows, \
1354 {pending_bytes} bytes pending; cap {row_cap} rows, \
1355 {byte_cap} bytes) — backpressure until next flush/commit"
1356 )));
1357 }
1358 }
1359
1360 if self.buffer_start_time.is_none() {
1362 self.buffer_start_time = Some(Instant::now());
1363 }
1364 self.buffer.push(batch.clone());
1365 self.buffered_rows += num_rows;
1366 self.buffered_bytes += estimated_bytes;
1367
1368 #[cfg(feature = "delta-lake")]
1369 if self.is_coordinated() && self.should_flush() {
1370 self.stage_coordinated_buffer(deadline).await?;
1371 } else if self.config.delivery_guarantee != DeliveryGuarantee::ExactlyOnce
1372 && self.should_flush()
1373 {
1374 if !self.staged_batches.is_empty() {
1375 self.flush_staged_to_delta(deadline).await?;
1376 }
1377 self.staged_batches = std::mem::take(&mut self.buffer);
1378 self.staged_rows = self.buffered_rows;
1379 self.staged_bytes = self.buffered_bytes;
1380 self.buffered_rows = 0;
1381 self.buffered_bytes = 0;
1382 self.buffer_start_time = None;
1383 self.flush_staged_to_delta(deadline).await?;
1384 }
1385
1386 Ok(WriteResult::new(0, 0))
1387 }
1388
1389 fn schema(&self) -> SchemaRef {
1390 self.schema
1391 .clone()
1392 .unwrap_or_else(|| Arc::new(arrow_schema::Schema::empty()))
1393 }
1394
1395 async fn begin_epoch(&mut self, epoch: u64) -> Result<(), ConnectorError> {
1396 #[cfg(feature = "delta-lake")]
1397 {
1398 self.ensure_coordinated_reconciled()?;
1399 self.ensure_write_generation_usable()?;
1400 }
1401 #[cfg(feature = "delta-lake")]
1402 let deadline = self.operation_deadline();
1403
1404 #[cfg(feature = "delta-lake")]
1406 if self.needs_deferred_delta_init {
1407 if self.schema.is_some() {
1412 info!("schema available — completing deferred Delta table init");
1413 match self.init_delta_table(deadline).await {
1414 Ok(()) => {
1415 self.needs_deferred_delta_init = false;
1416 self.state = ConnectorState::Running;
1417 info!("Delta Lake sink connector opened successfully (deferred)");
1418 }
1419 Err(e) => {
1420 self.state = ConnectorState::Failed;
1421 return Err(e);
1422 }
1423 }
1424 }
1425 }
1426
1427 #[cfg(feature = "delta-lake")]
1428 if self.is_coordinated()
1429 && (!self.buffer.is_empty()
1430 || !self.staged_batches.is_empty()
1431 || !self.coordinated_adds.is_empty())
1432 {
1433 return Err(ConnectorError::InvalidState {
1434 expected: "the previous coordinated epoch to be prepared or rolled back".into(),
1435 actual: "unresolved Delta staging data remains".into(),
1436 });
1437 }
1438
1439 self.current_epoch = epoch;
1440 self.buffer.clear();
1441 self.buffered_rows = 0;
1442 self.buffered_bytes = 0;
1443 self.buffer_start_time = None;
1444
1445 debug!(epoch, "Delta Lake: began epoch");
1446 Ok(())
1447 }
1448
1449 async fn pre_commit(&mut self, epoch: u64) -> Result<Option<Vec<u8>>, ConnectorError> {
1450 #[cfg(feature = "delta-lake")]
1454 if self.is_coordinated() {
1455 self.ensure_coordinated_reconciled()?;
1456 self.ensure_write_generation_usable()?;
1457 let deadline = self.operation_deadline();
1458 self.stage_coordinated_buffer(deadline).await?;
1459 if self.coordinated_adds.is_empty() {
1460 if self.coordinated_binding.is_some() {
1461 return Err(ConnectorError::Internal(
1462 "empty Delta coordinated cut retained a table binding".into(),
1463 ));
1464 }
1465 return Ok(None);
1466 }
1467
1468 let binding = self.coordinated_binding.as_ref().ok_or_else(|| {
1469 ConnectorError::Internal(
1470 "non-empty Delta coordinated cut has no table binding".into(),
1471 )
1472 })?;
1473 let descriptor =
1474 super::delta_io::encode_commit_descriptor(binding, &self.coordinated_adds)?;
1475 if descriptor.len() != self.coordinated_descriptor_bytes
1476 || descriptor.len() > MAX_COORDINATED_COMMIT_PAYLOAD_BYTES
1477 {
1478 return Err(ConnectorError::Internal(format!(
1479 "Delta coordinated descriptor accounting mismatch: tracked {}, encoded {}",
1480 self.coordinated_descriptor_bytes,
1481 descriptor.len()
1482 )));
1483 }
1484 self.coordinated_adds.clear();
1485 self.coordinated_binding = None;
1486 self.coordinated_descriptor_bytes = 0;
1487 return Ok(Some(descriptor));
1488 }
1489
1490 if !self.buffer.is_empty() {
1494 self.staged_batches = std::mem::take(&mut self.buffer);
1495 self.staged_rows = self.buffered_rows;
1496 self.staged_bytes = self.buffered_bytes;
1497 self.buffered_rows = 0;
1498 self.buffered_bytes = 0;
1499 self.buffer_start_time = None;
1500 }
1501
1502 debug!(epoch, "Delta Lake: pre-committed (batches staged)");
1503 Ok(None)
1504 }
1505
1506 async fn rollback_epoch(&mut self, epoch: u64) -> Result<(), ConnectorError> {
1507 self.buffer.clear();
1511 self.buffered_rows = 0;
1512 self.buffered_bytes = 0;
1513 self.buffer_start_time = None;
1514 self.staged_batches.clear();
1515 self.staged_rows = 0;
1516 self.staged_bytes = 0;
1517 #[cfg(feature = "delta-lake")]
1518 {
1519 self.coordinated_adds.clear();
1520 self.coordinated_binding = None;
1521 self.coordinated_descriptor_bytes = 0;
1522 }
1523
1524 self.metrics.record_rollback();
1525 warn!(epoch, "Delta Lake: rolled back epoch");
1526 Ok(())
1527 }
1528
1529 fn suggested_write_timeout(&self) -> Duration {
1530 self.config.write_timeout
1531 }
1532
1533 async fn flush(&mut self) -> Result<(), ConnectorError> {
1534 #[cfg(feature = "delta-lake")]
1535 {
1536 self.ensure_coordinated_reconciled()?;
1537 self.ensure_write_generation_usable()?;
1538 }
1539 #[cfg(feature = "delta-lake")]
1540 let deadline = self.operation_deadline();
1541
1542 #[cfg(feature = "delta-lake")]
1545 if self.config.delivery_guarantee != DeliveryGuarantee::ExactlyOnce {
1546 if !self.staged_batches.is_empty() {
1550 self.flush_staged_to_delta(deadline).await?;
1551 }
1552
1553 if !self.buffer.is_empty() {
1555 self.staged_batches = std::mem::take(&mut self.buffer);
1556 self.staged_rows = self.buffered_rows;
1557 self.staged_bytes = self.buffered_bytes;
1558 self.buffered_rows = 0;
1559 self.buffered_bytes = 0;
1560 self.buffer_start_time = None;
1561
1562 self.flush_staged_to_delta(deadline).await?;
1563 }
1564 return Ok(());
1565 }
1566
1567 #[cfg(feature = "delta-lake")]
1568 return self.stage_coordinated_buffer(deadline).await;
1569
1570 #[cfg(not(feature = "delta-lake"))]
1571 Ok(())
1572 }
1573
1574 async fn close(&mut self) -> Result<(), ConnectorError> {
1575 info!("closing Delta Lake sink connector");
1576
1577 #[cfg(feature = "delta-lake")]
1580 if self.config.delivery_guarantee == DeliveryGuarantee::ExactlyOnce
1581 || self.unresolved_delta_write
1582 {
1583 self.buffer.clear();
1584 self.staged_batches.clear();
1585 self.buffered_rows = 0;
1586 self.buffered_bytes = 0;
1587 self.staged_rows = 0;
1588 self.staged_bytes = 0;
1589 self.coordinated_adds.clear();
1590 self.coordinated_binding = None;
1591 self.coordinated_descriptor_bytes = 0;
1592 } else {
1593 self.flush().await?;
1594 }
1595
1596 #[cfg(feature = "delta-lake")]
1598 {
1599 self.table = None;
1600 }
1601
1602 self.state = ConnectorState::Closed;
1603
1604 info!(
1605 table_path = %self.config.table_path,
1606 delta_version = self.delta_version,
1607 "Delta Lake sink connector closed"
1608 );
1609
1610 Ok(())
1611 }
1612
1613 #[cfg(feature = "delta-lake")]
1614 fn as_coordinated_committer(&self) -> Option<&dyn crate::connector::CoordinatedCommitter> {
1615 self.is_coordinated()
1616 .then_some(self as &dyn crate::connector::CoordinatedCommitter)
1617 }
1618}
1619
1620#[cfg(feature = "delta-lake")]
1621#[async_trait]
1622impl crate::connector::CoordinatedCommitter for DeltaLakeSink {
1623 async fn commit_aggregated(
1624 &self,
1625 batch: crate::connector::CoordinatedCommitBatch,
1626 context: crate::connector::CoordinatedCommitContext,
1627 ) -> Result<(), ConnectorError> {
1628 let deadline = context.deadline();
1629 let publication_budget = context.remaining();
1630 if publication_budget.is_zero() {
1631 return Err(ConnectorError::TransactionError(
1632 "Delta coordinated publication deadline elapsed before I/O".into(),
1633 ));
1634 }
1635
1636 batch.validate_shape().map_err(|error| {
1637 ConnectorError::TransactionError(format!(
1638 "Delta coordinated batch validation failed: {error}"
1639 ))
1640 })?;
1641 let external_key = batch.namespace.external_key();
1642 let exact_batch_fingerprint = batch.exact_fingerprint();
1643 let target_cursor = crate::connector::CoordinatedCommitCursor {
1644 checkpoint_id: batch.target.checkpoint_id,
1645 fencing_token: batch.fencing_token,
1646 };
1647 let pending = UnresolvedDeltaPublication {
1648 external_key,
1649 target: target_cursor,
1650 exact_batch_fingerprint,
1651 };
1652 {
1653 let mut unresolved = self.coordinated_unresolved_publication.lock();
1654 if unresolved
1655 .as_ref()
1656 .is_some_and(|unresolved| unresolved != &pending)
1657 {
1658 return Err(ConnectorError::TransactionError(
1659 "Delta has a different unresolved coordinated publication; only that exact cut may be retried"
1660 .into(),
1661 ));
1662 }
1663 *unresolved = Some(pending.clone());
1664 }
1665
1666 let operation = publish_coordinated_delta_batch(
1667 self.resolved_table_path.clone(),
1668 self.resolved_storage_options.clone(),
1669 Arc::clone(&self.coordinated_unresolved_publication),
1670 pending.clone(),
1671 batch,
1672 deadline,
1673 publication_budget,
1674 );
1675 #[cfg(test)]
1678 let operation = {
1679 let delay = super::delta_io::DELAY_COORDINATED_CATALOG_COMMIT
1680 .try_with(Clone::clone)
1681 .ok();
1682 async move {
1683 if let Some(delay) = delay {
1684 super::delta_io::DELAY_COORDINATED_CATALOG_COMMIT
1685 .scope(delay, operation)
1686 .await
1687 } else {
1688 operation.await
1689 }
1690 }
1691 };
1692 let task = match self.spawn_tracked_delta_task(operation) {
1693 Ok(task) => task,
1694 Err(error) => {
1695 let mut unresolved = self.coordinated_unresolved_publication.lock();
1696 if unresolved.as_ref() == Some(&pending) {
1697 *unresolved = None;
1698 }
1699 return Err(error);
1700 }
1701 };
1702
1703 match tokio::time::timeout_at(deadline, task).await {
1704 Ok(Ok(result)) => result,
1705 Ok(Err(error)) => Err(ConnectorError::outcome_unknown(
1706 format!("Delta coordinated publication task terminated unexpectedly: {error}"),
1707 true,
1708 )),
1709 Err(_) => Err(ConnectorError::outcome_unknown(
1710 format!(
1711 "Delta coordinated publication exceeded its {publication_budget:?} remaining \
1712 budget; reconcile the exact cursor before replay"
1713 ),
1714 true,
1715 )),
1716 }
1717 }
1718
1719 async fn committed_cursor(
1720 &self,
1721 namespace: &crate::connector::CoordinatedCommitNamespace,
1722 ) -> Result<Option<crate::connector::CoordinatedCommitCursor>, ConnectorError> {
1723 let table = super::delta_io::open_or_create_table(
1724 &self.resolved_table_path,
1725 self.resolved_storage_options.clone(),
1726 None,
1727 )
1728 .await?;
1729 let external_key = namespace.external_key();
1730 let observed = super::delta_io::get_coordinated_cursor(&table, &external_key).await?;
1731 let mut unresolved = self.coordinated_unresolved_publication.lock();
1732 if unresolved.as_ref().is_some_and(|pending| {
1733 pending.external_key == external_key && pending.reconciled_by(observed)
1734 }) {
1735 *unresolved = None;
1736 }
1737 Ok(observed)
1738 }
1739}
1740
1741impl std::fmt::Debug for DeltaLakeSink {
1742 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1743 f.debug_struct("DeltaLakeSink")
1744 .field("state", &self.state)
1745 .field("table_path", &self.config.table_path)
1746 .field("mode", &self.config.write_mode)
1747 .field("guarantee", &self.config.delivery_guarantee)
1748 .field("current_epoch", &self.current_epoch)
1749 .field("buffered_rows", &self.buffered_rows)
1750 .field("delta_version", &self.delta_version)
1751 .finish_non_exhaustive()
1752 }
1753}
1754
1755fn filter_and_project(
1763 batch: &RecordBatch,
1764 mask: Vec<bool>,
1765 col_indices: &[usize],
1766) -> Result<RecordBatch, ConnectorError> {
1767 use arrow_array::BooleanArray;
1768 use arrow_select::filter::filter_record_batch;
1769
1770 let bool_array = BooleanArray::from(mask);
1771
1772 let projected = batch
1773 .project(col_indices)
1774 .map_err(|e| ConnectorError::Internal(format!("batch projection failed: {e}")))?;
1775
1776 filter_record_batch(&projected, &bool_array)
1777 .map_err(|e| ConnectorError::Internal(format!("arrow filter failed: {e}")))
1778}
1779
1780#[cfg(test)]
1781#[allow(clippy::cast_possible_wrap)]
1782#[allow(clippy::cast_precision_loss)]
1783#[allow(clippy::float_cmp)]
1784mod tests;