1mod cursor;
9#[cfg(feature = "delta-lake")]
10mod reader;
11#[cfg(feature = "delta-lake")]
12mod reader_contract;
13
14use std::sync::Arc;
15
16#[cfg(feature = "delta-lake")]
17use arrow_array::{BinaryArray, RecordBatch, UInt32Array};
18use arrow_schema::SchemaRef;
19use async_trait::async_trait;
20#[cfg(feature = "delta-lake")]
21use std::time::Instant;
22use tracing::info;
23
24#[cfg(feature = "delta-lake")]
25use deltalake::DeltaTable;
26
27use crate::checkpoint::SourceCheckpoint;
28use crate::config::{ConnectorConfig, ConnectorState};
29use crate::connector::{
30 ConnectorTaskOwner, ConnectorTaskTracker, SourceBatch, SourceCheckpointUnavailablePolicy,
31 SourceConnector, SourceConsistency, SourceContract, SourceInputMode, SourcePosition,
32 SourceRowPositionCapability, SourceStart, SourceTopology,
33};
34use crate::error::ConnectorError;
35
36use super::delta_source_config::DeltaSourceConfig;
37#[cfg(feature = "delta-lake")]
38use super::delta_source_config::{DeltaReadMode, DeltaStartingVersion};
39use cursor::DeltaCursor;
40#[cfg(feature = "delta-lake")]
41use cursor::DeltaPhase;
42
43#[cfg(feature = "delta-lake")]
45const MAX_COMMITS_PER_SCAN: u64 = 64;
46
47const PARTIAL_UNIT_ERROR: &str = "[LDB-DELTA-PARTIAL-UNIT] Delta read failed after part of a \
48 commit or snapshot file was emitted; recovery from the last \
49 durable cursor is required";
50
51pub struct DeltaSource {
65 config: DeltaSourceConfig,
66 state: ConnectorState,
67 schema: Option<SchemaRef>,
69 #[cfg(feature = "delta-lake")]
70 schema_binding: Option<crate::schema::resolution::SchemaBinding>,
71 #[cfg(feature = "delta-lake")]
72 task_owner: ConnectorTaskOwner,
73 task_tracker: ConnectorTaskTracker,
74 cursor: Option<DeltaCursor>,
76 #[cfg(feature = "delta-lake")]
78 open_unit: Option<OpenUnit>,
79 #[cfg(feature = "delta-lake")]
80 reader: Option<Reader>,
81 records_read: u64,
82}
83
84#[cfg(feature = "delta-lake")]
85#[derive(Debug, Clone, Copy)]
86struct OpenUnit {
87 unit: reader::UnitKey,
88 rows_emitted: u32,
89}
90
91#[cfg(feature = "delta-lake")]
92struct Pending {
93 batch: RecordBatch,
94 unit: reader::UnitKey,
95 completes: Option<DeltaPhase>,
96 _permit: tokio::sync::OwnedSemaphorePermit,
97}
98
99#[cfg(feature = "delta-lake")]
101struct Reader {
102 table: DeltaTable,
104 resolved: super::delta_io::ResolvedCatalogTable,
106 context: reader::ReadContext,
107 scan: Option<reader::ScanTask>,
108 pending: Option<Pending>,
109 last_version_check: Option<Instant>,
110 known_latest: Option<u64>,
112}
113
114impl DeltaSource {
115 #[must_use]
117 pub fn new(config: DeltaSourceConfig, _registry: Option<&prometheus::Registry>) -> Self {
118 let (task_owner, task_tracker) = ConnectorTaskOwner::new();
119 #[cfg(not(feature = "delta-lake"))]
120 drop(task_owner);
121 Self {
122 config,
123 state: ConnectorState::Created,
124 schema: None,
125 #[cfg(feature = "delta-lake")]
126 schema_binding: None,
127 #[cfg(feature = "delta-lake")]
128 task_owner,
129 task_tracker,
130 cursor: None,
131 #[cfg(feature = "delta-lake")]
132 open_unit: None,
133 #[cfg(feature = "delta-lake")]
134 reader: None,
135 records_read: 0,
136 }
137 }
138
139 #[must_use]
141 pub fn state(&self) -> ConnectorState {
142 self.state
143 }
144
145 #[must_use]
147 pub fn config(&self) -> &DeltaSourceConfig {
148 &self.config
149 }
150}
151
152#[cfg(feature = "delta-lake")]
153impl DeltaSource {
154 async fn open(
155 &mut self,
156 config: &ConnectorConfig,
157 recovered: Option<DeltaCursor>,
158 ) -> Result<(), ConnectorError> {
159 use super::delta_io;
160
161 let resolved = self.resolve_table().await?;
162 let resolved_storage = crate::storage::StorageCredentialResolver::resolve(
163 &resolved.location,
164 &resolved.storage_options,
165 );
166 info!(
167 operation_class = "connector-open",
168 storage_provider = %resolved_storage.provider,
169 storage_endpoint_class = %resolved_storage.endpoint_class(),
170 storage_auth_source = %resolved_storage.auth_source,
171 read_mode = ?self.config.read_mode,
172 starting_version = ?self.config.starting_version,
173 resumed = recovered.is_some(),
174 "opening Delta Lake source connector"
175 );
176 let table =
177 delta_io::open_table_metadata(&resolved.location, resolved.storage_options.clone())
178 .await?;
179 self.install_reader_contract(config, &table)?;
180 let snapshot = table.snapshot().map_err(|error| {
181 delta_io::classify_delta_metadata_error("read Delta table metadata", &error)
182 })?;
183 let table_id = snapshot.metadata().id().to_string();
184 self.check_read_features(snapshot.metadata().configuration())?;
185 let latest = snapshot.version();
186 let cursor = match recovered {
187 Some(cursor) => self.validate_recovered(cursor, &table_id)?,
188 None => self.initial_cursor(table_id, latest)?,
189 };
190
191 let log_store = table.log_store();
192 let mut table_root = log_store.root_url().clone();
193 if !table_root.path().ends_with('/') {
194 table_root.set_path(&format!("{}/", table_root.path()));
195 }
196 let output_schema = self
197 .schema
198 .clone()
199 .ok_or_else(|| ConnectorError::InvalidState {
200 expected: "bound Delta reader schema".into(),
201 actual: "schema missing".into(),
202 })?;
203 let context = reader::ReadContext {
204 engine: log_store.engine(None),
205 table_root,
206 output_schema,
207 budget: Arc::new(tokio::sync::Semaphore::new(
208 reader::MAX_QUEUED_BYTES as usize,
209 )),
210 renew_at: delta_io::credentials_refresh_at(resolved.credentials_expire_at),
211 };
212 info!(
213 latest_version = latest,
214 cursor = ?cursor.phase,
215 "Delta Lake source: installed read position"
216 );
217 self.cursor = Some(cursor);
218 self.reader = Some(Reader {
219 table,
220 resolved,
221 context,
222 scan: None,
223 pending: None,
224 last_version_check: None,
225 known_latest: None,
226 });
227 Ok(())
228 }
229
230 async fn resolve_table(&self) -> Result<super::delta_io::ResolvedCatalogTable, ConnectorError> {
233 let mut resolved = super::delta_io::resolve_catalog_options(
234 &self.config.catalog_type,
235 self.config.catalog_database.as_deref(),
236 self.config.catalog_name.as_deref(),
237 self.config.catalog_schema.as_deref(),
238 &self.config.table_path,
239 &self.config.stable_storage_options(),
240 super::delta_io::CatalogAccess::Read,
241 )
242 .await?;
243 resolved.storage_options = crate::storage::StorageCredentialResolver::resolve(
244 &resolved.location,
245 &resolved.storage_options,
246 )
247 .options;
248 Ok(resolved)
249 }
250
251 async fn renew_credentials(&mut self) -> Result<(), ConnectorError> {
254 let resolved = self.resolve_table().await?;
255 let table_id = self.cursor()?.table_id.clone();
256 let reader = self.reader_mut()?;
257 if resolved.location != reader.resolved.location {
258 return Err(ConnectorError::ConfigurationError(
259 "the catalog moved the Delta table to another storage location; reset the source"
260 .into(),
261 ));
262 }
263 let table = super::delta_io::open_table_metadata(
264 &resolved.location,
265 resolved.storage_options.clone(),
266 )
267 .await?;
268 ensure_same_table(&table, &table_id)?;
269 reader.context.engine = table.log_store().engine(None);
270 reader.context.renew_at =
271 super::delta_io::credentials_refresh_at(resolved.credentials_expire_at);
272 reader.table = table;
273 reader.resolved = resolved;
274 info!("Delta Lake source: renewed vended storage credentials");
275 Ok(())
276 }
277
278 fn check_read_features(
281 &self,
282 configuration: &std::collections::HashMap<String, String>,
283 ) -> Result<(), ConnectorError> {
284 if self.config.read_mode == DeltaReadMode::Snapshot {
285 return Ok(());
286 }
287 let enabled = configuration
288 .get("delta.enableChangeDataFeed")
289 .is_some_and(|value| value.eq_ignore_ascii_case("true"));
290 if !enabled {
291 return Err(ConnectorError::ConfigurationError(
292 "Delta read.mode=changes requires the table property \
293 delta.enableChangeDataFeed=true; enable it or use read.mode=snapshot"
294 .into(),
295 ));
296 }
297 if configuration
298 .get("delta.columnMapping.mode")
299 .is_some_and(|mode| !mode.eq_ignore_ascii_case("none"))
300 {
301 return Err(ConnectorError::ConfigurationError(
302 "the Delta kernel change data feed does not support column-mapped tables; use \
303 read.mode=snapshot"
304 .into(),
305 ));
306 }
307 Ok(())
308 }
309
310 fn initial_cursor(&self, table_id: String, latest: u64) -> Result<DeltaCursor, ConnectorError> {
311 let after_latest = latest
312 .checked_add(1)
313 .ok_or_else(|| ConnectorError::ReadError("Delta version overflowed".into()))?;
314 let phase = match (self.config.read_mode, self.config.starting_version) {
315 (DeltaReadMode::Snapshot, _)
316 | (DeltaReadMode::Changes, DeltaStartingVersion::Snapshot) => DeltaPhase::Snapshot {
317 version: latest,
318 files_done: 0,
319 last_file: None,
320 },
321 (DeltaReadMode::Changes, DeltaStartingVersion::Latest) => DeltaPhase::Changes {
322 next_version: after_latest,
323 },
324 (DeltaReadMode::Changes, DeltaStartingVersion::Version(version)) => {
325 if version > after_latest {
326 return Err(ConnectorError::ConfigurationError(format!(
327 "starting.version {version} is beyond the table's next version \
328 {after_latest}"
329 )));
330 }
331 DeltaPhase::Changes {
332 next_version: version,
333 }
334 }
335 };
336 Ok(DeltaCursor { table_id, phase })
337 }
338
339 fn validate_recovered(
340 &self,
341 cursor: DeltaCursor,
342 table_id: &str,
343 ) -> Result<DeltaCursor, ConnectorError> {
344 if cursor.table_id != table_id {
345 return Err(ConnectorError::ConfigurationError(
346 "the Delta table was replaced since this checkpoint (its table id changed); \
347 the old cursor is not valid for the new table — reset the source"
348 .into(),
349 ));
350 }
351 let compatible = match (&cursor.phase, self.config.read_mode) {
352 (DeltaPhase::Snapshot { .. }, _)
353 | (DeltaPhase::Changes { .. }, DeltaReadMode::Changes)
354 | (DeltaPhase::Complete { .. }, DeltaReadMode::Snapshot) => true,
355 (DeltaPhase::Changes { .. }, DeltaReadMode::Snapshot)
356 | (DeltaPhase::Complete { .. }, DeltaReadMode::Changes) => false,
357 };
358 if !compatible {
359 return Err(ConnectorError::ConfigurationError(format!(
360 "checkpointed Delta position {:?} does not belong to read.mode={:?}",
361 cursor.phase, self.config.read_mode
362 )));
363 }
364 Ok(cursor)
365 }
366
367 fn after_snapshot(&self, version: u64) -> Result<DeltaPhase, ConnectorError> {
368 Ok(match self.config.read_mode {
369 DeltaReadMode::Snapshot => DeltaPhase::Complete { version },
370 DeltaReadMode::Changes => DeltaPhase::Changes {
371 next_version: version
372 .checked_add(1)
373 .ok_or_else(|| ConnectorError::ReadError("Delta version overflowed".into()))?,
374 },
375 })
376 }
377
378 fn reader_mut(&mut self) -> Result<&mut Reader, ConnectorError> {
379 self.reader
380 .as_mut()
381 .ok_or_else(|| ConnectorError::InvalidState {
382 expected: "started Delta source".into(),
383 actual: "reader not open".into(),
384 })
385 }
386
387 fn cursor(&self) -> Result<&DeltaCursor, ConnectorError> {
388 self.cursor
389 .as_ref()
390 .ok_or_else(|| ConnectorError::InvalidState {
391 expected: "installed Delta cursor".into(),
392 actual: "cursor missing".into(),
393 })
394 }
395
396 fn emit_pending(&mut self, max_records: usize) -> Result<Option<SourceBatch>, ConnectorError> {
399 let open_unit = self.open_unit;
400 let Some(pending) = self.reader_mut()?.pending.as_ref() else {
401 return Ok(None);
402 };
403 let first_row = match open_unit {
404 Some(open) if open.unit == pending.unit => open.rows_emitted,
405 Some(_) => {
406 return Err(ConnectorError::Internal(
407 "Delta reader started a replay unit before ending the previous one".into(),
408 ))
409 }
410 None => 0,
411 };
412 let rows = pending.batch.num_rows();
413 let take = rows.min(max_records);
414 let emitted = positioned(pending.batch.slice(0, take), pending.unit, first_row)?;
415 let rows_emitted = u32::try_from(take)
416 .ok()
417 .and_then(|take| first_row.checked_add(take))
418 .ok_or_else(row_position_overflow)?;
419 let Some(mut pending) = self.reader_mut()?.pending.take() else {
420 return Err(ConnectorError::Internal(
421 "Delta pending batch vanished during emission".into(),
422 ));
423 };
424
425 if take < rows {
426 pending.batch = pending.batch.slice(take, rows - take);
427 self.open_unit = Some(OpenUnit {
428 unit: pending.unit,
429 rows_emitted,
430 });
431 self.reader_mut()?.pending = Some(pending);
432 } else if let Some(phase) = pending.completes.take() {
433 self.complete_unit(phase)?;
434 } else {
435 self.open_unit = Some(OpenUnit {
436 unit: pending.unit,
437 rows_emitted,
438 });
439 }
440 self.records_read = self.records_read.saturating_add(take as u64);
441 Ok(Some(emitted.with_checkpoint(self.checkpoint())))
442 }
443
444 fn complete_unit(&mut self, phase: DeltaPhase) -> Result<(), ConnectorError> {
445 let cursor = self
446 .cursor
447 .as_mut()
448 .ok_or_else(|| ConnectorError::InvalidState {
449 expected: "installed Delta cursor".into(),
450 actual: "cursor missing".into(),
451 })?;
452 cursor.phase = phase;
453 self.open_unit = None;
454 Ok(())
455 }
456
457 async fn next_output(&mut self) -> Result<Option<reader::ScanOutput>, ConnectorError> {
460 let reader = self.reader_mut()?;
461 let Some(scan) = reader.scan.as_mut() else {
462 return Ok(None);
463 };
464 let received = scan.receiver.recv().await;
465 if !matches!(received, Some(Ok(_))) {
466 reader.scan = None;
467 }
468 match received {
469 Some(Ok(output)) => Ok(Some(output)),
470 Some(Err(error)) => Err(self.fail_read(error)),
471 None if self.open_unit.is_some() => Err(self.fail_read(ConnectorError::ReadError(
472 "Delta reader stopped inside a replay unit".into(),
473 ))),
474 None => Ok(None),
475 }
476 }
477
478 fn fail_read(&mut self, error: ConnectorError) -> ConnectorError {
479 if self.open_unit.is_none() {
480 return error;
481 }
482 self.state = ConnectorState::Failed;
483 ConnectorError::TransactionError(format!("{PARTIAL_UNIT_ERROR}: {error}"))
484 }
485
486 async fn start_next_scan(&mut self) -> Result<bool, ConnectorError> {
488 let phase = self.cursor()?.phase.clone();
489 if !matches!(phase, DeltaPhase::Complete { .. })
490 && super::delta_io::credentials_need_refresh(
491 self.reader_mut()?.resolved.credentials_expire_at,
492 )
493 {
494 self.renew_credentials().await?;
495 }
496 let guard = self.task_owner.track().ok_or(ConnectorError::Closed)?;
497 match phase {
498 DeltaPhase::Complete { .. } => Ok(false),
499 DeltaPhase::Snapshot {
500 version,
501 files_done,
502 last_file,
503 } => {
504 let after = self.after_snapshot(version)?;
505 let reader = self.reader_mut()?;
506 reader.scan = Some(reader::spawn_snapshot(
507 reader.context.clone(),
508 guard,
509 version,
510 files_done,
511 last_file,
512 after,
513 ));
514 Ok(true)
515 }
516 DeltaPhase::Changes { next_version } => {
517 let poll_interval = self.config.poll_interval;
518 let reader = self.reader_mut()?;
519 let backlog = reader
522 .known_latest
523 .is_some_and(|known| next_version <= known);
524 if !backlog
525 && reader
526 .last_version_check
527 .is_some_and(|checked| checked.elapsed() < poll_interval)
528 {
529 return Ok(false);
530 }
531 let latest = self.refresh_table(next_version).await?;
532 self.reader_mut()?.known_latest = Some(latest);
533 if latest < next_version {
534 return Ok(false);
535 }
536 let end = next_version
537 .saturating_add(MAX_COMMITS_PER_SCAN - 1)
538 .min(latest);
539 let reader = self.reader_mut()?;
540 reader.scan = Some(reader::spawn_changes(
541 reader.context.clone(),
542 guard,
543 next_version,
544 end,
545 ));
546 Ok(true)
547 }
548 }
549 }
550
551 async fn refresh_table(&mut self, next_version: u64) -> Result<u64, ConnectorError> {
554 use super::delta_io;
555
556 let table_id = self.cursor()?.table_id.clone();
557 let reader = self.reader_mut()?;
558 reader.last_version_check = Some(Instant::now());
559 let listed = delta_io::get_latest_version(&mut reader.table).await;
562 if let Ok(latest) = &listed {
563 let latest = delta_io::to_delta_version(*latest)?;
564 if latest < next_version {
565 return Ok(latest);
566 }
567 }
568 let fresh = delta_io::open_table_metadata(
571 &reader.resolved.location,
572 reader.resolved.storage_options.clone(),
573 )
574 .await?;
575 ensure_same_table(&fresh, &table_id)?;
576 listed?;
577 let latest = fresh.version().ok_or_else(|| {
578 ConnectorError::ReadError("opened Delta table has no committed version".into())
579 })?;
580 if latest
581 .checked_add(1)
582 .is_none_or(|after| after < next_version)
583 {
584 return Err(ConnectorError::ConfigurationError(format!(
585 "the Delta log regressed to version {latest} behind the cursor at {next_version}; \
586 the table was replaced or its history rewritten — reset the source"
587 )));
588 }
589 reader.table = fresh;
590 if latest < next_version {
591 return Ok(latest);
592 }
593 let current = super::schema_resolution::delta_read_schema(&delta_io::get_table_schema(
594 &reader.table,
595 )?);
596 let bound = self
597 .schema_binding
598 .as_ref()
599 .and_then(|binding| binding.external.as_ref());
600 if bound.is_some_and(|bound| bound != current.as_ref()) {
601 return Err(ConnectorError::SchemaMismatch(format!(
602 "the Delta table schema changed by version {latest}; the source binding is \
603 fixed — migrate the pipeline (DROP/CREATE) to read the new schema"
604 )));
605 }
606 Ok(latest)
607 }
608}
609
610#[cfg(feature = "delta-lake")]
611fn ensure_same_table(table: &DeltaTable, table_id: &str) -> Result<(), ConnectorError> {
612 let snapshot = table.snapshot().map_err(|error| {
613 super::delta_io::classify_delta_metadata_error("read Delta table metadata", &error)
614 })?;
615 if snapshot.metadata().id() != table_id {
616 return Err(ConnectorError::ConfigurationError(
617 "the Delta table was replaced at its location (its table id changed); reset the \
618 source to read the new table"
619 .into(),
620 ));
621 }
622 Ok(())
623}
624
625#[cfg(feature = "delta-lake")]
627fn row_position_overflow() -> ConnectorError {
628 ConnectorError::ConfigurationError(
629 "one Delta commit or snapshot file has more than u32::MAX rows, beyond the source's \
630 row-position range"
631 .into(),
632 )
633}
634
635#[cfg(feature = "delta-lake")]
638fn positioned(
639 batch: RecordBatch,
640 unit: reader::UnitKey,
641 first_row: u32,
642) -> Result<SourceBatch, ConnectorError> {
643 let rows = batch.num_rows();
644 let end = u32::try_from(rows)
645 .ok()
646 .and_then(|rows| first_row.checked_add(rows))
647 .ok_or_else(row_position_overflow)?;
648 let key = unit.order_key();
649 let positions = crate::connector::SourceRowPositions::try_new(
650 BinaryArray::from_iter_values(std::iter::repeat_n(&[][..], rows)),
651 BinaryArray::from_iter_values(std::iter::repeat_n(&key[..], rows)),
652 UInt32Array::from_iter_values(first_row..end),
653 )?;
654 SourceBatch::positioned(batch, positions)
655}
656
657#[async_trait]
658impl SourceConnector for DeltaSource {
659 fn terminal_task_tracker(&self) -> Option<ConnectorTaskTracker> {
660 Some(self.task_tracker.clone())
661 }
662
663 fn contract(&self, config: &ConnectorConfig) -> Result<SourceContract, ConnectorError> {
664 if config.properties().is_empty() {
665 self.config.validate()?;
666 } else {
667 DeltaSourceConfig::from_config(config)?.validate()?;
668 }
669 Ok(SourceContract::new(
672 SourceConsistency::Replayable,
673 SourceTopology::Singleton,
674 SourceInputMode::FullChangelog,
675 )
676 .with_row_positions(SourceRowPositionCapability::OrderedDeterministic))
677 }
678
679 async fn resolve_schema(
680 &mut self,
681 config: &ConnectorConfig,
682 explicit: Option<SchemaRef>,
683 ) -> Result<crate::schema::resolution::SchemaBinding, ConnectorError> {
684 #[cfg(feature = "delta-lake")]
685 {
686 super::schema_resolution::delta_source(config, explicit).await
687 }
688 #[cfg(not(feature = "delta-lake"))]
689 {
690 let _ = (config, explicit);
691 Err(ConnectorError::FeatureUnsupported(
692 "Delta schema discovery requires the delta-lake feature".into(),
693 ))
694 }
695 }
696
697 async fn start(&mut self, request: SourceStart) -> Result<(), ConnectorError> {
698 let (config, position, _) = request.into_parts();
699 let recovered = match &position {
700 SourcePosition::Initialized { .. } => {
701 return Err(ConnectorError::ConfigurationError(
702 "Delta Lake has no sealed topology startup contract".into(),
703 ));
704 }
705 SourcePosition::Initial => None,
706 SourcePosition::Resume { checkpoint, .. } => {
707 Some(DeltaCursor::from_checkpoint(checkpoint)?)
708 }
709 };
710 if !config.properties().is_empty() {
711 self.config = DeltaSourceConfig::from_config(&config)?;
712 }
713 self.config.validate()?;
714 self.state = ConnectorState::Initializing;
715
716 #[cfg(feature = "delta-lake")]
717 {
718 if let Err(error) = self.open(&config, recovered).await {
719 self.state = ConnectorState::Failed;
720 return Err(error);
721 }
722 self.state = ConnectorState::Running;
723 Ok(())
724 }
725
726 #[cfg(not(feature = "delta-lake"))]
727 {
728 let _ = (config, recovered);
729 self.state = ConnectorState::Failed;
730 Err(ConnectorError::ConfigurationError(
731 "Delta Lake source requires the 'delta-lake' feature to be enabled. \
732 Build with: cargo build --features delta-lake"
733 .into(),
734 ))
735 }
736 }
737
738 async fn poll_batch(
739 &mut self,
740 max_records: usize,
741 ) -> Result<Option<SourceBatch>, ConnectorError> {
742 if max_records == 0 {
743 return Err(ConnectorError::ConfigurationError(
744 "Delta source max_records must be greater than zero".into(),
745 ));
746 }
747 match self.state {
748 ConnectorState::Running => {}
749 ConnectorState::Failed if cfg!(feature = "delta-lake") => {
750 return Err(ConnectorError::TransactionError(PARTIAL_UNIT_ERROR.into()));
751 }
752 state => {
753 return Err(ConnectorError::InvalidState {
754 expected: "Running".into(),
755 actual: state.to_string(),
756 });
757 }
758 }
759
760 #[cfg(feature = "delta-lake")]
761 loop {
762 if let Some(batch) = self.emit_pending(max_records)? {
763 return Ok(Some(batch));
764 }
765 if let Some(output) = self.next_output().await? {
766 match output {
767 reader::ScanOutput::Batch {
768 batch,
769 unit,
770 permit,
771 completes,
772 } => {
773 self.reader_mut()?.pending = Some(Pending {
774 batch,
775 unit,
776 completes,
777 _permit: permit,
778 });
779 }
780 reader::ScanOutput::UnitEnd(phase) => {
781 if self.open_unit.is_some() {
782 return Err(ConnectorError::Internal(
783 "Delta reader ended a replay unit without its final rows".into(),
784 ));
785 }
786 self.complete_unit(phase)?;
787 }
788 }
789 continue;
790 }
791 if !self.start_next_scan().await? {
792 return Ok(None);
793 }
794 }
795
796 #[cfg(not(feature = "delta-lake"))]
797 Err(ConnectorError::FeatureUnsupported(
798 "Delta Lake source requires the 'delta-lake' feature".into(),
799 ))
800 }
801
802 fn schema(&self) -> SchemaRef {
803 self.schema
804 .clone()
805 .unwrap_or_else(|| Arc::new(arrow_schema::Schema::empty()))
806 }
807
808 fn checkpoint(&self) -> SourceCheckpoint {
809 self.cursor
810 .as_ref()
811 .map_or_else(SourceCheckpoint::new, DeltaCursor::to_checkpoint)
812 }
813
814 fn try_checkpoint(&self) -> Result<Option<SourceCheckpoint>, ConnectorError> {
815 #[cfg(feature = "delta-lake")]
816 if self.open_unit.is_some() {
817 return Ok(None);
818 }
819 Ok(Some(self.checkpoint()))
820 }
821
822 fn checkpoint_unavailable_policy(&self) -> SourceCheckpointUnavailablePolicy {
823 SourceCheckpointUnavailablePolicy::PollToReplayBoundary
824 }
825
826 async fn close(&mut self) -> Result<(), ConnectorError> {
827 info!("closing Delta Lake source connector");
828
829 #[cfg(feature = "delta-lake")]
830 if let Some(reader) = self.reader.take() {
831 reader.context.budget.close();
834 drop(reader);
835 self.open_unit = None;
836 }
837 self.state = ConnectorState::Closed;
838 info!(
839 records_read = self.records_read,
840 "Delta Lake source connector closed"
841 );
842 Ok(())
843 }
844}
845
846impl std::fmt::Debug for DeltaSource {
847 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
848 f.debug_struct("DeltaSource")
849 .field("state", &self.state)
850 .field("table_path", &"<configured>")
851 .field("cursor", &self.cursor)
852 .field("records_read", &self.records_read)
853 .finish_non_exhaustive()
854 }
855}
856
857#[cfg(test)]
858mod tests;