delta_funnel/orchestrator/session/registry/
source.rs1use std::fmt;
2
3use datafusion::arrow::datatypes::SchemaRef;
4
5use crate::{
6 DeltaFunnelError, DeltaProtocolReport, DeltaSourceConfig, DeltaTableProviderConfig,
7 PhaseTimingReport, RegisteredDeltaSource, register_delta_sources_with_scan_execution_options,
8 report::PhaseTimer,
9 table_formats::{load_delta_source_with_tracing, preflight_delta_protocol_with_tracing},
10};
11
12use super::super::{DeltaFunnelSession, LazyTable};
13
14const SOURCE_LOADING_PHASE: &str = "source_loading";
15const PROTOCOL_PREFLIGHT_PHASE: &str = "protocol_preflight";
16const DATAFUSION_REGISTRATION_PHASE: &str = "datafusion_registration";
17
18#[derive(Clone, PartialEq, Eq)]
20pub struct RegisteredSessionSource {
21 table: LazyTable,
22 source_uri: String,
23 snapshot_version: u64,
24 schema: SchemaRef,
25 protocol: DeltaProtocolReport,
26 phase_timings: Vec<PhaseTimingReport>,
27}
28
29impl RegisteredSessionSource {
30 pub(super) fn from_registered(
31 table: LazyTable,
32 registered: RegisteredDeltaSource,
33 phase_timings: Vec<PhaseTimingReport>,
34 ) -> Self {
35 Self {
36 table,
37 source_uri: registered.table_uri,
38 snapshot_version: registered.snapshot_version,
39 schema: registered.schema,
40 protocol: registered.protocol,
41 phase_timings,
42 }
43 }
44
45 #[must_use]
47 pub const fn table(&self) -> &LazyTable {
48 &self.table
49 }
50
51 #[must_use]
53 pub fn name(&self) -> &str {
54 self.table.name()
55 }
56
57 #[must_use]
59 pub fn source_uri(&self) -> &str {
60 &self.source_uri
61 }
62
63 #[must_use]
65 pub const fn snapshot_version(&self) -> u64 {
66 self.snapshot_version
67 }
68
69 #[must_use]
71 pub fn schema(&self) -> &SchemaRef {
72 &self.schema
73 }
74
75 #[must_use]
77 pub const fn protocol(&self) -> &DeltaProtocolReport {
78 &self.protocol
79 }
80
81 #[must_use]
83 pub fn phase_timings(&self) -> &[PhaseTimingReport] {
84 &self.phase_timings
85 }
86}
87
88impl fmt::Debug for RegisteredSessionSource {
89 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
90 formatter
91 .debug_struct("RegisteredSessionSource")
92 .field("table", &self.table)
93 .field("source_uri", &self.source_uri)
94 .field("snapshot_version", &self.snapshot_version)
95 .field("schema", &self.schema)
96 .field("protocol", &self.protocol)
97 .field("phase_timings", &self.phase_timings)
98 .finish()
99 }
100}
101
102impl DeltaFunnelSession {
103 pub fn delta_lake(&mut self, source: DeltaSourceConfig) -> Result<LazyTable, DeltaFunnelError> {
116 self.reject_registered_alias_name(&source.name)?;
117 let mut phase_timings = Vec::new();
118
119 let source_timer = PhaseTimer::start(SOURCE_LOADING_PHASE);
120 let planned = match load_delta_source_with_tracing(source) {
121 Ok(planned) => {
122 phase_timings.push(source_timer.completed());
123 planned
124 }
125 Err(error) => {
126 phase_timings.push(source_timer.failed());
127 return Err(error);
128 }
129 };
130
131 let preflight_timer = PhaseTimer::start(PROTOCOL_PREFLIGHT_PHASE);
132 let preflight = match preflight_delta_protocol_with_tracing(&planned) {
133 Ok(preflight) => {
134 phase_timings.push(preflight_timer.completed());
135 preflight
136 }
137 Err(error) => {
138 phase_timings.push(preflight_timer.failed());
139 return Err(error);
140 }
141 };
142
143 let registration_timer = PhaseTimer::start(DATAFUSION_REGISTRATION_PHASE);
144 let configs = vec![DeltaTableProviderConfig {
145 source: planned,
146 protocol: preflight,
147 scan_target_partitions: None,
148 }];
149 let registered = match register_delta_sources_with_scan_execution_options(
150 &self.context,
151 configs,
152 self.options.provider_scan_options(),
153 ) {
154 Ok(registered) => {
155 phase_timings.push(registration_timer.completed());
156 registered
157 }
158 Err(error) => {
159 phase_timings.push(registration_timer.failed());
160 return Err(error);
161 }
162 };
163 let registered =
164 registered
165 .sources
166 .into_iter()
167 .next()
168 .ok_or_else(|| DeltaFunnelError::Config {
169 message: "Delta source registration returned no registered source".to_owned(),
170 })?;
171 let table = self.allocate_delta_source_table(registered.name.clone());
172 let session_source =
173 RegisteredSessionSource::from_registered(table.clone(), registered, phase_timings);
174 self.sources.push(session_source);
175 Ok(table)
176 }
177
178 fn allocate_delta_source_table(&mut self, name: String) -> LazyTable {
179 let id = self.next_table_id;
180 self.next_table_id = self.next_table_id.saturating_add(1);
181 LazyTable::delta_source(id, name)
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use super::{DATAFUSION_REGISTRATION_PHASE, PROTOCOL_PREFLIGHT_PHASE, SOURCE_LOADING_PHASE};
188
189 use crate::{
190 DeltaFunnelError, DeltaProviderReaderBackend, DeltaProviderScanExecutionOptions,
191 DeltaSourceConfig, DeltaStorageOptions, QueryOptions,
192 };
193
194 use super::super::super::{
195 DeltaFunnelSession, LazyTableKind, SessionOptions, SourceUsageStatus,
196 test_support::DeltaLogTable,
197 };
198
199 const UNSUPPORTED_PROTOCOL_JSON: &str =
200 r#"{"protocol":{"minReaderVersion":99,"minWriterVersion":2}}"#;
201
202 #[test]
203 fn delta_lake_registers_source_and_returns_lazy_table() -> Result<(), Box<dyn std::error::Error>>
204 {
205 let table = DeltaLogTable::new("orders")?;
206 let mut session = DeltaFunnelSession::new(SessionOptions::default())?;
207
208 let lazy = session.delta_lake(DeltaSourceConfig::new("orders", table.uri()))?;
209
210 assert_eq!(lazy.id(), 0);
211 assert_eq!(lazy.kind(), LazyTableKind::DeltaSource);
212 assert_eq!(lazy.name(), "orders");
213 assert_eq!(session.next_table_id(), 1);
214 assert_eq!(session.sources().len(), 1);
215 let registered = session
216 .registered_source("ORDERS")
217 .ok_or("expected registered source")?;
218 assert_eq!(registered.table(), &lazy);
219 assert_eq!(registered.name(), "orders");
220 assert!(registered.source_uri().starts_with("file://"));
221 assert_eq!(registered.snapshot_version(), 1);
222 assert_eq!(registered.protocol().source_name, "orders");
223 assert_eq!(registered.schema().fields().len(), 2);
224 let source_reports = session.source_reports();
225 assert_eq!(source_reports.len(), 1);
226 let report = &source_reports[0];
227 assert_eq!(report.source_name(), "orders");
228 assert_eq!(report.source_uri(), registered.source_uri());
229 assert_eq!(report.snapshot_version(), 1);
230 assert_eq!(report.protocol().source_name, "orders");
231 assert_eq!(report.scheduling().query_target_partitions(), None);
232 assert_eq!(
233 report.scheduling().reader_backend(),
234 DeltaProviderReaderBackend::NativeAsync
235 );
236 assert_eq!(
237 report.scheduling().max_concurrent_file_reads_per_scan(),
238 None
239 );
240 assert_eq!(report.file_count(), crate::FileCount::unavailable());
241 assert_eq!(
242 report.file_count_reason(),
243 Some(crate::ReportReasonCode::CostAvoidance)
244 );
245 assert!(!report.scan_metadata_exhausted());
246 assert_eq!(report.usage_status(), SourceUsageStatus::Unknown);
247 assert!(report.used_by_output_names().is_empty());
248 assert!(report.provider_read_stats().is_none());
249 assert_eq!(
250 report.provider_stats_reason(),
251 Some(crate::ReportReasonCode::NotExecuted)
252 );
253 let phase_timings = report.phase_timings();
254 assert_eq!(
255 phase_timings
256 .iter()
257 .map(crate::PhaseTimingReport::phase_name)
258 .collect::<Vec<_>>(),
259 vec![
260 SOURCE_LOADING_PHASE,
261 PROTOCOL_PREFLIGHT_PHASE,
262 DATAFUSION_REGISTRATION_PHASE
263 ]
264 );
265 assert!(
266 phase_timings
267 .iter()
268 .all(|timing| timing.status().is_completed())
269 );
270 assert!(
271 phase_timings
272 .iter()
273 .all(|timing| timing.elapsed_micros().is_some())
274 );
275
276 Ok(())
277 }
278
279 #[test]
280 fn delta_lake_registers_multiple_distinct_sources() -> Result<(), Box<dyn std::error::Error>> {
281 let orders = DeltaLogTable::new("orders")?;
282 let customers = DeltaLogTable::new("customers")?;
283 let mut session = DeltaFunnelSession::new(SessionOptions::default())?;
284
285 let orders = session.delta_lake(DeltaSourceConfig::new("orders", orders.uri()))?;
286 let customers = session.delta_lake(DeltaSourceConfig::new("customers", customers.uri()))?;
287
288 assert_eq!(orders.id(), 0);
289 assert_eq!(customers.id(), 1);
290 assert_eq!(session.sources().len(), 2);
291 assert!(session.registered_source("orders").is_some());
292 assert!(session.registered_source("customers").is_some());
293 Ok(())
294 }
295
296 #[test]
297 fn duplicate_source_alias_fails_before_loading_second_source()
298 -> Result<(), Box<dyn std::error::Error>> {
299 let table = DeltaLogTable::new("orders")?;
300 let mut session = DeltaFunnelSession::new(SessionOptions::default())?;
301 session.delta_lake(DeltaSourceConfig::new("orders", table.uri()))?;
302
303 let error = session.delta_lake(DeltaSourceConfig::new("ORDERS", ""));
304
305 assert!(matches!(
306 error,
307 Err(DeltaFunnelError::DuplicateSourceName { name }) if name == "ORDERS"
308 ));
309 assert_eq!(session.sources().len(), 1);
310 assert_eq!(session.next_table_id(), 1);
311 Ok(())
312 }
313
314 #[test]
315 fn invalid_source_alias_fails_before_registration() -> Result<(), DeltaFunnelError> {
316 let mut session = DeltaFunnelSession::new(SessionOptions::default())?;
317
318 let error = session.delta_lake(DeltaSourceConfig::new("select", ""));
319
320 assert!(matches!(
321 error,
322 Err(DeltaFunnelError::InvalidSourceName { name, .. }) if name == "select"
323 ));
324 assert!(session.sources().is_empty());
325 assert_eq!(session.next_table_id(), 0);
326 Ok(())
327 }
328
329 #[test]
330 fn protocol_preflight_failure_does_not_register_source()
331 -> Result<(), Box<dyn std::error::Error>> {
332 let table = DeltaLogTable::new_with_protocol("unsupported", UNSUPPORTED_PROTOCOL_JSON)?;
333 let mut session = DeltaFunnelSession::new(SessionOptions::default())?;
334
335 let error = session.delta_lake(DeltaSourceConfig::new("unsupported", table.uri()));
336
337 let display = format!("{}", error.as_ref().err().ok_or("expected error")?);
338 assert!(display.contains("unsupported"));
339 assert!(display.contains("unsupported Delta minReaderVersion"));
340 assert!(matches!(
341 error,
342 Err(DeltaFunnelError::DeltaProtocolCompatibility { .. })
343 ));
344 assert!(session.sources().is_empty());
345 assert_eq!(session.next_table_id(), 0);
346 Ok(())
347 }
348
349 #[test]
350 fn protocol_preflight_failure_redacts_secret_uri_parts()
351 -> Result<(), Box<dyn std::error::Error>> {
352 let table = DeltaLogTable::new_with_protocol("unsupported", UNSUPPORTED_PROTOCOL_JSON)?;
353 let mut session = DeltaFunnelSession::new(SessionOptions::default())?;
354
355 let error = session
356 .delta_lake(DeltaSourceConfig::new(
357 "unsupported",
358 table.file_uri_with_secret_parts()?,
359 ))
360 .map(|_| ())
361 .map_err(|error| error.to_string());
362
363 assert!(
364 matches!(error, Err(display) if display.contains("unsupported")
365 && display.contains("unsupported Delta minReaderVersion")
366 && !display.contains("super-secret")
367 && !display.contains("debug-secret")
368 && !display.contains("token"))
369 );
370 assert!(session.sources().is_empty());
371 assert_eq!(session.next_table_id(), 0);
372 Ok(())
373 }
374
375 #[tokio::test]
376 async fn protocol_preflight_failure_does_not_leak_datafusion_alias()
377 -> Result<(), Box<dyn std::error::Error>> {
378 let table = DeltaLogTable::new_with_protocol("unsupported", UNSUPPORTED_PROTOCOL_JSON)?;
379 let mut session = DeltaFunnelSession::new(SessionOptions::default())?;
380
381 let error = session.delta_lake(DeltaSourceConfig::new("unsupported", table.uri()));
382
383 assert!(matches!(
384 error,
385 Err(DeltaFunnelError::DeltaProtocolCompatibility { .. })
386 ));
387 assert!(session.context().table("unsupported").await.is_err());
388 assert!(session.sources().is_empty());
389 assert_eq!(session.next_table_id(), 0);
390 Ok(())
391 }
392
393 #[tokio::test]
394 async fn registered_source_sql_analysis_does_not_read_data_files_for_rows()
395 -> Result<(), Box<dyn std::error::Error>> {
396 let table = DeltaLogTable::new("orders")?;
397 let mut session = DeltaFunnelSession::new(SessionOptions::default())?;
398
399 session.delta_lake(DeltaSourceConfig::new("orders", table.uri()))?;
400 let dataframe = session
401 .context()
402 .sql("select id, customer_name from orders")
403 .await?;
404 let schema = dataframe.schema();
405
406 assert_eq!(schema.fields().len(), 2);
407 assert_eq!(schema.field(0).name(), "id");
408 assert_eq!(schema.field(1).name(), "customer_name");
409 assert_eq!(session.sources().len(), 1);
410 Ok(())
411 }
412
413 #[test]
414 fn source_debug_does_not_expose_storage_option_values() -> Result<(), Box<dyn std::error::Error>>
415 {
416 let table = DeltaLogTable::new("storage-options")?;
417 let mut storage_options = DeltaStorageOptions::new();
418 storage_options.insert(
419 "AWS_SECRET_ACCESS_KEY".to_owned(),
420 "super-secret".to_owned(),
421 );
422 let mut session = DeltaFunnelSession::new(SessionOptions::default())?;
423
424 session.delta_lake(
425 DeltaSourceConfig::new("orders", table.uri()).with_storage_options(storage_options),
426 )?;
427
428 let debug = format!("{session:?}");
429 assert!(debug.contains("orders"));
430 assert!(!debug.contains("super-secret"));
431 assert!(!debug.contains("AWS_SECRET_ACCESS_KEY"));
432 let report_debug = format!("{:?}", session.source_reports());
433 assert!(report_debug.contains("orders"));
434 assert!(!report_debug.contains("super-secret"));
435 assert!(!report_debug.contains("AWS_SECRET_ACCESS_KEY"));
436 Ok(())
437 }
438
439 #[test]
440 fn source_registration_honors_configured_provider_options()
441 -> Result<(), Box<dyn std::error::Error>> {
442 let table = DeltaLogTable::new("configured-provider")?;
443 let provider_scan_options = DeltaProviderScanExecutionOptions::try_new_with_reader_backend(
444 DeltaProviderReaderBackend::OfficialKernel,
445 2,
446 1,
447 )?
448 .with_output_buffer_capacity_per_partition(3)?;
449 let mut session = DeltaFunnelSession::new(
450 SessionOptions::new()
451 .with_query_options(QueryOptions {
452 target_partitions: Some(4),
453 output_batch_size: None,
454 })
455 .with_provider_scan_options(provider_scan_options),
456 )?;
457
458 session.delta_lake(DeltaSourceConfig::new("orders", table.uri()))?;
459
460 assert_eq!(session.sources().len(), 1);
461 assert!(session.registered_source("orders").is_some());
462 let reports = session.source_reports();
463 assert_eq!(reports.len(), 1);
464 let scheduling = reports[0].scheduling();
465 assert_eq!(scheduling.query_target_partitions(), Some(4));
466 assert_eq!(
467 scheduling.reader_backend(),
468 DeltaProviderReaderBackend::OfficialKernel
469 );
470 assert_eq!(scheduling.max_concurrent_file_reads_per_scan(), Some(2));
471 assert_eq!(scheduling.max_concurrent_file_reads_per_partition(), 1);
472 assert_eq!(scheduling.output_buffer_capacity_per_partition(), 3);
473 assert_eq!(
474 scheduling.native_async_prefetch_file_count_per_partition(),
475 0
476 );
477 Ok(())
478 }
479}