laminar_connectors/lakehouse/delta/
mod.rs1mod lifecycle;
25mod operations;
26mod publication;
27mod schema_binding;
28
29#[cfg(feature = "delta-lake")]
30use std::future::Future;
31use std::sync::Arc;
32use std::time::{Duration, Instant};
33
34use arrow_array::{Array, RecordBatch};
35#[cfg(feature = "delta-lake")]
36use arrow_schema::DataType;
37use arrow_schema::SchemaRef;
38use async_trait::async_trait;
39use tracing::{debug, info, warn};
40
41#[cfg(feature = "delta-lake")]
42use deltalake::DeltaTable;
43
44#[cfg(feature = "delta-lake")]
45use deltalake::protocol::SaveMode;
46
47use crate::config::{ConnectorConfig, ConnectorState};
48#[cfg(feature = "delta-lake")]
49use crate::connector::{ConnectorTaskGuard, MAX_COORDINATED_COMMIT_PAYLOAD_BYTES};
50use crate::connector::{
51 ConnectorTaskOwner, ConnectorTaskTracker, SinkConnector, SinkConsistency, SinkContract,
52 SinkInputMode, SinkTopology, WriteResult,
53};
54use crate::error::ConnectorError;
55use crate::storage::{StorageCredentialResolver, StorageProvider};
56
57use super::delta_config::{DeltaLakeSinkConfig, DeltaWriteMode};
58use super::delta_metrics::DeltaLakeSinkMetrics;
59use crate::connector::DeliveryGuarantee;
60
61#[cfg(feature = "delta-lake")]
62use publication::{
63 classify_delta_attempt_error, count_collapsed_ops, publish_coordinated_delta_batch,
64 retry_delta_metadata_until, run_tracked_delta_task, DeltaWriteTaskSuccess,
65 UnresolvedDeltaPublication,
66};
67
68pub struct DeltaLakeSink {
82 #[cfg(feature = "delta-lake")]
84 task_owner: ConnectorTaskOwner,
85 task_tracker: ConnectorTaskTracker,
87 config: DeltaLakeSinkConfig,
89 schema: Option<SchemaRef>,
91 query_schema: Option<SchemaRef>,
92 writer_projection: Option<Vec<usize>>,
93 #[cfg(feature = "delta-lake")]
94 schema_binding: Option<crate::schema::resolution::SchemaBinding>,
95 state: ConnectorState,
97 current_epoch: u64,
99 buffer: Vec<RecordBatch>,
101 buffered_rows: usize,
103 buffered_bytes: u64,
105 delta_version: u64,
107 buffer_start_time: Option<Instant>,
109 metrics: DeltaLakeSinkMetrics,
111 #[cfg(feature = "delta-lake")]
113 table: Option<DeltaTable>,
114 staged_batches: Vec<RecordBatch>,
118 staged_rows: usize,
120 staged_bytes: u64,
122 #[cfg(feature = "delta-lake")]
125 coordinated_adds: Vec<deltalake::kernel::Add>,
126 #[cfg(feature = "delta-lake")]
129 coordinated_binding: Option<super::commit_descriptor::DeltaTableBinding>,
130 #[cfg(feature = "delta-lake")]
133 coordinated_descriptor_bytes: usize,
134 #[cfg(feature = "delta-lake")]
138 coordinated_unresolved_publication: Arc<parking_lot::Mutex<Option<UnresolvedDeltaPublication>>>,
139 #[cfg(feature = "delta-lake")]
143 resolved_table_path: String,
144 #[cfg(feature = "delta-lake")]
146 resolved_storage_options: std::collections::HashMap<String, String>,
147 #[cfg(feature = "delta-lake")]
149 credentials_expire_at: Option<std::time::SystemTime>,
150 #[cfg(feature = "delta-lake")]
155 cached_writer_properties: Option<deltalake::parquet::file::properties::WriterProperties>,
156 #[cfg(feature = "delta-lake")]
161 merge_session: Option<datafusion::prelude::SessionContext>,
162 #[cfg(feature = "delta-lake")]
165 unresolved_delta_write: bool,
166 #[cfg(all(test, feature = "delta-lake"))]
169 stall_descriptor_write: bool,
170}
171
172#[cfg(all(feature = "delta-lake", feature = "delta-lake-unity"))]
176pub(super) async fn ensure_uc_table_exists(
177 config: &DeltaLakeSinkConfig,
178 schema: Option<&SchemaRef>,
179) -> Result<(), ConnectorError> {
180 let super::delta_config::DeltaCatalogType::Unity(ref unity) = config.catalog_type else {
181 return Ok(());
182 };
183 let super::delta_config::UnityAuth::Token(ref access_token) = unity.auth else {
184 return Err(ConnectorError::ConfigurationError(
185 "Unity Catalog table registration requires catalog.access_token".into(),
186 ));
187 };
188 let workspace_url = &unity.workspace_url;
189
190 let Some(ref storage_location) = config.catalog_storage_location else {
191 return Ok(());
192 };
193
194 let Some(arrow_schema) = schema else {
195 warn!(
196 "catalog.storage.location is set but no schema available — \
197 skipping Unity Catalog auto-create"
198 );
199 return Ok(());
200 };
201
202 let catalog = config.catalog_name.as_deref().unwrap_or_default();
203 let schema_name = config.catalog_schema.as_deref().unwrap_or_default();
204 let table_name = config
205 .table_path
206 .strip_prefix("uc://")
207 .and_then(|s| s.rsplit('.').next())
208 .unwrap_or(&config.table_path);
209
210 let columns =
211 super::unity_catalog::arrow_to_uc_columns(arrow_schema, &config.partition_columns)?;
212 super::unity_catalog::create_uc_table(
213 workspace_url,
214 access_token,
215 catalog,
216 schema_name,
217 table_name,
218 storage_location,
219 &columns,
220 )
221 .await
222}
223
224impl std::fmt::Debug for DeltaLakeSink {
225 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
226 f.debug_struct("DeltaLakeSink")
227 .field("state", &self.state)
228 .field("table_path", &"<configured>")
229 .field("mode", &self.config.write_mode)
230 .field("guarantee", &self.config.delivery_guarantee)
231 .field("current_epoch", &self.current_epoch)
232 .field("buffered_rows", &self.buffered_rows)
233 .field("delta_version", &self.delta_version)
234 .finish_non_exhaustive()
235 }
236}
237
238fn filter_and_project(
246 batch: &RecordBatch,
247 mask: Vec<bool>,
248 col_indices: &[usize],
249) -> Result<RecordBatch, ConnectorError> {
250 use arrow_array::BooleanArray;
251 use arrow_select::filter::filter_record_batch;
252
253 let bool_array = BooleanArray::from(mask);
254
255 let projected = batch
256 .project(col_indices)
257 .map_err(|e| ConnectorError::Internal(format!("batch projection failed: {e}")))?;
258
259 filter_record_batch(&projected, &bool_array)
260 .map_err(|e| ConnectorError::Internal(format!("arrow filter failed: {e}")))
261}
262
263#[cfg(test)]
264#[allow(clippy::cast_possible_wrap)]
265#[allow(clippy::cast_precision_loss)]
266#[allow(clippy::float_cmp)]
267mod tests;