laminar_connectors/lakehouse/delta_io/
read.rs1use super::{
4 debug, from_delta_version, to_delta_version, Arc, ConnectorError, DeltaTable, RecordBatch,
5 SchemaRef,
6};
7
8#[cfg(feature = "delta-lake")]
12#[must_use]
13pub fn get_partition_columns(table: &DeltaTable) -> Vec<String> {
14 match table.snapshot() {
15 Ok(snapshot) => snapshot.snapshot().metadata().partition_columns().to_vec(),
16 Err(_) => Vec::new(),
17 }
18}
19
20#[cfg(feature = "delta-lake")]
34pub fn get_table_schema(table: &DeltaTable) -> Result<SchemaRef, ConnectorError> {
35 let state = table
36 .snapshot()
37 .map_err(|e| ConnectorError::SchemaMismatch(format!("table has no snapshot: {e}")))?;
38
39 Ok(state.snapshot().arrow_schema())
41}
42
43#[cfg(feature = "delta-lake")]
49pub async fn get_latest_version(table: &mut DeltaTable) -> Result<i64, ConnectorError> {
50 let log_store = table.log_store();
51 let current = table.version().unwrap_or(0);
52 let version = log_store
53 .get_latest_version(current)
54 .await
55 .map_err(|e| ConnectorError::ReadError(format!("failed to get latest version: {e}")))?;
56 from_delta_version(version)
57}
58
59#[cfg(feature = "delta-lake")]
79pub(crate) async fn read_batches_at_version(
80 table: &mut DeltaTable,
81 version: i64,
82 max_records: usize,
83) -> Result<(Vec<RecordBatch>, bool), ConnectorError> {
84 use datafusion::prelude::SessionContext;
85 use tokio_stream::StreamExt;
86
87 let delta_version = to_delta_version(version)?;
89 table
90 .load_version(delta_version)
91 .await
92 .map_err(|e| ConnectorError::ReadError(format!("failed to load version {version}: {e}")))?;
93
94 debug!(version, "Delta Lake: loaded version for reading");
95
96 let provider =
98 table.table_provider().build().await.map_err(|e| {
99 ConnectorError::ReadError(format!("failed to build table provider: {e}"))
100 })?;
101
102 let ctx = SessionContext::new();
103 ctx.register_table("delta_source_scan", Arc::new(provider))
104 .map_err(|e| ConnectorError::ReadError(format!("failed to register scan table: {e}")))?;
105
106 let df = ctx
108 .sql("SELECT * FROM delta_source_scan")
109 .await
110 .map_err(|e| ConnectorError::ReadError(format!("scan query failed: {e}")))?;
111
112 let df = if max_records < usize::MAX {
113 df.limit(0, Some(max_records))
114 .map_err(|e| ConnectorError::ReadError(format!("limit failed: {e}")))?
115 } else {
116 df
117 };
118
119 let mut stream = df
121 .execute_stream()
122 .await
123 .map_err(|e| ConnectorError::ReadError(format!("stream execution failed: {e}")))?;
124
125 let mut batches = Vec::new();
126 let mut total_rows: usize = 0;
127
128 while let Some(result) = stream.next().await {
129 let batch =
130 result.map_err(|e| ConnectorError::ReadError(format!("stream batch failed: {e}")))?;
131 if batch.num_rows() == 0 {
132 continue;
133 }
134 total_rows += batch.num_rows();
135 batches.push(batch);
136
137 if total_rows >= max_records {
139 break;
140 }
141 }
142
143 let fully_consumed = if total_rows >= max_records {
147 stream.next().await.is_none()
148 } else {
149 true
150 };
151
152 debug!(
153 version,
154 num_batches = batches.len(),
155 total_rows,
156 fully_consumed,
157 "Delta Lake: scanned version"
158 );
159
160 Ok((batches, fully_consumed))
161}
162
163#[cfg(feature = "delta-lake")]
174pub async fn read_cdf_batches(
175 table: DeltaTable,
176 start_version: i64,
177 end_version: i64,
178 max_rows: usize,
179 max_bytes: usize,
180) -> Result<Vec<RecordBatch>, ConnectorError> {
181 use datafusion::prelude::SessionContext;
182 use tokio_stream::StreamExt;
183
184 debug!(start_version, end_version, "reading CDF batches");
185
186 let ctx = SessionContext::new();
187
188 let session_state = ctx.state();
190
191 let start_delta_version = to_delta_version(start_version)?;
192 let end_delta_version = to_delta_version(end_version)?;
193 let cdf_builder = table
194 .scan_cdf()
195 .with_starting_version(start_delta_version)
196 .with_ending_version(end_delta_version);
197
198 let plan = cdf_builder
199 .build(&session_state, None)
200 .await
201 .map_err(map_cdf_scan_build_error)?;
202
203 let task_ctx = ctx.task_ctx();
205 let mut stream = datafusion::physical_plan::execute_stream(plan, task_ctx)
206 .map_err(|e| ConnectorError::ReadError(format!("CDF stream execution failed: {e}")))?;
207
208 let mut batches = Vec::new();
209 let mut rows = 0;
210 let mut bytes = 0;
211 while let Some(result) = stream.next().await {
212 let batch: RecordBatch = result
213 .map_err(|e| ConnectorError::ReadError(format!("CDF stream batch failed: {e}")))?;
214 if batch.num_rows() > 0 {
215 (rows, bytes) = checked_cdf_commit_usage(rows, bytes, &batch, max_rows, max_bytes)?;
216 batches.push(batch);
217 }
218 }
219
220 debug!(
221 start_version,
222 end_version,
223 num_batches = batches.len(),
224 rows,
225 bytes,
226 "CDF scan complete"
227 );
228
229 Ok(batches)
230}
231
232#[cfg(feature = "delta-lake")]
233pub(super) fn map_cdf_scan_build_error(error: deltalake::DeltaTableError) -> ConnectorError {
234 match error {
235 error @ (deltalake::DeltaTableError::ChangeDataNotEnabled { .. }
236 | deltalake::DeltaTableError::ChangeDataNotRecorded { .. }) => {
237 ConnectorError::ConfigurationError(format!(
238 "Delta CDF is unavailable for the requested history: {error}"
239 ))
240 }
241 error => ConnectorError::ReadError(format!("CDF scan build failed: {error}")),
242 }
243}
244
245#[cfg(feature = "delta-lake")]
246pub(super) fn checked_cdf_commit_usage(
247 rows: usize,
248 bytes: usize,
249 batch: &RecordBatch,
250 max_rows: usize,
251 max_bytes: usize,
252) -> Result<(usize, usize), ConnectorError> {
253 let rows = rows.checked_add(batch.num_rows()).ok_or_else(|| {
254 ConnectorError::ConfigurationError("Delta CDF commit row count overflowed".into())
255 })?;
256 let bytes = bytes
257 .checked_add(batch.get_array_memory_size())
258 .ok_or_else(|| {
259 ConnectorError::ConfigurationError("Delta CDF commit byte count overflowed".into())
260 })?;
261 if rows > max_rows {
262 return Err(ConnectorError::ConfigurationError(format!(
263 "Delta CDF commit exceeds the hard row limit: rows={rows}, limit={max_rows}"
264 )));
265 }
266 if bytes > max_bytes {
267 return Err(ConnectorError::ConfigurationError(format!(
268 "Delta CDF commit exceeds the hard byte limit: bytes={bytes}, limit={max_bytes}"
269 )));
270 }
271 Ok((rows, bytes))
272}
273
274#[cfg(feature = "delta-lake")]
282pub fn map_cdf_to_changelog(batch: &RecordBatch) -> Result<RecordBatch, ConnectorError> {
283 use arrow_array::{Array, Int64Array, StringArray};
284 use laminar_core::changelog::WEIGHT_COLUMN;
285
286 let schema = batch.schema();
287 let ct_idx = schema
288 .index_of("_change_type")
289 .map_err(|_| ConnectorError::ReadError("CDF batch is missing _change_type".into()))?;
290 if let Some(field) = schema.fields().iter().find(|field| {
291 ["_op", "__op", WEIGHT_COLUMN]
292 .iter()
293 .any(|reserved| field.name().eq_ignore_ascii_case(reserved))
294 }) {
295 return Err(ConnectorError::ReadError(format!(
296 "CDF batch contains reserved mutation column '{}'",
297 field.name()
298 )));
299 }
300
301 let change_type = batch
302 .column(ct_idx)
303 .as_any()
304 .downcast_ref::<StringArray>()
305 .ok_or_else(|| ConnectorError::ReadError("_change_type is not Utf8".into()))?;
306
307 let mut weights = Vec::with_capacity(batch.num_rows());
308 for row in 0..batch.num_rows() {
309 if change_type.is_null(row) {
310 return Err(ConnectorError::ReadError(format!(
311 "CDF _change_type is null at row {row}"
312 )));
313 }
314 weights.push(match change_type.value(row) {
315 "insert" | "update_postimage" => 1,
316 "delete" | "update_preimage" => -1,
317 value => {
318 return Err(ConnectorError::ReadError(format!(
319 "unknown CDF _change_type '{value}' at row {row}"
320 )));
321 }
322 });
323 }
324
325 let cdf_meta = ["_change_type", "_commit_version", "_commit_timestamp"];
327 let mut fields = Vec::new();
328 let mut columns: Vec<Arc<dyn arrow_array::Array>> = Vec::new();
329 for (i, field) in schema.fields().iter().enumerate() {
330 if !cdf_meta.contains(&field.name().as_str()) {
331 fields.push(field.clone());
332 columns.push(batch.column(i).clone());
333 }
334 }
335 fields.push(Arc::new(arrow_schema::Field::new(
336 WEIGHT_COLUMN,
337 arrow_schema::DataType::Int64,
338 false,
339 )));
340 columns.push(Arc::new(Int64Array::from(weights)));
341
342 RecordBatch::try_new(
343 Arc::new(arrow_schema::Schema::new_with_metadata(
344 fields,
345 schema.metadata().clone(),
346 )),
347 columns,
348 )
349 .map_err(|e| ConnectorError::ReadError(format!("CDF batch rebuild: {e}")))
350}