1use std::{fmt, sync::Arc};
2
3use datafusion::{
4 arrow::datatypes::SchemaRef,
5 datasource::TableProvider,
6 prelude::{DataFrame, SQLOptions},
7};
8
9use crate::{
10 DeltaFunnelError, PhaseTimingReport, SqlTablePhase, report::PhaseTimer,
11 table_formats::validate_table_source_names,
12};
13
14use super::super::{
15 DeltaFunnelSession, LazyTable, LazyTableKind,
16 errors::{sql_table_error, unknown_lazy_table_error},
17};
18use super::{DerivedTableLineage, RegisteredSessionSource};
19
20const LAZY_SQL_PLANNING_PHASE: &str = "lazy_sql_planning";
21
22#[derive(Clone, PartialEq, Eq)]
24pub struct RegisteredDerivedTable {
25 table: LazyTable,
26 schema: SchemaRef,
27 pub(in crate::orchestrator::session) sql_text: String,
28 pub(in crate::orchestrator::session) lineage: DerivedTableLineage,
29 phase_timings: Vec<PhaseTimingReport>,
30}
31
32impl RegisteredDerivedTable {
33 pub(super) fn new(
34 table: LazyTable,
35 schema: SchemaRef,
36 sql_text: String,
37 lineage: DerivedTableLineage,
38 phase_timings: Vec<PhaseTimingReport>,
39 ) -> Self {
40 Self {
41 table,
42 schema,
43 sql_text,
44 lineage,
45 phase_timings,
46 }
47 }
48
49 #[must_use]
51 pub const fn table(&self) -> &LazyTable {
52 &self.table
53 }
54
55 #[must_use]
57 pub fn name(&self) -> &str {
58 self.table.name()
59 }
60
61 #[must_use]
63 pub fn schema(&self) -> &SchemaRef {
64 &self.schema
65 }
66
67 #[allow(dead_code)]
69 #[must_use]
70 pub(crate) fn sql_text(&self) -> &str {
71 &self.sql_text
72 }
73
74 #[allow(dead_code)]
76 #[must_use]
77 pub(crate) const fn lineage(&self) -> &DerivedTableLineage {
78 &self.lineage
79 }
80
81 #[must_use]
83 pub fn phase_timings(&self) -> &[PhaseTimingReport] {
84 &self.phase_timings
85 }
86}
87
88impl fmt::Debug for RegisteredDerivedTable {
89 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
90 formatter
91 .debug_struct("RegisteredDerivedTable")
92 .field("table", &self.table)
93 .field("schema", &self.schema)
94 .field("sql_text", &"<redacted>")
95 .field("lineage", &self.lineage)
96 .field("phase_timings", &self.phase_timings)
97 .finish()
98 }
99}
100
101#[derive(Clone)]
102pub(in crate::orchestrator::session) struct PendingDerivedTable {
103 pub(in crate::orchestrator::session) table: LazyTable,
104 pub(in crate::orchestrator::session) provider: Arc<dyn TableProvider>,
105 pub(in crate::orchestrator::session) schema: SchemaRef,
106 pub(in crate::orchestrator::session) sql_text: String,
107 pub(in crate::orchestrator::session) lineage: DerivedTableLineage,
108 pub(in crate::orchestrator::session) phase_timings: Vec<PhaseTimingReport>,
109}
110
111impl DeltaFunnelSession {
112 pub async fn table_from_sql(&mut self, sql: &str) -> Result<LazyTable, DeltaFunnelError> {
124 let sql = sql.trim();
125 if sql.is_empty() {
126 return sql_table_error(SqlTablePhase::ValidateSql, "SQL text must not be empty");
127 }
128
129 let planning_timer = PhaseTimer::start(LAZY_SQL_PLANNING_PHASE);
130 let dataframe = match self.plan_read_only_sql(sql).await {
131 Ok(dataframe) => dataframe,
132 Err(error) => {
133 let _ = planning_timer.failed();
134 return Err(error);
135 }
136 };
137 let schema = Arc::new(dataframe.schema().as_arrow().clone());
138 let provider = dataframe.into_view();
139 let lineage = self.derive_table_lineage_from_sql(sql);
140 let phase_timings = vec![planning_timer.completed()];
141 let table = self.allocate_derived_sql_table();
142 self.pending_derived_tables.push(PendingDerivedTable {
143 table: table.clone(),
144 provider,
145 schema,
146 sql_text: sql.to_owned(),
147 lineage,
148 phase_timings,
149 });
150 Ok(table)
151 }
152
153 pub fn register_alias(
167 &mut self,
168 name: impl Into<String>,
169 table: &LazyTable,
170 ) -> Result<LazyTable, DeltaFunnelError> {
171 let name = name.into();
172 validate_table_source_names([name.as_str()])?;
173 self.reject_registered_alias_name(&name)?;
174
175 let Some(index) = self.find_pending_derived_table(table) else {
176 return sql_table_error(
177 SqlTablePhase::RegisterDerivedAlias,
178 "lazy table is not a pending SQL-derived table owned by this session",
179 );
180 };
181 let pending = self.pending_derived_tables.remove(index);
182
183 if let Err(error) = self
184 .context
185 .register_table(name.as_str(), Arc::clone(&pending.provider))
186 {
187 let message = error.to_string();
188 self.pending_derived_tables.push(pending);
189 return sql_table_error(SqlTablePhase::RegisterDerivedAlias, message);
190 }
191
192 let alias_table = pending.table.with_name(name);
193 self.derived_tables.push(RegisteredDerivedTable::new(
194 alias_table.clone(),
195 pending.schema,
196 pending.sql_text,
197 pending.lineage,
198 pending.phase_timings,
199 ));
200 Ok(alias_table)
201 }
202
203 #[must_use]
205 pub fn registered_source(&self, name: &str) -> Option<&RegisteredSessionSource> {
206 self.sources
207 .iter()
208 .find(|source| source.name().eq_ignore_ascii_case(name))
209 }
210
211 #[must_use]
213 pub fn derived_tables(&self) -> &[RegisteredDerivedTable] {
214 &self.derived_tables
215 }
216
217 #[must_use]
219 pub fn registered_derived_table(&self, name: &str) -> Option<&RegisteredDerivedTable> {
220 self.derived_tables
221 .iter()
222 .find(|table| table.name().eq_ignore_ascii_case(name))
223 }
224
225 pub(in crate::orchestrator::session) fn registered_derived_table_by_id(
226 &self,
227 table_id: u64,
228 ) -> Option<&RegisteredDerivedTable> {
229 self.derived_tables
230 .iter()
231 .find(|table| table.table().id() == table_id)
232 }
233
234 pub(in crate::orchestrator::session) fn phase_timings_for_lazy_table(
235 &self,
236 table: &LazyTable,
237 ) -> Result<Vec<PhaseTimingReport>, DeltaFunnelError> {
238 if table.kind() == LazyTableKind::DeltaSource {
239 return Ok(Vec::new());
240 }
241
242 self.derived_tables
243 .iter()
244 .find(|derived| derived.table().id() == table.id())
245 .map(|derived| derived.phase_timings().to_vec())
246 .or_else(|| {
247 self.pending_derived_tables
248 .iter()
249 .find(|pending| pending.table.id() == table.id())
250 .map(|pending| pending.phase_timings.clone())
251 })
252 .ok_or_else(|| unknown_lazy_table_error(table))
253 }
254
255 pub(in crate::orchestrator::session) fn registered_derived_for_scoped_cache_alias(
261 &self,
262 table: &LazyTable,
263 ) -> Result<&RegisteredDerivedTable, DeltaFunnelError> {
264 if table.kind() != LazyTableKind::DerivedSql {
265 return Err(unknown_lazy_table_error(table));
266 }
267
268 self.registered_derived_table_by_id(table.id())
269 .ok_or_else(|| unknown_lazy_table_error(table))
270 }
271
272 #[allow(dead_code)]
273 pub(crate) fn sql_text_for_derived_table(
274 &self,
275 table: &LazyTable,
276 ) -> Result<&str, DeltaFunnelError> {
277 if table.kind() != LazyTableKind::DerivedSql {
278 return Err(unknown_lazy_table_error(table));
279 }
280
281 self.derived_tables
282 .iter()
283 .find(|derived| derived.table().id() == table.id())
284 .map(RegisteredDerivedTable::sql_text)
285 .or_else(|| {
286 self.pending_derived_tables
287 .iter()
288 .find(|pending| pending.table.id() == table.id())
289 .map(|pending| pending.sql_text.as_str())
290 })
291 .ok_or_else(|| unknown_lazy_table_error(table))
292 }
293
294 pub(super) async fn plan_read_only_sql(
295 &self,
296 sql: &str,
297 ) -> Result<DataFrame, DeltaFunnelError> {
298 self.context
299 .sql_with_options(sql, read_only_sql_options())
300 .await
301 .map_err(|error| DeltaFunnelError::SqlTable {
302 phase: classify_sql_error_phase(&error.to_string()),
303 message: error.to_string(),
304 })
305 }
306
307 fn allocate_derived_sql_table(&mut self) -> LazyTable {
308 let id = self.next_table_id;
309 self.next_table_id = self.next_table_id.saturating_add(1);
310 LazyTable::derived_sql(id)
311 }
312
313 pub(super) fn reject_registered_alias_name(&self, name: &str) -> Result<(), DeltaFunnelError> {
314 if self.registered_source(name).is_some() || self.registered_derived_table(name).is_some() {
315 return Err(DeltaFunnelError::DuplicateSourceName {
316 name: name.to_owned(),
317 });
318 }
319 Ok(())
320 }
321
322 fn find_pending_derived_table(&self, table: &LazyTable) -> Option<usize> {
323 if table.kind() != LazyTableKind::DerivedSql {
324 return None;
325 }
326
327 self.pending_derived_tables
328 .iter()
329 .position(|pending| pending.table.id() == table.id())
330 }
331}
332
333pub(in crate::orchestrator::session) fn read_only_sql_options() -> SQLOptions {
334 SQLOptions::new()
335 .with_allow_ddl(false)
336 .with_allow_dml(false)
337 .with_allow_statements(false)
338}
339
340fn classify_sql_error_phase(error: &str) -> SqlTablePhase {
341 if error.contains("DDL not supported")
342 || error.contains("DML not supported")
343 || error.contains("Statement not supported")
344 || error.contains("only supports a single SQL statement")
345 {
346 SqlTablePhase::ValidateSql
347 } else {
348 SqlTablePhase::PlanSql
349 }
350}
351
352#[cfg(test)]
353mod tests {
354 use datafusion::{
355 common::tree_node::{TreeNode, TreeNodeRecursion},
356 error::Result as DataFusionResult,
357 logical_expr::LogicalPlan,
358 };
359
360 use crate::{DeltaFunnelError, DeltaSourceConfig, SqlTablePhase};
361
362 use super::super::super::{
363 DeltaFunnelSession, LazyTableKind, SessionOptions, test_support::DeltaLogTable,
364 };
365 use super::LAZY_SQL_PLANNING_PHASE;
366
367 #[derive(Debug, Clone, PartialEq, Eq)]
368 struct TableScanProofReference {
369 table_name: String,
370 nested_table_names: Vec<String>,
371 }
372
373 fn table_scan_proof_references(
374 plan: &LogicalPlan,
375 ) -> DataFusionResult<Vec<TableScanProofReference>> {
376 let mut references = Vec::new();
377
378 plan.apply(|node| {
379 if let LogicalPlan::TableScan(scan) = node {
380 let nested_table_names = scan
381 .source
382 .get_logical_plan()
383 .map(|nested| table_scan_table_names(nested.as_ref()))
384 .transpose()?
385 .unwrap_or_default();
386 references.push(TableScanProofReference {
387 table_name: scan.table_name.table().to_owned(),
388 nested_table_names,
389 });
390 }
391
392 Ok(TreeNodeRecursion::Continue)
393 })?;
394
395 Ok(references)
396 }
397
398 fn table_scan_table_names(plan: &LogicalPlan) -> DataFusionResult<Vec<String>> {
399 let mut names = Vec::new();
400
401 plan.apply(|node| {
402 if let LogicalPlan::TableScan(scan) = node {
403 names.push(scan.table_name.table().to_owned());
404 }
405
406 Ok(TreeNodeRecursion::Continue)
407 })?;
408
409 Ok(names)
410 }
411
412 #[tokio::test]
413 async fn table_from_sql_builds_lazy_derived_table_without_row_execution()
414 -> Result<(), Box<dyn std::error::Error>> {
415 let table = DeltaLogTable::new("orders")?;
416 let mut session = DeltaFunnelSession::new(SessionOptions::default())?;
417 session.delta_lake(DeltaSourceConfig::new("orders", table.uri()))?;
418
419 let derived = session
420 .table_from_sql("select id, customer_name from orders")
421 .await?;
422
423 assert_eq!(derived.id(), 1);
424 assert_eq!(derived.kind(), LazyTableKind::DerivedSql);
425 assert_eq!(derived.name(), "table_1");
426 assert_eq!(session.next_table_id(), 2);
427 assert!(session.derived_tables().is_empty());
428 let phase_timings = session.phase_timings_for_lazy_table(&derived)?;
429 assert_eq!(
430 phase_timings
431 .iter()
432 .map(crate::PhaseTimingReport::phase_name)
433 .collect::<Vec<_>>(),
434 vec![LAZY_SQL_PLANNING_PHASE]
435 );
436 assert!(
437 phase_timings
438 .iter()
439 .all(|timing| timing.status().is_completed())
440 );
441 assert!(
442 phase_timings
443 .iter()
444 .all(|timing| timing.elapsed_micros().is_some())
445 );
446 Ok(())
447 }
448
449 #[tokio::test]
450 async fn table_from_sql_retains_trimmed_pending_sql_text()
451 -> Result<(), Box<dyn std::error::Error>> {
452 let table = DeltaLogTable::new("orders")?;
453 let mut session = DeltaFunnelSession::new(SessionOptions::default())?;
454 session.delta_lake(DeltaSourceConfig::new("orders", table.uri()))?;
455
456 let derived = session
457 .table_from_sql(" \n\t select id from orders \t ")
458 .await?;
459
460 assert_eq!(
461 session.sql_text_for_derived_table(&derived)?,
462 "select id from orders"
463 );
464 assert!(session.derived_tables().is_empty());
465 Ok(())
466 }
467
468 #[tokio::test]
469 async fn registered_derived_alias_can_be_referenced_by_later_sql()
470 -> Result<(), Box<dyn std::error::Error>> {
471 let table = DeltaLogTable::new("orders")?;
472 let mut session = DeltaFunnelSession::new(SessionOptions::default())?;
473 session.delta_lake(DeltaSourceConfig::new("orders", table.uri()))?;
474 let derived = session
475 .table_from_sql("select id, customer_name from orders")
476 .await?;
477
478 let alias = session.register_alias("recent_orders", &derived)?;
479 let second = session
480 .table_from_sql("select id from recent_orders")
481 .await?;
482
483 assert_eq!(alias.id(), derived.id());
484 assert_eq!(alias.name(), "recent_orders");
485 assert_eq!(
486 session.phase_timings_for_lazy_table(&alias)?,
487 session.phase_timings_for_lazy_table(&derived)?
488 );
489 assert_eq!(second.id(), 2);
490 assert_eq!(second.kind(), LazyTableKind::DerivedSql);
491 assert_eq!(session.derived_tables().len(), 1);
492 let registered = session
493 .registered_derived_table("RECENT_ORDERS")
494 .ok_or("registered derived alias missing")?;
495 assert_eq!(registered.table(), &alias);
496 assert_eq!(registered.schema().fields().len(), 2);
497 Ok(())
498 }
499
500 #[tokio::test]
501 async fn registered_derived_alias_retains_sql_text() -> Result<(), Box<dyn std::error::Error>> {
502 let table = DeltaLogTable::new("orders")?;
503 let mut session = DeltaFunnelSession::new(SessionOptions::default())?;
504 session.delta_lake(DeltaSourceConfig::new("orders", table.uri()))?;
505 let derived = session
506 .table_from_sql("select id, customer_name from orders")
507 .await?;
508
509 let alias = session.register_alias("recent_orders", &derived)?;
510 let registered = session
511 .registered_derived_table("RECENT_ORDERS")
512 .ok_or("registered derived alias missing")?;
513
514 assert_eq!(
515 session.sql_text_for_derived_table(&alias)?,
516 "select id, customer_name from orders"
517 );
518 assert_eq!(
519 registered.sql_text(),
520 "select id, customer_name from orders"
521 );
522 assert_eq!(
523 session.sql_text_for_derived_table(&derived)?,
524 "select id, customer_name from orders"
525 );
526 Ok(())
527 }
528
529 #[tokio::test]
530 async fn empty_sql_fails_before_lazy_table_allocation() -> Result<(), DeltaFunnelError> {
531 let mut session = DeltaFunnelSession::new(SessionOptions::default())?;
532
533 let error = session.table_from_sql(" \n\t ").await;
534
535 assert!(matches!(
536 error,
537 Err(DeltaFunnelError::SqlTable {
538 phase: SqlTablePhase::ValidateSql,
539 message,
540 }) if message.contains("must not be empty")
541 ));
542 assert_eq!(session.next_table_id(), 0);
543 assert!(session.derived_tables().is_empty());
544 Ok(())
545 }
546
547 #[tokio::test]
548 async fn multiple_sql_statements_fail_before_lazy_table_allocation()
549 -> Result<(), Box<dyn std::error::Error>> {
550 let table = DeltaLogTable::new("orders")?;
551 let mut session = DeltaFunnelSession::new(SessionOptions::default())?;
552 session.delta_lake(DeltaSourceConfig::new("orders", table.uri()))?;
553
554 let error = session
555 .table_from_sql("select id from orders; select id from orders")
556 .await;
557
558 assert!(matches!(
559 error,
560 Err(DeltaFunnelError::SqlTable {
561 phase: SqlTablePhase::ValidateSql,
562 ..
563 })
564 ));
565 assert_eq!(session.next_table_id(), 1);
566 assert!(session.derived_tables().is_empty());
567 Ok(())
568 }
569
570 #[tokio::test]
571 async fn ddl_sql_fails_before_alias_registration() -> Result<(), Box<dyn std::error::Error>> {
572 let table = DeltaLogTable::new("orders")?;
573 let mut session = DeltaFunnelSession::new(SessionOptions::default())?;
574 session.delta_lake(DeltaSourceConfig::new("orders", table.uri()))?;
575
576 let error = session
577 .table_from_sql("create table created_orders as select id from orders")
578 .await;
579
580 assert!(matches!(
581 error,
582 Err(DeltaFunnelError::SqlTable {
583 phase: SqlTablePhase::ValidateSql,
584 ..
585 })
586 ));
587 assert_eq!(session.next_table_id(), 1);
588 assert!(session.derived_tables().is_empty());
589 Ok(())
590 }
591
592 #[tokio::test]
593 async fn dml_sql_fails_before_alias_registration() -> Result<(), Box<dyn std::error::Error>> {
594 let table = DeltaLogTable::new("orders")?;
595 let mut session = DeltaFunnelSession::new(SessionOptions::default())?;
596 session.delta_lake(DeltaSourceConfig::new("orders", table.uri()))?;
597
598 let error = session
599 .table_from_sql("insert into orders select id, customer_name from orders")
600 .await;
601
602 assert!(matches!(
603 error,
604 Err(DeltaFunnelError::SqlTable {
605 phase: SqlTablePhase::ValidateSql,
606 ..
607 })
608 ));
609 assert_eq!(session.next_table_id(), 1);
610 assert!(session.derived_tables().is_empty());
611 Ok(())
612 }
613
614 #[tokio::test]
615 async fn missing_table_sql_fails_with_planning_context() -> Result<(), DeltaFunnelError> {
616 let mut session = DeltaFunnelSession::new(SessionOptions::default())?;
617
618 let error = session
619 .table_from_sql("select id from missing_orders")
620 .await;
621
622 assert!(matches!(
623 error,
624 Err(DeltaFunnelError::SqlTable {
625 phase: SqlTablePhase::PlanSql,
626 message,
627 }) if message.contains("missing_orders")
628 ));
629 assert_eq!(session.next_table_id(), 0);
630 assert!(session.derived_tables().is_empty());
631 Ok(())
632 }
633
634 #[tokio::test]
635 async fn derived_alias_duplicate_with_source_fails_before_registration()
636 -> Result<(), Box<dyn std::error::Error>> {
637 let table = DeltaLogTable::new("orders")?;
638 let mut session = DeltaFunnelSession::new(SessionOptions::default())?;
639 session.delta_lake(DeltaSourceConfig::new("orders", table.uri()))?;
640 let derived = session.table_from_sql("select id from orders").await?;
641
642 let error = session.register_alias("ORDERS", &derived);
643
644 assert!(matches!(
645 error,
646 Err(DeltaFunnelError::DuplicateSourceName { name }) if name == "ORDERS"
647 ));
648 assert!(session.derived_tables().is_empty());
649 assert!(session.context().table("ORDERS").await.is_ok());
650 let alias = session.register_alias("recent_orders", &derived)?;
651 assert_eq!(alias.name(), "recent_orders");
652 assert_eq!(session.derived_tables().len(), 1);
653 Ok(())
654 }
655
656 #[tokio::test]
657 async fn invalid_derived_alias_fails_without_consuming_pending_table()
658 -> Result<(), Box<dyn std::error::Error>> {
659 let table = DeltaLogTable::new("orders")?;
660 let mut session = DeltaFunnelSession::new(SessionOptions::default())?;
661 session.delta_lake(DeltaSourceConfig::new("orders", table.uri()))?;
662 let derived = session.table_from_sql("select id from orders").await?;
663
664 let error = session.register_alias("select", &derived);
665
666 assert!(matches!(
667 error,
668 Err(DeltaFunnelError::InvalidSourceName { name, .. }) if name == "select"
669 ));
670 assert!(session.derived_tables().is_empty());
671 let alias = session.register_alias("recent_orders", &derived)?;
672 assert_eq!(alias.name(), "recent_orders");
673 assert_eq!(session.derived_tables().len(), 1);
674 Ok(())
675 }
676
677 #[tokio::test]
678 async fn invalid_derived_alias_preserves_pending_sql_text()
679 -> Result<(), Box<dyn std::error::Error>> {
680 let table = DeltaLogTable::new("orders")?;
681 let mut session = DeltaFunnelSession::new(SessionOptions::default())?;
682 session.delta_lake(DeltaSourceConfig::new("orders", table.uri()))?;
683 let derived = session.table_from_sql("select id from orders").await?;
684
685 let error = session.register_alias("select", &derived);
686
687 assert!(matches!(
688 error,
689 Err(DeltaFunnelError::InvalidSourceName { name, .. }) if name == "select"
690 ));
691 assert_eq!(
692 session.sql_text_for_derived_table(&derived)?,
693 "select id from orders"
694 );
695 let alias = session.register_alias("recent_orders", &derived)?;
696 assert_eq!(
697 session.sql_text_for_derived_table(&alias)?,
698 "select id from orders"
699 );
700 Ok(())
701 }
702
703 #[tokio::test]
704 async fn register_alias_rejects_non_pending_table_handle()
705 -> Result<(), Box<dyn std::error::Error>> {
706 let table = DeltaLogTable::new("orders")?;
707 let mut session = DeltaFunnelSession::new(SessionOptions::default())?;
708 let source = session.delta_lake(DeltaSourceConfig::new("orders", table.uri()))?;
709
710 let error = session.register_alias("recent_orders", &source);
711
712 assert!(matches!(
713 error,
714 Err(DeltaFunnelError::SqlTable {
715 phase: SqlTablePhase::RegisterDerivedAlias,
716 message,
717 }) if message.contains("pending SQL-derived table")
718 ));
719 assert!(session.derived_tables().is_empty());
720 assert!(session.context().table("recent_orders").await.is_err());
721 Ok(())
722 }
723
724 #[tokio::test]
725 async fn derived_alias_duplicate_with_derived_alias_fails_before_registration()
726 -> Result<(), Box<dyn std::error::Error>> {
727 let table = DeltaLogTable::new("orders")?;
728 let mut session = DeltaFunnelSession::new(SessionOptions::default())?;
729 session.delta_lake(DeltaSourceConfig::new("orders", table.uri()))?;
730 let first = session.table_from_sql("select id from orders").await?;
731 session.register_alias("recent_orders", &first)?;
732 let second = session.table_from_sql("select id from orders").await?;
733
734 let error = session.register_alias("RECENT_ORDERS", &second);
735
736 assert!(matches!(
737 error,
738 Err(DeltaFunnelError::DuplicateSourceName { name }) if name == "RECENT_ORDERS"
739 ));
740 assert_eq!(session.derived_tables().len(), 1);
741 assert!(session.context().table("RECENT_ORDERS").await.is_ok());
742 Ok(())
743 }
744
745 #[tokio::test]
746 async fn registered_derived_debug_redacts_retained_sql_text()
747 -> Result<(), Box<dyn std::error::Error>> {
748 let mut session = DeltaFunnelSession::new(SessionOptions::default())?;
749 let derived = session
750 .table_from_sql("select 'super-secret-literal' as marker")
751 .await?;
752 session.register_alias("secret_marker", &derived)?;
753 let registered = session
754 .registered_derived_table("secret_marker")
755 .ok_or("registered derived alias missing")?;
756
757 let debug = format!("{registered:?}");
758
759 assert!(debug.contains("sql_text"));
760 assert!(debug.contains("<redacted>"));
761 assert!(!debug.contains("super-secret-literal"));
762 assert!(!debug.contains("select '"));
763 Ok(())
764 }
765
766 #[tokio::test]
767 async fn source_alias_duplicate_with_derived_alias_fails_before_loading_source()
768 -> Result<(), Box<dyn std::error::Error>> {
769 let table = DeltaLogTable::new("orders")?;
770 let mut session = DeltaFunnelSession::new(SessionOptions::default())?;
771 session.delta_lake(DeltaSourceConfig::new("orders", table.uri()))?;
772 let derived = session.table_from_sql("select id from orders").await?;
773 session.register_alias("recent_orders", &derived)?;
774
775 let error = session.delta_lake(DeltaSourceConfig::new("RECENT_ORDERS", ""));
776
777 assert!(matches!(
778 error,
779 Err(DeltaFunnelError::DuplicateSourceName { name }) if name == "RECENT_ORDERS"
780 ));
781 assert_eq!(session.sources().len(), 1);
782 assert_eq!(session.derived_tables().len(), 1);
783 Ok(())
784 }
785
786 #[tokio::test]
787 async fn planned_downstream_sql_expands_registered_derived_alias_reference()
788 -> Result<(), Box<dyn std::error::Error>> {
789 const MARKER_REGION_SCHEMA_FIELDS_JSON: &str = r#"[{\"name\":\"marker\",\"type\":\"string\",\"nullable\":false,\"metadata\":{}},{\"name\":\"region\",\"type\":\"string\",\"nullable\":false,\"metadata\":{}}]"#;
790
791 let table = DeltaLogTable::new_with_schema("orders", MARKER_REGION_SCHEMA_FIELDS_JSON)?;
792 let mut session = DeltaFunnelSession::new(SessionOptions::default())?;
793 session.delta_lake(DeltaSourceConfig::new("orders", table.uri()))?;
794
795 let source_dataframe = session
796 .plan_read_only_sql("select marker from orders")
797 .await?;
798 let source_references = table_scan_proof_references(source_dataframe.logical_plan())?;
799 assert_eq!(
800 source_references,
801 vec![TableScanProofReference {
802 table_name: "orders".to_owned(),
803 nested_table_names: Vec::new(),
804 }]
805 );
806 assert!(session.registered_source("orders").is_some());
807 assert!(session.registered_derived_table("orders").is_none());
808
809 let pending_big = session
810 .table_from_sql("select marker, region from orders")
811 .await?;
812 let _big = session.register_alias("big", &pending_big)?;
813 let west_dataframe = session
814 .plan_read_only_sql("select marker from BIG where region = 'west'")
815 .await?;
816 let east_dataframe = session
817 .plan_read_only_sql("select marker from big where region = 'east'")
818 .await?;
819 let west_references = table_scan_proof_references(west_dataframe.logical_plan())?;
820 let east_references = table_scan_proof_references(east_dataframe.logical_plan())?;
821
822 for references in [&west_references, &east_references] {
823 assert_eq!(
824 references,
825 &vec![TableScanProofReference {
826 table_name: "orders".to_owned(),
827 nested_table_names: Vec::new(),
828 }]
829 );
830 assert!(
831 session
832 .registered_source(&references[0].table_name)
833 .is_some()
834 );
835 assert!(
836 session
837 .registered_derived_table(&references[0].table_name)
838 .is_none()
839 );
840 }
841
842 assert!(
846 !west_references
847 .iter()
848 .any(|reference| reference.table_name.eq_ignore_ascii_case("big"))
849 );
850 assert!(
851 !east_references
852 .iter()
853 .any(|reference| reference.table_name.eq_ignore_ascii_case("big"))
854 );
855 assert!(session.registered_derived_table("big").is_some());
856 assert!(session.registered_source("big").is_none());
857 Ok(())
858 }
859}