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