Skip to main content

delta_funnel/report/sql_server/
write_all.rs

1use std::fmt;
2
3use crate::{
4    DeltaSourceReport, MssqlOutputWriteStatus, MssqlWorkflowWriteReport, PhaseTimingReport,
5    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 conservative cache decision for calls that reached
111/// the sequential output workflow. Cache materialization failures occur before
112/// the 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 by conservative 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 by conservative 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 direct writes of the selected alias and dependent outputs whose
175/// retained SQL was replanned against the active cached alias.
176#[derive(Clone, PartialEq, Eq)]
177pub struct WriteAllCacheAliasReport {
178    table_id: u64,
179    alias: String,
180    output_indexes: Vec<usize>,
181    status: WriteAllCacheAliasStatus,
182}
183
184impl WriteAllCacheAliasReport {
185    pub(crate) fn new(
186        table_id: u64,
187        alias: impl Into<String>,
188        output_indexes: Vec<usize>,
189        status: WriteAllCacheAliasStatus,
190    ) -> Self {
191        Self {
192            table_id,
193            alias: alias.into(),
194            output_indexes,
195            status,
196        }
197    }
198
199    /// Returns the selected registered derived table id.
200    #[must_use]
201    pub const fn table_id(&self) -> u64 {
202        self.table_id
203    }
204
205    /// Returns the selected registered derived alias.
206    #[must_use]
207    pub fn alias(&self) -> &str {
208        &self.alias
209    }
210
211    /// Returns selected output indexes that use this alias.
212    #[must_use]
213    pub fn output_indexes(&self) -> &[usize] {
214        &self.output_indexes
215    }
216
217    /// Returns this alias cache lifecycle status for the `write_all` call.
218    #[must_use]
219    pub const fn status(&self) -> WriteAllCacheAliasStatus {
220        self.status
221    }
222}
223
224impl fmt::Debug for WriteAllCacheAliasReport {
225    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
226        formatter
227            .debug_struct("WriteAllCacheAliasReport")
228            .field("table_id", &self.table_id)
229            .field("alias", &sanitize_text_for_display(&self.alias))
230            .field("output_indexes", &self.output_indexes)
231            .field("status", &self.status)
232            .finish()
233    }
234}
235
236/// Cache lifecycle status for one selected alias in a `write_all` report.
237#[derive(Debug, Clone, Copy, PartialEq, Eq)]
238pub enum WriteAllCacheAliasStatus {
239    /// The alias was selected by cache planning but has no completed workflow.
240    ///
241    /// This status is reserved for plan-shaped metadata. Normal successful
242    /// public `write_all` reports use [`Self::MaterializedAndRestored`] for
243    /// selected aliases because the scoped catalog replacement has already
244    /// been cleaned up before the report is returned.
245    Selected,
246    /// The alias was materialized, used for the workflow, and restored.
247    ///
248    /// The workflow may still contain per-output failures. This status only
249    /// states that cache setup and restoration completed for the alias.
250    MaterializedAndRestored,
251}
252
253/// Registered derived alias skipped during conservative cache selection.
254#[derive(Clone, PartialEq, Eq)]
255pub struct WriteAllCacheCandidateSkip {
256    table_id: u64,
257    alias: String,
258    reason: WriteAllCacheCandidateSkipReason,
259}
260
261impl WriteAllCacheCandidateSkip {
262    pub(crate) fn new(
263        table_id: u64,
264        alias: impl Into<String>,
265        reason: WriteAllCacheCandidateSkipReason,
266    ) -> Self {
267        Self {
268            table_id,
269            alias: alias.into(),
270            reason,
271        }
272    }
273
274    /// Returns the skipped registered derived table id.
275    #[must_use]
276    pub const fn table_id(&self) -> u64 {
277        self.table_id
278    }
279
280    /// Returns the skipped registered derived alias.
281    #[must_use]
282    pub fn alias(&self) -> &str {
283        &self.alias
284    }
285
286    /// Returns why this candidate was skipped.
287    #[must_use]
288    pub const fn reason(&self) -> &WriteAllCacheCandidateSkipReason {
289        &self.reason
290    }
291}
292
293impl fmt::Debug for WriteAllCacheCandidateSkip {
294    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
295        formatter
296            .debug_struct("WriteAllCacheCandidateSkip")
297            .field("table_id", &self.table_id)
298            .field("alias", &sanitize_text_for_display(&self.alias))
299            .field("reason", &self.reason)
300            .finish()
301    }
302}
303
304/// Reason a cache candidate was skipped by conservative `write_all` planning.
305#[derive(Debug, Clone, PartialEq, Eq)]
306pub enum WriteAllCacheCandidateSkipReason {
307    /// Fewer than two selected outputs use this candidate.
308    NotShared {
309        /// Number of selected outputs that use this candidate.
310        output_count: usize,
311    },
312    /// Retained SQL text was missing, so later replanning would be unsafe.
313    MissingSqlText,
314    /// Lineage was incomplete or could not be trusted.
315    IncompleteLineage,
316    /// A deeper shared alias is closer to all dependent outputs.
317    CoveredByDeeperSharedAlias {
318        /// Table id of the selected deeper alias that covers this candidate.
319        selected_table_id: u64,
320    },
321    /// The candidate's relative depth could not be ordered deterministically.
322    AmbiguousDepth,
323}