Skip to main content

delta_funnel/report/
execution_profile.rs

1use crate::DeltaDataFusionMetricsSnapshot;
2
3/// Controls whether a query execution profile is collected.
4#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
5pub enum ExecutionProfileMode {
6    /// Do not collect detailed query execution metrics.
7    #[default]
8    Disabled,
9    /// Collect a detailed query execution profile.
10    Detailed,
11}
12
13/// Identifies why Delta Funnel executed a query plan.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum QueryExecutionScope {
16    /// A bounded table preview.
17    Preview,
18    /// One SQL Server output write.
19    MssqlOutput,
20    /// Materialization of a selected write-all cache alias.
21    WriteAllCacheAlias,
22}
23
24impl QueryExecutionScope {
25    /// Returns the stable JSON spelling for this execution scope.
26    #[must_use]
27    pub const fn as_str(self) -> &'static str {
28        match self {
29            Self::Preview => "preview",
30            Self::MssqlOutput => "mssql_output",
31            Self::WriteAllCacheAlias => "write_all_cache_alias",
32        }
33    }
34}
35
36/// Describes how query execution reached its terminal state.
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum QueryExecutionOutcome {
39    /// The query stream reached normal exhaustion.
40    Success,
41    /// Query execution returned an error.
42    Error,
43    /// Required stream ownership was dropped before normal exhaustion.
44    Cancelled,
45}
46
47impl QueryExecutionOutcome {
48    /// Returns the stable JSON spelling for this execution outcome.
49    #[must_use]
50    pub const fn as_str(self) -> &'static str {
51        match self {
52            Self::Success => "success",
53            Self::Error => "error",
54            Self::Cancelled => "cancelled",
55        }
56    }
57}
58
59/// Stability category assigned to a DataFusion execution metric.
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum QueryExecutionMetricCategory {
62    /// A stable metric intended for normal profile consumers.
63    Summary,
64    /// A development metric that may change with DataFusion.
65    Dev,
66}
67
68impl QueryExecutionMetricCategory {
69    /// Returns the stable JSON spelling for this metric category.
70    #[must_use]
71    pub const fn as_str(self) -> &'static str {
72        match self {
73            Self::Summary => "summary",
74            Self::Dev => "dev",
75        }
76    }
77}
78
79/// Typed value captured for one DataFusion execution metric.
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub enum QueryExecutionMetricValue {
82    /// An unsigned item count.
83    Count(u64),
84    /// An unsigned byte count.
85    Bytes(u64),
86    /// An unsigned elapsed duration in nanoseconds.
87    Nanoseconds(u64),
88    /// An unsigned point-in-time gauge.
89    Gauge(u64),
90    /// An optional signed Unix epoch timestamp in nanoseconds.
91    TimestampNanoseconds(Option<i64>),
92    /// File-pruning counters.
93    Pruning {
94        /// Files pruned.
95        pruned: u64,
96        /// Files partially matched.
97        matched: u64,
98        /// Files fully matched.
99        fully_matched: u64,
100    },
101    /// The two unsigned components of a ratio.
102    Ratio {
103        /// Ratio numerator.
104        part: u64,
105        /// Ratio denominator.
106        total: u64,
107    },
108    /// An unsigned custom metric value.
109    Custom(u64),
110}
111
112impl QueryExecutionMetricValue {
113    /// Returns the stable JSON kind for this metric value.
114    #[must_use]
115    pub const fn value_kind(&self) -> &'static str {
116        match self {
117            Self::Count(_) => "count",
118            Self::Bytes(_) => "bytes",
119            Self::Nanoseconds(_) => "nanoseconds",
120            Self::Gauge(_) => "gauge",
121            Self::TimestampNanoseconds(_) => "timestamp_nanoseconds",
122            Self::Pruning { .. } => "pruning",
123            Self::Ratio { .. } => "ratio",
124            Self::Custom(_) => "custom",
125        }
126    }
127}
128
129/// One redacted metric attached to a query execution operator.
130#[derive(Debug, Clone, PartialEq, Eq)]
131pub struct QueryExecutionMetric {
132    name: String,
133    category: QueryExecutionMetricCategory,
134    partition: Option<u64>,
135    output_partition: Option<u64>,
136    value: QueryExecutionMetricValue,
137}
138
139impl QueryExecutionMetric {
140    pub(crate) fn new(
141        name: impl Into<String>,
142        category: QueryExecutionMetricCategory,
143        partition: Option<u64>,
144        output_partition: Option<u64>,
145        value: QueryExecutionMetricValue,
146    ) -> Self {
147        Self {
148            name: name.into(),
149            category,
150            partition,
151            output_partition,
152            value,
153        }
154    }
155
156    /// Returns the DataFusion metric name.
157    #[must_use]
158    pub fn name(&self) -> &str {
159        &self.name
160    }
161
162    /// Returns the metric stability category.
163    #[must_use]
164    pub const fn category(&self) -> QueryExecutionMetricCategory {
165        self.category
166    }
167
168    /// Returns the DataFusion execution partition, when present.
169    #[must_use]
170    pub const fn partition(&self) -> Option<u64> {
171        self.partition
172    }
173
174    /// Returns the normalized output partition label, when present.
175    #[must_use]
176    pub const fn output_partition(&self) -> Option<u64> {
177        self.output_partition
178    }
179
180    /// Returns the typed metric value.
181    #[must_use]
182    pub const fn value(&self) -> &QueryExecutionMetricValue {
183        &self.value
184    }
185}
186
187/// Immutable profile for one unique physical execution-plan node.
188#[derive(Debug, Clone, PartialEq, Eq)]
189pub struct QueryExecutionOperatorProfile {
190    node_id: u64,
191    parent_node_id: Option<u64>,
192    operator_name: String,
193    output_partition_count: u64,
194    metrics_available: bool,
195    aggregated_metrics: Vec<QueryExecutionMetric>,
196    metrics: Vec<QueryExecutionMetric>,
197    delta_provider_source_name: Option<String>,
198    delta_provider_read_stats: Option<DeltaDataFusionMetricsSnapshot>,
199}
200
201impl QueryExecutionOperatorProfile {
202    #[allow(clippy::too_many_arguments)]
203    pub(crate) fn new(
204        node_id: u64,
205        parent_node_id: Option<u64>,
206        operator_name: impl Into<String>,
207        output_partition_count: u64,
208        metrics_available: bool,
209        aggregated_metrics: Vec<QueryExecutionMetric>,
210        metrics: Vec<QueryExecutionMetric>,
211        delta_provider_source_name: Option<String>,
212        delta_provider_read_stats: Option<DeltaDataFusionMetricsSnapshot>,
213    ) -> Self {
214        Self {
215            node_id,
216            parent_node_id,
217            operator_name: operator_name.into(),
218            output_partition_count,
219            metrics_available,
220            aggregated_metrics,
221            metrics,
222            delta_provider_source_name,
223            delta_provider_read_stats,
224        }
225    }
226
227    /// Returns the profile-local node identifier.
228    #[must_use]
229    pub const fn node_id(&self) -> u64 {
230        self.node_id
231    }
232
233    /// Returns the first-seen parent node identifier, or `None` for the root.
234    #[must_use]
235    pub const fn parent_node_id(&self) -> Option<u64> {
236        self.parent_node_id
237    }
238
239    /// Returns the exact DataFusion execution-plan short name.
240    #[must_use]
241    pub fn operator_name(&self) -> &str {
242        &self.operator_name
243    }
244
245    /// Returns the number of output partitions produced by this operator.
246    #[must_use]
247    pub const fn output_partition_count(&self) -> u64 {
248        self.output_partition_count
249    }
250
251    /// Returns whether DataFusion exposed a metric set for this operator.
252    #[must_use]
253    pub const fn metrics_available(&self) -> bool {
254        self.metrics_available
255    }
256
257    /// Returns operator metrics aggregated by name.
258    #[must_use]
259    pub fn aggregated_metrics(&self) -> &[QueryExecutionMetric] {
260        &self.aggregated_metrics
261    }
262
263    /// Returns the original per-partition operator metrics.
264    #[must_use]
265    pub fn metrics(&self) -> &[QueryExecutionMetric] {
266        &self.metrics
267    }
268
269    /// Returns the registered source name for an exact Delta scan node.
270    #[must_use]
271    pub fn delta_provider_source_name(&self) -> Option<&str> {
272        self.delta_provider_source_name.as_deref()
273    }
274
275    /// Returns the immutable provider snapshot for an exact Delta scan node.
276    #[must_use]
277    pub const fn delta_provider_read_stats(&self) -> Option<&DeltaDataFusionMetricsSnapshot> {
278        self.delta_provider_read_stats.as_ref()
279    }
280}
281
282/// Immutable execution profile for one Delta Funnel query scope.
283#[derive(Debug, Clone, PartialEq, Eq)]
284pub struct QueryExecutionProfile {
285    scope: QueryExecutionScope,
286    outcome: QueryExecutionOutcome,
287    delta_funnel_row_limit: Option<u64>,
288    operators: Vec<QueryExecutionOperatorProfile>,
289}
290
291impl QueryExecutionProfile {
292    pub(crate) fn preview(
293        outcome: QueryExecutionOutcome,
294        delta_funnel_row_limit: u64,
295        operators: Vec<QueryExecutionOperatorProfile>,
296    ) -> Self {
297        Self::new(
298            QueryExecutionScope::Preview,
299            outcome,
300            Some(delta_funnel_row_limit),
301            operators,
302        )
303    }
304
305    pub(crate) fn mssql_output(
306        outcome: QueryExecutionOutcome,
307        operators: Vec<QueryExecutionOperatorProfile>,
308    ) -> Self {
309        Self::new(QueryExecutionScope::MssqlOutput, outcome, None, operators)
310    }
311
312    pub(crate) fn write_all_cache_alias(
313        outcome: QueryExecutionOutcome,
314        operators: Vec<QueryExecutionOperatorProfile>,
315    ) -> Self {
316        Self::new(
317            QueryExecutionScope::WriteAllCacheAlias,
318            outcome,
319            None,
320            operators,
321        )
322    }
323
324    fn new(
325        scope: QueryExecutionScope,
326        outcome: QueryExecutionOutcome,
327        delta_funnel_row_limit: Option<u64>,
328        operators: Vec<QueryExecutionOperatorProfile>,
329    ) -> Self {
330        Self {
331            scope,
332            outcome,
333            delta_funnel_row_limit,
334            operators,
335        }
336    }
337
338    /// Returns why Delta Funnel executed the plan.
339    #[must_use]
340    pub const fn scope(&self) -> QueryExecutionScope {
341        self.scope
342    }
343
344    /// Returns how query execution reached its terminal state.
345    #[must_use]
346    pub const fn outcome(&self) -> QueryExecutionOutcome {
347        self.outcome
348    }
349
350    /// Returns whether execution ended before successful exhaustion.
351    #[must_use]
352    pub const fn partial(&self) -> bool {
353        !matches!(self.outcome, QueryExecutionOutcome::Success)
354    }
355
356    /// Returns the exact Delta Funnel preview limit for preview profiles.
357    #[must_use]
358    pub const fn delta_funnel_row_limit(&self) -> Option<u64> {
359        self.delta_funnel_row_limit
360    }
361
362    /// Returns unique physical operators in first-seen pre-order.
363    #[must_use]
364    pub fn operators(&self) -> &[QueryExecutionOperatorProfile] {
365        &self.operators
366    }
367}
368
369#[cfg(test)]
370mod tests {
371    use serde_json::json;
372
373    use super::*;
374
375    #[test]
376    fn profile_mode_defaults_to_disabled() {
377        assert_eq!(
378            ExecutionProfileMode::default(),
379            ExecutionProfileMode::Disabled
380        );
381        assert_ne!(
382            ExecutionProfileMode::Detailed,
383            ExecutionProfileMode::Disabled
384        );
385    }
386
387    #[test]
388    fn enums_expose_stable_json_spellings() {
389        assert_eq!(QueryExecutionScope::Preview.as_str(), "preview");
390        assert_eq!(QueryExecutionScope::MssqlOutput.as_str(), "mssql_output");
391        assert_eq!(
392            QueryExecutionScope::WriteAllCacheAlias.as_str(),
393            "write_all_cache_alias"
394        );
395        assert_eq!(QueryExecutionOutcome::Success.as_str(), "success");
396        assert_eq!(QueryExecutionOutcome::Error.as_str(), "error");
397        assert_eq!(QueryExecutionOutcome::Cancelled.as_str(), "cancelled");
398        assert_eq!(QueryExecutionMetricCategory::Summary.as_str(), "summary");
399        assert_eq!(QueryExecutionMetricCategory::Dev.as_str(), "dev");
400    }
401
402    #[test]
403    fn profile_derives_partial_and_normalizes_scope_limit() {
404        let success =
405            QueryExecutionProfile::preview(QueryExecutionOutcome::Success, 20, Vec::new());
406        let error = QueryExecutionProfile::mssql_output(QueryExecutionOutcome::Error, Vec::new());
407        let cancelled = QueryExecutionProfile::write_all_cache_alias(
408            QueryExecutionOutcome::Cancelled,
409            Vec::new(),
410        );
411
412        assert!(!success.partial());
413        assert_eq!(success.delta_funnel_row_limit(), Some(20));
414        assert!(error.partial());
415        assert_eq!(error.delta_funnel_row_limit(), None);
416        assert!(cancelled.partial());
417        assert_eq!(cancelled.delta_funnel_row_limit(), None);
418    }
419
420    #[test]
421    fn profile_and_nested_models_expose_typed_accessors_and_json() {
422        let metric = QueryExecutionMetric::new(
423            "output_rows",
424            QueryExecutionMetricCategory::Summary,
425            Some(0),
426            None,
427            QueryExecutionMetricValue::Count(42),
428        );
429        let operator = QueryExecutionOperatorProfile::new(
430            0,
431            None,
432            "GlobalLimitExec",
433            1,
434            true,
435            Vec::new(),
436            vec![metric],
437            None,
438            None,
439        );
440        let profile =
441            QueryExecutionProfile::preview(QueryExecutionOutcome::Success, 20, vec![operator]);
442
443        let operator = &profile.operators()[0];
444        let metric = &operator.metrics()[0];
445        assert_eq!(profile.scope(), QueryExecutionScope::Preview);
446        assert_eq!(profile.outcome(), QueryExecutionOutcome::Success);
447        assert_eq!(operator.node_id(), 0);
448        assert_eq!(operator.parent_node_id(), None);
449        assert_eq!(operator.operator_name(), "GlobalLimitExec");
450        assert_eq!(operator.output_partition_count(), 1);
451        assert!(operator.metrics_available());
452        assert!(operator.aggregated_metrics().is_empty());
453        assert_eq!(operator.delta_provider_read_stats(), None);
454        assert_eq!(metric.name(), "output_rows");
455        assert_eq!(metric.category(), QueryExecutionMetricCategory::Summary);
456        assert_eq!(metric.partition(), Some(0));
457        assert_eq!(metric.output_partition(), None);
458        assert_eq!(metric.value(), &QueryExecutionMetricValue::Count(42));
459        assert_eq!(
460            profile.to_json_value(),
461            json!({
462                "scope": "preview",
463                "outcome": "success",
464                "partial": false,
465                "delta_funnel_row_limit": 20,
466                "operators": [{
467                    "node_id": 0,
468                    "parent_node_id": null,
469                    "operator_name": "GlobalLimitExec",
470                    "output_partition_count": 1,
471                    "metrics_available": true,
472                    "aggregated_metrics": [],
473                    "metrics": [{
474                        "name": "output_rows",
475                        "category": "summary",
476                        "partition": 0,
477                        "output_partition": null,
478                        "value_kind": "count",
479                        "value": 42,
480                        "components": null
481                    }],
482                    "delta_provider_read_stats": null
483                }]
484            })
485        );
486    }
487
488    #[test]
489    fn metric_values_expose_typed_kinds_and_json_shapes() {
490        let cases = [
491            (
492                QueryExecutionMetricValue::Count(1),
493                "count",
494                json!(1),
495                json!(null),
496            ),
497            (
498                QueryExecutionMetricValue::Bytes(2),
499                "bytes",
500                json!(2),
501                json!(null),
502            ),
503            (
504                QueryExecutionMetricValue::Nanoseconds(3),
505                "nanoseconds",
506                json!(3),
507                json!(null),
508            ),
509            (
510                QueryExecutionMetricValue::Gauge(4),
511                "gauge",
512                json!(4),
513                json!(null),
514            ),
515            (
516                QueryExecutionMetricValue::TimestampNanoseconds(Some(-5)),
517                "timestamp_nanoseconds",
518                json!(-5),
519                json!(null),
520            ),
521            (
522                QueryExecutionMetricValue::TimestampNanoseconds(None),
523                "timestamp_nanoseconds",
524                json!(null),
525                json!(null),
526            ),
527            (
528                QueryExecutionMetricValue::Pruning {
529                    pruned: 6,
530                    matched: 7,
531                    fully_matched: 8,
532                },
533                "pruning",
534                json!(null),
535                json!({"pruned": 6, "matched": 7, "fully_matched": 8}),
536            ),
537            (
538                QueryExecutionMetricValue::Ratio { part: 9, total: 10 },
539                "ratio",
540                json!(null),
541                json!({"part": 9, "total": 10}),
542            ),
543            (
544                QueryExecutionMetricValue::Custom(11),
545                "custom",
546                json!(11),
547                json!(null),
548            ),
549        ];
550
551        for (value, kind, scalar, components) in cases {
552            let metric = QueryExecutionMetric::new(
553                "metric",
554                QueryExecutionMetricCategory::Dev,
555                None,
556                Some(12),
557                value,
558            );
559            let json = metric.to_json_value();
560
561            assert_eq!(metric.value().value_kind(), kind);
562            assert_eq!(json["value_kind"], kind);
563            assert_eq!(json["value"], scalar);
564            assert_eq!(json["components"], components);
565        }
566    }
567}