1use std::fmt;
2
3use crate::{
4 ExecutionProfileMode, MssqlTargetConfig, MssqlTargetOutputPlan, PhaseTimingReport,
5 QueryExecutionProfile, ReportReasonCode, ResolvedMssqlTarget,
6 support::sanitize_text_for_display,
7};
8
9pub(crate) const PREVIEW_DATAFRAME_PLANNING_PHASE: &str = "preview_dataframe_planning";
10pub(crate) const PREVIEW_PHYSICAL_PLANNING_PHASE: &str = "preview_physical_planning";
11pub(crate) const PREVIEW_STREAM_SETUP_PHASE: &str = "preview_stream_setup";
12pub(crate) const PREVIEW_EXECUTE_COLLECT_PHASE: &str = "preview_execute_collect";
13pub(crate) const PREVIEW_FORMAT_TEXT_PHASE: &str = "preview_format_text";
14pub(crate) const PREVIEW_FORMAT_HTML_PHASE: &str = "preview_format_html";
15pub(crate) const PREVIEW_TOTAL_PHASE: &str = "preview_total";
16
17pub(crate) const PREVIEW_PHASE_NAMES: [&str; 7] = [
18 PREVIEW_DATAFRAME_PLANNING_PHASE,
19 PREVIEW_PHYSICAL_PLANNING_PHASE,
20 PREVIEW_STREAM_SETUP_PHASE,
21 PREVIEW_EXECUTE_COLLECT_PHASE,
22 PREVIEW_FORMAT_TEXT_PHASE,
23 PREVIEW_FORMAT_HTML_PHASE,
24 PREVIEW_TOTAL_PHASE,
25];
26
27#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
29pub enum RunMode {
30 #[default]
32 Execute,
33 DryRun,
35}
36
37#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct LazyTable {
40 id: LazyTableId,
41 kind: LazyTableKind,
42 name: String,
43}
44
45impl LazyTable {
46 #[cfg(test)]
48 #[must_use]
49 pub(crate) fn placeholder(id: u64, kind: LazyTableKind) -> Self {
50 Self {
51 id: LazyTableId(id),
52 kind,
53 name: format!("table_{id}"),
54 }
55 }
56
57 pub(super) fn delta_source(id: u64, name: String) -> Self {
58 Self {
59 id: LazyTableId(id),
60 kind: LazyTableKind::DeltaSource,
61 name,
62 }
63 }
64
65 pub(super) fn derived_sql(id: u64) -> Self {
66 Self {
67 id: LazyTableId(id),
68 kind: LazyTableKind::DerivedSql,
69 name: format!("table_{id}"),
70 }
71 }
72
73 pub(super) fn with_name(&self, name: String) -> Self {
74 Self {
75 id: self.id,
76 kind: self.kind,
77 name,
78 }
79 }
80
81 #[must_use]
83 pub const fn id(&self) -> u64 {
84 self.id.0
85 }
86
87 #[must_use]
89 pub const fn kind(&self) -> LazyTableKind {
90 self.kind
91 }
92
93 #[must_use]
95 pub fn name(&self) -> &str {
96 &self.name
97 }
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101struct LazyTableId(u64);
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub enum LazyTableKind {
106 DeltaSource,
108 DerivedSql,
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114pub struct PreviewOptions {
115 limit: usize,
116 execution_profile_mode: ExecutionProfileMode,
117}
118
119impl PreviewOptions {
120 #[must_use]
122 pub const fn new(limit: usize) -> Self {
123 Self {
124 limit,
125 execution_profile_mode: ExecutionProfileMode::Disabled,
126 }
127 }
128
129 #[must_use]
131 pub const fn with_execution_profile_mode(mut self, mode: ExecutionProfileMode) -> Self {
132 self.execution_profile_mode = mode;
133 self
134 }
135
136 #[must_use]
138 pub const fn limit(&self) -> usize {
139 self.limit
140 }
141
142 #[must_use]
144 pub const fn execution_profile_mode(&self) -> ExecutionProfileMode {
145 self.execution_profile_mode
146 }
147}
148
149#[derive(Debug, Clone, PartialEq, Eq)]
151pub struct PreviewFailureContext {
152 failed_phase: String,
153 phase_timings: Vec<PhaseTimingReport>,
154 execution_profile: Option<QueryExecutionProfile>,
155}
156
157impl PreviewFailureContext {
158 pub(crate) fn new(
159 failed_phase: String,
160 phase_timings: Vec<PhaseTimingReport>,
161 execution_profile: Option<QueryExecutionProfile>,
162 ) -> Self {
163 Self {
164 failed_phase,
165 phase_timings,
166 execution_profile,
167 }
168 }
169
170 #[must_use]
172 pub fn failed_phase(&self) -> &str {
173 &self.failed_phase
174 }
175
176 #[must_use]
178 pub fn phase_timings(&self) -> &[PhaseTimingReport] {
179 &self.phase_timings
180 }
181
182 #[must_use]
185 pub const fn execution_profile(&self) -> Option<&QueryExecutionProfile> {
186 self.execution_profile.as_ref()
187 }
188}
189
190#[derive(Debug, Clone, PartialEq, Eq)]
192pub struct TablePreview {
193 text: String,
194 html: String,
195 phase_timings: Vec<PhaseTimingReport>,
196 execution_profile: Option<QueryExecutionProfile>,
197}
198
199impl TablePreview {
200 #[must_use]
205 pub fn new(text: String, html: String) -> Self {
206 Self::from_execution(
207 text,
208 html,
209 PREVIEW_PHASE_NAMES
210 .into_iter()
211 .map(|phase_name| {
212 PhaseTimingReport::unavailable(phase_name, ReportReasonCode::NotExecuted)
213 })
214 .collect(),
215 None,
216 )
217 }
218
219 pub(crate) fn from_execution(
220 text: String,
221 html: String,
222 phase_timings: Vec<PhaseTimingReport>,
223 execution_profile: Option<QueryExecutionProfile>,
224 ) -> Self {
225 Self {
226 text,
227 html,
228 phase_timings,
229 execution_profile,
230 }
231 }
232
233 #[must_use]
235 pub fn text(&self) -> &str {
236 &self.text
237 }
238
239 #[must_use]
241 pub fn html(&self) -> &str {
242 &self.html
243 }
244
245 #[must_use]
247 pub fn phase_timings(&self) -> &[PhaseTimingReport] {
248 &self.phase_timings
249 }
250
251 #[must_use]
253 pub const fn execution_profile(&self) -> Option<&QueryExecutionProfile> {
254 self.execution_profile.as_ref()
255 }
256}
257
258#[derive(Clone, PartialEq, Eq)]
260pub struct MssqlOutputTarget {
261 output_name: String,
262 target: MssqlTargetConfig,
263 run_mode: RunMode,
264}
265
266impl MssqlOutputTarget {
267 #[must_use]
269 pub fn new(
270 output_name: impl Into<String>,
271 target: MssqlTargetConfig,
272 run_mode: RunMode,
273 ) -> Self {
274 Self {
275 output_name: output_name.into(),
276 target,
277 run_mode,
278 }
279 }
280
281 #[must_use]
283 pub fn output_name(&self) -> &str {
284 &self.output_name
285 }
286
287 #[must_use]
289 pub const fn target(&self) -> &MssqlTargetConfig {
290 &self.target
291 }
292
293 #[must_use]
295 pub const fn run_mode(&self) -> RunMode {
296 self.run_mode
297 }
298}
299
300impl fmt::Debug for MssqlOutputTarget {
301 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
302 formatter
303 .debug_struct("MssqlOutputTarget")
304 .field("output_name", &sanitize_text_for_display(&self.output_name))
305 .field("target", &self.target)
306 .field("run_mode", &self.run_mode)
307 .finish()
308 }
309}
310
311#[derive(Debug, Clone, PartialEq, Eq)]
313pub struct OutputWritePlan {
314 table: LazyTable,
315 target: MssqlOutputTarget,
316}
317
318impl OutputWritePlan {
319 #[must_use]
321 pub const fn new(table: LazyTable, target: MssqlOutputTarget) -> Self {
322 Self { table, target }
323 }
324
325 #[must_use]
327 pub const fn table(&self) -> &LazyTable {
328 &self.table
329 }
330
331 #[must_use]
333 pub const fn target(&self) -> &MssqlOutputTarget {
334 &self.target
335 }
336}
337
338#[derive(Debug, Clone, PartialEq, Eq)]
340pub struct PlannedMssqlOutput {
341 request: OutputWritePlan,
342 resolved_target: ResolvedMssqlTarget,
343 output_plan: MssqlTargetOutputPlan,
344 phase_timings: Vec<PhaseTimingReport>,
345}
346
347impl PlannedMssqlOutput {
348 pub(super) fn new(
349 request: OutputWritePlan,
350 resolved_target: ResolvedMssqlTarget,
351 output_plan: MssqlTargetOutputPlan,
352 phase_timings: Vec<PhaseTimingReport>,
353 ) -> Self {
354 Self {
355 request,
356 resolved_target,
357 output_plan,
358 phase_timings,
359 }
360 }
361
362 #[must_use]
364 pub const fn request(&self) -> &OutputWritePlan {
365 &self.request
366 }
367
368 #[must_use]
370 pub const fn table(&self) -> &LazyTable {
371 self.request.table()
372 }
373
374 #[must_use]
376 pub const fn target(&self) -> &MssqlOutputTarget {
377 self.request.target()
378 }
379
380 #[must_use]
382 pub const fn resolved_target(&self) -> &ResolvedMssqlTarget {
383 &self.resolved_target
384 }
385
386 #[must_use]
388 pub const fn output_plan(&self) -> &MssqlTargetOutputPlan {
389 &self.output_plan
390 }
391
392 #[must_use]
394 pub fn phase_timings(&self) -> &[PhaseTimingReport] {
395 &self.phase_timings
396 }
397}
398
399#[cfg(test)]
400mod tests {
401 use crate::{
402 DeltaFunnelError, ExecutionProfileMode, LoadMode, MssqlConnectionConfig, MssqlTargetConfig,
403 MssqlTargetTable, PhaseStatus, ReportReasonCode,
404 };
405
406 use super::{
407 LazyTable, LazyTableKind, MssqlOutputTarget, OutputWritePlan, PREVIEW_PHASE_NAMES,
408 PreviewOptions, RunMode, TablePreview,
409 };
410
411 #[test]
412 fn preview_options_default_to_disabled_profiling() {
413 let default = PreviewOptions::new(20);
414 let detailed = default.with_execution_profile_mode(ExecutionProfileMode::Detailed);
415
416 assert_eq!(default.limit(), 20);
417 assert_eq!(
418 default.execution_profile_mode(),
419 ExecutionProfileMode::Disabled
420 );
421 assert_eq!(detailed.limit(), 20);
422 assert_eq!(
423 detailed.execution_profile_mode(),
424 ExecutionProfileMode::Detailed
425 );
426 }
427
428 #[test]
429 fn legacy_table_preview_has_unavailable_timings_and_no_profile() {
430 let preview = TablePreview::new("text".to_owned(), "html".to_owned());
431
432 assert_eq!(preview.text(), "text");
433 assert_eq!(preview.html(), "html");
434 assert_eq!(preview.phase_timings().len(), PREVIEW_PHASE_NAMES.len());
435 for (timing, phase_name) in preview.phase_timings().iter().zip(PREVIEW_PHASE_NAMES) {
436 assert_eq!(timing.phase_name(), phase_name);
437 assert_eq!(
438 timing.status(),
439 PhaseStatus::unavailable(ReportReasonCode::NotExecuted)
440 );
441 assert_eq!(timing.elapsed_micros(), None);
442 }
443 assert_eq!(preview.execution_profile(), None);
444 }
445
446 #[test]
447 fn output_request_shapes_preserve_table_target_and_run_mode() -> Result<(), DeltaFunnelError> {
448 let table = LazyTable::placeholder(7, LazyTableKind::DerivedSql);
449 let target_config = MssqlTargetConfig::new(MssqlTargetTable::new("dbo", "orders")?)
450 .with_load_mode(LoadMode::CreateAndLoad)
451 .with_connection(
452 MssqlConnectionConfig::new(
453 "server=tcp:sql.example.com;database=warehouse;user=admin;password=secret-token",
454 )?
455 .with_display_label("warehouse-primary"),
456 );
457 let target = MssqlOutputTarget::new("orders_output", target_config, RunMode::DryRun);
458 let plan = OutputWritePlan::new(table.clone(), target.clone());
459
460 assert_eq!(table.id(), 7);
461 assert_eq!(table.kind(), LazyTableKind::DerivedSql);
462 assert_eq!(target.output_name(), "orders_output");
463 assert_eq!(target.run_mode(), RunMode::DryRun);
464 assert_eq!(target.target().load_mode(), LoadMode::CreateAndLoad);
465 assert_eq!(plan.table(), &table);
466 assert_eq!(plan.target(), &target);
467
468 let debug = format!("{target:?}");
469 assert!(debug.contains("orders_output"));
470 assert!(!debug.contains("secret-token"));
471 assert!(!debug.contains("password"));
472 assert!(!debug.contains("server=tcp"));
473 Ok(())
474 }
475}