laminar_connectors/lakehouse/delta_reference/
mod.rs1use arrow_array::RecordBatch;
4use arrow_schema::SchemaRef;
5use deltalake::DeltaTable;
6use tracing::info;
7
8use crate::config::ConnectorConfig;
9use crate::error::ConnectorError;
10use crate::lakehouse::delta_source_config::DeltaSourceConfig;
11use crate::reference::ReferenceTableSource;
12
13use super::snapshot_schema::{conform_snapshot_batch, validate_snapshot_schema};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16enum Phase {
17 Ready,
18 Draining,
19 Done,
20 Closed,
21}
22
23pub struct DeltaReferenceTableSource {
25 config: DeltaSourceConfig,
26 declared_schema: SchemaRef,
27 phase: Phase,
28 table: Option<DeltaTable>,
29 stream: Option<datafusion::physical_plan::SendableRecordBatchStream>,
30 emitted_rows: usize,
31 binding: Option<crate::schema::resolution::SchemaBinding>,
32}
33
34impl DeltaReferenceTableSource {
35 #[must_use]
37 pub fn from_source_config(config: DeltaSourceConfig, declared_schema: SchemaRef) -> Self {
38 Self {
39 config,
40 declared_schema,
41 phase: Phase::Ready,
42 table: None,
43 stream: None,
44 emitted_rows: 0,
45 binding: None,
46 }
47 }
48
49 pub fn from_connector_config(
55 config: &ConnectorConfig,
56 declared_schema: SchemaRef,
57 ) -> Result<Self, ConnectorError> {
58 let mut source =
59 Self::from_source_config(DeltaSourceConfig::from_config(config)?, declared_schema);
60 source.binding = config.schema_binding().cloned();
61 Ok(source)
62 }
63
64 async fn open_table(&mut self) -> Result<(), ConnectorError> {
65 use crate::lakehouse::delta_io;
66
67 let resolved = delta_io::resolve_catalog_options(
68 &self.config.catalog_type,
69 self.config.catalog_database.as_deref(),
70 self.config.catalog_name.as_deref(),
71 self.config.catalog_schema.as_deref(),
72 &self.config.table_path,
73 &self.config.storage_options,
74 delta_io::CatalogAccess::Read,
75 )
76 .await?;
77 self.table = Some(
78 delta_io::open_or_create_table(&resolved.location, resolved.storage_options, None)
79 .await?,
80 );
81 if let Some(table) = &self.table {
82 super::schema_resolution::verify_identity(
83 self.binding.as_ref(),
84 &super::schema_resolution::delta_native(table)?,
85 )?;
86 }
87 Ok(())
88 }
89
90 async fn load_snapshot(&mut self) -> Result<(), ConnectorError> {
91 use crate::lakehouse::delta_io;
92
93 let table = self
94 .table
95 .as_mut()
96 .ok_or_else(|| ConnectorError::Internal("Delta table is not open".into()))?;
97 let version = delta_io::get_latest_version(table).await?;
98 let table_schema = delta_io::get_table_schema(table).map_err(|error| {
99 ConnectorError::ReadError(format!("read Delta snapshot schema: {error}"))
100 })?;
101 validate_snapshot_schema(table_schema.as_ref(), self.declared_schema.as_ref())?;
102 self.stream = Some(delta_io::snapshot_stream(table, version).await?);
103 info!(version, "Delta reference snapshot streaming");
104 Ok(())
105 }
106}
107
108#[async_trait::async_trait]
109impl ReferenceTableSource for DeltaReferenceTableSource {
110 async fn resolve_schema(
111 &mut self,
112 config: &ConnectorConfig,
113 explicit: Option<SchemaRef>,
114 ) -> Result<crate::schema::resolution::SchemaBinding, ConnectorError> {
115 super::schema_resolution::delta_reference(config, explicit).await
116 }
117
118 async fn poll_snapshot(&mut self) -> Result<Option<RecordBatch>, ConnectorError> {
119 match self.phase {
120 Phase::Closed => {
121 return Err(ConnectorError::InvalidState {
122 expected: "open reference snapshot source".into(),
123 actual: "closed".into(),
124 });
125 }
126 Phase::Done => return Ok(None),
127 Phase::Ready => {
128 self.open_table().await?;
129 self.load_snapshot().await?;
130 self.phase = Phase::Draining;
131 }
132 Phase::Draining => {}
133 }
134
135 let stream = self
136 .stream
137 .as_mut()
138 .ok_or_else(|| ConnectorError::Internal("Delta snapshot stream is not open".into()))?;
139 while let Some(batch) = tokio_stream::StreamExt::next(stream).await {
140 let batch = batch.map_err(|error| {
141 ConnectorError::ReadError(format!("Delta snapshot batch failed: {error}"))
142 })?;
143 if batch.num_rows() == 0 {
144 continue;
145 }
146 self.emitted_rows = self.emitted_rows.saturating_add(batch.num_rows());
147 return conform_snapshot_batch(&batch, &self.declared_schema).map(Some);
148 }
149 info!(rows = self.emitted_rows, "Delta reference snapshot loaded");
150 self.stream = None;
151 self.phase = Phase::Done;
152 Ok(None)
153 }
154
155 async fn close(&mut self) -> Result<(), ConnectorError> {
156 self.phase = Phase::Closed;
157 self.stream = None;
158 self.table = None;
159 Ok(())
160 }
161}
162
163#[cfg(test)]
164mod tests;