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")]
67pub(crate) async fn snapshot_stream(
68 table: &mut DeltaTable,
69 version: i64,
70) -> Result<datafusion::physical_plan::SendableRecordBatchStream, ConnectorError> {
71 use datafusion::prelude::SessionContext;
72
73 let delta_version = to_delta_version(version)?;
74 table
75 .load_version(delta_version)
76 .await
77 .map_err(|e| ConnectorError::ReadError(format!("failed to load version {version}: {e}")))?;
78 debug!(version, "Delta Lake: loaded version for reading");
79
80 let provider =
81 table.table_provider().build().await.map_err(|e| {
82 ConnectorError::ReadError(format!("failed to build table provider: {e}"))
83 })?;
84 let ctx = SessionContext::new();
85 ctx.register_table("delta_source_scan", Arc::new(provider))
86 .map_err(|e| ConnectorError::ReadError(format!("failed to register scan table: {e}")))?;
87 ctx.sql("SELECT * FROM delta_source_scan")
88 .await
89 .map_err(|e| ConnectorError::ReadError(format!("scan query failed: {e}")))?
90 .execute_stream()
91 .await
92 .map_err(|e| ConnectorError::ReadError(format!("stream execution failed: {e}")))
93}
94
95#[cfg(all(feature = "delta-lake", test))]
97pub(crate) async fn read_batches_at_version(
98 table: &mut DeltaTable,
99 version: i64,
100 max_records: usize,
101) -> Result<(Vec<RecordBatch>, bool), ConnectorError> {
102 use tokio_stream::StreamExt;
103
104 let mut stream = snapshot_stream(table, version).await?;
105 let mut batches = Vec::new();
106 let mut total_rows: usize = 0;
107 while let Some(result) = stream.next().await {
108 let batch =
109 result.map_err(|e| ConnectorError::ReadError(format!("stream batch failed: {e}")))?;
110 if batch.num_rows() == 0 {
111 continue;
112 }
113 total_rows += batch.num_rows();
114 batches.push(batch);
115 if total_rows >= max_records {
116 break;
117 }
118 }
119 let fully_consumed = total_rows < max_records || stream.next().await.is_none();
120 Ok((batches, fully_consumed))
121}
122
123#[cfg(feature = "delta-lake")]
131pub fn map_cdf_to_changelog(batch: &RecordBatch) -> Result<RecordBatch, ConnectorError> {
132 use arrow_array::{Array, Int64Array, StringArray};
133 use laminar_core::changelog::WEIGHT_COLUMN;
134
135 let schema = batch.schema();
136 let ct_idx = schema
137 .index_of("_change_type")
138 .map_err(|_| ConnectorError::ReadError("CDF batch is missing _change_type".into()))?;
139 if let Some(field) = schema.fields().iter().find(|field| {
140 ["_op", "__op", WEIGHT_COLUMN]
141 .iter()
142 .any(|reserved| field.name().eq_ignore_ascii_case(reserved))
143 }) {
144 return Err(ConnectorError::ReadError(format!(
145 "CDF batch contains reserved mutation column '{}'",
146 field.name()
147 )));
148 }
149
150 let change_type = batch
151 .column(ct_idx)
152 .as_any()
153 .downcast_ref::<StringArray>()
154 .ok_or_else(|| ConnectorError::ReadError("_change_type is not Utf8".into()))?;
155
156 let mut weights = Vec::with_capacity(batch.num_rows());
157 for row in 0..batch.num_rows() {
158 if change_type.is_null(row) {
159 return Err(ConnectorError::ReadError(format!(
160 "CDF _change_type is null at row {row}"
161 )));
162 }
163 weights.push(match change_type.value(row) {
164 "insert" | "update_postimage" => 1,
165 "delete" | "update_preimage" => -1,
166 value => {
167 return Err(ConnectorError::ReadError(format!(
168 "unknown CDF _change_type '{value}' at row {row}"
169 )));
170 }
171 });
172 }
173
174 let cdf_meta = ["_change_type", "_commit_version", "_commit_timestamp"];
176 let mut fields = Vec::new();
177 let mut columns: Vec<Arc<dyn arrow_array::Array>> = Vec::new();
178 for (i, field) in schema.fields().iter().enumerate() {
179 if !cdf_meta.contains(&field.name().as_str()) {
180 fields.push(field.clone());
181 columns.push(batch.column(i).clone());
182 }
183 }
184 fields.push(Arc::new(arrow_schema::Field::new(
185 WEIGHT_COLUMN,
186 arrow_schema::DataType::Int64,
187 false,
188 )));
189 columns.push(Arc::new(Int64Array::from(weights)));
190
191 RecordBatch::try_new(
192 Arc::new(arrow_schema::Schema::new_with_metadata(
193 fields,
194 schema.metadata().clone(),
195 )),
196 columns,
197 )
198 .map_err(|e| ConnectorError::ReadError(format!("CDF batch rebuild: {e}")))
199}