1use crate::DeltaDataFusionMetricsSnapshot;
2
3#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
5pub enum ExecutionProfileMode {
6 #[default]
8 Disabled,
9 Detailed,
11}
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum QueryExecutionScope {
16 Preview,
18 MssqlOutput,
20 WriteAllCacheAlias,
22}
23
24impl QueryExecutionScope {
25 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum QueryExecutionOutcome {
39 Success,
41 Error,
43 Cancelled,
45}
46
47impl QueryExecutionOutcome {
48 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum QueryExecutionMetricCategory {
62 Summary,
64 Dev,
66}
67
68impl QueryExecutionMetricCategory {
69 #[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#[derive(Debug, Clone, PartialEq, Eq)]
81pub enum QueryExecutionMetricValue {
82 Count(u64),
84 Bytes(u64),
86 Nanoseconds(u64),
88 Gauge(u64),
90 TimestampNanoseconds(Option<i64>),
92 Pruning {
94 pruned: u64,
96 matched: u64,
98 fully_matched: u64,
100 },
101 Ratio {
103 part: u64,
105 total: u64,
107 },
108 Custom(u64),
110}
111
112impl QueryExecutionMetricValue {
113 #[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#[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 #[must_use]
158 pub fn name(&self) -> &str {
159 &self.name
160 }
161
162 #[must_use]
164 pub const fn category(&self) -> QueryExecutionMetricCategory {
165 self.category
166 }
167
168 #[must_use]
170 pub const fn partition(&self) -> Option<u64> {
171 self.partition
172 }
173
174 #[must_use]
176 pub const fn output_partition(&self) -> Option<u64> {
177 self.output_partition
178 }
179
180 #[must_use]
182 pub const fn value(&self) -> &QueryExecutionMetricValue {
183 &self.value
184 }
185}
186
187#[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 #[must_use]
229 pub const fn node_id(&self) -> u64 {
230 self.node_id
231 }
232
233 #[must_use]
235 pub const fn parent_node_id(&self) -> Option<u64> {
236 self.parent_node_id
237 }
238
239 #[must_use]
241 pub fn operator_name(&self) -> &str {
242 &self.operator_name
243 }
244
245 #[must_use]
247 pub const fn output_partition_count(&self) -> u64 {
248 self.output_partition_count
249 }
250
251 #[must_use]
253 pub const fn metrics_available(&self) -> bool {
254 self.metrics_available
255 }
256
257 #[must_use]
259 pub fn aggregated_metrics(&self) -> &[QueryExecutionMetric] {
260 &self.aggregated_metrics
261 }
262
263 #[must_use]
265 pub fn metrics(&self) -> &[QueryExecutionMetric] {
266 &self.metrics
267 }
268
269 #[must_use]
271 pub fn delta_provider_source_name(&self) -> Option<&str> {
272 self.delta_provider_source_name.as_deref()
273 }
274
275 #[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#[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 #[must_use]
340 pub const fn scope(&self) -> QueryExecutionScope {
341 self.scope
342 }
343
344 #[must_use]
346 pub const fn outcome(&self) -> QueryExecutionOutcome {
347 self.outcome
348 }
349
350 #[must_use]
352 pub const fn partial(&self) -> bool {
353 !matches!(self.outcome, QueryExecutionOutcome::Success)
354 }
355
356 #[must_use]
358 pub const fn delta_funnel_row_limit(&self) -> Option<u64> {
359 self.delta_funnel_row_limit
360 }
361
362 #[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}