Skip to main content

query/query_engine/
state.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::collections::HashMap;
16use std::fmt;
17use std::num::NonZeroUsize;
18use std::sync::{Arc, RwLock};
19
20use async_trait::async_trait;
21use catalog::CatalogManagerRef;
22use catalog::information_schema::InformationExtensionRef;
23use common_base::Plugins;
24use common_function::aggrs::aggr_wrapper::fix_order::FixStateUdafOrderingAnalyzer;
25use common_function::function_factory::ScalarFunctionFactory;
26use common_function::function_registry::FUNCTION_REGISTRY;
27use common_function::handlers::{
28    FlowServiceHandlerRef, ProcedureServiceHandlerRef, TableMutationHandlerRef,
29};
30use common_function::state::FunctionState;
31use common_stat::get_total_memory_bytes;
32use common_telemetry::warn;
33use datafusion::catalog::{Session, TableFunction};
34use datafusion::dataframe::DataFrame;
35use datafusion::error::Result as DfResult;
36use datafusion::execution::SessionStateBuilder;
37use datafusion::execution::context::{QueryPlanner, SessionConfig, SessionContext, SessionState};
38use datafusion::execution::memory_pool::{
39    FairSpillPool, GreedyMemoryPool, MemoryConsumer, MemoryLimit, MemoryPool, MemoryReservation,
40    TrackConsumersPool,
41};
42use datafusion::physical_optimizer::PhysicalOptimizerRule;
43use datafusion::physical_optimizer::optimizer::PhysicalOptimizer;
44use datafusion::physical_optimizer::sanity_checker::SanityCheckPlan;
45use datafusion::physical_plan::ExecutionPlan;
46use datafusion::physical_planner::{DefaultPhysicalPlanner, ExtensionPlanner, PhysicalPlanner};
47use datafusion_expr::{AggregateUDF, LogicalPlan as DfLogicalPlan, WindowUDF};
48use datafusion_optimizer::Analyzer;
49use datafusion_optimizer::analyzer::function_rewrite::ApplyFunctionRewrites;
50use datafusion_optimizer::optimizer::Optimizer;
51use partition::manager::PartitionRuleManagerRef;
52use promql::extension_plan::PromExtensionPlanner;
53use session::context::QueryContextRef;
54use table::TableRef;
55use table::table::adapter::DfTableProviderAdapter;
56
57use crate::QueryEngineContext;
58use crate::dist_plan::{
59    DistExtensionPlanner, DistPlannerAnalyzer, DistPlannerOptions, DynFilterRegistryManager,
60    MergeSortExtensionPlanner, RemoteDynFilterReceiverExtensionPlanner,
61    RemoteDynFilterRegistryLease,
62};
63use crate::error::InvalidQueryContextExtensionSnafu;
64use crate::metrics::{QUERY_MEMORY_POOL_REJECTED_TOTAL, QUERY_MEMORY_POOL_USAGE_BYTES};
65use crate::optimizer::ExtensionAnalyzerRule;
66use crate::optimizer::const_normalization::ConstNormalizationRule;
67use crate::optimizer::constant_term::MatchesConstantTermOptimizer;
68use crate::optimizer::count_nest_aggr::CountNestAggrRule;
69use crate::optimizer::count_wildcard::CountWildcardToTimeIndexRule;
70use crate::optimizer::enforce_sorting::EnforceSorting;
71use crate::optimizer::expr_depth;
72use crate::optimizer::global_limit::EnsureGlobalLimitForFetch;
73use crate::optimizer::infer_aggregate_input_bounds::InferAggregateInputBoundsRule;
74use crate::optimizer::json_schema_concretize::JsonSchemaConcretizeRule;
75use crate::optimizer::json_type_concretize::JsonTypeConcretizeRule;
76use crate::optimizer::merge_scan_projection::MergeScanProjection;
77use crate::optimizer::parallelize_scan::ParallelizeScan;
78use crate::optimizer::promql_tsid_narrow_join::PromqlTsidNarrowJoin;
79use crate::optimizer::remove_duplicate::RemoveDuplicate;
80use crate::optimizer::scan_hint::ScanHintRule;
81use crate::optimizer::sst_manifest_aggregate::{
82    SstManifestAggregatePlanner, SstManifestAggregateRule,
83};
84use crate::optimizer::string_normalization::StringNormalizationRule;
85use crate::optimizer::transcribe_atat::TranscribeAtatRule;
86use crate::optimizer::type_conversion::TypeConversionRule;
87use crate::optimizer::windowed_sort::WindowedSortPhysicalRule;
88use crate::options::{QueryMemoryPoolPolicy, QueryOptions as QueryOptionsNew};
89use crate::query_engine::DefaultSerializer;
90use crate::query_engine::options::QueryOptions;
91use crate::query_engine::runtime::{
92    DefaultQueryRuntimeProvider, QueryRuntimeContext, QueryRuntimeProvider, QueryRuntimeProviderRef,
93};
94use crate::range_select::planner::RangeSelectPlanner;
95use crate::region_query::RegionQueryHandlerRef;
96
97/// Query engine global state
98#[derive(Clone)]
99pub struct QueryEngineState {
100    df_context: SessionContext,
101    catalog_manager: CatalogManagerRef,
102    dyn_filter_registry_manager: Arc<DynFilterRegistryManager>,
103    function_state: Arc<FunctionState>,
104    scalar_functions: Arc<RwLock<HashMap<String, ScalarFunctionFactory>>>,
105    aggr_functions: Arc<RwLock<HashMap<String, AggregateUDF>>>,
106    table_functions: Arc<RwLock<HashMap<String, Arc<TableFunction>>>>,
107    extension_rules: Vec<Arc<dyn ExtensionAnalyzerRule + Send + Sync>>,
108    plugins: Plugins,
109}
110
111impl fmt::Debug for QueryEngineState {
112    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
113        f.debug_struct("QueryEngineState")
114            .field("state", &self.df_context.state())
115            .finish()
116    }
117}
118
119impl QueryEngineState {
120    #[allow(clippy::too_many_arguments)]
121    pub fn new(
122        catalog_list: CatalogManagerRef,
123        partition_rule_manager: Option<PartitionRuleManagerRef>,
124        region_query_handler: Option<RegionQueryHandlerRef>,
125        table_mutation_handler: Option<TableMutationHandlerRef>,
126        procedure_service_handler: Option<ProcedureServiceHandlerRef>,
127        flow_service_handler: Option<FlowServiceHandlerRef>,
128        with_dist_planner: bool,
129        plugins: Plugins,
130        options: QueryOptionsNew,
131    ) -> Self {
132        Self::try_new(
133            catalog_list,
134            partition_rule_manager,
135            region_query_handler,
136            table_mutation_handler,
137            procedure_service_handler,
138            flow_service_handler,
139            with_dist_planner,
140            plugins,
141            options,
142        )
143        .expect("Failed to build query engine state")
144    }
145
146    #[allow(clippy::too_many_arguments)]
147    pub fn try_new(
148        catalog_list: CatalogManagerRef,
149        partition_rule_manager: Option<PartitionRuleManagerRef>,
150        region_query_handler: Option<RegionQueryHandlerRef>,
151        table_mutation_handler: Option<TableMutationHandlerRef>,
152        procedure_service_handler: Option<ProcedureServiceHandlerRef>,
153        flow_service_handler: Option<FlowServiceHandlerRef>,
154        with_dist_planner: bool,
155        plugins: Plugins,
156        options: QueryOptionsNew,
157    ) -> DfResult<Self> {
158        let total_memory = get_total_memory_bytes().max(0) as u64;
159        let memory_pool_size = options.memory_pool_size.resolve(total_memory) as usize;
160        let runtime_provider = plugins.get::<QueryRuntimeProviderRef>();
161        let mut session_config = SessionConfig::new().with_create_default_catalog_and_schema(false);
162        if options.parallelism > 0 {
163            session_config = session_config.with_target_partitions(options.parallelism);
164        }
165        if options.allow_query_fallback {
166            session_config
167                .options_mut()
168                .extensions
169                .insert(DistPlannerOptions {
170                    allow_query_fallback: true,
171                });
172        }
173
174        // todo(hl): This serves as a workaround for https://github.com/GreptimeTeam/greptimedb/issues/5659
175        // and we can add that check back once we upgrade datafusion.
176        session_config
177            .options_mut()
178            .execution
179            .skip_physical_aggregate_schema_check = true;
180
181        let runtime_context = QueryRuntimeContext::new(&options, memory_pool_size);
182        let default_runtime_provider = DefaultQueryRuntimeProvider;
183        default_runtime_provider.configure_session_config(runtime_context, &mut session_config);
184        if let Some(provider) = runtime_provider.as_ref() {
185            provider.configure_session_config(runtime_context, &mut session_config);
186        }
187        let runtime_builder = DefaultQueryRuntimeProvider::runtime_env_builder(runtime_context);
188        let runtime_env = match runtime_provider {
189            Some(provider) => provider.build_runtime_env(runtime_context, runtime_builder)?,
190            None => default_runtime_provider.build_runtime_env(runtime_context, runtime_builder)?,
191        };
192
193        // Apply extension rules
194        let mut extension_rules = Vec::new();
195
196        // The [`TypeConversionRule`] must be at first
197        extension_rules.insert(0, Arc::new(TypeConversionRule) as _);
198        extension_rules.push(Arc::new(CountNestAggrRule) as _);
199
200        // Apply the datafusion rules
201        let mut analyzer = Analyzer::new();
202        analyzer.rules.insert(0, Arc::new(TranscribeAtatRule));
203        analyzer.rules.insert(0, Arc::new(StringNormalizationRule));
204        analyzer
205            .rules
206            .insert(0, Arc::new(CountWildcardToTimeIndexRule));
207        analyzer.rules.push(Arc::new(ConstNormalizationRule));
208
209        // Add ApplyFunctionRewrites rule,
210        // Note we cannot use `analyzer.add_function_rewrite`
211        // because only rules are copied into session_state
212        analyzer.rules.insert(
213            0,
214            Arc::new(ApplyFunctionRewrites::new(
215                FUNCTION_REGISTRY.function_rewrites(),
216            )),
217        );
218        if inspect_query_extension(&catalog_list).is_some() {
219            analyzer.rules.push(Arc::new(SstManifestAggregateRule));
220        }
221        if with_dist_planner {
222            analyzer.rules.push(Arc::new(DistPlannerAnalyzer));
223            analyzer.rules.push(Arc::new(JsonSchemaConcretizeRule));
224        }
225        analyzer.rules.push(Arc::new(FixStateUdafOrderingAnalyzer));
226        analyzer.rules.push(Arc::new(expr_depth::ExprDepthRule));
227
228        // Note: Postgres oid-alias string coercion (`'x'::regclass`,
229        // `'public'::regnamespace`, ...) is handled by the
230        // `PostgresCompatibilityParser`'s built-in `RewriteRegCastToSubquery`
231        // rule at SQL-parse time, so no analyzer rule is registered here. The
232        // implicit `oid_col = 'name'` form is not resolved (it needs schema
233        // awareness the parser lacks); the few client queries that use it are
234        // handled by the parser's blacklist.
235
236        let mut optimizer = Optimizer::new();
237        // Constants must be simplified, but CASE arguments must not yet be
238        // hidden by common-subexpression extraction.
239        let filter_pushdown_index = optimizer
240            .rules
241            .iter()
242            .position(|rule| rule.name() == "push_down_filter")
243            .ok_or_else(|| {
244                datafusion_common::internal_datafusion_err!("Missing PushDownFilter rule")
245            })?;
246        optimizer.rules.insert(
247            filter_pushdown_index,
248            Arc::new(InferAggregateInputBoundsRule),
249        );
250        if with_dist_planner {
251            optimizer.rules.push(Arc::new(MergeScanProjection));
252        }
253        optimizer.rules.push(Arc::new(ScanHintRule));
254        optimizer.rules.push(Arc::new(JsonTypeConcretizeRule));
255
256        // add physical optimizer
257        let mut physical_optimizer = PhysicalOptimizer::new();
258        // Change TableScan's partition right before enforcing distribution
259        physical_optimizer
260            .rules
261            .insert(5, Arc::new(ParallelizeScan));
262        // Prefer collecting narrow PromQL build sides over repartitioning wide label streams.
263        physical_optimizer
264            .rules
265            .insert(6, Arc::new(PromqlTsidNarrowJoin));
266        // Re-enforce sorting after custom rules update scan partitioning and distribution.
267        // Keep it immediately before DataFusion's default EnsureRequirements.
268        physical_optimizer.rules.insert(7, Arc::new(EnforceSorting));
269        // Add rule for windowed sort
270        physical_optimizer
271            .rules
272            .push(Arc::new(WindowedSortPhysicalRule));
273        // explicitly not do filter pushdown for windowed sort&part sort
274        // (notice that `PartSortExec` create another new dyn filter that need to be pushdown if want to use dyn filter optimization)
275        // benchmark shows it can cause performance regression due to useless filtering and extra shuffle.
276        // We can add a rule to do filter pushdown for windowed sort in the future if we find a way to avoid the performance regression.
277        physical_optimizer
278            .rules
279            .push(Arc::new(MatchesConstantTermOptimizer));
280        physical_optimizer
281            .rules
282            .push(Arc::new(EnsureGlobalLimitForFetch));
283        // Add rule to remove duplicate nodes generated by other rules. Run this in the last.
284        physical_optimizer.rules.push(Arc::new(RemoveDuplicate));
285        // Place SanityCheckPlan at the end of the list to ensure that it runs after all other rules.
286        Self::remove_physical_optimizer_rule(
287            &mut physical_optimizer.rules,
288            SanityCheckPlan {}.name(),
289        );
290        physical_optimizer.rules.push(Arc::new(SanityCheckPlan {}));
291
292        let session_state = SessionStateBuilder::new()
293            .with_config(session_config)
294            .with_runtime_env(runtime_env)
295            .with_default_features()
296            .with_analyzer_rules(analyzer.rules)
297            .with_serializer_registry(Arc::new(DefaultSerializer))
298            .with_query_planner(Arc::new(DfQueryPlanner::new(
299                catalog_list.clone(),
300                partition_rule_manager,
301                region_query_handler.clone(),
302                options.enable_per_region_metrics,
303            )))
304            .with_optimizer_rules(optimizer.rules)
305            .with_physical_optimizer_rules(physical_optimizer.rules)
306            .build();
307        let df_context = SessionContext::new_with_state(session_state);
308        register_function_aliases(&df_context);
309        register_pg_catalog_compat(&df_context);
310
311        Ok(Self {
312            df_context,
313            catalog_manager: catalog_list,
314            dyn_filter_registry_manager: Arc::new(DynFilterRegistryManager::default()),
315            function_state: Arc::new(FunctionState {
316                plugins: plugins.clone(),
317                table_mutation_handler,
318                procedure_service_handler,
319                flow_service_handler,
320            }),
321            aggr_functions: Arc::new(RwLock::new(HashMap::new())),
322            table_functions: Arc::new(RwLock::new(HashMap::new())),
323            extension_rules,
324            plugins,
325            scalar_functions: Arc::new(RwLock::new(HashMap::new())),
326        })
327    }
328
329    fn remove_physical_optimizer_rule(
330        rules: &mut Vec<Arc<dyn PhysicalOptimizerRule + Send + Sync>>,
331        name: &str,
332    ) {
333        rules.retain(|rule| rule.name() != name);
334    }
335
336    /// Applies validated query options to one per-query DataFusion configuration.
337    pub fn apply_query_options(
338        &self,
339        state: &mut SessionState,
340        query_ctx: QueryContextRef,
341    ) -> crate::error::Result<QueryContextRef> {
342        let query_ctx = Arc::new(query_ctx.query_option_snapshot().map_err(|error| {
343            InvalidQueryContextExtensionSnafu {
344                reason: error.to_string(),
345            }
346            .build()
347        })?);
348        let options = query_ctx.extensions();
349        const MASTER: &str = "datafusion.optimizer.enable_dynamic_filter_pushdown";
350        // The master switch cascades to children, so apply explicit child overrides afterward.
351        for (key, value) in options
352            .get_key_value(MASTER)
353            .into_iter()
354            .chain(options.iter().filter(|(key, _)| key.as_str() != MASTER))
355        {
356            if key.starts_with("datafusion.") {
357                state
358                    .config_mut()
359                    .options_mut()
360                    .set(key, value)
361                    .map_err(|error| {
362                        InvalidQueryContextExtensionSnafu {
363                            reason: error.to_string(),
364                        }
365                        .build()
366                    })?;
367            }
368        }
369        if let Some(partitions) = options.get("query.parallelism") {
370            let partitions = partitions.parse::<usize>().map_err(|error| {
371                InvalidQueryContextExtensionSnafu {
372                    reason: error.to_string(),
373                }
374                .build()
375            })?;
376            *state.config_mut() = state.config().clone().with_target_partitions(partitions);
377        }
378        if let Some(fallback) = options.get("query.allow_query_fallback") {
379            state
380                .config_mut()
381                .options_mut()
382                .extensions
383                .insert(DistPlannerOptions {
384                    allow_query_fallback: fallback == "true",
385                });
386        }
387        Ok(query_ctx)
388    }
389
390    /// Optimize the logical plan using registered extension analyzer rules.
391    pub fn optimize_by_extension_rules(
392        &self,
393        plan: DfLogicalPlan,
394        context: &QueryEngineContext,
395    ) -> DfResult<DfLogicalPlan> {
396        expr_depth::validate_plan(&plan)?;
397        self.extension_rules
398            .iter()
399            .try_fold(plan, |acc_plan, rule| {
400                let plan = rule.analyze(acc_plan, context, context.state().config_options())?;
401                expr_depth::validate_plan(&plan)?;
402                Ok(plan)
403            })
404    }
405
406    /// Run the full logical plan optimize phase for the given plan.
407    pub fn optimize_logical_plan(&self, plan: DfLogicalPlan) -> DfResult<DfLogicalPlan> {
408        expr_depth::validate_plan(&plan)?;
409        self.session_state().optimize(&plan)
410    }
411
412    /// Retrieve the scalar function by name
413    pub fn scalar_function(&self, function_name: &str) -> Option<ScalarFunctionFactory> {
414        self.scalar_functions
415            .read()
416            .unwrap()
417            .get(function_name)
418            .cloned()
419    }
420
421    /// Retrieve scalar function names.
422    pub fn scalar_names(&self) -> Vec<String> {
423        self.scalar_functions
424            .read()
425            .unwrap()
426            .keys()
427            .cloned()
428            .collect()
429    }
430
431    /// Retrieve the aggregate function by name
432    pub fn aggr_function(&self, function_name: &str) -> Option<AggregateUDF> {
433        self.aggr_functions
434            .read()
435            .unwrap()
436            .get(function_name)
437            .cloned()
438    }
439
440    /// Retrieve aggregate function names.
441    pub fn aggr_names(&self) -> Vec<String> {
442        self.aggr_functions
443            .read()
444            .unwrap()
445            .keys()
446            .cloned()
447            .collect()
448    }
449
450    /// Retrieve table function by name
451    pub fn table_function(&self, function_name: &str) -> Option<Arc<TableFunction>> {
452        self.table_functions
453            .read()
454            .unwrap()
455            .get(function_name)
456            .cloned()
457    }
458
459    /// Retrieve table function names.
460    pub fn table_function_names(&self) -> Vec<String> {
461        self.table_functions
462            .read()
463            .unwrap()
464            .keys()
465            .cloned()
466            .collect()
467    }
468
469    /// Register an scalar function.
470    /// Will override if the function with same name is already registered.
471    pub fn register_scalar_function(&self, func: ScalarFunctionFactory) {
472        let name = func.name().to_string();
473        let x = self
474            .scalar_functions
475            .write()
476            .unwrap()
477            .insert(name.clone(), func);
478
479        if x.is_some() {
480            warn!("Already registered scalar function '{name}'");
481        }
482    }
483
484    /// Register an aggregate function.
485    ///
486    /// # Panics
487    /// Will panic if the function with same name is already registered.
488    ///
489    /// Panicking consideration: currently the aggregated functions are all statically registered,
490    /// user cannot define their own aggregate functions on the fly. So we can panic here. If that
491    /// invariant is broken in the future, we should return an error instead of panicking.
492    pub fn register_aggr_function(&self, func: AggregateUDF) {
493        let name = func.name().to_string();
494        let x = self
495            .aggr_functions
496            .write()
497            .unwrap()
498            .insert(name.clone(), func);
499        assert!(
500            x.is_none(),
501            "Already registered aggregate function '{name}'"
502        );
503    }
504
505    pub fn register_table_function(&self, func: Arc<TableFunction>) {
506        let name = func.name();
507        let x = self
508            .table_functions
509            .write()
510            .unwrap()
511            .insert(name.to_string(), func.clone());
512
513        if x.is_some() {
514            warn!("Already registered table function '{name}'");
515        }
516    }
517
518    /// Register a window function (UDWF) directly on the DataFusion SessionContext.
519    ///
520    /// This makes the function visible via `session_state.window_functions()`,
521    /// which is used by `DfContextProviderAdapter::get_window_meta`.
522    pub fn register_window_function(&self, func: WindowUDF) {
523        self.df_context.register_udwf(func);
524    }
525
526    pub fn catalog_manager(&self) -> &CatalogManagerRef {
527        &self.catalog_manager
528    }
529
530    pub fn dyn_filter_registry_manager(&self) -> Arc<DynFilterRegistryManager> {
531        self.dyn_filter_registry_manager.clone()
532    }
533
534    pub fn acquire_remote_dyn_filter_registry_lease(
535        &self,
536        query_ctx: &QueryContextRef,
537    ) -> Option<RemoteDynFilterRegistryLease> {
538        let query_id = query_ctx.remote_query_id_value()?;
539        Some(
540            self.dyn_filter_registry_manager
541                .clone()
542                .acquire_lease(query_id),
543        )
544    }
545
546    pub fn function_state(&self) -> Arc<FunctionState> {
547        self.function_state.clone()
548    }
549
550    /// Returns the [`TableMutationHandlerRef`] in state.
551    pub fn table_mutation_handler(&self) -> Option<&TableMutationHandlerRef> {
552        self.function_state.table_mutation_handler.as_ref()
553    }
554
555    /// Returns the [`ProcedureServiceHandlerRef`] in state.
556    pub fn procedure_service_handler(&self) -> Option<&ProcedureServiceHandlerRef> {
557        self.function_state.procedure_service_handler.as_ref()
558    }
559
560    pub(crate) fn disallow_cross_catalog_query(&self) -> bool {
561        self.plugins
562            .map::<QueryOptions, _, _>(|x| x.disallow_cross_catalog_query)
563            .unwrap_or(false)
564    }
565
566    pub fn session_state(&self) -> SessionState {
567        self.df_context.state()
568    }
569
570    /// Create a DataFrame for a table
571    pub fn read_table(&self, table: TableRef) -> DfResult<DataFrame> {
572        self.df_context
573            .read_table(Arc::new(DfTableProviderAdapter::new(table)))
574    }
575}
576
577struct DfQueryPlanner {
578    physical_planner: DefaultPhysicalPlanner,
579}
580
581impl fmt::Debug for DfQueryPlanner {
582    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
583        f.debug_struct("DfQueryPlanner").finish()
584    }
585}
586
587#[async_trait]
588impl QueryPlanner for DfQueryPlanner {
589    async fn create_physical_plan(
590        &self,
591        logical_plan: &DfLogicalPlan,
592        session: &dyn Session,
593    ) -> DfResult<Arc<dyn ExecutionPlan>> {
594        self.physical_planner
595            .create_physical_plan(logical_plan, session)
596            .await
597    }
598}
599
600/// MySQL-compatible scalar function aliases: (target_name, alias)
601const SCALAR_FUNCTION_ALIASES: &[(&str, &str)] = &[
602    ("upper", "ucase"),
603    ("lower", "lcase"),
604    ("ceil", "ceiling"),
605    ("substr", "mid"),
606    ("random", "rand"),
607];
608
609/// MySQL-compatible aggregate function aliases: (target_name, alias)
610const AGGREGATE_FUNCTION_ALIASES: &[(&str, &str)] =
611    &[("stddev_pop", "std"), ("var_pop", "variance")];
612
613/// Register function aliases.
614///
615/// This function adds aliases like `ucase` -> `upper`, `lcase` -> `lower`, etc.
616/// to make GreptimeDB more compatible with MySQL syntax.
617fn register_function_aliases(ctx: &SessionContext) {
618    let state = ctx.state();
619
620    for (target, alias) in SCALAR_FUNCTION_ALIASES {
621        if let Some(func) = state.scalar_functions().get(*target) {
622            let aliased = func.as_ref().clone().with_aliases([*alias]);
623            ctx.register_udf(aliased);
624        }
625    }
626
627    for (target, alias) in AGGREGATE_FUNCTION_ALIASES {
628        if let Some(func) = state.aggregate_functions().get(*target) {
629            let aliased = func.as_ref().clone().with_aliases([*alias]);
630            ctx.register_udaf(aliased);
631        }
632    }
633}
634
635/// Register the session-level (not function-registry) Postgres-compatibility
636/// extension sourced from `datafusion-pg-catalog`: widen Postgres `int4`
637/// bounds to `int8` for `generate_series`/`range` (their stock implementations
638/// require `int8` literals, and DF invokes them at planning time, before any
639/// analyzer rule can run).
640///
641/// The oid-alias type planner is supplied per SQL-planner context by
642/// [`DfContextProviderAdapter::get_type_planner`](crate::datafusion::planner::DfContextProviderAdapter::get_type_planner).
643/// Forward oid-alias name->oid resolution is handled at SQL-parse time by the
644/// `PostgresCompatibilityParser`'s built-in rewrite rule, not here.
645fn register_pg_catalog_compat(ctx: &SessionContext) {
646    datafusion_pg_catalog::pg_catalog::generate_series_arg_coercion::CoerceIntArgsToBigInt::widen(
647        ctx,
648    );
649}
650
651fn inspect_query_extension(catalog_manager: &CatalogManagerRef) -> Option<InformationExtensionRef> {
652    catalog_manager
653        .as_any()
654        .downcast_ref::<catalog::kvbackend::KvBackendCatalogManager>()
655        .map(|catalog| catalog.information_extension())
656        .filter(|extension| extension.supports_inspect_query_plan())
657}
658
659impl DfQueryPlanner {
660    fn new(
661        catalog_manager: CatalogManagerRef,
662        partition_rule_manager: Option<PartitionRuleManagerRef>,
663        region_query_handler: Option<RegionQueryHandlerRef>,
664        enable_per_region_metrics: bool,
665    ) -> Self {
666        let mut planners: Vec<Arc<dyn ExtensionPlanner + Send + Sync>> = vec![
667            Arc::new(PromExtensionPlanner),
668            Arc::new(RangeSelectPlanner),
669            Arc::new(RemoteDynFilterReceiverExtensionPlanner),
670        ];
671        if let Some(extension) = inspect_query_extension(&catalog_manager) {
672            planners.push(Arc::new(SstManifestAggregatePlanner::new(extension)));
673        }
674        if let (Some(region_query_handler), Some(partition_rule_manager)) =
675            (region_query_handler, partition_rule_manager)
676        {
677            planners.push(Arc::new(DistExtensionPlanner::new(
678                catalog_manager,
679                partition_rule_manager,
680                region_query_handler,
681                enable_per_region_metrics,
682            )));
683            planners.push(Arc::new(MergeSortExtensionPlanner {}));
684        }
685        Self {
686            physical_planner: DefaultPhysicalPlanner::with_extension_planners(planners),
687        }
688    }
689}
690
691/// A wrapper around a memory pool that records metrics.
692///
693/// This wrapper intercepts all memory pool operations and updates
694/// Prometheus metrics for monitoring query memory usage and rejections.
695///
696/// The inner pool is wrapped with `TrackConsumersPool` to preserve
697/// top-consumer error context on rejection.
698#[derive(Debug)]
699pub(super) struct MetricsMemoryPool {
700    inner: Arc<dyn MemoryPool>,
701}
702
703impl MetricsMemoryPool {
704    // Number of top memory consumers to report in OOM error messages
705    const TOP_CONSUMERS_TO_REPORT: usize = 5;
706
707    /// Create a new metrics-wrapped memory pool with the given size limit and
708    /// allocation policy.
709    pub(super) fn new(limit: usize, policy: QueryMemoryPoolPolicy) -> Self {
710        let top_n = NonZeroUsize::new(Self::TOP_CONSUMERS_TO_REPORT).unwrap();
711        let inner: Arc<dyn MemoryPool> = match policy {
712            QueryMemoryPoolPolicy::Greedy => {
713                Arc::new(TrackConsumersPool::new(GreedyMemoryPool::new(limit), top_n))
714            }
715            QueryMemoryPoolPolicy::Fair => {
716                Arc::new(TrackConsumersPool::new(FairSpillPool::new(limit), top_n))
717            }
718        };
719        Self { inner }
720    }
721
722    #[inline]
723    fn update_metrics(&self) {
724        QUERY_MEMORY_POOL_USAGE_BYTES.set(self.inner.reserved() as i64);
725    }
726}
727
728impl fmt::Display for MetricsMemoryPool {
729    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
730        write!(f, "{}(inner_pool: {})", self.name(), self.inner)
731    }
732}
733
734impl MemoryPool for MetricsMemoryPool {
735    fn name(&self) -> &str {
736        "metrics"
737    }
738
739    fn register(&self, consumer: &MemoryConsumer) {
740        self.inner.register(consumer);
741    }
742
743    fn unregister(&self, consumer: &MemoryConsumer) {
744        self.inner.unregister(consumer);
745    }
746
747    fn grow(&self, reservation: &MemoryReservation, additional: usize) {
748        self.inner.grow(reservation, additional);
749        self.update_metrics();
750    }
751
752    fn shrink(&self, reservation: &MemoryReservation, shrink: usize) {
753        self.inner.shrink(reservation, shrink);
754        self.update_metrics();
755    }
756
757    fn try_grow(
758        &self,
759        reservation: &MemoryReservation,
760        additional: usize,
761    ) -> datafusion_common::Result<()> {
762        let result = self.inner.try_grow(reservation, additional);
763        if result.is_err() {
764            QUERY_MEMORY_POOL_REJECTED_TOTAL.inc();
765        }
766        self.update_metrics();
767        result
768    }
769
770    fn reserved(&self) -> usize {
771        self.inner.reserved()
772    }
773
774    fn memory_limit(&self) -> MemoryLimit {
775        self.inner.memory_limit()
776    }
777}
778
779#[cfg(test)]
780mod tests {
781    use std::sync::atomic::{AtomicBool, Ordering};
782
783    use common_base::Plugins;
784    use common_base::memory_limit::MemoryLimit;
785    use common_base::readable_size::ReadableSize;
786    use datafusion::error::DataFusionError;
787    use datafusion::execution::memory_pool::{GreedyMemoryPool, MemoryLimit as DfMemoryLimit};
788    use datafusion::execution::runtime_env::{RuntimeEnv, RuntimeEnvBuilder};
789    use datafusion_common::config::SpillCompression;
790    use session::context::QueryContext;
791
792    use super::*;
793    use crate::options::{QueryOptions, QuerySpillCompression, QuerySpillMode};
794    use crate::query_engine::runtime::{
795        DefaultQueryRuntimeProvider, QueryRuntimeContext, QueryRuntimeProvider,
796        QueryRuntimeProviderRef,
797    };
798
799    fn new_query_engine_state() -> QueryEngineState {
800        new_query_engine_state_with(Plugins::default(), QueryOptions::default())
801    }
802
803    fn new_query_engine_state_with(plugins: Plugins, options: QueryOptions) -> QueryEngineState {
804        QueryEngineState::new(
805            catalog::memory::new_memory_catalog_manager().unwrap(),
806            None,
807            None,
808            None,
809            None,
810            None,
811            false,
812            plugins,
813            options,
814        )
815    }
816
817    struct TestRuntimeProvider {
818        build_called: AtomicBool,
819        configure_called: AtomicBool,
820    }
821
822    impl TestRuntimeProvider {
823        fn new() -> Self {
824            Self {
825                build_called: AtomicBool::new(false),
826                configure_called: AtomicBool::new(false),
827            }
828        }
829    }
830
831    impl QueryRuntimeProvider for TestRuntimeProvider {
832        fn configure_session_config(
833            &self,
834            ctx: QueryRuntimeContext<'_>,
835            config: &mut SessionConfig,
836        ) {
837            assert_eq!(ctx.resolved_memory_pool_size, 1024);
838            self.configure_called.store(true, Ordering::SeqCst);
839            *config = config.clone().with_target_partitions(7);
840        }
841
842        fn build_runtime_env(
843            &self,
844            ctx: QueryRuntimeContext<'_>,
845            builder: RuntimeEnvBuilder,
846        ) -> DfResult<Arc<RuntimeEnv>> {
847            assert_eq!(ctx.resolved_memory_pool_size, 1024);
848            self.build_called.store(true, Ordering::SeqCst);
849            builder
850                .with_memory_pool(Arc::new(GreedyMemoryPool::new(2048)))
851                .build()
852                .map(Arc::new)
853        }
854    }
855
856    struct ErrorRuntimeProvider;
857
858    impl QueryRuntimeProvider for ErrorRuntimeProvider {
859        fn build_runtime_env(
860            &self,
861            _ctx: QueryRuntimeContext<'_>,
862            _builder: RuntimeEnvBuilder,
863        ) -> DfResult<Arc<RuntimeEnv>> {
864            Err(DataFusionError::Execution("runtime provider error".into()))
865        }
866    }
867
868    #[test]
869    fn query_runtime_default_provider_keeps_bounded_memory_pool() {
870        let state = new_query_engine_state_with(
871            Plugins::default(),
872            QueryOptions {
873                memory_pool_size: MemoryLimit::Size(ReadableSize(1024)),
874                ..Default::default()
875            },
876        );
877
878        assert!(matches!(
879            state
880                .session_state()
881                .runtime_env()
882                .memory_pool
883                .memory_limit(),
884            DfMemoryLimit::Finite(1024)
885        ));
886    }
887
888    #[test]
889    fn query_runtime_provider_from_plugins_builds_runtime_env() {
890        let plugins = Plugins::default();
891        let provider = Arc::new(TestRuntimeProvider::new());
892        plugins.insert::<QueryRuntimeProviderRef>(provider.clone());
893
894        let state = new_query_engine_state_with(
895            plugins,
896            QueryOptions {
897                memory_pool_size: MemoryLimit::Size(ReadableSize(1024)),
898                ..Default::default()
899            },
900        );
901
902        assert!(provider.build_called.load(Ordering::SeqCst));
903        assert!(matches!(
904            state
905                .session_state()
906                .runtime_env()
907                .memory_pool
908                .memory_limit(),
909            DfMemoryLimit::Finite(2048)
910        ));
911    }
912
913    #[test]
914    fn query_runtime_provider_from_plugins_configures_session_config() {
915        let plugins = Plugins::default();
916        let provider = Arc::new(TestRuntimeProvider::new());
917        plugins.insert::<QueryRuntimeProviderRef>(provider.clone());
918
919        let state = new_query_engine_state_with(
920            plugins,
921            QueryOptions {
922                memory_pool_size: MemoryLimit::Size(ReadableSize(1024)),
923                experimental_spill_mode: QuerySpillMode::Custom,
924                experimental_spill_compression: QuerySpillCompression::Zstd,
925                ..Default::default()
926            },
927        );
928
929        assert!(provider.configure_called.load(Ordering::SeqCst));
930        assert_eq!(7, state.session_state().config().target_partitions());
931        assert_eq!(
932            SpillCompression::Zstd,
933            state
934                .session_state()
935                .config()
936                .options()
937                .execution
938                .spill_compression
939        );
940    }
941
942    #[test]
943    fn query_runtime_provider_error_is_returned_by_try_new() {
944        let plugins = Plugins::default();
945        plugins.insert::<QueryRuntimeProviderRef>(Arc::new(ErrorRuntimeProvider));
946
947        let err = match QueryEngineState::try_new(
948            catalog::memory::new_memory_catalog_manager().unwrap(),
949            None,
950            None,
951            None,
952            None,
953            None,
954            false,
955            plugins,
956            QueryOptions::default(),
957        ) {
958            Err(err) => err,
959            Ok(_) => panic!("expected runtime provider error"),
960        };
961
962        assert!(
963            matches!(err, DataFusionError::Execution(message) if message == "runtime provider error")
964        );
965    }
966
967    #[test]
968    fn query_engine_state_reuses_query_scoped_dyn_filter_registry_lease() {
969        let state = new_query_engine_state();
970        let query_ctx = QueryContext::arc();
971
972        let first = state
973            .acquire_remote_dyn_filter_registry_lease(&query_ctx)
974            .unwrap();
975        let second = state
976            .acquire_remote_dyn_filter_registry_lease(&query_ctx)
977            .unwrap();
978
979        assert!(first.ptr_eq(&second));
980        assert_eq!(state.dyn_filter_registry_manager().registry_count(), 1);
981        assert_eq!(
982            first.registry().query_id(),
983            query_ctx.remote_query_id_value().unwrap()
984        );
985    }
986
987    #[test]
988    fn query_engine_state_relies_on_query_context_remote_query_id_contract() {
989        let state = new_query_engine_state();
990        let query_ctx = QueryContext::arc();
991
992        assert!(query_ctx.remote_query_id_value().is_some());
993
994        let lease = state
995            .acquire_remote_dyn_filter_registry_lease(&query_ctx)
996            .unwrap();
997
998        assert_eq!(
999            lease.registry().query_id(),
1000            query_ctx.remote_query_id_value().unwrap()
1001        );
1002        assert_eq!(state.dyn_filter_registry_manager().registry_count(), 1);
1003    }
1004
1005    #[test]
1006    fn query_engine_state_separates_registries_for_different_query_contexts() {
1007        let state = new_query_engine_state();
1008        let first_query_ctx = QueryContext::arc();
1009        let second_query_ctx = QueryContext::arc();
1010
1011        let first = state
1012            .acquire_remote_dyn_filter_registry_lease(&first_query_ctx)
1013            .unwrap();
1014        let second = state
1015            .acquire_remote_dyn_filter_registry_lease(&second_query_ctx)
1016            .unwrap();
1017
1018        assert!(!first.ptr_eq(&second));
1019        assert_eq!(state.dyn_filter_registry_manager().registry_count(), 2);
1020        assert_eq!(
1021            first.registry().query_id(),
1022            first_query_ctx.remote_query_id_value().unwrap()
1023        );
1024        assert_eq!(
1025            second.registry().query_id(),
1026            second_query_ctx.remote_query_id_value().unwrap()
1027        );
1028    }
1029
1030    /// Builds a runtime env through the default provider seam, mirroring what
1031    /// [`QueryEngineState::try_new`] does.
1032    fn build_runtime_env(options: &QueryOptions, memory_pool_size: usize) -> Arc<RuntimeEnv> {
1033        let ctx = QueryRuntimeContext::new(options, memory_pool_size);
1034        let builder = DefaultQueryRuntimeProvider::runtime_env_builder(ctx);
1035        DefaultQueryRuntimeProvider
1036            .build_runtime_env(ctx, builder)
1037            .expect("Failed to build RuntimeEnv")
1038    }
1039
1040    #[test]
1041    fn test_build_runtime_env_custom_mode_with_path() {
1042        // Use a temp directory managed manually so we don't need the `tempfile` crate.
1043        let spill_dir = std::env::temp_dir().join(format!("df_spill_test_{}", std::process::id()));
1044        let _ = std::fs::remove_dir_all(&spill_dir);
1045        let opts = QueryOptions {
1046            experimental_spill_mode: QuerySpillMode::Custom,
1047            experimental_spill_path: Some(spill_dir.clone()),
1048            experimental_spill_max_temp_directory_size: ReadableSize::gb(1),
1049            ..Default::default()
1050        };
1051        let env = build_runtime_env(&opts, 0);
1052
1053        assert!(env.disk_manager.tmp_files_enabled());
1054        let tmp_file = env.disk_manager.create_tmp_file("test spill");
1055        assert!(tmp_file.is_ok());
1056        assert!(spill_dir.exists());
1057
1058        let _ = std::fs::remove_dir_all(&spill_dir);
1059    }
1060
1061    #[test]
1062    fn test_build_runtime_env_disabled_mode() {
1063        let opts = QueryOptions {
1064            experimental_spill_mode: QuerySpillMode::Disabled,
1065            ..Default::default()
1066        };
1067        let env = build_runtime_env(&opts, 0);
1068
1069        assert!(!env.disk_manager.tmp_files_enabled());
1070        let result = env.disk_manager.create_tmp_file("test spill");
1071        assert!(result.is_err());
1072        if let Err(error) = result {
1073            assert!(format!("{error}").contains("DiskManager is disabled"));
1074        }
1075    }
1076
1077    #[test]
1078    fn test_metrics_memory_pool_policy_differs_with_multiple_spillable_consumers() {
1079        for (policy, greedy) in [
1080            (QueryMemoryPoolPolicy::Greedy, true),
1081            (QueryMemoryPoolPolicy::Fair, false),
1082        ] {
1083            let pool: Arc<dyn MemoryPool> = Arc::new(MetricsMemoryPool::new(100, policy));
1084            let first = MemoryConsumer::new("first")
1085                .with_can_spill(true)
1086                .register(&pool);
1087            let second = MemoryConsumer::new("second")
1088                .with_can_spill(true)
1089                .register(&pool);
1090
1091            let first_result = first.try_grow(75);
1092            assert_eq!(first_result.is_ok(), greedy);
1093            if greedy {
1094                assert!(second.try_grow(26).is_err());
1095            } else {
1096                first.try_grow(50).unwrap();
1097                second.try_grow(50).unwrap();
1098            }
1099        }
1100    }
1101
1102    /// Builds a runtime with custom spill configuration and a bounded memory
1103    /// pool, then runs sort queries that probe spill-to-disk behaviour:
1104    ///
1105    /// - A pool large enough for merge chunks but still much smaller
1106    ///   than the total data.  Executes the physical plan directly so we can
1107    ///   walk the plan tree afterwards and sum `spill_count` / `spilled_rows` /
1108    ///   `spilled_bytes` on `SortExec` nodes.  All three must be > 0.
1109    #[tokio::test]
1110    async fn test_sort_spill_smoke_with_custom_runtime() {
1111        use arrow::array::Int32Array;
1112        use arrow::datatypes::{DataType, Field, Schema};
1113        use arrow::record_batch::RecordBatch;
1114        use datafusion::datasource::MemTable;
1115        use datafusion::physical_plan::collect as df_collect;
1116
1117        let spill_dir =
1118            std::env::temp_dir().join(format!("greptime_spill_smoke_{}", std::process::id()));
1119        let _ = std::fs::remove_dir_all(&spill_dir);
1120
1121        let opts = QueryOptions {
1122            experimental_spill_mode: QuerySpillMode::Custom,
1123            experimental_spill_path: Some(spill_dir.clone()),
1124            experimental_spill_max_temp_directory_size: ReadableSize::gb(1),
1125            experimental_memory_pool_policy: QueryMemoryPoolPolicy::Greedy,
1126            ..Default::default()
1127        };
1128
1129        let session_config = SessionConfig::new()
1130            .with_target_partitions(1)
1131            .with_sort_in_place_threshold_bytes(0)
1132            .with_sort_spill_reservation_bytes(64 * 1024);
1133
1134        let schema = Arc::new(Schema::new(vec![
1135            Field::new("id", DataType::Int32, false),
1136            Field::new("val", DataType::Int32, false),
1137        ]));
1138        let n_rows: i32 = 200_000;
1139        let batch_size: i32 = 5_000;
1140        let partitions = 1usize;
1141
1142        let mut table_partitions: Vec<Vec<RecordBatch>> = Vec::with_capacity(partitions);
1143        for _ in 0..partitions {
1144            let mut batches = Vec::new();
1145            let mut row_offset: i32 = 0;
1146            while row_offset < n_rows {
1147                let chunk_end = (row_offset + batch_size).min(n_rows);
1148                let chunk_len = (chunk_end - row_offset) as usize;
1149                let mut id_builder = Int32Array::builder(chunk_len);
1150                let mut val_builder = Int32Array::builder(chunk_len);
1151                for i in row_offset..chunk_end {
1152                    id_builder.append_value(i);
1153                    val_builder.append_value(n_rows - 1 - i);
1154                }
1155                batches.push(
1156                    RecordBatch::try_new(
1157                        Arc::clone(&schema),
1158                        vec![
1159                            Arc::new(id_builder.finish()),
1160                            Arc::new(val_builder.finish()),
1161                        ],
1162                    )
1163                    .unwrap(),
1164                );
1165                row_offset = chunk_end;
1166            }
1167            table_partitions.push(batches);
1168        }
1169
1170        fn build_ctx(
1171            session_config: &SessionConfig,
1172            runtime: &Arc<RuntimeEnv>,
1173            schema: &Arc<Schema>,
1174            partitions: &[Vec<RecordBatch>],
1175        ) -> SessionContext {
1176            let session_state = SessionStateBuilder::new()
1177                .with_config(session_config.clone())
1178                .with_runtime_env(Arc::clone(runtime))
1179                .with_default_features()
1180                .build();
1181            let ctx = SessionContext::new_with_state(session_state);
1182            let table = MemTable::try_new(Arc::clone(schema), partitions.to_vec()).unwrap();
1183            ctx.register_table("t", Arc::new(table)).unwrap();
1184            ctx
1185        }
1186
1187        {
1188            let mem_limit: usize = 512 * 1024; // 512 KB pool, 64 KB reserved for merge
1189
1190            let runtime = build_runtime_env(&opts, mem_limit);
1191            assert_eq!(runtime.memory_pool.reserved(), 0);
1192            let ctx = build_ctx(&session_config, &runtime, &schema, &table_partitions);
1193
1194            let df = ctx
1195                .sql("SELECT val FROM t ORDER BY val ASC")
1196                .await
1197                .expect("planning ORDER BY");
1198            let plan = df
1199                .create_physical_plan()
1200                .await
1201                .expect("creating physical plan");
1202            let task_ctx = ctx.task_ctx();
1203
1204            let batches = df_collect(Arc::clone(&plan), task_ctx)
1205                .await
1206                .expect("executing ORDER BY");
1207
1208            let (spill_count, spilled_rows, spilled_bytes) = sum_sort_spill_metrics(&plan);
1209            assert!(
1210                spill_count > 0,
1211                "expected SortExec spill_count > 0 (pool={} KB, reservation=64 KB), got 0",
1212                mem_limit / 1024,
1213            );
1214            assert!(
1215                spilled_rows > 0,
1216                "expected SortExec spilled_rows > 0, got 0 \
1217                 (spill_count={spill_count}, spilled_bytes={spilled_bytes})",
1218            );
1219            assert!(
1220                spilled_bytes > 0,
1221                "expected SortExec spilled_bytes > 0, got 0 \
1222                 (spill_count={spill_count}, spilled_rows={spilled_rows})",
1223            );
1224
1225            let vals: Vec<i32> = batches
1226                .iter()
1227                .flat_map(|b| {
1228                    let col = b.column(0).as_any().downcast_ref::<Int32Array>().unwrap();
1229                    (0..b.num_rows()).map(move |i| col.value(i))
1230                })
1231                .collect();
1232            assert_eq!(vals.len(), n_rows as usize);
1233            for w in vals.windows(2) {
1234                assert!(w[0] <= w[1], "sort order violation: {} > {}", w[0], w[1]);
1235            }
1236
1237            assert!(
1238                std::fs::read_dir(&spill_dir)
1239                    .ok()
1240                    .map(|mut entries| entries.any(|e| {
1241                        e.as_ref()
1242                            .map(|de| de.file_name().to_string_lossy().starts_with("datafusion-"))
1243                            .unwrap_or(false)
1244                    }))
1245                    .unwrap_or(false),
1246                "Expected 'datafusion-*' directory in spill path {:?} \
1247                 (DiskManager should create it on build).",
1248                spill_dir,
1249            );
1250
1251            assert_eq!(runtime.memory_pool.reserved(), 0);
1252        }
1253
1254        let _ = std::fs::remove_dir_all(&spill_dir);
1255    }
1256
1257    /// Walk a physical plan tree, summing spill metrics for every node whose
1258    /// name starts with `"SortExec"`.
1259    fn sum_sort_spill_metrics(plan: &Arc<dyn ExecutionPlan>) -> (usize, usize, usize) {
1260        let mut spill_count = 0usize;
1261        let mut spilled_rows = 0usize;
1262        let mut spilled_bytes = 0usize;
1263
1264        fn walk(plan: &Arc<dyn ExecutionPlan>, sc: &mut usize, sr: &mut usize, sb: &mut usize) {
1265            let name = plan.name();
1266            if name.starts_with("SortExec")
1267                && let Some(m) = plan.metrics()
1268            {
1269                *sc += m.spill_count().unwrap_or(0);
1270                *sr += m.spilled_rows().unwrap_or(0);
1271                *sb += m.spilled_bytes().unwrap_or(0);
1272            }
1273            for child in plan.children() {
1274                walk(child, sc, sr, sb);
1275            }
1276        }
1277
1278        walk(
1279            plan,
1280            &mut spill_count,
1281            &mut spilled_rows,
1282            &mut spilled_bytes,
1283        );
1284        (spill_count, spilled_rows, spilled_bytes)
1285    }
1286}