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