1use delta_arrow_reader::ParquetReaderBackend;
9use serde_json::{Value, json};
10
11use crate::{
12 DeltaDataFusionMetricsSnapshot, DeltaSourceReport, FileCount, LazyTableKind, LoadMode,
13 MssqlDryRunOutputFieldReport, MssqlDryRunOutputReport, MssqlDryRunSqlIdentityReport,
14 MssqlDryRunWorkflowReport, MssqlOutputBatchValidationReport, MssqlOutputFieldReport,
15 MssqlOutputWriteStatus, MssqlTargetCleanupStatus, MssqlTargetTable, MssqlWorkflowWriteReport,
16 MssqlWriteFailureContext, MssqlWriteFailureReport, MssqlWritePhase, MssqlWriteReport,
17 MssqlWriteSkippedReason, MssqlWriteSkippedReport, MssqlWriteStats, OutputStatus, PhaseStatus,
18 PhaseTimingReport, QueryExecutionMetric, QueryExecutionMetricValue,
19 QueryExecutionOperatorProfile, QueryExecutionProfile, ReportReasonCode, RowCount, RunMode,
20 ValidationStatus, WorkflowStatus, WriteAllCacheAliasReport, WriteAllCacheAliasStatus,
21 WriteAllCacheCandidateSkip, WriteAllCacheCandidateSkipReason, WriteAllCacheFailure,
22 WriteAllCacheReport, WriteAllNoCacheReason, WriteAllReport,
23};
24
25impl RowCount {
26 #[must_use]
28 pub fn to_json_value(self) -> Value {
29 count_value(self.kind().as_str(), self.value())
30 }
31}
32
33impl FileCount {
34 #[must_use]
36 pub fn to_json_value(self) -> Value {
37 count_value(self.kind().as_str(), self.value())
38 }
39}
40
41impl ValidationStatus {
42 #[must_use]
44 pub fn to_json_value(self) -> Value {
45 status_value(self.kind().as_str(), self.reason())
46 }
47}
48
49impl PhaseStatus {
50 #[must_use]
52 pub fn to_json_value(self) -> Value {
53 status_value(self.kind().as_str(), self.reason())
54 }
55}
56
57impl OutputStatus {
58 #[must_use]
60 pub fn to_json_value(self) -> Value {
61 json!({
62 "kind": self.kind().as_str(),
63 "reason": reason_value(self.reason()),
64 "validation": self.validation().map(ValidationStatus::to_json_value),
65 })
66 }
67}
68
69impl WorkflowStatus {
70 #[must_use]
72 pub fn to_json_value(self) -> Value {
73 status_value(self.kind().as_str(), self.reason())
74 }
75}
76
77impl PhaseTimingReport {
78 #[must_use]
80 pub fn to_json_value(&self) -> Value {
81 json!({
82 "phase_name": self.phase_name(),
83 "status": self.status().to_json_value(),
84 "elapsed_micros": self.elapsed_micros(),
85 })
86 }
87}
88
89impl QueryExecutionProfile {
90 #[must_use]
92 pub fn to_json_value(&self) -> Value {
93 json!({
94 "scope": self.scope().as_str(),
95 "outcome": self.outcome().as_str(),
96 "partial": self.partial(),
97 "delta_funnel_row_limit": self.delta_funnel_row_limit(),
98 "operators": self
99 .operators()
100 .iter()
101 .map(QueryExecutionOperatorProfile::to_json_value)
102 .collect::<Vec<_>>(),
103 })
104 }
105}
106
107impl crate::PreviewFailureContext {
108 #[must_use]
110 pub fn to_json_value(&self) -> Value {
111 json!({
112 "failed_phase": self.failed_phase(),
113 "phase_timings": self
114 .phase_timings()
115 .iter()
116 .map(PhaseTimingReport::to_json_value)
117 .collect::<Vec<_>>(),
118 "execution_profile": self
119 .execution_profile()
120 .map(QueryExecutionProfile::to_json_value),
121 })
122 }
123}
124
125impl QueryExecutionOperatorProfile {
126 #[must_use]
128 pub fn to_json_value(&self) -> Value {
129 json!({
130 "node_id": self.node_id(),
131 "parent_node_id": self.parent_node_id(),
132 "operator_name": self.operator_name(),
133 "output_partition_count": self.output_partition_count(),
134 "metrics_available": self.metrics_available(),
135 "aggregated_metrics": self
136 .aggregated_metrics()
137 .iter()
138 .map(QueryExecutionMetric::to_json_value)
139 .collect::<Vec<_>>(),
140 "metrics": self
141 .metrics()
142 .iter()
143 .map(QueryExecutionMetric::to_json_value)
144 .collect::<Vec<_>>(),
145 "delta_provider_read_stats": self
146 .delta_provider_read_stats()
147 .map(|stats| {
148 provider_read_stats_value(stats, self.delta_provider_source_name())
149 }),
150 })
151 }
152}
153
154impl QueryExecutionMetric {
155 #[must_use]
157 pub fn to_json_value(&self) -> Value {
158 let (value, components) = execution_metric_value(self.value());
159
160 json!({
161 "name": self.name(),
162 "category": self.category().as_str(),
163 "partition": self.partition(),
164 "output_partition": self.output_partition(),
165 "value_kind": self.value().value_kind(),
166 "value": value,
167 "components": components,
168 })
169 }
170}
171
172impl DeltaSourceReport {
173 #[must_use]
175 pub fn to_json_value(&self) -> Value {
176 let protocol = self.protocol();
177 let scheduling = self.scheduling();
178
179 json!({
180 "source_name": self.source_name(),
181 "source_uri": self.source_uri(),
182 "snapshot_version": self.snapshot_version(),
183 "protocol": {
184 "source_name": protocol.source_name,
185 "table_uri": protocol.table_uri,
186 "snapshot_version": protocol.snapshot_version,
187 "min_reader_version": protocol.min_reader_version,
188 "min_writer_version": protocol.min_writer_version,
189 "reader_features": protocol.reader_features,
190 "writer_features": protocol.writer_features,
191 },
192 "scheduling": {
193 "query_target_partitions": scheduling.query_target_partitions(),
194 "reader_backend": reader_backend(scheduling.reader_backend()),
195 "max_concurrent_file_reads_per_scan": scheduling.max_concurrent_file_reads_per_scan(),
196 "max_concurrent_file_reads_per_partition": scheduling.max_concurrent_file_reads_per_partition(),
197 "output_buffer_capacity_per_partition": scheduling.output_buffer_capacity_per_partition(),
198 "native_async_prefetch_file_count_per_partition": scheduling.native_async_prefetch_file_count_per_partition(),
199 "parquet_metadata_size_hint": scheduling.parquet_metadata_size_hint(),
200 "parquet_full_file_read_threshold": scheduling.parquet_full_file_read_threshold(),
201 },
202 "file_count": count_with_reason_value(
203 self.file_count().kind().as_str(),
204 self.file_count().value(),
205 self.file_count_reason()
206 ),
207 "scan_metadata_exhausted": self.scan_metadata_exhausted(),
208 "usage_status": self.usage_status().as_str(),
209 "used_by_output_names": self.used_by_output_names(),
210 "provider_read_stats_available": self.provider_read_stats().is_some(),
211 "provider_read_stats": self.provider_read_stats().map(|stats| {
212 provider_read_stats_value(stats, Some(self.source_name()))
213 }),
214 "provider_stats_reason": reason_value(self.provider_stats_reason()),
215 "phase_timings": phase_timings_value(self.phase_timings()),
216 })
217 }
218}
219
220impl MssqlDryRunOutputFieldReport {
221 #[must_use]
223 pub fn to_json_value(&self) -> Value {
224 json!({
225 "index": self.index(),
226 "name": self.name(),
227 "arrow_type": self.arrow_type(),
228 "nullable": self.nullable(),
229 })
230 }
231}
232
233impl MssqlDryRunSqlIdentityReport {
234 #[must_use]
236 pub fn to_json_value(&self) -> Value {
237 json!({
238 "state": self.state().as_str(),
239 "hash": self.hash(),
240 "reason": reason_value(self.reason()),
241 })
242 }
243}
244
245impl MssqlDryRunOutputReport {
246 #[must_use]
248 pub fn to_json_value(&self) -> Value {
249 json!({
250 "output_name": self.output_name(),
251 "run_mode": run_mode(self.run_mode()),
252 "status": self.status().to_json_value(),
253 "table": {
254 "id": self.table_id(),
255 "kind": lazy_table_kind(self.table_kind()),
256 "name": self.table_name(),
257 },
258 "target_table": target_table_value(self.target_table()),
259 "load_mode": load_mode(self.load_mode()),
260 "output_schema": self.output_schema()
261 .iter()
262 .map(MssqlDryRunOutputFieldReport::to_json_value)
263 .collect::<Vec<_>>(),
264 "target_schema_plan": {
265 "output_field_count": self.target_schema_plan().mappings().len(),
266 "diagnostic_count": self.target_schema_plan().diagnostic_reports().len(),
267 },
268 "target_ddl_plan": {
269 "create_table_sql_present": self.target_ddl_plan().create_table_sql_present(),
270 },
271 "target_lifecycle_plan": {
272 "create_table_sql_required": self.target_lifecycle_plan().create_table_sql_required(),
273 "create_table_sql_present": self.target_lifecycle_plan().create_table_sql_present(),
274 "executable_in_mvp": self.target_lifecycle_plan().executable_in_mvp(),
275 },
276 "sql_identity": self.sql_identity().to_json_value(),
277 "source_usage_status": self.source_usage_status().as_str(),
278 "used_source_names": self.used_source_names(),
279 "output_row_count": count_with_reason_value(
280 self.output_row_count().kind().as_str(),
281 self.output_row_count().value(),
282 self.output_row_count_reason()
283 ),
284 "validation_status": self.validation_status().to_json_value(),
285 "phase_timings": phase_timings_value(self.phase_timings()),
286 "dry_run": {
287 "sql_server_contacted": self.sql_server_contacted(),
288 "row_production_started": self.row_production_started(),
289 "table_lifecycle_started": self.table_lifecycle_started(),
290 "bulk_writer_started": self.bulk_writer_started(),
291 },
292 })
293 }
294}
295
296impl MssqlDryRunWorkflowReport {
297 #[must_use]
299 pub fn to_json_value(&self) -> Value {
300 json!({
301 "run_mode": run_mode(self.run_mode()),
302 "status": self.status().to_json_value(),
303 "output_count": self.len(),
304 "query_used_source_scan_metadata_exhausted": self.query_used_source_scan_metadata_exhausted(),
305 "sources": self.sources()
306 .iter()
307 .map(DeltaSourceReport::to_json_value)
308 .collect::<Vec<_>>(),
309 "outputs": self.outputs()
310 .iter()
311 .map(MssqlDryRunOutputReport::to_json_value)
312 .collect::<Vec<_>>(),
313 "phase_timings": phase_timings_value(self.phase_timings()),
314 "dry_run": {
315 "sql_server_contacted": self.sql_server_contacted(),
316 "row_production_started": self.row_production_started(),
317 "table_lifecycle_started": self.table_lifecycle_started(),
318 "bulk_writer_started": self.bulk_writer_started(),
319 },
320 })
321 }
322}
323
324impl MssqlOutputFieldReport {
325 #[must_use]
327 pub fn to_json_value(&self) -> Value {
328 json!({
329 "index": self.index(),
330 "name": self.name(),
331 "arrow_type": self.arrow_type(),
332 "nullable": self.nullable(),
333 })
334 }
335}
336
337impl MssqlWriteStats {
338 #[must_use]
340 pub fn to_json_value(&self) -> Value {
341 json!({
342 "output_name": self.output_name(),
343 "rows_written": self.rows_written(),
344 "batches_written": self.batches_written(),
345 "elapsed_ms": self.elapsed_ms(),
346 })
347 }
348}
349
350impl MssqlWriteReport {
351 #[must_use]
353 pub fn to_json_value(&self) -> Value {
354 json!({
355 "output_name": self.output_name(),
356 "run_mode": run_mode(RunMode::Execute),
357 "target_table": target_table_value(self.target_table()),
358 "load_mode": load_mode(self.load_mode()),
359 "connection_source": connection_source(self.connection_source()),
360 "connection": {
361 "display_label": self.connection().display_label(),
362 },
363 "output_schema": self.output_schema()
364 .iter()
365 .map(MssqlOutputFieldReport::to_json_value)
366 .collect::<Vec<_>>(),
367 "output_row_count": self.output_row_count().to_json_value(),
368 "target_row_count_before_write": self.target_row_count_before_write().to_json_value(),
369 "target_row_count_after_write": self.target_row_count_after_write().to_json_value(),
370 "target_row_count": self.target_row_count().to_json_value(),
371 "validation_status": self.validation_status().to_json_value(),
372 "batch_shaping": batch_shaping_value(self.batch_shaping()),
373 "phase_timings": phase_timings_value(self.phase_timings()),
374 "execution_profile": self
375 .execution_profile()
376 .map(QueryExecutionProfile::to_json_value),
377 "write_stats": self.stats().to_json_value(),
378 "partial_write_possible": self.partial_write_possible(),
379 "cleanup": cleanup_status(self.cleanup()),
380 })
381 }
382}
383
384impl MssqlOutputBatchValidationReport {
385 #[must_use]
387 pub fn to_json_value(&self) -> Value {
388 json!({
389 "output_name": self.output_name(),
390 "target_table": target_table_value(self.target_table()),
391 "load_mode": load_mode(self.load_mode()),
392 "connection_source": connection_source(self.connection_source()),
393 "connection": {
394 "display_label": self.connection().display_label(),
395 },
396 })
397 }
398}
399
400impl MssqlOutputWriteStatus {
401 #[must_use]
403 pub fn to_json_value(&self) -> Value {
404 match self {
405 Self::Succeeded(report) => json!({
406 "kind": "succeeded",
407 "output_name": self.output_name(),
408 "target_table": target_table_value(self.target_table()),
409 "load_mode": load_mode(self.load_mode()),
410 "connection_source": connection_source(self.connection_source()),
411 "output_row_count": self.output_row_count().to_json_value(),
412 "target_row_count": self.target_row_count().to_json_value(),
413 "validation_status": self.validation_status().to_json_value(),
414 "batch_shaping": batch_shaping_value(self.batch_shaping()),
415 "phase_timings": phase_timings_value(self.phase_timings()),
416 "report": report.to_json_value(),
417 }),
418 Self::Failed(report) => json!({
419 "kind": "failed",
420 "output_name": self.output_name(),
421 "target_table": target_table_value(self.target_table()),
422 "load_mode": load_mode(self.load_mode()),
423 "connection_source": connection_source(self.connection_source()),
424 "output_row_count": self.output_row_count().to_json_value(),
425 "target_row_count": self.target_row_count().to_json_value(),
426 "validation_status": self.validation_status().to_json_value(),
427 "batch_shaping": batch_shaping_value(self.batch_shaping()),
428 "phase_timings": phase_timings_value(self.phase_timings()),
429 "failure": report.to_json_value(),
430 }),
431 Self::Skipped(report) => json!({
432 "kind": "skipped",
433 "output_name": self.output_name(),
434 "target_table": target_table_value(self.target_table()),
435 "load_mode": load_mode(self.load_mode()),
436 "connection_source": connection_source(self.connection_source()),
437 "output_row_count": self.output_row_count().to_json_value(),
438 "target_row_count": self.target_row_count().to_json_value(),
439 "validation_status": self.validation_status().to_json_value(),
440 "batch_shaping": batch_shaping_value(self.batch_shaping()),
441 "phase_timings": phase_timings_value(self.phase_timings()),
442 "skipped": report.to_json_value(),
443 }),
444 }
445 }
446}
447
448impl MssqlWorkflowWriteReport {
449 #[must_use]
451 pub fn to_json_value(&self) -> Value {
452 json!({
453 "output_count": self.len(),
454 "all_succeeded": self.all_succeeded(),
455 "succeeded_count": self.succeeded_count(),
456 "failed_count": self.failed_count(),
457 "skipped_count": self.skipped_count(),
458 "outputs": self.outputs()
459 .iter()
460 .map(MssqlOutputWriteStatus::to_json_value)
461 .collect::<Vec<_>>(),
462 })
463 }
464}
465
466impl MssqlWriteFailureReport {
467 #[must_use]
469 pub fn to_json_value(&self) -> Value {
470 json!({
471 "output_name": self.output_name(),
472 "target_table": target_table_value(self.target().table()),
473 "load_mode": load_mode(self.target().load_mode()),
474 "connection_source": connection_source(self.target().connection_source()),
475 "error": self.error(),
476 "context": self.context().map(MssqlWriteFailureContext::to_json_value),
477 "output_row_count": self.output_row_count().to_json_value(),
478 "target_row_count": self.target_row_count().to_json_value(),
479 "validation_status": self.validation_status().to_json_value(),
480 "batch_shaping": batch_shaping_value(self.batch_shaping()),
481 "phase_timings": phase_timings_value(self.phase_timings()),
482 })
483 }
484}
485
486impl MssqlWriteFailureContext {
487 #[must_use]
489 pub fn to_json_value(&self) -> Value {
490 json!({
491 "phase": write_phase(self.phase()),
492 "output_name": self.output_name(),
493 "target_table": target_table_value(self.target_table()),
494 "load_mode": load_mode(self.load_mode()),
495 "connection_source": connection_source(self.connection_source()),
496 "connection": {
497 "display_label": self.connection().display_label(),
498 },
499 "write_stats": self.stats().to_json_value(),
500 "output_row_count": self.output_row_count().to_json_value(),
501 "target_row_count_before_write": self.target_row_count_before_write().to_json_value(),
502 "target_row_count_after_write": self.target_row_count_after_write().to_json_value(),
503 "target_row_count": self.target_row_count().to_json_value(),
504 "validation_status": self.validation_status().to_json_value(),
505 "batch_shaping": batch_shaping_value(self.batch_shaping()),
506 "partial_write_possible": self.partial_write_possible(),
507 "cleanup": cleanup_status(self.cleanup()),
508 "cleanup_error": self.cleanup_error(),
509 "diagnostics": self.diagnostics()
510 .iter()
511 .map(write_diagnostic_value)
512 .collect::<Vec<_>>(),
513 "phase_timings": phase_timings_value(self.phase_timings()),
514 "report": self.report().to_json_value(),
515 })
516 }
517}
518
519impl MssqlWriteSkippedReport {
520 #[must_use]
522 pub fn to_json_value(&self) -> Value {
523 json!({
524 "output_name": self.output_name(),
525 "target_table": target_table_value(self.target().table()),
526 "load_mode": load_mode(self.target().load_mode()),
527 "connection_source": connection_source(self.target().connection_source()),
528 "reason": skipped_reason_value(self.reason()),
529 "output_row_count": self.output_row_count().to_json_value(),
530 "target_row_count": self.target_row_count().to_json_value(),
531 "validation_status": self.validation_status().to_json_value(),
532 "batch_shaping": batch_shaping_value(self.batch_shaping()),
533 "phase_timings": phase_timings_value(self.phase_timings()),
534 "execution_profile": Value::Null,
535 })
536 }
537}
538
539impl WriteAllReport {
540 #[must_use]
542 pub fn to_json_value(&self) -> Value {
543 json!({
544 "workflow": self.workflow().to_json_value(),
545 "cache": self.cache().to_json_value(),
546 "sources": self.sources()
547 .iter()
548 .map(DeltaSourceReport::to_json_value)
549 .collect::<Vec<_>>(),
550 "phase_timings": phase_timings_value(self.phase_timings()),
551 "output_count": self.len(),
552 "all_succeeded": self.all_succeeded(),
553 "succeeded_count": self.succeeded_count(),
554 "failed_count": self.failed_count(),
555 "skipped_count": self.skipped_count(),
556 })
557 }
558}
559
560impl WriteAllCacheReport {
561 #[must_use]
563 pub fn to_json_value(&self) -> Value {
564 match self {
565 Self::Disabled => json!({
566 "kind": "disabled",
567 "reason": null,
568 "aliases": [],
569 "skipped_candidates": [],
570 }),
571 Self::NoCache {
572 reason,
573 skipped_candidates,
574 } => json!({
575 "kind": "no_cache",
576 "reason": no_cache_reason(*reason),
577 "aliases": [],
578 "skipped_candidates": skipped_candidates
579 .iter()
580 .map(WriteAllCacheCandidateSkip::to_json_value)
581 .collect::<Vec<_>>(),
582 }),
583 Self::CacheAliases {
584 aliases,
585 skipped_candidates,
586 } => json!({
587 "kind": "cache_aliases",
588 "reason": null,
589 "aliases": aliases
590 .iter()
591 .map(WriteAllCacheAliasReport::to_json_value)
592 .collect::<Vec<_>>(),
593 "skipped_candidates": skipped_candidates
594 .iter()
595 .map(WriteAllCacheCandidateSkip::to_json_value)
596 .collect::<Vec<_>>(),
597 }),
598 }
599 }
600}
601
602impl WriteAllCacheAliasReport {
603 #[must_use]
605 pub fn to_json_value(&self) -> Value {
606 match self.status() {
607 WriteAllCacheAliasStatus::Selected => json!({
608 "table_id": self.table_id(),
609 "alias": self.alias(),
610 "output_indexes": self.output_indexes(),
611 "status": self.status().as_str(),
612 }),
613 WriteAllCacheAliasStatus::MaterializedAndRestored
614 | WriteAllCacheAliasStatus::Failed => json!({
615 "table_id": self.table_id(),
616 "alias": self.alias(),
617 "output_indexes": self.output_indexes(),
618 "status": self.status().as_str(),
619 "phase_timings": phase_timings_value(self.phase_timings()),
620 "failed_phase": self.failed_phase(),
621 "execution_profile": self
622 .execution_profile()
623 .map(QueryExecutionProfile::to_json_value),
624 }),
625 }
626 }
627}
628
629impl WriteAllCacheFailure {
630 #[must_use]
632 pub fn to_json_value(&self) -> Value {
633 json!({
634 "aliases": self
635 .aliases()
636 .iter()
637 .map(WriteAllCacheAliasReport::to_json_value)
638 .collect::<Vec<_>>(),
639 "primary_failed_alias_table_id": self.primary_failed_alias_table_id(),
640 "workflow": self.workflow().map(MssqlWorkflowWriteReport::to_json_value),
641 })
642 }
643}
644
645impl WriteAllCacheCandidateSkip {
646 #[must_use]
648 pub fn to_json_value(&self) -> Value {
649 json!({
650 "table_id": self.table_id(),
651 "alias": self.alias(),
652 "reason": cache_candidate_skip_reason(self.reason()),
653 })
654 }
655}
656
657fn count_value(kind: &str, value: Option<u64>) -> Value {
658 json!({
659 "kind": kind,
660 "value": value,
661 })
662}
663
664fn count_with_reason_value(
665 kind: &str,
666 value: Option<u64>,
667 reason: Option<ReportReasonCode>,
668) -> Value {
669 json!({
670 "kind": kind,
671 "value": value,
672 "reason": reason_value(reason),
673 })
674}
675
676fn phase_timings_value(timings: &[PhaseTimingReport]) -> Vec<Value> {
677 timings
678 .iter()
679 .map(PhaseTimingReport::to_json_value)
680 .collect()
681}
682
683fn status_value(kind: &str, reason: Option<ReportReasonCode>) -> Value {
684 json!({
685 "kind": kind,
686 "reason": reason_value(reason),
687 })
688}
689
690fn reason_value(reason: Option<ReportReasonCode>) -> Option<&'static str> {
691 reason.map(ReportReasonCode::as_str)
692}
693
694fn run_mode(mode: RunMode) -> &'static str {
695 match mode {
696 RunMode::Execute => "execute",
697 RunMode::DryRun => "dry_run",
698 }
699}
700
701fn lazy_table_kind(kind: LazyTableKind) -> &'static str {
702 match kind {
703 LazyTableKind::DeltaSource => "delta_source",
704 LazyTableKind::DerivedSql => "derived_sql",
705 }
706}
707
708fn load_mode(mode: LoadMode) -> &'static str {
709 match mode {
710 LoadMode::AppendExisting => "append_existing",
711 LoadMode::CreateAndLoad => "create_and_load",
712 LoadMode::Replace => "replace",
713 }
714}
715
716fn target_table_value(table: &MssqlTargetTable) -> Value {
717 json!({
718 "schema": table.schema(),
719 "table": table.table(),
720 })
721}
722
723fn connection_source(source: crate::MssqlConnectionSource) -> &'static str {
724 match source {
725 crate::MssqlConnectionSource::TargetOverride => "target_override",
726 crate::MssqlConnectionSource::ContextDefault => "context_default",
727 }
728}
729
730fn cleanup_status(status: MssqlTargetCleanupStatus) -> &'static str {
731 match status {
732 MssqlTargetCleanupStatus::NotApplicable => "not_applicable",
733 MssqlTargetCleanupStatus::NotAttempted => "not_attempted",
734 MssqlTargetCleanupStatus::Succeeded => "succeeded",
735 MssqlTargetCleanupStatus::Failed => "failed",
736 }
737}
738
739fn write_phase(phase: MssqlWritePhase) -> &'static str {
740 match phase {
741 MssqlWritePhase::QueryDataFramePlanning => "query_dataframe_planning",
742 MssqlWritePhase::QueryPhysicalPlanning => "query_physical_planning",
743 MssqlWritePhase::QueryStreamSetup => "query_stream_setup",
744 MssqlWritePhase::Connect => "connect",
745 MssqlWritePhase::PrepareTargetLifecycle => "prepare_target_lifecycle",
746 MssqlWritePhase::InitializeWriter => "initialize_writer",
747 MssqlWritePhase::PollBatchStream => "poll_batch_stream",
748 MssqlWritePhase::ValidateBatchSchema => "validate_batch_schema",
749 MssqlWritePhase::WriteBatch => "write_batch",
750 MssqlWritePhase::Finalize => "finalize",
751 MssqlWritePhase::Validation => "validation",
752 MssqlWritePhase::SwapTarget => "swap_target",
753 MssqlWritePhase::Cleanup => "cleanup",
754 }
755}
756
757fn write_diagnostic_value(
758 diagnostic: &crate::report::sql_server::write::MssqlWriteDiagnostic,
759) -> Value {
760 json!({
761 "severity": diagnostic_severity(diagnostic.severity()),
762 "code": diagnostic_code(diagnostic.code()),
763 "message": diagnostic.message(),
764 "field": diagnostic.field().map(|field| json!({
765 "index": field.index(),
766 "name": field.name(),
767 })),
768 "row": diagnostic.row(),
769 })
770}
771
772fn diagnostic_severity(severity: arrow_sql_server::DiagnosticSeverity) -> &'static str {
773 match severity {
774 arrow_sql_server::DiagnosticSeverity::Warning => "warning",
775 arrow_sql_server::DiagnosticSeverity::Error => "error",
776 }
777}
778
779fn diagnostic_code(code: arrow_sql_server::DiagnosticCode) -> &'static str {
780 match code {
781 arrow_sql_server::DiagnosticCode::UnsupportedArrowType => "unsupported_arrow_type",
782 arrow_sql_server::DiagnosticCode::LossyConversionRequiresPolicy => {
783 "lossy_conversion_requires_policy"
784 }
785 arrow_sql_server::DiagnosticCode::PolicyApplied => "policy_applied",
786 arrow_sql_server::DiagnosticCode::IdentifierInvalid => "identifier_invalid",
787 arrow_sql_server::DiagnosticCode::IdentifierTooLong => "identifier_too_long",
788 arrow_sql_server::DiagnosticCode::DecimalOutOfRange => "decimal_out_of_range",
789 arrow_sql_server::DiagnosticCode::IntegerOutOfRange => "integer_out_of_range",
790 arrow_sql_server::DiagnosticCode::TimestampOutOfRange => "timestamp_out_of_range",
791 arrow_sql_server::DiagnosticCode::TimezoneUnsupported => "timezone_unsupported",
792 arrow_sql_server::DiagnosticCode::SchemaMismatch => "schema_mismatch",
793 arrow_sql_server::DiagnosticCode::BackendUnavailable => "backend_unavailable",
794 arrow_sql_server::DiagnosticCode::ProfileDependentConversion => {
795 "profile_dependent_conversion"
796 }
797 arrow_sql_server::DiagnosticCode::ObservedDataRequired => "observed_data_required",
798 arrow_sql_server::DiagnosticCode::ValueConversionUnsupported => {
799 "value_conversion_unsupported"
800 }
801 arrow_sql_server::DiagnosticCode::ValueTypeMismatch => "value_type_mismatch",
802 arrow_sql_server::DiagnosticCode::NullInNonNullableColumn => "null_in_non_nullable_column",
803 arrow_sql_server::DiagnosticCode::NonFiniteFloat => "non_finite_float",
804 arrow_sql_server::DiagnosticCode::ValueTooLong => "value_too_long",
805 arrow_sql_server::DiagnosticCode::RowIndexOutOfBounds => "row_index_out_of_bounds",
806 arrow_sql_server::DiagnosticCode::DirectEncodingInvalidPayload => {
807 "direct_encoding_invalid_payload"
808 }
809 arrow_sql_server::DiagnosticCode::DirectEncodingUnsupportedMapping => {
810 "direct_encoding_unsupported_mapping"
811 }
812 arrow_sql_server::DiagnosticCode::DirectEncodingUnsupportedBatch => {
813 "direct_encoding_unsupported_batch"
814 }
815 _ => "unknown",
816 }
817}
818
819fn batch_shaping_value(report: crate::MssqlBatchShapingReport) -> Value {
820 json!({
821 "status": report.status().to_json_value(),
822 "input_batches": report.input_batches(),
823 "input_rows": report.input_rows(),
824 "output_batches": report.output_batches(),
825 "output_rows": report.output_rows(),
826 })
827}
828
829fn provider_read_stats_value(
830 stats: &DeltaDataFusionMetricsSnapshot,
831 source_name: Option<&str>,
832) -> Value {
833 json!({
834 "source_name": source_name.unwrap_or(""),
835 "snapshot_version": stats.reader.snapshot_version,
836 "reader_backend": reader_backend(stats.reader.reader_backend),
837 "use_view_types": stats.use_view_types,
838 "scan_metadata_exhausted": stats.reader.scan_metadata_exhausted,
839 "scan_partitions_planned": stats.reader.scan_partitions_planned,
840 "files_planned": stats.reader.files_planned,
841 "approximate_files_filtered_during_planning": stats.reader.files_filtered_during_planning,
842 "estimated_rows": stats.reader.estimated_rows,
843 "estimated_bytes": stats.reader.estimated_bytes,
844 "parquet_data_file_range_get_operations": stats.reader.parquet_data_file_range_get_operations,
845 "parquet_data_file_full_get_operations": stats.reader.parquet_data_file_full_get_operations,
846 "parquet_data_file_bytes_received": stats.reader.parquet_data_file_bytes_received,
847 "parquet_data_file_opened_bytes": stats.reader.parquet_data_file_opened_bytes,
848 "datafusion_output_batch_size": stats.output_batch_size,
849 "scan_partitions_started": stats.reader.scan_partitions_started,
850 "scan_partitions_completed": stats.reader.scan_partitions_completed,
851 "files_started": stats.reader.files_started,
852 "files_completed": stats.reader.files_completed,
853 "dynamic_partition_files_pruned": stats.dynamic_partition_files_pruned,
854 "dynamic_partition_files_kept": stats.dynamic_partition_files_kept,
855 "dynamic_filters_received": stats.dynamic_filters_received,
856 "dynamic_filters_accepted": stats.dynamic_filters_accepted,
857 "dynamic_filters_unsupported": stats.dynamic_filters_unsupported,
858 "dynamic_filter_snapshots": stats.dynamic_filter_snapshots,
859 "dynamic_partition_files_not_pruned_missing_metadata": stats.dynamic_files_not_pruned_missing_metadata,
860 "dynamic_partition_files_not_pruned_unsupported_expression": stats.dynamic_files_not_pruned_unsupported_expression,
861 "batches_produced": stats.reader.batches_produced,
862 "rows_produced": stats.reader.rows_produced,
863 "deletion_vector_payloads_loaded": stats.reader.deletion_vector_payloads_loaded,
864 "deletion_vectors_applied": stats.reader.deletion_vectors_applied,
865 "deletion_vector_rows_deleted": stats.reader.deletion_vector_rows_deleted,
866 "deletion_vector_failures": stats.reader.deletion_vector_failures,
867 "deletion_vector_rejections": stats.reader.deletion_vector_rejections,
868 })
869}
870
871fn execution_metric_value(value: &QueryExecutionMetricValue) -> (Value, Value) {
872 let no_components = Value::Null;
873
874 match value {
875 QueryExecutionMetricValue::Count(value)
876 | QueryExecutionMetricValue::Bytes(value)
877 | QueryExecutionMetricValue::Nanoseconds(value)
878 | QueryExecutionMetricValue::Gauge(value)
879 | QueryExecutionMetricValue::Custom(value) => (json!(value), no_components),
880 QueryExecutionMetricValue::TimestampNanoseconds(value) => (json!(value), no_components),
881 QueryExecutionMetricValue::Pruning {
882 pruned,
883 matched,
884 fully_matched,
885 } => (
886 Value::Null,
887 json!({
888 "pruned": pruned,
889 "matched": matched,
890 "fully_matched": fully_matched,
891 }),
892 ),
893 QueryExecutionMetricValue::Ratio { part, total } => (
894 Value::Null,
895 json!({
896 "part": part,
897 "total": total,
898 }),
899 ),
900 }
901}
902
903fn skipped_reason_value(reason: &MssqlWriteSkippedReason) -> Value {
904 match reason {
905 MssqlWriteSkippedReason::PreviousOutputFailed { failed_output_name } => json!({
906 "kind": "previous_output_failed",
907 "failed_output_name": failed_output_name,
908 }),
909 }
910}
911
912fn reader_backend(backend: ParquetReaderBackend) -> &'static str {
913 match backend {
914 ParquetReaderBackend::DeltaKernel => "official_kernel",
915 ParquetReaderBackend::Direct => "native_async",
916 }
917}
918
919fn no_cache_reason(reason: WriteAllNoCacheReason) -> &'static str {
920 match reason {
921 WriteAllNoCacheReason::FewerThanTwoOutputs => "fewer_than_two_outputs",
922 WriteAllNoCacheReason::NoSharedRegisteredDerivedAlias => {
923 "no_shared_registered_derived_alias"
924 }
925 WriteAllNoCacheReason::AmbiguousSharedDerivedAlias => "ambiguous_shared_derived_alias",
926 }
927}
928
929fn cache_candidate_skip_reason(reason: &WriteAllCacheCandidateSkipReason) -> Value {
930 match reason {
931 WriteAllCacheCandidateSkipReason::NotShared { output_count } => json!({
932 "kind": "not_shared",
933 "output_count": output_count,
934 }),
935 WriteAllCacheCandidateSkipReason::MissingSqlText => json!({
936 "kind": "missing_sql_text",
937 }),
938 WriteAllCacheCandidateSkipReason::IncompleteLineage => json!({
939 "kind": "incomplete_lineage",
940 }),
941 WriteAllCacheCandidateSkipReason::CoveredByDeeperSharedAlias { selected_table_id } => {
942 json!({
943 "kind": "covered_by_deeper_shared_alias",
944 "selected_table_id": selected_table_id,
945 })
946 }
947 WriteAllCacheCandidateSkipReason::AmbiguousDepth => json!({
948 "kind": "ambiguous_depth",
949 }),
950 WriteAllCacheCandidateSkipReason::NotExplicitlySelected => json!({
951 "kind": "not_explicitly_selected",
952 }),
953 }
954}
955
956#[cfg(test)]
957mod tests {
958 use std::{collections::VecDeque, error::Error, sync::Arc, time::Duration};
959
960 use async_trait::async_trait;
961 use futures_util::stream;
962 use serde_json::{Value, json};
963
964 use super::*;
965 use crate::MssqlWorkflowOutputWriter;
966 use crate::{
967 DeltaProtocolReport, DeltaProviderSchedulingReport, MssqlConnectionConfig,
968 MssqlOutputBatchStream, MssqlOutputWriteJob, MssqlSchemaPlanOptions, MssqlTargetConfig,
969 MssqlTargetOutputPlan, MssqlTargetResolutionContext, MssqlWorkflowWriteOptions,
970 MssqlWriteBackend, QueryOptions, ResolvedMssqlTarget, ValidationOptions,
971 plan_mssql_target_for_output, write_mssql_outputs_with_writer,
972 };
973 use arrow_schema::{DataType, Field, Schema, SchemaRef};
974 use arrow_sql_server::PlanOptions;
975 use delta_arrow_reader::DeltaScanExecutionOptions;
976
977 type TestResult<T> = Result<T, Box<dyn Error + Send + Sync + 'static>>;
978
979 struct FakeWorkflowWriter {
980 outcomes: VecDeque<Result<MssqlWriteReport, crate::DeltaFunnelError>>,
981 }
982
983 impl FakeWorkflowWriter {
984 fn new(outcomes: Vec<Result<MssqlWriteReport, crate::DeltaFunnelError>>) -> Self {
985 Self {
986 outcomes: outcomes.into(),
987 }
988 }
989 }
990
991 #[async_trait]
992 impl MssqlWorkflowOutputWriter for FakeWorkflowWriter {
993 async fn write_output(
994 &mut self,
995 _output_schema: SchemaRef,
996 _resolved_target: ResolvedMssqlTarget,
997 _schema_options: MssqlSchemaPlanOptions,
998 _batches: MssqlOutputBatchStream,
999 _write_backend: MssqlWriteBackend,
1000 _validation_options: ValidationOptions,
1001 _reporter: Option<&crate::progress::ProgressReporter>,
1002 _stage_context: crate::profiling::OperationStageContext<'_>,
1003 ) -> Result<MssqlWriteReport, crate::DeltaFunnelError> {
1004 self.outcomes.pop_front().ok_or_else(|| {
1005 crate::DeltaFunnelError::MssqlWorkflowPlanning {
1006 message: "missing fake writer outcome".to_owned(),
1007 }
1008 })?
1009 }
1010 }
1011
1012 #[test]
1013 fn row_count_json_preserves_kind_and_value() {
1014 assert_eq!(
1015 RowCount::exact(3).to_json_value(),
1016 json!({"kind": "exact", "value": 3})
1017 );
1018 assert_eq!(
1019 RowCount::estimated(5).to_json_value(),
1020 json!({"kind": "estimated", "value": 5})
1021 );
1022 assert_eq!(
1023 RowCount::partial(2).to_json_value(),
1024 json!({"kind": "partial", "value": 2})
1025 );
1026 assert_eq!(
1027 RowCount::unavailable().to_json_value(),
1028 json!({"kind": "unavailable", "value": null})
1029 );
1030 }
1031
1032 #[test]
1033 fn file_count_json_preserves_non_numeric_kinds() {
1034 assert_eq!(
1035 FileCount::skipped().to_json_value(),
1036 json!({"kind": "skipped", "value": null})
1037 );
1038 assert_eq!(
1039 FileCount::not_executed().to_json_value(),
1040 json!({"kind": "not_executed", "value": null})
1041 );
1042 }
1043
1044 #[test]
1045 fn status_json_preserves_stable_kind_and_reason_strings() {
1046 assert_eq!(
1047 ValidationStatus::skipped(ReportReasonCode::DryRun).to_json_value(),
1048 json!({"kind": "skipped", "reason": "dry_run"})
1049 );
1050 assert_eq!(
1051 PhaseStatus::not_started(ReportReasonCode::NotExecuted).to_json_value(),
1052 json!({"kind": "not_started", "reason": "not_executed"})
1053 );
1054 assert_eq!(
1055 WorkflowStatus::no_op(ReportReasonCode::NotExecuted).to_json_value(),
1056 json!({"kind": "no_op", "reason": "not_executed"})
1057 );
1058 }
1059
1060 #[test]
1061 fn output_status_json_preserves_nested_validation_status() {
1062 assert_eq!(
1063 OutputStatus::validation_failed(ValidationStatus::required_but_failed(
1064 ReportReasonCode::MissingExactOutputRows
1065 ))
1066 .to_json_value(),
1067 json!({
1068 "kind": "validation_failed",
1069 "reason": null,
1070 "validation": {
1071 "kind": "required_but_failed",
1072 "reason": "missing_exact_output_rows"
1073 }
1074 })
1075 );
1076 }
1077
1078 #[test]
1079 fn phase_timing_json_is_json_round_trippable() -> Result<(), serde_json::Error> {
1080 let value =
1081 PhaseTimingReport::completed("load_sources", Duration::from_micros(42)).to_json_value();
1082
1083 assert_eq!(
1084 value,
1085 json!({
1086 "phase_name": "load_sources",
1087 "status": {"kind": "completed", "reason": null},
1088 "elapsed_micros": 42
1089 })
1090 );
1091 serde_json::from_str::<Value>(&serde_json::to_string(&value)?).map(|_| ())
1092 }
1093
1094 #[test]
1095 fn execute_write_report_json_exposes_stats_counts_and_validation() -> TestResult<()> {
1096 let output_plan = output_plan()?;
1097 let report = MssqlWriteReport::from_output_plan(
1098 &output_plan,
1099 42,
1100 3,
1101 125,
1102 false,
1103 MssqlTargetCleanupStatus::NotApplicable,
1104 )
1105 .with_target_delta_validation(
1106 RowCount::exact(10),
1107 RowCount::exact(52),
1108 ValidationStatus::passed(),
1109 PhaseTimingReport::completed("mssql_target_validation", Duration::from_micros(7)),
1110 );
1111
1112 let value = report.to_json_value();
1113
1114 assert_eq!(value["run_mode"], "execute");
1115 assert!(value.get("status").is_none());
1116 assert_eq!(value["output_name"], "orders_output");
1117 assert_eq!(value["target_table"]["schema"], "dbo");
1118 assert_eq!(value["target_table"]["table"], "orders");
1119 assert_eq!(value["connection_source"], "context_default");
1120 assert_eq!(value["connection"]["display_label"], "warehouse");
1121 assert_eq!(value["output_schema"][0]["name"], "id");
1122 assert_eq!(
1123 value["output_row_count"],
1124 json!({"kind": "exact", "value": 42})
1125 );
1126 assert_eq!(
1127 value["target_row_count_before_write"],
1128 json!({"kind": "exact", "value": 10})
1129 );
1130 assert_eq!(
1131 value["target_row_count_after_write"],
1132 json!({"kind": "exact", "value": 52})
1133 );
1134 assert_eq!(
1135 value["validation_status"],
1136 json!({"kind": "passed", "reason": null})
1137 );
1138 assert_eq!(value["batch_shaping"]["input_batches"], 3);
1139 assert_eq!(value["batch_shaping"]["input_rows"], 42);
1140 assert_eq!(value["write_stats"]["rows_written"], 42);
1141 assert_eq!(value["write_stats"]["batches_written"], 3);
1142 assert_eq!(value["write_stats"]["elapsed_ms"], 125);
1143 assert_eq!(report.execution_profile(), None);
1144 assert!(value["execution_profile"].is_null());
1145 assert!(value.get("operation_timeline").is_none());
1146 assert_eq!(value["cleanup"], "not_applicable");
1147 assert_json_safe(&value)?;
1148 assert_no_secret_or_raw_sql_text(&value);
1149
1150 Ok(())
1151 }
1152
1153 #[test]
1154 fn batch_validation_report_json_exposes_safe_target_context() -> TestResult<()> {
1155 let value =
1156 MssqlOutputBatchValidationReport::from_output_plan(&output_plan()?).to_json_value();
1157
1158 assert_eq!(value["output_name"], "orders_output");
1159 assert_eq!(
1160 value["target_table"],
1161 json!({"schema": "dbo", "table": "orders"})
1162 );
1163 assert_eq!(value["connection_source"], "context_default");
1164 assert_eq!(value["connection"]["display_label"], "warehouse");
1165 assert_json_safe(&value)?;
1166 assert_no_secret_or_raw_sql_text(&value);
1167
1168 Ok(())
1169 }
1170
1171 #[test]
1172 fn workflow_output_status_json_wraps_successful_write_report() -> TestResult<()> {
1173 let output_plan = output_plan()?;
1174 let report = MssqlWriteReport::from_output_plan(
1175 &output_plan,
1176 7,
1177 1,
1178 25,
1179 false,
1180 MssqlTargetCleanupStatus::NotApplicable,
1181 );
1182
1183 let value = MssqlOutputWriteStatus::Succeeded(report).to_json_value();
1184
1185 assert_eq!(value["kind"], "succeeded");
1186 assert_eq!(value["output_name"], "orders_output");
1187 assert_eq!(
1188 value["output_row_count"],
1189 json!({"kind": "exact", "value": 7})
1190 );
1191 assert_eq!(value["report"]["write_stats"]["rows_written"], 7);
1192 assert!(value.get("execution_profile").is_none());
1193 assert!(value["report"]["execution_profile"].is_null());
1194 assert_json_safe(&value)?;
1195 assert_no_secret_or_raw_sql_text(&value);
1196
1197 Ok(())
1198 }
1199
1200 #[test]
1201 fn failure_context_json_exposes_structured_context_without_success_status() -> TestResult<()> {
1202 let output_plan = output_plan()?;
1203 let context = MssqlWriteFailureContext::from_output_plan(
1204 &output_plan,
1205 MssqlWritePhase::WriteBatch,
1206 4,
1207 1,
1208 25,
1209 true,
1210 MssqlTargetCleanupStatus::Failed,
1211 );
1212
1213 let value = context.to_json_value();
1214
1215 assert_eq!(value["phase"], "write_batch");
1216 assert_eq!(
1217 value["output_row_count"],
1218 json!({"kind": "partial", "value": 4})
1219 );
1220 assert_eq!(value["partial_write_possible"], true);
1221 assert_eq!(value["cleanup"], "failed");
1222 assert!(value.get("execution_profile").is_none());
1223 assert!(value["report"]["execution_profile"].is_null());
1224 assert!(value["report"].get("operation_timeline").is_none());
1225 assert!(value["report"].get("status").is_none());
1226 assert_json_safe(&value)?;
1227 assert_no_secret_or_raw_sql_text(&value);
1228
1229 Ok(())
1230 }
1231
1232 #[tokio::test]
1233 async fn workflow_json_covers_real_failed_and_skipped_statuses() -> TestResult<()> {
1234 let first = output_plan_named("first_output")?;
1235 let second = output_plan_named("second_output")?;
1236 let third = output_plan_named("third_output")?;
1237 let first_report = MssqlWriteReport::from_output_plan(
1238 &first,
1239 7,
1240 1,
1241 25,
1242 false,
1243 MssqlTargetCleanupStatus::NotApplicable,
1244 );
1245 let failure_context = MssqlWriteFailureContext::from_output_plan(
1246 &second,
1247 MssqlWritePhase::WriteBatch,
1248 4,
1249 1,
1250 25,
1251 true,
1252 MssqlTargetCleanupStatus::NotApplicable,
1253 );
1254 let failure = crate::DeltaFunnelError::MssqlWritePhase {
1255 context: Box::new(failure_context),
1256 message: "failed to write output".to_owned(),
1257 };
1258 let writer = FakeWorkflowWriter::new(vec![Ok(first_report), Err(failure)]);
1259
1260 let report = write_mssql_outputs_with_writer(
1261 vec![job(first)?, job(second)?, job(third)?],
1262 MssqlWorkflowWriteOptions::default(),
1263 writer,
1264 )
1265 .await?;
1266
1267 let value = report.to_json_value();
1268
1269 assert_eq!(value["succeeded_count"], 1);
1270 assert_eq!(value["failed_count"], 1);
1271 assert_eq!(value["skipped_count"], 1);
1272 assert_eq!(value["outputs"][0]["kind"], "succeeded");
1273 assert_eq!(value["outputs"][1]["kind"], "failed");
1274 assert_eq!(
1275 value["outputs"][1]["failure"]["context"]["phase"],
1276 "write_batch"
1277 );
1278 assert_eq!(
1279 value["outputs"][1]["failure"]["context"]["output_row_count"],
1280 json!({"kind": "partial", "value": 4})
1281 );
1282 assert_eq!(value["outputs"][2]["kind"], "skipped");
1283 assert!(value["outputs"][2].get("execution_profile").is_none());
1284 assert!(value["outputs"][2]["skipped"]["execution_profile"].is_null());
1285 assert_eq!(
1286 value["outputs"][2]["skipped"]["reason"],
1287 json!({
1288 "kind": "previous_output_failed",
1289 "failed_output_name": "second_output"
1290 })
1291 );
1292 assert_json_safe(&value)?;
1293 assert_no_secret_or_raw_sql_text(&value);
1294
1295 Ok(())
1296 }
1297
1298 #[test]
1299 fn write_all_cache_json_preserves_decision_aliases_and_skip_reasons() -> TestResult<()> {
1300 let value = WriteAllCacheReport::CacheAliases {
1301 aliases: vec![WriteAllCacheAliasReport::executed(
1302 9,
1303 "shared_orders",
1304 vec![0, 2],
1305 WriteAllCacheAliasStatus::MaterializedAndRestored,
1306 vec![PhaseTimingReport::completed(
1307 "cache_alias_restore",
1308 Duration::from_micros(3),
1309 )],
1310 None,
1311 )],
1312 skipped_candidates: vec![
1313 WriteAllCacheCandidateSkip::new(
1314 7,
1315 "lonely_orders",
1316 WriteAllCacheCandidateSkipReason::NotShared { output_count: 1 },
1317 ),
1318 WriteAllCacheCandidateSkip::new(
1319 8,
1320 "missing_sql_orders",
1321 WriteAllCacheCandidateSkipReason::MissingSqlText,
1322 ),
1323 WriteAllCacheCandidateSkip::new(
1324 10,
1325 "unselected_orders",
1326 WriteAllCacheCandidateSkipReason::NotExplicitlySelected,
1327 ),
1328 ],
1329 }
1330 .to_json_value();
1331
1332 assert_eq!(value["kind"], "cache_aliases");
1333 assert_eq!(value["aliases"][0]["alias"], "shared_orders");
1334 assert_eq!(value["aliases"][0]["status"], "materialized_and_restored");
1335 assert!(value["aliases"][0]["execution_profile"].is_null());
1336 assert!(value["aliases"][0].get("operation_timeline").is_none());
1337 assert_eq!(
1338 value["aliases"][0]["phase_timings"][0]["phase_name"],
1339 "cache_alias_restore"
1340 );
1341 assert!(value["aliases"][0]["failed_phase"].is_null());
1342 assert_eq!(
1343 value["skipped_candidates"][0]["reason"],
1344 json!({"kind": "not_shared", "output_count": 1})
1345 );
1346 assert_eq!(
1347 value["skipped_candidates"][1]["reason"],
1348 json!({"kind": "missing_sql_text"})
1349 );
1350 assert_eq!(
1351 value["skipped_candidates"][2]["reason"],
1352 json!({"kind": "not_explicitly_selected"})
1353 );
1354 assert_json_safe(&value)?;
1355 assert_no_secret_or_raw_sql_text(&value);
1356
1357 Ok(())
1358 }
1359
1360 #[test]
1361 fn write_all_cache_alias_json_separates_plan_and_failed_execution_fields() {
1362 let selected = WriteAllCacheAliasReport::selected(9, "shared_orders", vec![0, 2]);
1363 let selected_value = selected.to_json_value();
1364 assert_eq!(selected.status().to_string(), "selected");
1365 assert!(selected.phase_timings().is_empty());
1366 assert_eq!(selected.failed_phase(), None);
1367 assert_eq!(selected.execution_profile(), None);
1368 assert!(selected_value.get("phase_timings").is_none());
1369 assert!(selected_value.get("failed_phase").is_none());
1370 assert!(selected_value.get("execution_profile").is_none());
1371
1372 let failed = WriteAllCacheAliasReport::executed(
1373 9,
1374 "shared_orders",
1375 vec![0, 2],
1376 WriteAllCacheAliasStatus::Failed,
1377 vec![PhaseTimingReport::failed(
1378 "cache_alias_install",
1379 Duration::from_micros(4),
1380 )],
1381 Some("cache_alias_install"),
1382 );
1383 let failed_value = failed.to_json_value();
1384 assert_eq!(failed.status().to_string(), "failed");
1385 assert_eq!(failed_value["status"], "failed");
1386 assert_eq!(failed_value["failed_phase"], "cache_alias_install");
1387 assert!(failed_value["execution_profile"].is_null());
1388 assert_eq!(failed_value["phase_timings"][0]["status"]["kind"], "failed");
1389 }
1390
1391 #[test]
1392 fn empty_write_all_cache_failure_has_the_exact_json_shape() {
1393 let failure = WriteAllCacheFailure::new(Vec::new(), None, None);
1394
1395 assert_eq!(
1396 failure.to_json_value(),
1397 json!({
1398 "aliases": [],
1399 "primary_failed_alias_table_id": null,
1400 "workflow": null,
1401 })
1402 );
1403 }
1404
1405 #[test]
1406 fn source_report_json_exposes_provider_read_stats_details() -> TestResult<()> {
1407 let source = DeltaSourceReport::metadata_only(
1408 "orders",
1409 "s3://user:password@example.com/tmp/orders?token=secret#debug",
1410 3,
1411 DeltaProtocolReport {
1412 source_name: "orders".to_owned(),
1413 table_uri: "s3://example.com/tmp/orders".to_owned(),
1414 snapshot_version: 3,
1415 min_reader_version: 1,
1416 min_writer_version: 2,
1417 reader_features: vec!["deletionVectors".to_owned()],
1418 writer_features: Vec::new(),
1419 },
1420 DeltaProviderSchedulingReport::from_options(
1421 QueryOptions {
1422 target_partitions: Some(4),
1423 output_batch_size: Some(128),
1424 },
1425 DeltaScanExecutionOptions::default()
1426 .with_parquet_metadata_size_hint_bytes(Some(16_384))?
1427 .with_parquet_full_file_read_threshold_bytes(Some(2_097_152))?,
1428 ),
1429 )
1430 .with_provider_read_stats(provider_read_stats_snapshot());
1431
1432 let value = source.to_json_value();
1433
1434 assert_eq!(value["source_uri"], "s3://example.com/tmp/orders");
1435 assert_eq!(
1436 value["protocol"]["table_uri"],
1437 "s3://example.com/tmp/orders"
1438 );
1439 assert_eq!(
1440 value["file_count"],
1441 json!({"kind": "exact", "value": 5, "reason": null})
1442 );
1443 assert_eq!(value["provider_read_stats_available"], true);
1444 assert_eq!(value["provider_stats_reason"], Value::Null);
1445 assert_eq!(value["scheduling"]["parquet_metadata_size_hint"], 16_384);
1446 assert_eq!(
1447 value["scheduling"]["parquet_full_file_read_threshold"],
1448 2_097_152
1449 );
1450 assert_eq!(
1451 value["provider_read_stats"]["reader_backend"],
1452 "native_async"
1453 );
1454 assert_eq!(value["provider_read_stats"]["use_view_types"], false);
1455 assert_eq!(value["provider_read_stats"]["files_planned"], 5);
1456 assert_eq!(
1457 value["provider_read_stats"]["parquet_data_file_range_get_operations"],
1458 4
1459 );
1460 assert_eq!(
1461 value["provider_read_stats"]["parquet_data_file_full_get_operations"],
1462 0
1463 );
1464 assert_eq!(
1465 value["provider_read_stats"]["parquet_data_file_bytes_received"],
1466 512
1467 );
1468 assert_eq!(
1469 value["provider_read_stats"]["parquet_data_file_opened_bytes"],
1470 2048
1471 );
1472 assert_eq!(
1473 value["provider_read_stats"]["approximate_files_filtered_during_planning"],
1474 8
1475 );
1476 assert_eq!(value["provider_read_stats"]["rows_produced"], 10);
1477 assert_eq!(
1478 value["provider_read_stats"]["dynamic_partition_files_pruned"],
1479 2
1480 );
1481 assert_json_safe(&value)?;
1482 assert_no_secret_or_raw_sql_text(&value);
1483
1484 Ok(())
1485 }
1486
1487 #[test]
1488 fn provider_read_stats_json_preserves_unavailable_parquet_io_metrics() {
1489 let mut stats = provider_read_stats_snapshot();
1490 stats.reader.reader_backend = ParquetReaderBackend::DeltaKernel;
1491 stats.reader.parquet_data_file_range_get_operations = None;
1492 stats.reader.parquet_data_file_full_get_operations = None;
1493 stats.reader.parquet_data_file_bytes_received = None;
1494 stats.reader.parquet_data_file_opened_bytes = None;
1495
1496 let value = provider_read_stats_value(&stats, Some("orders"));
1497
1498 assert_eq!(value["parquet_data_file_range_get_operations"], Value::Null);
1499 assert_eq!(value["parquet_data_file_full_get_operations"], Value::Null);
1500 assert_eq!(value["parquet_data_file_bytes_received"], Value::Null);
1501 assert_eq!(value["parquet_data_file_opened_bytes"], Value::Null);
1502 }
1503
1504 #[test]
1505 fn provider_read_stats_json_preserves_available_parquet_io_metric_zeros() {
1506 let mut stats = provider_read_stats_snapshot();
1507 stats.reader.parquet_data_file_range_get_operations = Some(0);
1508 stats.reader.parquet_data_file_full_get_operations = Some(0);
1509 stats.reader.parquet_data_file_bytes_received = Some(0);
1510 stats.reader.parquet_data_file_opened_bytes = Some(0);
1511
1512 let value = provider_read_stats_value(&stats, Some("orders"));
1513
1514 assert_eq!(value["parquet_data_file_range_get_operations"], 0);
1515 assert_eq!(value["parquet_data_file_full_get_operations"], 0);
1516 assert_eq!(value["parquet_data_file_bytes_received"], 0);
1517 assert_eq!(value["parquet_data_file_opened_bytes"], 0);
1518 }
1519
1520 fn output_plan() -> Result<MssqlTargetOutputPlan, crate::DeltaFunnelError> {
1521 output_plan_with_table("orders_output", "orders")
1522 }
1523
1524 fn output_plan_named(
1525 output_name: &str,
1526 ) -> Result<MssqlTargetOutputPlan, crate::DeltaFunnelError> {
1527 output_plan_with_table(output_name, format!("{output_name}_orders"))
1528 }
1529
1530 fn output_plan_with_table(
1531 output_name: &str,
1532 table_name: impl Into<String>,
1533 ) -> Result<MssqlTargetOutputPlan, crate::DeltaFunnelError> {
1534 let connection = MssqlConnectionConfig::new(
1535 "server=tcp:sql.example.com;database=warehouse;user=admin;password=secret-token",
1536 )?
1537 .with_display_label("warehouse");
1538 let target_config = MssqlTargetConfig::new(MssqlTargetTable::new("dbo", table_name)?);
1539
1540 plan_mssql_target_for_output(
1541 orders_schema(),
1542 output_name,
1543 &target_config,
1544 Some(&connection),
1545 PlanOptions::default(),
1546 )
1547 }
1548
1549 fn job(
1550 output_plan: MssqlTargetOutputPlan,
1551 ) -> Result<MssqlOutputWriteJob, crate::DeltaFunnelError> {
1552 Ok(MssqlOutputWriteJob::with_default_write_backend(
1553 orders_schema_ref(),
1554 resolved_target(output_plan)?,
1555 MssqlSchemaPlanOptions::default(),
1556 || async { Ok(stream::empty()) },
1557 ))
1558 }
1559
1560 fn resolved_target(
1561 output_plan: MssqlTargetOutputPlan,
1562 ) -> Result<ResolvedMssqlTarget, crate::DeltaFunnelError> {
1563 let connection = MssqlConnectionConfig::new(
1564 "server=tcp:sql.example.com;database=warehouse;user=admin;password=secret-token",
1565 )?
1566 .with_display_label("warehouse");
1567
1568 MssqlTargetConfig::new(output_plan.target_table().clone())
1569 .with_load_mode(output_plan.load_mode())
1570 .resolve(MssqlTargetResolutionContext {
1571 output_name: Some(output_plan.output_name()),
1572 default_connection: Some(&connection),
1573 })
1574 }
1575
1576 fn orders_schema_ref() -> SchemaRef {
1577 Arc::new(orders_schema())
1578 }
1579
1580 fn orders_schema() -> Schema {
1581 Schema::new(vec![
1582 Field::new("id", DataType::Int64, false),
1583 Field::new("region", DataType::Utf8, true),
1584 ])
1585 }
1586
1587 fn provider_read_stats_snapshot() -> DeltaDataFusionMetricsSnapshot {
1588 DeltaDataFusionMetricsSnapshot {
1589 reader: crate::DeltaReadMetricsSnapshot {
1590 snapshot_version: 3,
1591 reader_backend: ParquetReaderBackend::Direct,
1592 scan_metadata_exhausted: Some(true),
1593 scan_partitions_planned: 4,
1594 files_planned: 5,
1595 files_filtered_during_planning: Some(8),
1596 estimated_rows: Some(99),
1597 estimated_bytes: Some(2048),
1598 scan_partitions_started: 4,
1599 scan_partitions_completed: 4,
1600 files_started: 5,
1601 files_completed: 5,
1602 batches_produced: 2,
1603 rows_produced: 10,
1604 deletion_vector_payloads_loaded: 1,
1605 deletion_vectors_applied: 1,
1606 deletion_vector_rows_deleted: 2,
1607 deletion_vector_failures: 0,
1608 deletion_vector_rejections: 0,
1609 parquet_data_file_range_get_operations: Some(4),
1610 parquet_data_file_full_get_operations: Some(0),
1611 parquet_data_file_bytes_received: Some(512),
1612 parquet_data_file_opened_bytes: Some(2048),
1613 },
1614 use_view_types: false,
1615 output_batch_size: Some(128),
1616 dynamic_partition_files_pruned: 2,
1617 dynamic_partition_files_kept: 3,
1618 dynamic_filters_received: 1,
1619 dynamic_filters_accepted: 1,
1620 dynamic_filters_unsupported: 0,
1621 dynamic_filter_snapshots: 1,
1622 dynamic_files_not_pruned_missing_metadata: 0,
1623 dynamic_files_not_pruned_unsupported_expression: 0,
1624 }
1625 }
1626
1627 fn assert_json_safe(value: &Value) -> TestResult<()> {
1628 serde_json::from_str::<Value>(&serde_json::to_string(value)?)?;
1629 Ok(())
1630 }
1631
1632 fn assert_no_secret_or_raw_sql_text(value: &Value) {
1633 let text = value.to_string();
1634 assert!(!text.contains("secret-token"));
1635 assert!(!text.contains("password"));
1636 assert!(!text.contains("server=tcp"));
1637 assert!(!text.contains("select id"));
1638 }
1639}