Skip to main content

delta_funnel/report/sql_server/
write_all.rs

1use std::fmt;
2
3use crate::{
4    DeltaSourceReport, MssqlOutputWriteStatus, MssqlWorkflowWriteReport, PhaseTimingReport,
5    QueryExecutionProfile, QueryExecutionScope, support::sanitize_text_for_display,
6};
7
8/// Report for one `write_all` call that reached the sequential workflow.
9///
10/// Planning and cache setup failures are returned as errors before this report
11/// exists. Once the workflow starts, output write failures and dependent-output
12/// stream setup failures are represented in the wrapped workflow report while
13/// cache metadata remains available through [`WriteAllReport::cache`].
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct WriteAllReport {
16    workflow: MssqlWorkflowWriteReport,
17    cache: WriteAllCacheReport,
18    sources: Vec<DeltaSourceReport>,
19    phase_timings: Vec<PhaseTimingReport>,
20}
21
22impl WriteAllReport {
23    pub(crate) fn new(
24        workflow: MssqlWorkflowWriteReport,
25        cache: WriteAllCacheReport,
26        sources: Vec<DeltaSourceReport>,
27    ) -> Self {
28        Self {
29            workflow,
30            cache,
31            sources,
32            phase_timings: Vec::new(),
33        }
34    }
35
36    pub(crate) fn with_phase_timings(mut self, phase_timings: Vec<PhaseTimingReport>) -> Self {
37        self.phase_timings = phase_timings;
38        self
39    }
40
41    /// Returns the lower-level SQL Server workflow report.
42    #[must_use]
43    pub const fn workflow(&self) -> &MssqlWorkflowWriteReport {
44        &self.workflow
45    }
46
47    /// Returns cache planning, selection, and lifecycle metadata for this call.
48    #[must_use]
49    pub const fn cache(&self) -> &WriteAllCacheReport {
50        &self.cache
51    }
52
53    /// Returns Delta source reports in session registration order.
54    #[must_use]
55    pub fn sources(&self) -> &[DeltaSourceReport] {
56        &self.sources
57    }
58
59    /// Returns top-level `write_all` workflow phase timing reports.
60    #[must_use]
61    pub fn phase_timings(&self) -> &[PhaseTimingReport] {
62        &self.phase_timings
63    }
64
65    /// Returns the number of selected outputs represented by this report.
66    #[must_use]
67    pub fn len(&self) -> usize {
68        self.workflow.len()
69    }
70
71    /// Returns whether this report contains no selected outputs.
72    #[must_use]
73    pub fn is_empty(&self) -> bool {
74        self.workflow.is_empty()
75    }
76
77    /// Returns per-output SQL Server workflow statuses in caller-provided order.
78    #[must_use]
79    pub fn outputs(&self) -> &[MssqlOutputWriteStatus] {
80        self.workflow.outputs()
81    }
82
83    /// Returns whether every selected output completed successfully.
84    #[must_use]
85    pub fn all_succeeded(&self) -> bool {
86        self.workflow.all_succeeded()
87    }
88
89    /// Returns the number of outputs that completed successfully.
90    #[must_use]
91    pub fn succeeded_count(&self) -> usize {
92        self.workflow.succeeded_count()
93    }
94
95    /// Returns the number of outputs that failed.
96    #[must_use]
97    pub fn failed_count(&self) -> usize {
98        self.workflow.failed_count()
99    }
100
101    /// Returns the number of outputs skipped after a previous output failed.
102    #[must_use]
103    pub fn skipped_count(&self) -> usize {
104        self.workflow.skipped_count()
105    }
106}
107
108/// Cache metadata for one `write_all` call.
109///
110/// This report describes the cache decision for calls that reached the
111/// sequential output workflow. Cache materialization failures occur before the
112/// workflow can start, so they are returned as errors instead of as
113/// `WriteAllCacheReport` values.
114#[derive(Debug, Clone, PartialEq, Eq)]
115pub enum WriteAllCacheReport {
116    /// Cache planning was disabled for this call.
117    Disabled,
118    /// Cache planning ran but did not select a safe cache frontier.
119    NoCache {
120        /// Conservative reason no cache aliases were selected.
121        reason: WriteAllNoCacheReason,
122        /// Registered derived aliases skipped during cache planning.
123        skipped_candidates: Vec<WriteAllCacheCandidateSkip>,
124    },
125    /// Cache planning selected registered derived aliases for this call.
126    CacheAliases {
127        /// Selected registered derived aliases in deterministic planner order.
128        aliases: Vec<WriteAllCacheAliasReport>,
129        /// Registered derived aliases skipped during cache planning.
130        skipped_candidates: Vec<WriteAllCacheCandidateSkip>,
131    },
132}
133
134impl WriteAllCacheReport {
135    pub(crate) fn disabled() -> Self {
136        Self::Disabled
137    }
138
139    pub(crate) fn no_cache(
140        reason: WriteAllNoCacheReason,
141        skipped_candidates: Vec<WriteAllCacheCandidateSkip>,
142    ) -> Self {
143        Self::NoCache {
144            reason,
145            skipped_candidates,
146        }
147    }
148
149    pub(crate) fn cache_aliases(
150        aliases: Vec<WriteAllCacheAliasReport>,
151        skipped_candidates: Vec<WriteAllCacheCandidateSkip>,
152    ) -> Self {
153        Self::CacheAliases {
154            aliases,
155            skipped_candidates,
156        }
157    }
158}
159
160/// Conservative reason no cache alias was selected for `write_all`.
161#[derive(Debug, Clone, Copy, PartialEq, Eq)]
162pub enum WriteAllNoCacheReason {
163    /// Cache selection only helps when at least two outputs use a candidate.
164    FewerThanTwoOutputs,
165    /// No registered derived alias is shared by at least two selected outputs.
166    NoSharedRegisteredDerivedAlias,
167    /// Candidate relationships could not produce a deterministic cache frontier.
168    AmbiguousSharedDerivedAlias,
169}
170
171/// Selected registered derived alias cache metadata.
172///
173/// `output_indexes` uses caller-provided `write_all` request indexes. It
174/// includes outputs that use the selected alias directly or transitively. In an
175/// explicit cache chain, an upstream alias can serve an output through a later
176/// cached alias instead of through that output's retained SQL directly.
177#[derive(Clone, PartialEq, Eq)]
178pub struct WriteAllCacheAliasReport {
179    table_id: u64,
180    alias: String,
181    output_indexes: Vec<usize>,
182    status: WriteAllCacheAliasStatus,
183    phase_timings: Vec<PhaseTimingReport>,
184    failed_phase: Option<String>,
185    execution_profile: Option<QueryExecutionProfile>,
186}
187
188impl WriteAllCacheAliasReport {
189    pub(crate) fn selected(
190        table_id: u64,
191        alias: impl Into<String>,
192        output_indexes: Vec<usize>,
193    ) -> Self {
194        Self {
195            table_id,
196            alias: alias.into(),
197            output_indexes,
198            status: WriteAllCacheAliasStatus::Selected,
199            phase_timings: Vec::new(),
200            failed_phase: None,
201            execution_profile: None,
202        }
203    }
204
205    pub(crate) fn executed(
206        table_id: u64,
207        alias: impl Into<String>,
208        output_indexes: Vec<usize>,
209        status: WriteAllCacheAliasStatus,
210        phase_timings: Vec<PhaseTimingReport>,
211        failed_phase: Option<&'static str>,
212    ) -> Self {
213        debug_assert_ne!(status, WriteAllCacheAliasStatus::Selected);
214        debug_assert_eq!(
215            status == WriteAllCacheAliasStatus::Failed,
216            failed_phase.is_some()
217        );
218        Self {
219            table_id,
220            alias: alias.into(),
221            output_indexes,
222            status,
223            phase_timings,
224            failed_phase: failed_phase.map(str::to_owned),
225            execution_profile: None,
226        }
227    }
228
229    /// Returns the selected registered derived table id.
230    #[must_use]
231    pub const fn table_id(&self) -> u64 {
232        self.table_id
233    }
234
235    /// Returns the selected registered derived alias.
236    #[must_use]
237    pub fn alias(&self) -> &str {
238        &self.alias
239    }
240
241    /// Returns selected output indexes that use this alias.
242    #[must_use]
243    pub fn output_indexes(&self) -> &[usize] {
244        &self.output_indexes
245    }
246
247    /// Returns this alias cache lifecycle status for the `write_all` call.
248    #[must_use]
249    pub const fn status(&self) -> WriteAllCacheAliasStatus {
250        self.status
251    }
252
253    /// Returns lifecycle timings for an attempted cache alias.
254    ///
255    /// Plan-shaped [`WriteAllCacheAliasStatus::Selected`] reports return an
256    /// empty slice because no lifecycle phase was attempted.
257    #[must_use]
258    pub fn phase_timings(&self) -> &[PhaseTimingReport] {
259        &self.phase_timings
260    }
261
262    /// Returns the primary failed lifecycle phase for a failed alias.
263    #[must_use]
264    pub fn failed_phase(&self) -> Option<&str> {
265        self.failed_phase.as_deref()
266    }
267
268    /// Returns the terminal profile for this alias's cache materialization.
269    ///
270    /// Disabled profiling and failures before physical-plan creation return
271    /// `None`. Selected plan metadata is not executed and also returns `None`.
272    /// Output query profiles remain separate from this cache profile.
273    #[must_use]
274    pub const fn execution_profile(&self) -> Option<&QueryExecutionProfile> {
275        self.execution_profile.as_ref()
276    }
277
278    pub(crate) fn with_execution_profile(
279        mut self,
280        execution_profile: Option<QueryExecutionProfile>,
281    ) -> Self {
282        debug_assert!(execution_profile.as_ref().is_none_or(|profile| {
283            profile.scope() == QueryExecutionScope::WriteAllCacheAlias
284                && profile.delta_funnel_row_limit().is_none()
285        }));
286        self.execution_profile = execution_profile;
287        self
288    }
289}
290
291impl fmt::Debug for WriteAllCacheAliasReport {
292    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
293        formatter
294            .debug_struct("WriteAllCacheAliasReport")
295            .field("table_id", &self.table_id)
296            .field("alias", &sanitize_text_for_display(&self.alias))
297            .field("output_indexes", &self.output_indexes)
298            .field("status", &self.status)
299            .field("phase_timings", &self.phase_timings)
300            .field("failed_phase", &self.failed_phase)
301            .field("execution_profile", &self.execution_profile)
302            .finish()
303    }
304}
305
306/// Cache lifecycle status for one selected alias in a `write_all` report.
307#[derive(Debug, Clone, Copy, PartialEq, Eq)]
308pub enum WriteAllCacheAliasStatus {
309    /// The alias was selected by cache planning but has no completed workflow.
310    ///
311    /// This status is reserved for plan-shaped metadata. Normal successful
312    /// public `write_all` reports use [`Self::MaterializedAndRestored`] for
313    /// selected aliases because the scoped catalog replacement has already
314    /// been cleaned up before the report is returned.
315    Selected,
316    /// The alias was materialized, installed, and restored.
317    ///
318    /// The output workflow may fail or may not start. This status only states
319    /// that cache setup and restoration completed for the alias.
320    MaterializedAndRestored,
321    /// Cache materialization, installation, or restoration failed.
322    Failed,
323}
324
325/// Structured details retained when a cache-enabled `write_all` call fails.
326#[derive(Debug, Clone, PartialEq, Eq)]
327pub struct WriteAllCacheFailure {
328    aliases: Vec<WriteAllCacheAliasReport>,
329    primary_failed_alias_table_id: Option<u64>,
330    workflow: Option<MssqlWorkflowWriteReport>,
331}
332
333impl WriteAllCacheFailure {
334    pub(crate) fn new(
335        aliases: Vec<WriteAllCacheAliasReport>,
336        primary_failed_alias_table_id: Option<u64>,
337        workflow: Option<MssqlWorkflowWriteReport>,
338    ) -> Self {
339        Self {
340            aliases,
341            primary_failed_alias_table_id,
342            workflow,
343        }
344    }
345
346    /// Returns every attempted cache alias in deterministic selection order.
347    #[must_use]
348    pub fn aliases(&self) -> &[WriteAllCacheAliasReport] {
349        &self.aliases
350    }
351
352    /// Returns the table id whose cache phase caused the primary failure.
353    #[must_use]
354    pub const fn primary_failed_alias_table_id(&self) -> Option<u64> {
355        self.primary_failed_alias_table_id
356    }
357
358    /// Returns the completed output workflow when restoration later failed.
359    #[must_use]
360    pub const fn workflow(&self) -> Option<&MssqlWorkflowWriteReport> {
361        self.workflow.as_ref()
362    }
363}
364
365impl WriteAllCacheAliasStatus {
366    /// Returns the stable lower-snake-case report value.
367    #[must_use]
368    pub const fn as_str(self) -> &'static str {
369        match self {
370            Self::Selected => "selected",
371            Self::MaterializedAndRestored => "materialized_and_restored",
372            Self::Failed => "failed",
373        }
374    }
375}
376
377impl fmt::Display for WriteAllCacheAliasStatus {
378    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
379        formatter.write_str(self.as_str())
380    }
381}
382
383/// Registered derived alias skipped during cache selection.
384#[derive(Clone, PartialEq, Eq)]
385pub struct WriteAllCacheCandidateSkip {
386    table_id: u64,
387    alias: String,
388    reason: WriteAllCacheCandidateSkipReason,
389}
390
391impl WriteAllCacheCandidateSkip {
392    pub(crate) fn new(
393        table_id: u64,
394        alias: impl Into<String>,
395        reason: WriteAllCacheCandidateSkipReason,
396    ) -> Self {
397        Self {
398            table_id,
399            alias: alias.into(),
400            reason,
401        }
402    }
403
404    /// Returns the skipped registered derived table id.
405    #[must_use]
406    pub const fn table_id(&self) -> u64 {
407        self.table_id
408    }
409
410    /// Returns the skipped registered derived alias.
411    #[must_use]
412    pub fn alias(&self) -> &str {
413        &self.alias
414    }
415
416    /// Returns why this candidate was skipped.
417    #[must_use]
418    pub const fn reason(&self) -> &WriteAllCacheCandidateSkipReason {
419        &self.reason
420    }
421}
422
423impl fmt::Debug for WriteAllCacheCandidateSkip {
424    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
425        formatter
426            .debug_struct("WriteAllCacheCandidateSkip")
427            .field("table_id", &self.table_id)
428            .field("alias", &sanitize_text_for_display(&self.alias))
429            .field("reason", &self.reason)
430            .finish()
431    }
432}
433
434/// Reason a cache candidate was skipped during `write_all` cache planning.
435#[derive(Debug, Clone, PartialEq, Eq)]
436pub enum WriteAllCacheCandidateSkipReason {
437    /// Fewer than two selected outputs use this candidate.
438    NotShared {
439        /// Number of selected outputs that use this candidate.
440        output_count: usize,
441    },
442    /// Retained SQL text was missing, so later replanning would be unsafe.
443    MissingSqlText,
444    /// Lineage was incomplete or could not be trusted.
445    IncompleteLineage,
446    /// A deeper shared alias is closer to all dependent outputs.
447    CoveredByDeeperSharedAlias {
448        /// Table id of the selected deeper alias that covers this candidate.
449        selected_table_id: u64,
450    },
451    /// The candidate's relative depth could not be ordered deterministically.
452    AmbiguousDepth,
453    /// The candidate was eligible but absent from the explicit selection.
454    NotExplicitlySelected,
455}