@@ -206,6 +206,142 @@ impl ConfigField for ExplainFormat {
206206 }
207207}
208208
209+ /// Classifies a metric by what it measures.
210+ ///
211+ /// This is orthogonal to [`MetricType`] (SUMMARY / DEV), which controls
212+ /// *verbosity*. `MetricCategory` controls *what kind of value* is shown,
213+ /// so that `EXPLAIN ANALYZE` output can be narrowed to only the categories
214+ /// that are useful in a given context.
215+ ///
216+ /// For testing, the key property is **determinism**:
217+ /// - [`Rows`](Self::Rows) and [`Bytes`](Self::Bytes) depend on the plan
218+ /// and the data, so they are deterministic across runs (given the same
219+ /// input).
220+ /// - [`Timing`](Self::Timing) depends on hardware, system load, scheduling,
221+ /// etc., so it varies from run to run even on the same machine.
222+ ///
223+ /// [`MetricCategory`] is especially useful in sqllogictest (`.slt`) files:
224+ /// setting `datafusion.explain.analyze_categories = 'rows'` lets a test
225+ /// assert on row-count metrics without sprinkling `<slt:ignore>` over every
226+ /// timing value.
227+ ///
228+ /// Metrics that do not declare a category (the default for custom
229+ /// `Count` / `Gauge` metrics) are **always included** unless the config
230+ /// is set to `'none'`.
231+ ///
232+ /// [`MetricType`]: datafusion_physical_expr_common::metrics::MetricType
233+ #[ derive( Debug , Clone , Copy , PartialEq , Eq , Hash ) ]
234+ pub enum MetricCategory {
235+ /// Row counts and related dimensionless counters: `output_rows`,
236+ /// `spilled_rows`, `output_batches`, pruning metrics, ratios, etc.
237+ ///
238+ /// Deterministic given the same plan and data.
239+ Rows ,
240+ /// Byte measurements: `output_bytes`, `spilled_bytes`,
241+ /// `current_memory_usage`, `bytes_scanned`, etc.
242+ ///
243+ /// Deterministic given the same plan and data.
244+ Bytes ,
245+ /// Wall-clock durations and timestamps: `elapsed_compute`,
246+ /// operator-defined `Time` metrics, `start_timestamp` /
247+ /// `end_timestamp`, etc.
248+ ///
249+ /// **Non-deterministic** — varies across runs even on the same hardware.
250+ Timing ,
251+ }
252+
253+ /// Controls which [`MetricCategory`] values are shown in `EXPLAIN ANALYZE`.
254+ ///
255+ /// Set via `SET datafusion.explain.analyze_categories = '...'`.
256+ ///
257+ /// See [`MetricCategory`] for the determinism properties that motivate
258+ /// this filter.
259+ #[ derive( Debug , Clone , PartialEq , Eq , Hash ) ]
260+ pub enum ExplainAnalyzeCategories {
261+ /// Show all metrics regardless of category (the default).
262+ All ,
263+ /// Show only metrics whose category is in the list.
264+ /// Metrics that have no declared category are still included
265+ /// (they are treated as "always on").
266+ ///
267+ /// An **empty** vec means "plan only" — suppress all metrics.
268+ Only ( Vec < MetricCategory > ) ,
269+ }
270+
271+ impl Default for ExplainAnalyzeCategories {
272+ fn default ( ) -> Self {
273+ Self :: All
274+ }
275+ }
276+
277+ impl FromStr for ExplainAnalyzeCategories {
278+ type Err = DataFusionError ;
279+
280+ fn from_str ( s : & str ) -> Result < Self , Self :: Err > {
281+ let s = s. trim ( ) . to_lowercase ( ) ;
282+ match s. as_str ( ) {
283+ "all" => Ok ( Self :: All ) ,
284+ "none" => Ok ( Self :: Only ( vec ! [ ] ) ) ,
285+ other => {
286+ let mut cats = Vec :: new ( ) ;
287+ for part in other. split ( ',' ) {
288+ let part = part. trim ( ) ;
289+ match part {
290+ "rows" => cats. push ( MetricCategory :: Rows ) ,
291+ "bytes" => cats. push ( MetricCategory :: Bytes ) ,
292+ "timing" => cats. push ( MetricCategory :: Timing ) ,
293+ unknown => {
294+ return Err ( DataFusionError :: Configuration ( format ! (
295+ "Invalid metric category '{unknown}'. \
296+ Expected 'all', 'none', or comma-separated list of \
297+ 'rows', 'bytes', 'timing'."
298+ ) ) ) ;
299+ }
300+ }
301+ }
302+ cats. dedup ( ) ;
303+ Ok ( Self :: Only ( cats) )
304+ }
305+ }
306+ }
307+ }
308+
309+ impl Display for ExplainAnalyzeCategories {
310+ fn fmt ( & self , f : & mut fmt:: Formatter < ' _ > ) -> fmt:: Result {
311+ match self {
312+ Self :: All => write ! ( f, "all" ) ,
313+ Self :: Only ( cats) if cats. is_empty ( ) => write ! ( f, "none" ) ,
314+ Self :: Only ( cats) => {
315+ let mut first = true ;
316+ for cat in cats {
317+ if !first {
318+ write ! ( f, "," ) ?;
319+ }
320+ first = false ;
321+ let s = match cat {
322+ MetricCategory :: Rows => "rows" ,
323+ MetricCategory :: Bytes => "bytes" ,
324+ MetricCategory :: Timing => "timing" ,
325+ } ;
326+ write ! ( f, "{s}" ) ?;
327+ }
328+ Ok ( ( ) )
329+ }
330+ }
331+ }
332+ }
333+
334+ impl ConfigField for ExplainAnalyzeCategories {
335+ fn visit < V : Visit > ( & self , v : & mut V , key : & str , description : & ' static str ) {
336+ v. some ( key, self , description)
337+ }
338+
339+ fn set ( & mut self , _: & str , value : & str ) -> Result < ( ) > {
340+ * self = ExplainAnalyzeCategories :: from_str ( value) ?;
341+ Ok ( ( ) )
342+ }
343+ }
344+
209345/// Verbosity levels controlling how `EXPLAIN ANALYZE` renders metrics
210346#[ derive( Debug , Clone , Copy , PartialEq , Eq , Hash ) ]
211347pub enum ExplainAnalyzeLevel {
0 commit comments