delta_funnel/report/sql_server/
dry_run.rs1use std::fmt;
2
3use crate::{
4 DeltaSourceReport, LazyTableKind, LoadMode, MssqlDdlPlan, MssqlLifecyclePlan, MssqlSchemaPlan,
5 MssqlTargetTable, OutputStatus, PhaseTimingReport, PlannedMssqlOutput, ReportReasonCode,
6 RowCount, RunMode, SourceUsageStatus, ValidationStatus, WorkflowStatus,
7};
8
9#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct MssqlDryRunOutputFieldReport {
12 index: u64,
13 name: String,
14 arrow_type: String,
15 nullable: bool,
16}
17
18impl MssqlDryRunOutputFieldReport {
19 pub(crate) fn from_mapping(mapping: &arrow_tiberius::SchemaMapping) -> Self {
20 Self {
21 index: crate::usize_to_u64_saturating(mapping.arrow().index()),
22 name: mapping.arrow().name().to_owned(),
23 arrow_type: mapping.arrow().data_type().to_string(),
24 nullable: mapping.arrow().nullable(),
25 }
26 }
27
28 #[must_use]
30 pub const fn index(&self) -> u64 {
31 self.index
32 }
33
34 #[must_use]
36 pub fn name(&self) -> &str {
37 &self.name
38 }
39
40 #[must_use]
42 pub fn arrow_type(&self) -> &str {
43 &self.arrow_type
44 }
45
46 #[must_use]
48 pub const fn nullable(&self) -> bool {
49 self.nullable
50 }
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub enum MssqlDryRunSqlIdentityState {
56 Present,
58 Absent,
60 Unavailable,
62}
63
64impl MssqlDryRunSqlIdentityState {
65 #[must_use]
67 pub const fn as_str(self) -> &'static str {
68 match self {
69 Self::Present => "present",
70 Self::Absent => "absent",
71 Self::Unavailable => "unavailable",
72 }
73 }
74}
75
76impl fmt::Display for MssqlDryRunSqlIdentityState {
77 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
78 formatter.write_str(self.as_str())
79 }
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct MssqlDryRunSqlIdentityReport {
85 state: MssqlDryRunSqlIdentityState,
86 hash: Option<String>,
87 reason: Option<ReportReasonCode>,
88}
89
90impl MssqlDryRunSqlIdentityReport {
91 pub(crate) fn present(hash: String) -> Self {
92 Self {
93 state: MssqlDryRunSqlIdentityState::Present,
94 hash: Some(hash),
95 reason: None,
96 }
97 }
98
99 pub(crate) fn absent() -> Self {
100 Self {
101 state: MssqlDryRunSqlIdentityState::Absent,
102 hash: None,
103 reason: None,
104 }
105 }
106
107 pub(crate) fn unavailable(reason: ReportReasonCode) -> Self {
108 Self {
109 state: MssqlDryRunSqlIdentityState::Unavailable,
110 hash: None,
111 reason: Some(reason),
112 }
113 }
114
115 #[must_use]
117 pub const fn state(&self) -> MssqlDryRunSqlIdentityState {
118 self.state
119 }
120
121 #[must_use]
123 pub fn hash(&self) -> Option<&str> {
124 self.hash.as_deref()
125 }
126
127 #[must_use]
129 pub const fn reason(&self) -> Option<ReportReasonCode> {
130 self.reason
131 }
132}
133
134#[derive(Debug, Clone, PartialEq, Eq)]
140pub struct MssqlDryRunOutputReport {
141 planned_output: PlannedMssqlOutput,
142 output_schema: Vec<MssqlDryRunOutputFieldReport>,
143 sql_identity: MssqlDryRunSqlIdentityReport,
144 source_usage_status: SourceUsageStatus,
145 used_source_names: Vec<String>,
146 output_row_count: RowCount,
147 output_row_count_reason: Option<ReportReasonCode>,
148 status: OutputStatus,
149 validation_status: ValidationStatus,
150 phase_timings: Vec<PhaseTimingReport>,
151 sql_server_contacted: bool,
152 row_production_started: bool,
153 table_lifecycle_started: bool,
154 bulk_writer_started: bool,
155}
156
157impl MssqlDryRunOutputReport {
158 pub(crate) fn new(
159 planned_output: PlannedMssqlOutput,
160 sql_identity: MssqlDryRunSqlIdentityReport,
161 source_usage_status: SourceUsageStatus,
162 used_source_names: Vec<String>,
163 phase_timings: Vec<PhaseTimingReport>,
164 ) -> Self {
165 let output_schema = planned_output
166 .output_plan()
167 .schema_mappings()
168 .iter()
169 .map(MssqlDryRunOutputFieldReport::from_mapping)
170 .collect();
171
172 Self {
173 planned_output,
174 output_schema,
175 sql_identity,
176 source_usage_status,
177 used_source_names,
178 output_row_count: RowCount::unavailable(),
179 output_row_count_reason: Some(ReportReasonCode::NotExecuted),
180 status: OutputStatus::dry_run_planned(),
181 validation_status: ValidationStatus::skipped(ReportReasonCode::DryRun),
182 phase_timings,
183 sql_server_contacted: false,
184 row_production_started: false,
185 table_lifecycle_started: false,
186 bulk_writer_started: false,
187 }
188 }
189
190 #[must_use]
192 pub const fn planned_output(&self) -> &PlannedMssqlOutput {
193 &self.planned_output
194 }
195
196 #[must_use]
198 pub fn output_name(&self) -> &str {
199 self.planned_output.output_plan().output_name()
200 }
201
202 #[must_use]
204 pub const fn table_id(&self) -> u64 {
205 self.planned_output.table().id()
206 }
207
208 #[must_use]
210 pub const fn table_kind(&self) -> LazyTableKind {
211 self.planned_output.table().kind()
212 }
213
214 #[must_use]
216 pub fn table_name(&self) -> &str {
217 self.planned_output.table().name()
218 }
219
220 #[must_use]
222 pub fn output_schema(&self) -> &[MssqlDryRunOutputFieldReport] {
223 &self.output_schema
224 }
225
226 #[must_use]
228 pub fn target_table(&self) -> &MssqlTargetTable {
229 self.planned_output.output_plan().target_table()
230 }
231
232 #[must_use]
234 pub fn load_mode(&self) -> LoadMode {
235 self.planned_output.output_plan().load_mode()
236 }
237
238 #[must_use]
240 pub fn target_schema_plan(&self) -> &MssqlSchemaPlan {
241 self.planned_output.output_plan().schema_plan()
242 }
243
244 #[must_use]
246 pub fn target_ddl_plan(&self) -> &MssqlDdlPlan {
247 self.planned_output.output_plan().ddl_plan()
248 }
249
250 #[must_use]
252 pub fn target_lifecycle_plan(&self) -> &MssqlLifecyclePlan {
253 self.planned_output.output_plan().lifecycle_plan()
254 }
255
256 #[must_use]
258 pub const fn sql_identity(&self) -> &MssqlDryRunSqlIdentityReport {
259 &self.sql_identity
260 }
261
262 #[must_use]
264 pub const fn source_usage_status(&self) -> SourceUsageStatus {
265 self.source_usage_status
266 }
267
268 #[must_use]
270 pub fn used_source_names(&self) -> &[String] {
271 &self.used_source_names
272 }
273
274 #[must_use]
276 pub const fn output_row_count(&self) -> RowCount {
277 self.output_row_count
278 }
279
280 #[must_use]
282 pub const fn output_row_count_reason(&self) -> Option<ReportReasonCode> {
283 self.output_row_count_reason
284 }
285
286 #[must_use]
288 pub const fn status(&self) -> OutputStatus {
289 self.status
290 }
291
292 #[must_use]
294 pub const fn validation_status(&self) -> ValidationStatus {
295 self.validation_status
296 }
297
298 #[must_use]
300 pub fn phase_timings(&self) -> &[PhaseTimingReport] {
301 &self.phase_timings
302 }
303
304 #[must_use]
306 pub const fn run_mode(&self) -> RunMode {
307 RunMode::DryRun
308 }
309
310 #[must_use]
312 pub const fn sql_server_contacted(&self) -> bool {
313 self.sql_server_contacted
314 }
315
316 #[must_use]
318 pub const fn row_production_started(&self) -> bool {
319 self.row_production_started
320 }
321
322 #[must_use]
324 pub const fn table_lifecycle_started(&self) -> bool {
325 self.table_lifecycle_started
326 }
327
328 #[must_use]
330 pub const fn bulk_writer_started(&self) -> bool {
331 self.bulk_writer_started
332 }
333}
334
335#[derive(Debug, Clone, PartialEq, Eq)]
337pub struct MssqlDryRunWorkflowReport {
338 outputs: Vec<MssqlDryRunOutputReport>,
339 sources: Vec<DeltaSourceReport>,
340 status: WorkflowStatus,
341 phase_timings: Vec<PhaseTimingReport>,
342}
343
344impl MssqlDryRunWorkflowReport {
345 pub(crate) fn new(
346 outputs: Vec<MssqlDryRunOutputReport>,
347 sources: Vec<DeltaSourceReport>,
348 ) -> Self {
349 let status = if outputs.is_empty() {
350 WorkflowStatus::no_op(ReportReasonCode::NotExecuted)
351 } else {
352 WorkflowStatus::success()
353 };
354
355 Self {
356 outputs,
357 sources,
358 status,
359 phase_timings: Vec::new(),
360 }
361 }
362
363 pub(crate) fn with_phase_timings(mut self, phase_timings: Vec<PhaseTimingReport>) -> Self {
364 self.phase_timings = phase_timings;
365 self
366 }
367
368 #[must_use]
370 pub const fn run_mode(&self) -> RunMode {
371 RunMode::DryRun
372 }
373
374 #[must_use]
376 pub const fn status(&self) -> WorkflowStatus {
377 self.status
378 }
379
380 #[must_use]
382 pub fn len(&self) -> usize {
383 self.outputs.len()
384 }
385
386 #[must_use]
388 pub fn is_empty(&self) -> bool {
389 self.outputs.is_empty()
390 }
391
392 #[must_use]
394 pub fn outputs(&self) -> &[MssqlDryRunOutputReport] {
395 &self.outputs
396 }
397
398 #[must_use]
400 pub fn sources(&self) -> &[DeltaSourceReport] {
401 &self.sources
402 }
403
404 #[must_use]
406 pub fn phase_timings(&self) -> &[PhaseTimingReport] {
407 &self.phase_timings
408 }
409
410 #[must_use]
416 pub fn query_used_source_scan_metadata_exhausted(&self) -> bool {
417 let mut used_source_seen = false;
418 for source in &self.sources {
419 if source.usage_status() == SourceUsageStatus::Used {
420 used_source_seen = true;
421 if !source.scan_metadata_exhausted() {
422 return false;
423 }
424 }
425 }
426
427 used_source_seen
428 }
429
430 #[must_use]
432 pub fn sql_server_contacted(&self) -> bool {
433 self.outputs
434 .iter()
435 .any(MssqlDryRunOutputReport::sql_server_contacted)
436 }
437
438 #[must_use]
440 pub fn row_production_started(&self) -> bool {
441 self.outputs
442 .iter()
443 .any(MssqlDryRunOutputReport::row_production_started)
444 }
445
446 #[must_use]
448 pub fn table_lifecycle_started(&self) -> bool {
449 self.outputs
450 .iter()
451 .any(MssqlDryRunOutputReport::table_lifecycle_started)
452 }
453
454 #[must_use]
456 pub fn bulk_writer_started(&self) -> bool {
457 self.outputs
458 .iter()
459 .any(MssqlDryRunOutputReport::bulk_writer_started)
460 }
461}
462
463#[cfg(test)]
464mod tests {
465 use super::MssqlDryRunSqlIdentityState;
466
467 #[test]
468 fn sql_identity_status_exposes_stable_codes() {
469 assert_eq!(MssqlDryRunSqlIdentityState::Present.as_str(), "present");
470 assert_eq!(MssqlDryRunSqlIdentityState::Absent.to_string(), "absent");
471 assert_eq!(
472 MssqlDryRunSqlIdentityState::Unavailable.as_str(),
473 "unavailable"
474 );
475 }
476}