Skip to main content

query/promql/
planner.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::{BTreeSet, HashMap, HashSet, VecDeque};
16use std::sync::Arc;
17use std::time::UNIX_EPOCH;
18
19use arrow::datatypes::IntervalDayTime;
20use async_recursion::async_recursion;
21use catalog::table_source::DfTableSourceProvider;
22use common_error::ext::ErrorExt;
23use common_error::status_code::StatusCode;
24use common_function::function::FunctionContext;
25use common_query::prelude::greptime_value;
26use datafusion::common::DFSchemaRef;
27use datafusion::datasource::DefaultTableSource;
28use datafusion::functions_aggregate::average::avg_udaf;
29use datafusion::functions_aggregate::count::count_udaf;
30use datafusion::functions_aggregate::expr_fn::first_value;
31use datafusion::functions_aggregate::min_max::{max_udaf, min_udaf};
32use datafusion::functions_aggregate::stddev::stddev_pop_udaf;
33use datafusion::functions_aggregate::sum::sum_udaf;
34use datafusion::functions_aggregate::variance::var_pop_udaf;
35use datafusion::functions_window::row_number::RowNumber;
36use datafusion::logical_expr::expr::{Alias, ScalarFunction, WindowFunction};
37use datafusion::logical_expr::expr_rewriter::normalize_cols;
38use datafusion::logical_expr::{
39    BinaryExpr, Cast, Extension, LogicalPlan, LogicalPlanBuilder, Operator,
40    ScalarUDF as ScalarUdfDef, WindowFrame, WindowFunctionDefinition,
41};
42use datafusion::prelude as df_prelude;
43use datafusion::prelude::{Column, Expr as DfExpr, JoinType};
44use datafusion::scalar::ScalarValue;
45use datafusion::sql::TableReference;
46use datafusion_common::tree_node::{Transformed, TreeNode, TreeNodeRewriter};
47use datafusion_common::{DFSchema, NullEquality};
48use datafusion_expr::expr::WindowFunctionParams;
49use datafusion_expr::utils::conjunction;
50use datafusion_expr::{
51    ExprSchemable, Literal, Projection, SortExpr, TableScan, TableSource, col, lit,
52};
53use datafusion_functions::core::coalesce;
54use datatypes::arrow::datatypes::{DataType as ArrowDataType, TimeUnit as ArrowTimeUnit};
55use datatypes::data_type::ConcreteDataType;
56use itertools::Itertools;
57use once_cell::sync::Lazy;
58use promql::extension_plan::{
59    Absent, EmptyMetric, HistogramFold, InstantManipulate, Millisecond, RangeManipulate,
60    ScalarCalculate, SeriesDivide, SeriesNormalize, UnionDistinctOn, build_special_time_expr,
61};
62use promql::functions::{
63    AbsentOverTime, AvgOverTime, Changes, CountOverTime, Delta, Deriv, DoubleExponentialSmoothing,
64    IDelta, Increase, LastOverTime, MaxOverTime, MinOverTime, PredictLinear, PresentOverTime,
65    QuantileOverTime, Rate, Resets, Round, StddevOverTime, StdvarOverTime, SumOverTime,
66    quantile_udaf,
67};
68use promql_parser::label::{METRIC_NAME, MatchOp, Matcher, Matchers};
69use promql_parser::parser::token::TokenType;
70use promql_parser::parser::value::ValueType;
71use promql_parser::parser::{
72    AggregateExpr, BinModifier, BinaryExpr as PromBinaryExpr, Call, EvalStmt, Expr as PromExpr,
73    Function, FunctionArgs as PromFunctionArgs, LabelModifier, MatrixSelector, NumberLiteral,
74    Offset, ParenExpr, StringLiteral, SubqueryExpr, UnaryExpr, VectorMatchCardinality,
75    VectorSelector, token,
76};
77use regex::{self, Regex};
78use snafu::{OptionExt, ResultExt, ensure};
79use store_api::metric_engine_consts::{
80    DATA_SCHEMA_TABLE_ID_COLUMN_NAME, DATA_SCHEMA_TSID_COLUMN_NAME, LOGICAL_TABLE_METADATA_KEY,
81    METRIC_ENGINE_NAME, is_metric_engine_internal_column,
82};
83use table::table::adapter::DfTableProviderAdapter;
84
85use crate::parser::{
86    ALIAS_NODE_NAME, ANALYZE_NODE_NAME, ANALYZE_VERBOSE_NODE_NAME, AliasExpr, EXPLAIN_NODE_NAME,
87    EXPLAIN_VERBOSE_NODE_NAME,
88};
89use crate::promql::error::{
90    CatalogSnafu, ColumnNotFoundSnafu, CombineTableColumnMismatchSnafu, DataFusionPlanningSnafu,
91    ExpectRangeSelectorSnafu, FunctionInvalidArgumentSnafu, InvalidDestinationLabelNameSnafu,
92    InvalidRegularExpressionSnafu, InvalidTimeRangeSnafu, MultiFieldsNotSupportedSnafu,
93    MultipleMetricMatchersSnafu, MultipleVectorSnafu, NoMetricMatcherSnafu, PromqlPlanNodeSnafu,
94    Result, SameLabelSetSnafu, TableNameNotFoundSnafu, TimeIndexNotFoundSnafu,
95    UnexpectedPlanExprSnafu, UnexpectedTokenSnafu, UnknownTableSnafu, UnsupportedExprSnafu,
96    UnsupportedMatcherOpSnafu, UnsupportedVectorMatchSnafu, ValueNotFoundSnafu,
97    ZeroRangeSelectorSnafu,
98};
99use crate::query_engine::QueryEngineState;
100
101/// `time()` function in PromQL.
102const SPECIAL_TIME_FUNCTION: &str = "time";
103/// `scalar()` function in PromQL.
104const SCALAR_FUNCTION: &str = "scalar";
105/// `absent()` function in PromQL
106const SPECIAL_ABSENT_FUNCTION: &str = "absent";
107/// `histogram_quantile` function in PromQL
108const SPECIAL_HISTOGRAM_QUANTILE: &str = "histogram_quantile";
109/// `vector` function in PromQL
110const SPECIAL_VECTOR_FUNCTION: &str = "vector";
111/// `le` column for conventional histogram.
112const LE_COLUMN_NAME: &str = "le";
113
114/// Static regex for validating label names according to Prometheus specification.
115/// Label names must match the regex: [a-zA-Z_][a-zA-Z0-9_]*
116static LABEL_NAME_REGEX: Lazy<Regex> =
117    Lazy::new(|| Regex::new(r"^[a-zA-Z_][a-zA-Z0-9_]*$").unwrap());
118
119const DEFAULT_TIME_INDEX_COLUMN: &str = "time";
120
121/// default value column name for empty metric
122const DEFAULT_FIELD_COLUMN: &str = "value";
123
124/// Special modifier to project field columns under multi-field mode
125const FIELD_COLUMN_MATCHER: &str = "__field__";
126
127/// Special modifier for cross schema query
128const SCHEMA_COLUMN_MATCHER: &str = "__schema__";
129const DB_COLUMN_MATCHER: &str = "__database__";
130
131/// Prefix for generated binary island leaf aliases.
132const BINARY_ISLAND_LEAF_ALIAS_PREFIX: &str = "__prom_v";
133
134/// Threshold for scatter scan mode
135const MAX_SCATTER_POINTS: i64 = 400;
136
137/// Interval 1 hour in millisecond
138const INTERVAL_1H: i64 = 60 * 60 * 1000;
139
140#[derive(Default, Debug, Clone)]
141struct PromPlannerContext {
142    // query parameters
143    start: Millisecond,
144    end: Millisecond,
145    interval: Millisecond,
146    lookback_delta: Millisecond,
147
148    // planner states
149    table_name: Option<String>,
150    time_index_column: Option<String>,
151    field_columns: Vec<String>,
152    tag_columns: Vec<String>,
153    /// Use metric engine internal series identifier column (`__tsid`) as series key.
154    ///
155    /// This is enabled only when the underlying scan can provide `__tsid` (`UInt64`). The planner
156    /// uses it internally (e.g. as the series key for [`SeriesDivide`]) and strips it from the
157    /// final output.
158    use_tsid: bool,
159    /// The matcher for field columns `__field__`.
160    field_column_matcher: Option<Vec<Matcher>>,
161    /// The matcher for selectors (normal matchers).
162    selector_matcher: Vec<Matcher>,
163    schema_name: Option<String>,
164    /// The range in millisecond of range selector. None if there is no range selector.
165    range: Option<Millisecond>,
166}
167
168#[derive(Debug, Clone, PartialEq, Eq, Hash)]
169struct VectorLeafKey {
170    metric_name: String,
171    matchers: Vec<(String, String, String)>,
172    or_matchers: Vec<Vec<(String, String, String)>>,
173    offset_ms: i128,
174    at: String,
175}
176
177#[derive(Debug, Clone)]
178struct IslandLeaf {
179    selector: VectorSelector,
180    display_table: String,
181}
182
183#[derive(Debug, Clone)]
184enum IslandExpr {
185    VectorLeaf(usize),
186    Scalar(DfExpr),
187    Unary {
188        input: Box<IslandExpr>,
189    },
190    Binary {
191        op: TokenType,
192        lhs: Box<IslandExpr>,
193        rhs: Box<IslandExpr>,
194    },
195}
196
197impl IslandExpr {
198    fn try_new(expr: &PromExpr, env: &mut IslandCollectEnv) -> Option<Self> {
199        if let Some(expr) = PromPlanner::try_build_literal_expr(expr) {
200            return Some(Self::Scalar(expr));
201        }
202
203        match expr {
204            PromExpr::Paren(ParenExpr { expr }) => Self::try_new(expr, env),
205            PromExpr::VectorSelector(selector) => {
206                let leaf = env.intern_leaf(selector)?;
207                Some(Self::VectorLeaf(leaf))
208            }
209            PromExpr::Unary(UnaryExpr { expr }) => {
210                let input = Self::try_new(expr, env)?;
211                Some(Self::Unary {
212                    input: Box::new(input),
213                })
214            }
215            PromExpr::Binary(PromBinaryExpr {
216                lhs,
217                rhs,
218                op,
219                modifier,
220            }) if matches!(
221                op.id(),
222                token::T_ADD
223                    | token::T_SUB
224                    | token::T_MUL
225                    | token::T_DIV
226                    | token::T_MOD
227                    | token::T_POW
228                    | token::T_ATAN2
229            ) && modifier.as_ref().is_none_or(|modifier| {
230                !modifier.return_bool
231                    && modifier.matching.is_none()
232                    && matches!(modifier.card, VectorMatchCardinality::OneToOne)
233                    && modifier.fill_values.lhs.is_none()
234                    && modifier.fill_values.rhs.is_none()
235            }) =>
236            {
237                let lhs = Self::try_new(lhs, env)?;
238                let rhs = Self::try_new(rhs, env)?;
239                Some(Self::Binary {
240                    op: *op,
241                    lhs: Box::new(lhs),
242                    rhs: Box::new(rhs),
243                })
244            }
245            _ => None,
246        }
247    }
248}
249
250#[derive(Debug, Default)]
251struct IslandCollectEnv {
252    leaf_by_key: HashMap<VectorLeafKey, usize>,
253    leaves: Vec<IslandLeaf>,
254    vector_occurrences: usize,
255}
256
257#[derive(Debug)]
258struct PlannedIslandLeaf {
259    plan: LogicalPlan,
260    ctx: PromPlannerContext,
261    alias: TableReference,
262    display_table: String,
263}
264
265#[derive(Debug)]
266struct IslandFieldExprs {
267    exprs: Vec<DfExpr>,
268    names: Vec<String>,
269    scalar: bool,
270}
271
272impl VectorLeafKey {
273    fn from_selector(selector: &VectorSelector) -> Option<Self> {
274        let mut metric_name = selector.name.clone();
275        let mut matchers = Vec::with_capacity(selector.matchers.matchers.len());
276        let matcher_key = |matcher: &Matcher| {
277            (
278                matcher.name.clone(),
279                matcher.op.to_string(),
280                matcher.value.clone(),
281            )
282        };
283
284        for matcher in &selector.matchers.matchers {
285            if matcher.name == METRIC_NAME {
286                if matcher.op != MatchOp::Equal || metric_name.is_some() {
287                    return None;
288                }
289                metric_name = Some(matcher.value.clone());
290            } else {
291                matchers.push(matcher_key(matcher));
292            }
293        }
294        matchers.sort();
295
296        let mut or_matchers = selector
297            .matchers
298            .or_matchers
299            .iter()
300            .map(|group| {
301                let mut group = group.iter().map(matcher_key).collect::<Vec<_>>();
302                group.sort();
303                group
304            })
305            .collect::<Vec<_>>();
306        or_matchers.sort();
307
308        Some(Self {
309            metric_name: metric_name?,
310            matchers,
311            or_matchers,
312            offset_ms: match &selector.offset {
313                Some(Offset::Pos(duration)) => duration.as_millis() as i128,
314                Some(Offset::Neg(duration)) => -(duration.as_millis() as i128),
315                None => 0,
316            },
317            at: format!("{:?}", selector.at),
318        })
319    }
320}
321
322impl IslandCollectEnv {
323    fn intern_leaf(&mut self, selector: &VectorSelector) -> Option<usize> {
324        self.vector_occurrences += 1;
325        let key = VectorLeafKey::from_selector(selector)?;
326        if let Some(id) = self.leaf_by_key.get(&key) {
327            return Some(*id);
328        }
329
330        let id = self.leaves.len();
331        self.leaves.push(IslandLeaf {
332            selector: selector.clone(),
333            display_table: key.metric_name.clone(),
334        });
335        self.leaf_by_key.insert(key, id);
336        Some(id)
337    }
338}
339
340impl PromPlannerContext {
341    fn from_eval_stmt(stmt: &EvalStmt) -> Self {
342        Self {
343            start: stmt.start.duration_since(UNIX_EPOCH).unwrap().as_millis() as _,
344            end: stmt.end.duration_since(UNIX_EPOCH).unwrap().as_millis() as _,
345            interval: stmt.interval.as_millis() as _,
346            lookback_delta: stmt.lookback_delta.as_millis() as _,
347            ..Default::default()
348        }
349    }
350
351    /// Reset all planner states
352    fn reset(&mut self) {
353        self.table_name = None;
354        self.time_index_column = None;
355        self.field_columns = vec![];
356        self.tag_columns = vec![];
357        self.use_tsid = false;
358        self.field_column_matcher = None;
359        self.selector_matcher.clear();
360        self.schema_name = None;
361        self.range = None;
362    }
363
364    /// Reset table name and schema to empty
365    fn reset_table_name_and_schema(&mut self) {
366        self.table_name = Some(String::new());
367        self.schema_name = None;
368        self.use_tsid = false;
369    }
370
371    /// Check if `le` is present in tag columns
372    fn has_le_tag(&self) -> bool {
373        self.tag_columns.iter().any(|c| c.eq(&LE_COLUMN_NAME))
374    }
375}
376
377pub struct PromPlanner {
378    table_provider: DfTableSourceProvider,
379    ctx: PromPlannerContext,
380}
381
382impl PromPlanner {
383    pub async fn stmt_to_plan(
384        table_provider: DfTableSourceProvider,
385        stmt: &EvalStmt,
386        query_engine_state: &QueryEngineState,
387    ) -> Result<LogicalPlan> {
388        let mut planner = Self {
389            table_provider,
390            ctx: PromPlannerContext::from_eval_stmt(stmt),
391        };
392
393        let plan = planner
394            .prom_expr_to_plan(&stmt.expr, query_engine_state)
395            .await?;
396
397        // Never leak internal series identifier to output.
398        planner.strip_tsid_column(plan)
399    }
400
401    pub async fn prom_expr_to_plan(
402        &mut self,
403        prom_expr: &PromExpr,
404        query_engine_state: &QueryEngineState,
405    ) -> Result<LogicalPlan> {
406        self.prom_expr_to_plan_inner(prom_expr, false, query_engine_state)
407            .await
408    }
409
410    /**
411    Converts a PromQL expression to a logical plan.
412
413    NOTE:
414        The `timestamp_fn` indicates whether the PromQL `timestamp()` function is being evaluated in the current context.
415        If `true`, the planner generates a logical plan that projects the timestamp (time index) column
416        as the value column for each input row, implementing the PromQL `timestamp()` function semantics.
417        If `false`, the planner generates the standard logical plan for the given PromQL expression.
418    */
419    #[async_recursion]
420    async fn prom_expr_to_plan_inner(
421        &mut self,
422        prom_expr: &PromExpr,
423        timestamp_fn: bool,
424        query_engine_state: &QueryEngineState,
425    ) -> Result<LogicalPlan> {
426        let res = match prom_expr {
427            PromExpr::Aggregate(expr) => {
428                self.prom_aggr_expr_to_plan(query_engine_state, expr)
429                    .await?
430            }
431            PromExpr::Unary(expr) => {
432                self.prom_unary_expr_to_plan(query_engine_state, expr)
433                    .await?
434            }
435            PromExpr::Binary(expr) => {
436                self.prom_binary_expr_to_plan(query_engine_state, expr)
437                    .await?
438            }
439            PromExpr::Paren(ParenExpr { expr }) => {
440                self.prom_expr_to_plan_inner(expr, timestamp_fn, query_engine_state)
441                    .await?
442            }
443            PromExpr::Subquery(expr) => {
444                self.prom_subquery_expr_to_plan(query_engine_state, expr)
445                    .await?
446            }
447            PromExpr::NumberLiteral(lit) => self.prom_number_lit_to_plan(lit)?,
448            PromExpr::StringLiteral(lit) => self.prom_string_lit_to_plan(lit)?,
449            PromExpr::VectorSelector(selector) => {
450                self.prom_vector_selector_to_plan(selector, timestamp_fn)
451                    .await?
452            }
453            PromExpr::MatrixSelector(selector) => {
454                self.prom_matrix_selector_to_plan(selector).await?
455            }
456            PromExpr::Call(expr) => {
457                self.prom_call_expr_to_plan(query_engine_state, expr)
458                    .await?
459            }
460            PromExpr::Extension(expr) => {
461                self.prom_ext_expr_to_plan(query_engine_state, expr).await?
462            }
463        };
464
465        Ok(res)
466    }
467
468    async fn prom_subquery_expr_to_plan(
469        &mut self,
470        query_engine_state: &QueryEngineState,
471        subquery_expr: &SubqueryExpr,
472    ) -> Result<LogicalPlan> {
473        let SubqueryExpr {
474            expr, range, step, ..
475        } = subquery_expr;
476
477        let current_interval = self.ctx.interval;
478        if let Some(step) = step {
479            self.ctx.interval = step.as_millis() as _;
480        }
481        let current_start = self.ctx.start;
482        self.ctx.start -= range.as_millis() as i64 - self.ctx.interval;
483        let input = self.prom_expr_to_plan(expr, query_engine_state).await?;
484        self.ctx.interval = current_interval;
485        self.ctx.start = current_start;
486
487        ensure!(!range.is_zero(), ZeroRangeSelectorSnafu);
488        let range_ms = range.as_millis() as _;
489        self.ctx.range = Some(range_ms);
490
491        let time_index_column =
492            self.ctx
493                .time_index_column
494                .clone()
495                .with_context(|| TimeIndexNotFoundSnafu {
496                    table: self.ctx.table_name.clone().unwrap_or_default(),
497                })?;
498
499        // `RangeManipulate` assumes each input batch holds exactly one series
500        // (it takes tag column values from row 0 and applies them to every
501        // output row). The inner expression may emit batches that mix series,
502        // so sort by series key + time index and split into per-series batches
503        // with a `SeriesDivide` first.
504        let input_schema = input.schema();
505        let input_has_tsid = input_schema.fields().iter().any(|field| {
506            field.name() == DATA_SCHEMA_TSID_COLUMN_NAME
507                && field.data_type() == &ArrowDataType::UInt64
508        });
509        let (series_key_columns, mut sort_exprs) = if input_has_tsid {
510            (
511                vec![DATA_SCHEMA_TSID_COLUMN_NAME.to_string()],
512                vec![
513                    DfExpr::Column(Column::from_name(DATA_SCHEMA_TSID_COLUMN_NAME))
514                        .sort(true, true),
515                ],
516            )
517        } else {
518            // Only use tag columns that survive in the inner plan's schema —
519            // `ctx.tag_columns` can drift from the actual output.
520            let key_columns: Vec<String> = self
521                .ctx
522                .tag_columns
523                .iter()
524                .filter(|name| input_schema.has_column_with_unqualified_name(name))
525                .cloned()
526                .collect();
527            let sort = key_columns
528                .iter()
529                .map(|name| DfExpr::Column(Column::from_name(name)).sort(true, true))
530                .collect::<Vec<_>>();
531            (key_columns, sort)
532        };
533        sort_exprs.push(DfExpr::Column(Column::from_name(&time_index_column)).sort(true, true));
534
535        let sort_plan = LogicalPlanBuilder::from(input)
536            .sort(sort_exprs)
537            .context(DataFusionPlanningSnafu)?
538            .build()
539            .context(DataFusionPlanningSnafu)?;
540        let divide_plan = LogicalPlan::Extension(Extension {
541            node: Arc::new(SeriesDivide::new(
542                series_key_columns,
543                time_index_column.clone(),
544                sort_plan,
545            )),
546        });
547
548        let manipulate = RangeManipulate::new(
549            self.ctx.start,
550            self.ctx.end,
551            self.ctx.interval,
552            range_ms,
553            time_index_column,
554            self.ctx.field_columns.clone(),
555            divide_plan,
556        )
557        .context(DataFusionPlanningSnafu)?;
558
559        Ok(LogicalPlan::Extension(Extension {
560            node: Arc::new(manipulate),
561        }))
562    }
563
564    async fn prom_aggr_expr_to_plan(
565        &mut self,
566        query_engine_state: &QueryEngineState,
567        aggr_expr: &AggregateExpr,
568    ) -> Result<LogicalPlan> {
569        let AggregateExpr {
570            op,
571            expr,
572            modifier,
573            param,
574        } = aggr_expr;
575
576        let mut input = self.prom_expr_to_plan(expr, query_engine_state).await?;
577        let input_has_tsid = input.schema().fields().iter().any(|field| {
578            field.name() == DATA_SCHEMA_TSID_COLUMN_NAME
579                && field.data_type() == &ArrowDataType::UInt64
580        });
581
582        // `__tsid` based scan projection may prune tag columns. Ensure tags referenced in
583        // aggregation modifiers (`by`/`without`) are available before planning group keys.
584        let required_group_tags = match modifier {
585            None => BTreeSet::new(),
586            Some(LabelModifier::Include(labels)) => labels
587                .labels
588                .iter()
589                .filter(|label| !is_metric_engine_internal_column(label.as_str()))
590                .cloned()
591                .collect(),
592            Some(LabelModifier::Exclude(labels)) => {
593                let mut all_tags = self.collect_row_key_tag_columns_from_plan(&input)?;
594                for label in &labels.labels {
595                    let _ = all_tags.remove(label);
596                }
597                all_tags
598            }
599        };
600
601        if !required_group_tags.is_empty()
602            && required_group_tags
603                .iter()
604                .any(|tag| Self::find_case_sensitive_column(input.schema(), tag.as_str()).is_none())
605        {
606            input = self.ensure_tag_columns_available(input, &required_group_tags)?;
607            self.refresh_tag_columns_from_schema(input.schema());
608        }
609
610        match (*op).id() {
611            token::T_TOPK | token::T_BOTTOMK => {
612                self.prom_topk_bottomk_to_plan(aggr_expr, input).await
613            }
614            _ => {
615                // When `__tsid` is available, tag columns may have been pruned from the input plan.
616                // For `keep_tsid` decision we should compare against the full row-key label set,
617                // otherwise we may incorrectly treat label-reducing aggregates as preserving labels.
618                let input_tag_columns = if input_has_tsid {
619                    self.collect_row_key_tag_columns_from_plan(&input)?
620                        .into_iter()
621                        .collect::<Vec<_>>()
622                } else {
623                    self.ctx.tag_columns.clone()
624                };
625                // calculate columns to group by
626                // Need to append time index column into group by columns
627                let mut group_exprs = self.agg_modifier_to_col(input.schema(), modifier, true)?;
628                // convert op and value columns to aggregate exprs
629                let (mut aggr_exprs, prev_field_exprs) =
630                    self.create_aggregate_exprs(*op, param, &input)?;
631
632                let keep_tsid = op.id() != token::T_COUNT_VALUES
633                    && input_has_tsid
634                    && input_tag_columns.iter().collect::<HashSet<_>>()
635                        == self.ctx.tag_columns.iter().collect::<HashSet<_>>();
636
637                if keep_tsid {
638                    aggr_exprs.push(
639                        first_value(
640                            DfExpr::Column(Column::from_name(DATA_SCHEMA_TSID_COLUMN_NAME)),
641                            vec![],
642                        )
643                        .alias(DATA_SCHEMA_TSID_COLUMN_NAME),
644                    );
645                }
646                self.ctx.use_tsid = keep_tsid;
647
648                // create plan
649                let builder = LogicalPlanBuilder::from(input);
650                let builder = if op.id() == token::T_COUNT_VALUES {
651                    let label = Self::get_param_value_as_str(*op, param)?;
652                    // `count_values` must be grouped by fields,
653                    // and project the fields to the new label.
654                    group_exprs.extend(prev_field_exprs.clone());
655                    let project_fields = self
656                        .create_field_column_exprs()?
657                        .into_iter()
658                        .chain(self.create_tag_column_exprs()?)
659                        .chain(Some(self.create_time_index_column_expr()?))
660                        .chain(prev_field_exprs.into_iter().map(|expr| expr.alias(label)));
661
662                    builder
663                        .aggregate(group_exprs.clone(), aggr_exprs)
664                        .context(DataFusionPlanningSnafu)?
665                        .project(project_fields)
666                        .context(DataFusionPlanningSnafu)?
667                } else {
668                    builder
669                        .aggregate(group_exprs.clone(), aggr_exprs)
670                        .context(DataFusionPlanningSnafu)?
671                };
672
673                let sort_expr = group_exprs.into_iter().map(|expr| expr.sort(true, false));
674
675                builder
676                    .sort(sort_expr)
677                    .context(DataFusionPlanningSnafu)?
678                    .build()
679                    .context(DataFusionPlanningSnafu)
680            }
681        }
682    }
683
684    /// Create logical plan for PromQL topk and bottomk expr.
685    async fn prom_topk_bottomk_to_plan(
686        &mut self,
687        aggr_expr: &AggregateExpr,
688        input: LogicalPlan,
689    ) -> Result<LogicalPlan> {
690        let AggregateExpr {
691            op,
692            param,
693            modifier,
694            ..
695        } = aggr_expr;
696
697        let input_has_tsid = input.schema().fields().iter().any(|field| {
698            field.name() == DATA_SCHEMA_TSID_COLUMN_NAME
699                && field.data_type() == &ArrowDataType::UInt64
700        });
701        self.ctx.use_tsid = input_has_tsid;
702
703        let group_exprs = self.agg_modifier_to_col(input.schema(), modifier, false)?;
704
705        let val = Self::get_param_as_literal_expr(param, Some(*op), Some(ArrowDataType::Float64))?;
706
707        // convert op and value columns to window exprs.
708        let window_exprs = self.create_window_exprs(*op, group_exprs.clone(), &input)?;
709
710        let rank_columns: Vec<_> = window_exprs
711            .iter()
712            .map(|expr| expr.schema_name().to_string())
713            .collect();
714
715        // Create ranks filter with `Operator::Or`.
716        // Safety: at least one rank column
717        let filter: DfExpr = rank_columns
718            .iter()
719            .fold(None, |expr, rank| {
720                let predicate = DfExpr::BinaryExpr(BinaryExpr {
721                    left: Box::new(col(rank)),
722                    op: Operator::LtEq,
723                    right: Box::new(val.clone()),
724                });
725
726                match expr {
727                    None => Some(predicate),
728                    Some(expr) => Some(DfExpr::BinaryExpr(BinaryExpr {
729                        left: Box::new(expr),
730                        op: Operator::Or,
731                        right: Box::new(predicate),
732                    })),
733                }
734            })
735            .unwrap();
736
737        let rank_columns: Vec<_> = rank_columns.into_iter().map(col).collect();
738
739        let mut new_group_exprs = group_exprs.clone();
740        // Order by ranks
741        new_group_exprs.extend(rank_columns);
742
743        let group_sort_expr = new_group_exprs
744            .into_iter()
745            .map(|expr| expr.sort(true, false));
746
747        let project_fields = self
748            .create_field_column_exprs()?
749            .into_iter()
750            .chain(self.create_tag_column_exprs()?)
751            .chain(
752                self.ctx
753                    .use_tsid
754                    .then_some(DfExpr::Column(Column::from_name(
755                        DATA_SCHEMA_TSID_COLUMN_NAME,
756                    ))),
757            )
758            .chain(Some(self.create_time_index_column_expr()?));
759
760        LogicalPlanBuilder::from(input)
761            .window(window_exprs)
762            .context(DataFusionPlanningSnafu)?
763            .filter(filter)
764            .context(DataFusionPlanningSnafu)?
765            .sort(group_sort_expr)
766            .context(DataFusionPlanningSnafu)?
767            .project(project_fields)
768            .context(DataFusionPlanningSnafu)?
769            .build()
770            .context(DataFusionPlanningSnafu)
771    }
772
773    async fn prom_unary_expr_to_plan(
774        &mut self,
775        query_engine_state: &QueryEngineState,
776        unary_expr: &UnaryExpr,
777    ) -> Result<LogicalPlan> {
778        let UnaryExpr { expr } = unary_expr;
779        // Unary Expr in PromQL implys the `-` operator
780        let input = self.prom_expr_to_plan(expr, query_engine_state).await?;
781        self.projection_for_each_field_column(input, |col| {
782            Ok(DfExpr::Negative(Box::new(DfExpr::Column(col.into()))))
783        })
784    }
785
786    async fn try_plan_binary_island(
787        &mut self,
788        binary_expr: &PromBinaryExpr,
789    ) -> Result<Option<LogicalPlan>> {
790        let original_ctx = self.ctx.clone();
791        let mut collect_env = IslandCollectEnv::default();
792        let Some(island_expr) =
793            IslandExpr::try_new(&PromExpr::Binary(binary_expr.clone()), &mut collect_env)
794        else {
795            return Ok(None);
796        };
797
798        if collect_env.leaves.is_empty()
799            || collect_env.vector_occurrences <= collect_env.leaves.len()
800        {
801            return Ok(None);
802        }
803
804        let mut planned_leaves = Vec::with_capacity(collect_env.leaves.len());
805        for (idx, leaf) in collect_env.leaves.iter().enumerate() {
806            let plan = self
807                .prom_vector_selector_to_plan(&leaf.selector, false)
808                .await?;
809            let ctx = self.ctx.clone();
810            let alias = TableReference::bare(format!("{BINARY_ISLAND_LEAF_ALIAS_PREFIX}{idx}"));
811            let plan = LogicalPlanBuilder::from(plan)
812                .alias(alias.clone())
813                .context(DataFusionPlanningSnafu)?
814                .build()
815                .context(DataFusionPlanningSnafu)?;
816            planned_leaves.push(PlannedIslandLeaf {
817                plan,
818                ctx,
819                alias,
820                display_table: leaf.display_table.clone(),
821            });
822        }
823
824        if !Self::binary_island_join_contexts_supported(&planned_leaves) {
825            self.ctx = original_ctx;
826            return Ok(None);
827        }
828
829        let mut input = planned_leaves[0].plan.clone();
830        for right_idx in 1..planned_leaves.len() {
831            input = self.join_binary_island_leaf(
832                input,
833                &planned_leaves[0],
834                &planned_leaves[right_idx],
835            )?;
836        }
837
838        let field_exprs =
839            Self::build_binary_island_field_exprs(&island_expr, &planned_leaves, input.schema())?;
840        if field_exprs.scalar || field_exprs.exprs.is_empty() {
841            self.ctx = original_ctx;
842            return Ok(None);
843        }
844
845        let plan = self.project_binary_island(
846            input,
847            &planned_leaves[0].alias,
848            &planned_leaves[0].ctx,
849            field_exprs,
850        )?;
851        Ok(Some(plan))
852    }
853
854    fn binary_island_join_contexts_supported(leaves: &[PlannedIslandLeaf]) -> bool {
855        if leaves
856            .iter()
857            .any(|leaf| leaf.ctx.time_index_column.is_none())
858        {
859            return false;
860        }
861
862        if leaves.len() <= 1 {
863            return true;
864        }
865
866        let first_tags = leaves[0].ctx.tag_columns.iter().collect::<BTreeSet<_>>();
867
868        leaves.iter().skip(1).all(|leaf| {
869            (Self::plan_has_tsid_column(&leaves[0].plan) && Self::plan_has_tsid_column(&leaf.plan))
870                || leaf.ctx.tag_columns.iter().collect::<BTreeSet<_>>() == first_tags
871        })
872    }
873
874    fn join_binary_island_leaf(
875        &self,
876        left: LogicalPlan,
877        first_leaf: &PlannedIslandLeaf,
878        right_leaf: &PlannedIslandLeaf,
879    ) -> Result<LogicalPlan> {
880        let only_join_time_index =
881            first_leaf.ctx.tag_columns.is_empty() || right_leaf.ctx.tag_columns.is_empty();
882        let (mut left_keys, mut right_keys, force_empty_join) = self.binary_join_key_columns(
883            left.schema(),
884            right_leaf.plan.schema(),
885            &first_leaf.ctx,
886            &right_leaf.ctx,
887            only_join_time_index,
888            &None,
889        )?;
890
891        if let (Some(left_time_index_column), Some(right_time_index_column)) = (
892            first_leaf.ctx.time_index_column.clone(),
893            right_leaf.ctx.time_index_column.clone(),
894        ) {
895            left_keys.insert(left_time_index_column);
896            right_keys.insert(right_time_index_column);
897        }
898
899        LogicalPlanBuilder::from(left)
900            .join_detailed(
901                right_leaf.plan.clone(),
902                JoinType::Inner,
903                (
904                    left_keys
905                        .into_iter()
906                        .map(|name| Column::new(Some(first_leaf.alias.clone()), name))
907                        .collect::<Vec<_>>(),
908                    right_keys
909                        .into_iter()
910                        .map(|name| Column::new(Some(right_leaf.alias.clone()), name))
911                        .collect::<Vec<_>>(),
912                ),
913                force_empty_join.then_some(lit(false)),
914                NullEquality::NullEqualsNull,
915            )
916            .context(DataFusionPlanningSnafu)?
917            .build()
918            .context(DataFusionPlanningSnafu)
919    }
920
921    fn build_binary_island_field_exprs(
922        expr: &IslandExpr,
923        leaves: &[PlannedIslandLeaf],
924        schema: &DFSchemaRef,
925    ) -> Result<IslandFieldExprs> {
926        match expr {
927            IslandExpr::VectorLeaf(id) => {
928                let leaf = &leaves[*id];
929                let exprs = leaf
930                    .ctx
931                    .field_columns
932                    .iter()
933                    .map(|field| {
934                        schema
935                            .qualified_field_with_name(Some(&leaf.alias), field)
936                            .context(DataFusionPlanningSnafu)
937                            .map(|field| DfExpr::Column(field.into()))
938                    })
939                    .collect::<Result<Vec<_>>>()?;
940                let names = leaf
941                    .ctx
942                    .field_columns
943                    .iter()
944                    .map(|field| format!("{}.{}", leaf.display_table, field))
945                    .collect();
946                Ok(IslandFieldExprs {
947                    exprs,
948                    names,
949                    scalar: false,
950                })
951            }
952            IslandExpr::Scalar(expr) => Ok(IslandFieldExprs {
953                exprs: vec![expr.clone()],
954                names: vec![expr.schema_name().to_string()],
955                scalar: true,
956            }),
957            IslandExpr::Unary { input } => {
958                let input = Self::build_binary_island_field_exprs(input, leaves, schema)?;
959                let mut exprs = Vec::with_capacity(input.exprs.len());
960                let mut names = Vec::with_capacity(input.names.len());
961                for (expr, name) in input.exprs.into_iter().zip(input.names) {
962                    exprs.push(DfExpr::Negative(Box::new(expr)));
963                    names.push(format!("-{name}"));
964                }
965                Ok(IslandFieldExprs {
966                    exprs,
967                    names,
968                    scalar: input.scalar,
969                })
970            }
971            IslandExpr::Binary { op, lhs, rhs } => {
972                let same_leaf = match (&**lhs, &**rhs) {
973                    (IslandExpr::VectorLeaf(left), IslandExpr::VectorLeaf(right))
974                        if left == right =>
975                    {
976                        Some(*left)
977                    }
978                    _ => None,
979                };
980                let lhs = Self::build_binary_island_field_exprs(lhs, leaves, schema)?;
981                let rhs = Self::build_binary_island_field_exprs(rhs, leaves, schema)?;
982                let expr_builder = Self::prom_token_to_binary_expr_builder(*op)?;
983                let scalar = lhs.scalar && rhs.scalar;
984                let op = op.to_string();
985
986                let (exprs, names) = match (lhs.scalar, rhs.scalar) {
987                    (true, true) => {
988                        let expr = expr_builder(lhs.exprs[0].clone(), rhs.exprs[0].clone())?;
989                        let name = format!("{} {op} {}", lhs.names[0], rhs.names[0]);
990                        (vec![expr], vec![name])
991                    }
992                    (true, false) => {
993                        let mut exprs = Vec::with_capacity(rhs.exprs.len());
994                        let mut names = Vec::with_capacity(rhs.names.len());
995                        for (rhs_expr, rhs_name) in rhs.exprs.into_iter().zip(rhs.names) {
996                            exprs.push(expr_builder(lhs.exprs[0].clone(), rhs_expr)?);
997                            names.push(format!("{} {op} {rhs_name}", lhs.names[0]));
998                        }
999                        (exprs, names)
1000                    }
1001                    (false, true) => {
1002                        let mut exprs = Vec::with_capacity(lhs.exprs.len());
1003                        let mut names = Vec::with_capacity(lhs.names.len());
1004                        for (lhs_expr, lhs_name) in lhs.exprs.into_iter().zip(lhs.names) {
1005                            exprs.push(expr_builder(lhs_expr, rhs.exprs[0].clone())?);
1006                            names.push(format!("{lhs_name} {op} {}", rhs.names[0]));
1007                        }
1008                        (exprs, names)
1009                    }
1010                    (false, false) => {
1011                        let mut exprs = Vec::new();
1012                        let mut names = Vec::new();
1013                        for (idx, ((lhs_expr, rhs_expr), (mut lhs_name, mut rhs_name))) in lhs
1014                            .exprs
1015                            .into_iter()
1016                            .zip(rhs.exprs)
1017                            .zip(lhs.names.into_iter().zip(rhs.names))
1018                            .enumerate()
1019                        {
1020                            if let Some(leaf) = same_leaf {
1021                                let field = leaves[leaf]
1022                                    .ctx
1023                                    .field_columns
1024                                    .get(idx)
1025                                    .cloned()
1026                                    .unwrap_or_else(|| lhs_name.clone());
1027                                lhs_name = format!("lhs.{field}");
1028                                rhs_name = format!("rhs.{field}");
1029                            }
1030                            exprs.push(expr_builder(lhs_expr, rhs_expr)?);
1031                            names.push(format!("{lhs_name} {op} {rhs_name}"));
1032                        }
1033                        (exprs, names)
1034                    }
1035                };
1036
1037                Ok(IslandFieldExprs {
1038                    exprs,
1039                    names,
1040                    scalar,
1041                })
1042            }
1043        }
1044    }
1045
1046    fn project_binary_island(
1047        &mut self,
1048        input: LogicalPlan,
1049        base_alias: &TableReference,
1050        base_ctx: &PromPlannerContext,
1051        field_exprs: IslandFieldExprs,
1052    ) -> Result<LogicalPlan> {
1053        self.ctx = base_ctx.clone();
1054
1055        let schema = input.schema();
1056        let non_field_exprs = base_ctx
1057            .tag_columns
1058            .iter()
1059            .chain(base_ctx.time_index_column.iter())
1060            .map(|column| {
1061                schema
1062                    .qualified_field_with_name(Some(base_alias), column)
1063                    .context(DataFusionPlanningSnafu)
1064                    .map(|field| DfExpr::Column(field.into()))
1065            });
1066        let tsid_expr = Self::optional_tsid_projection(schema, Some(base_alias), base_ctx.use_tsid)
1067            .into_iter()
1068            .map(Ok);
1069
1070        self.ctx.field_columns = field_exprs.names;
1071        let field_exprs = field_exprs
1072            .exprs
1073            .into_iter()
1074            .zip(self.ctx.field_columns.iter())
1075            .map(|(expr, name)| Ok(DfExpr::Alias(Alias::new(expr, None::<String>, name))));
1076
1077        let project_exprs = non_field_exprs
1078            .chain(tsid_expr)
1079            .chain(field_exprs)
1080            .collect::<Result<Vec<_>>>()?;
1081
1082        let plan = LogicalPlanBuilder::from(input)
1083            .project(project_exprs)
1084            .context(DataFusionPlanningSnafu)?
1085            .build()
1086            .context(DataFusionPlanningSnafu)?;
1087
1088        self.ctx.table_name = None;
1089        self.ctx.schema_name = None;
1090
1091        Ok(plan)
1092    }
1093
1094    async fn prom_binary_expr_to_plan(
1095        &mut self,
1096        query_engine_state: &QueryEngineState,
1097        binary_expr: &PromBinaryExpr,
1098    ) -> Result<LogicalPlan> {
1099        // promql-parser accepts fill modifiers, but Greptime does not implement the
1100        // required outer joins and missing-value substitution. Reject them before the
1101        // binary-island fast path so they cannot silently behave like normal inner joins.
1102        if let Some(modifier) = &binary_expr.modifier {
1103            ensure!(
1104                modifier.fill_values.lhs.is_none() && modifier.fill_values.rhs.is_none(),
1105                UnsupportedExprSnafu {
1106                    name: "PromQL fill modifiers"
1107                }
1108            );
1109        }
1110
1111        if let Some(plan) = self.try_plan_binary_island(binary_expr).await? {
1112            return Ok(plan);
1113        }
1114
1115        let PromBinaryExpr {
1116            lhs,
1117            rhs,
1118            op,
1119            modifier,
1120        } = binary_expr;
1121
1122        // if set to true, comparison operator will return 0/1 (for true/false) instead of
1123        // filter on the result column
1124        let should_return_bool = if let Some(m) = modifier {
1125            m.return_bool
1126        } else {
1127            false
1128        };
1129        let is_comparison_op = Self::is_token_a_comparison_op(*op);
1130
1131        // we should build a filter plan here if the op is comparison op and need not
1132        // to return 0/1. Otherwise, we should build a projection plan
1133        match (
1134            Self::try_build_literal_expr(lhs),
1135            Self::try_build_literal_expr(rhs),
1136        ) {
1137            (Some(lhs), Some(rhs)) => {
1138                self.ctx.time_index_column = Some(DEFAULT_TIME_INDEX_COLUMN.to_string());
1139                self.ctx.field_columns = vec![DEFAULT_FIELD_COLUMN.to_string()];
1140                self.ctx.reset_table_name_and_schema();
1141                let field_expr_builder = Self::prom_token_to_binary_expr_builder(*op)?;
1142                let mut field_expr = field_expr_builder(lhs, rhs)?;
1143
1144                if is_comparison_op && should_return_bool {
1145                    field_expr = DfExpr::Cast(Cast {
1146                        expr: Box::new(field_expr),
1147                        data_type: ArrowDataType::Float64,
1148                    });
1149                }
1150
1151                Ok(LogicalPlan::Extension(Extension {
1152                    node: Arc::new(
1153                        EmptyMetric::new(
1154                            self.ctx.start,
1155                            self.ctx.end,
1156                            self.ctx.interval,
1157                            SPECIAL_TIME_FUNCTION.to_string(),
1158                            DEFAULT_FIELD_COLUMN.to_string(),
1159                            Some(field_expr),
1160                        )
1161                        .context(DataFusionPlanningSnafu)?,
1162                    ),
1163                }))
1164            }
1165            // lhs is a literal, rhs is a column
1166            (Some(mut expr), None) => {
1167                let input = self.prom_expr_to_plan(rhs, query_engine_state).await?;
1168                // check if the literal is a special time expr
1169                if let Some(time_expr) = self.try_build_special_time_expr_with_context(lhs) {
1170                    expr = time_expr
1171                }
1172                let bin_expr_builder = |col: &String| {
1173                    let binary_expr_builder = Self::prom_token_to_binary_expr_builder(*op)?;
1174                    let mut binary_expr =
1175                        binary_expr_builder(expr.clone(), DfExpr::Column(col.into()))?;
1176
1177                    if is_comparison_op && should_return_bool {
1178                        binary_expr = DfExpr::Cast(Cast {
1179                            expr: Box::new(binary_expr),
1180                            data_type: ArrowDataType::Float64,
1181                        });
1182                    }
1183                    Ok(binary_expr)
1184                };
1185                if is_comparison_op && !should_return_bool {
1186                    self.filter_on_field_column(input, bin_expr_builder)
1187                } else {
1188                    self.projection_for_each_field_column(input, bin_expr_builder)
1189                }
1190            }
1191            // lhs is a column, rhs is a literal
1192            (None, Some(mut expr)) => {
1193                let input = self.prom_expr_to_plan(lhs, query_engine_state).await?;
1194                // check if the literal is a special time expr
1195                if let Some(time_expr) = self.try_build_special_time_expr_with_context(rhs) {
1196                    expr = time_expr
1197                }
1198                let bin_expr_builder = |col: &String| {
1199                    let binary_expr_builder = Self::prom_token_to_binary_expr_builder(*op)?;
1200                    let mut binary_expr =
1201                        binary_expr_builder(DfExpr::Column(col.into()), expr.clone())?;
1202
1203                    if is_comparison_op && should_return_bool {
1204                        binary_expr = DfExpr::Cast(Cast {
1205                            expr: Box::new(binary_expr),
1206                            data_type: ArrowDataType::Float64,
1207                        });
1208                    }
1209                    Ok(binary_expr)
1210                };
1211                if is_comparison_op && !should_return_bool {
1212                    self.filter_on_field_column(input, bin_expr_builder)
1213                } else {
1214                    self.projection_for_each_field_column(input, bin_expr_builder)
1215                }
1216            }
1217            // both are columns. join them on time index
1218            (None, None) => {
1219                let left_input = self.prom_expr_to_plan(lhs, query_engine_state).await?;
1220                let left_field_columns = self.ctx.field_columns.clone();
1221                let left_time_index_column = self.ctx.time_index_column.clone();
1222                let mut left_table_ref = self
1223                    .table_ref()
1224                    .unwrap_or_else(|_| TableReference::bare(""));
1225                let left_context = self.ctx.clone();
1226
1227                let right_input = self.prom_expr_to_plan(rhs, query_engine_state).await?;
1228                let right_field_columns = self.ctx.field_columns.clone();
1229                let right_time_index_column = self.ctx.time_index_column.clone();
1230                let mut right_table_ref = self
1231                    .table_ref()
1232                    .unwrap_or_else(|_| TableReference::bare(""));
1233                let right_context = self.ctx.clone();
1234
1235                // TODO(ruihang): avoid join if left and right are the same table
1236
1237                // set op has "special" join semantics
1238                if Self::is_token_a_set_op(*op) {
1239                    return self.set_op_on_non_field_columns(
1240                        left_input,
1241                        right_input,
1242                        left_context,
1243                        right_context,
1244                        *op,
1245                        modifier,
1246                    );
1247                }
1248
1249                // normal join
1250                if left_table_ref == right_table_ref {
1251                    // rename table references to avoid ambiguity
1252                    left_table_ref = TableReference::bare("lhs");
1253                    right_table_ref = TableReference::bare("rhs");
1254                    // `self.ctx` have ctx in right plan, if right plan have no tag,
1255                    // we use left plan ctx as the ctx for subsequent calculations,
1256                    // to avoid case like `host + scalar(...)`
1257                    // we need preserve tag column on `host` table in subsequent projection,
1258                    // which only show in left plan ctx.
1259                    if self.ctx.tag_columns.is_empty() {
1260                        self.ctx = left_context.clone();
1261                        self.ctx.table_name = Some("lhs".to_string());
1262                    } else {
1263                        self.ctx.table_name = Some("rhs".to_string());
1264                    }
1265                }
1266                let (output_field_columns, field_columns) =
1267                    Self::align_binary_field_columns(&left_field_columns, &right_field_columns);
1268                let left_aligned_field_columns = field_columns
1269                    .iter()
1270                    .map(|(left_col_name, _)| (*left_col_name).clone())
1271                    .collect::<Vec<_>>();
1272                let right_aligned_field_columns = field_columns
1273                    .iter()
1274                    .map(|(_, right_col_name)| (*right_col_name).clone())
1275                    .collect::<Vec<_>>();
1276                // PromQL binary arithmetic only combines the shared prefix of value columns.
1277                // Keep the output field count aligned with that zipped prefix so planning
1278                // remains stable even when the two sides have uneven multi-field schemas.
1279                self.ctx.field_columns = output_field_columns;
1280                let mut field_columns = field_columns.into_iter();
1281
1282                let join_plan = self.join_on_non_field_columns(
1283                    left_input,
1284                    right_input,
1285                    left_table_ref.clone(),
1286                    right_table_ref.clone(),
1287                    left_time_index_column,
1288                    right_time_index_column,
1289                    // if left plan or right plan tag is empty, means case like `scalar(...) + host` or `host + scalar(...)`
1290                    // under this case we only join on time index
1291                    left_context.tag_columns.is_empty() || right_context.tag_columns.is_empty(),
1292                    modifier,
1293                    &left_context,
1294                    &right_context,
1295                )?;
1296                let join_plan_schema = join_plan.schema().clone();
1297
1298                let bin_expr_builder = |_: &String| {
1299                    let (left_col_name, right_col_name) = field_columns.next().unwrap();
1300                    let left_col = join_plan_schema
1301                        .qualified_field_with_name(Some(&left_table_ref), left_col_name)
1302                        .context(DataFusionPlanningSnafu)?
1303                        .into();
1304                    let right_col = join_plan_schema
1305                        .qualified_field_with_name(Some(&right_table_ref), right_col_name)
1306                        .context(DataFusionPlanningSnafu)?
1307                        .into();
1308
1309                    let binary_expr_builder = Self::prom_token_to_binary_expr_builder(*op)?;
1310                    let mut binary_expr =
1311                        binary_expr_builder(DfExpr::Column(left_col), DfExpr::Column(right_col))?;
1312                    if is_comparison_op && should_return_bool {
1313                        binary_expr = DfExpr::Cast(Cast {
1314                            expr: Box::new(binary_expr),
1315                            data_type: ArrowDataType::Float64,
1316                        });
1317                    }
1318                    Ok(binary_expr)
1319                };
1320                if is_comparison_op && !should_return_bool {
1321                    // PromQL comparison operators without `bool` are filters:
1322                    //   - keep the instant-vector side sample values
1323                    //   - drop samples where the comparison is false
1324                    //
1325                    // So we filter on the join result and then project only the side that should
1326                    // be preserved according to PromQL semantics.
1327                    let filtered = self.filter_on_field_column(join_plan, bin_expr_builder)?;
1328                    let (project_table_ref, mut project_context, project_field_columns) =
1329                        match (lhs.value_type(), rhs.value_type()) {
1330                            (ValueType::Scalar, ValueType::Vector) => (
1331                                &right_table_ref,
1332                                right_context.clone(),
1333                                right_aligned_field_columns,
1334                            ),
1335                            _ => (
1336                                &left_table_ref,
1337                                left_context.clone(),
1338                                left_aligned_field_columns,
1339                            ),
1340                        };
1341                    project_context.field_columns = project_field_columns;
1342                    self.project_binary_join_side(filtered, project_table_ref, &project_context)
1343                } else {
1344                    self.projection_for_each_field_column(join_plan, bin_expr_builder)
1345                }
1346            }
1347        }
1348    }
1349
1350    fn project_binary_join_side(
1351        &mut self,
1352        input: LogicalPlan,
1353        table_ref: &TableReference,
1354        context: &PromPlannerContext,
1355    ) -> Result<LogicalPlan> {
1356        let schema = input.schema();
1357
1358        let mut project_exprs =
1359            Vec::with_capacity(context.tag_columns.len() + context.field_columns.len() + 2);
1360
1361        // Project time index from the chosen side.
1362        if let Some(time_index_column) = &context.time_index_column {
1363            let time_index_col = schema
1364                .qualified_field_with_name(Some(table_ref), time_index_column)
1365                .context(DataFusionPlanningSnafu)?
1366                .into();
1367            project_exprs.push(DfExpr::Column(time_index_col));
1368        }
1369
1370        // Project field columns from the chosen side.
1371        for field_column in &context.field_columns {
1372            let field_col = schema
1373                .qualified_field_with_name(Some(table_ref), field_column)
1374                .context(DataFusionPlanningSnafu)?
1375                .into();
1376            project_exprs.push(DfExpr::Column(field_col));
1377        }
1378
1379        // Project tag columns from the chosen side.
1380        for tag_column in &context.tag_columns {
1381            let tag_col = schema
1382                .qualified_field_with_name(Some(table_ref), tag_column)
1383                .context(DataFusionPlanningSnafu)?
1384                .into();
1385            project_exprs.push(DfExpr::Column(tag_col));
1386        }
1387
1388        // Preserve `__tsid` if present, so it can still be used internally downstream. It's
1389        // stripped from the final output anyway.
1390        if let Some(tsid_col) =
1391            Self::optional_tsid_projection(schema, Some(table_ref), context.use_tsid)
1392        {
1393            project_exprs.push(tsid_col);
1394        }
1395
1396        let plan = LogicalPlanBuilder::from(input)
1397            .project(project_exprs)
1398            .context(DataFusionPlanningSnafu)?
1399            .build()
1400            .context(DataFusionPlanningSnafu)?;
1401
1402        // Update context to reflect the projected schema. Don't keep a table qualifier since
1403        // the result is a derived expression.
1404        self.ctx = context.clone();
1405        self.ctx.table_name = None;
1406        self.ctx.schema_name = None;
1407
1408        Ok(plan)
1409    }
1410
1411    fn prom_number_lit_to_plan(&mut self, number_literal: &NumberLiteral) -> Result<LogicalPlan> {
1412        let NumberLiteral { val } = number_literal;
1413        self.ctx.time_index_column = Some(DEFAULT_TIME_INDEX_COLUMN.to_string());
1414        self.ctx.field_columns = vec![DEFAULT_FIELD_COLUMN.to_string()];
1415        self.ctx.reset_table_name_and_schema();
1416        let literal_expr = df_prelude::lit(*val);
1417
1418        let plan = LogicalPlan::Extension(Extension {
1419            node: Arc::new(
1420                EmptyMetric::new(
1421                    self.ctx.start,
1422                    self.ctx.end,
1423                    self.ctx.interval,
1424                    SPECIAL_TIME_FUNCTION.to_string(),
1425                    DEFAULT_FIELD_COLUMN.to_string(),
1426                    Some(literal_expr),
1427                )
1428                .context(DataFusionPlanningSnafu)?,
1429            ),
1430        });
1431        Ok(plan)
1432    }
1433
1434    fn prom_string_lit_to_plan(&mut self, string_literal: &StringLiteral) -> Result<LogicalPlan> {
1435        let StringLiteral { val } = string_literal;
1436        self.ctx.time_index_column = Some(DEFAULT_TIME_INDEX_COLUMN.to_string());
1437        self.ctx.field_columns = vec![DEFAULT_FIELD_COLUMN.to_string()];
1438        self.ctx.reset_table_name_and_schema();
1439        let literal_expr = df_prelude::lit(val.clone());
1440
1441        let plan = LogicalPlan::Extension(Extension {
1442            node: Arc::new(
1443                EmptyMetric::new(
1444                    self.ctx.start,
1445                    self.ctx.end,
1446                    self.ctx.interval,
1447                    SPECIAL_TIME_FUNCTION.to_string(),
1448                    DEFAULT_FIELD_COLUMN.to_string(),
1449                    Some(literal_expr),
1450                )
1451                .context(DataFusionPlanningSnafu)?,
1452            ),
1453        });
1454        Ok(plan)
1455    }
1456
1457    async fn prom_vector_selector_to_plan(
1458        &mut self,
1459        vector_selector: &VectorSelector,
1460        timestamp_fn: bool,
1461    ) -> Result<LogicalPlan> {
1462        let VectorSelector {
1463            name,
1464            offset,
1465            matchers,
1466            at: _,
1467        } = vector_selector;
1468        let matchers = self.preprocess_label_matchers(matchers, name)?;
1469        if let Some(empty_plan) = self.setup_context().await? {
1470            return Ok(empty_plan);
1471        }
1472        let normalize = self
1473            .selector_to_series_normalize_plan(offset, matchers, false)
1474            .await?;
1475
1476        let normalize = if timestamp_fn {
1477            // If evaluating the PromQL `timestamp()` function, project the time index column as the value column
1478            // before wrapping with [`InstantManipulate`], so the output matches PromQL's `timestamp()` semantics.
1479            self.create_timestamp_func_plan(normalize)?
1480        } else {
1481            normalize
1482        };
1483
1484        let manipulate = InstantManipulate::new(
1485            self.ctx.start,
1486            self.ctx.end,
1487            self.ctx.lookback_delta,
1488            self.ctx.interval,
1489            self.ctx
1490                .time_index_column
1491                .clone()
1492                .expect("time index should be set in `setup_context`"),
1493            if self.ctx.use_tsid {
1494                vec![DATA_SCHEMA_TSID_COLUMN_NAME.to_string()]
1495            } else {
1496                self.ctx.tag_columns.clone()
1497            },
1498            self.ctx.field_columns.first().cloned(),
1499            normalize,
1500        );
1501        Ok(LogicalPlan::Extension(Extension {
1502            node: Arc::new(manipulate),
1503        }))
1504    }
1505
1506    /// Builds a projection plan for the PromQL `timestamp()` function.
1507    /// Projects the time index column as the value column for each row.
1508    ///
1509    /// # Arguments
1510    /// * `normalize` - Input [`LogicalPlan`] for the normalized series.
1511    ///
1512    /// # Returns
1513    /// Returns a [`Result<LogicalPlan>`] where the resulting logical plan projects the timestamp
1514    /// column as the value column, along with the original tag and time index columns.
1515    ///
1516    /// # Timestamp vs. Time Function
1517    ///
1518    /// - **Timestamp Function (`timestamp()`)**: In PromQL, the `timestamp()` function returns the
1519    ///   timestamp (time index) of each sample as the value column.
1520    ///
1521    /// - **Time Function (`time()`)**: The `time()` function returns the evaluation time of the query
1522    ///   as a scalar value.
1523    ///
1524    /// # Side Effects
1525    /// Updates the planner context's field columns to the timestamp column name.
1526    ///
1527    fn create_timestamp_func_plan(&mut self, normalize: LogicalPlan) -> Result<LogicalPlan> {
1528        let time_expr = build_special_time_expr(self.ctx.time_index_column.as_ref().unwrap())
1529            .alias(DEFAULT_FIELD_COLUMN);
1530        self.ctx.field_columns = vec![time_expr.schema_name().to_string()];
1531        let mut project_exprs = Vec::with_capacity(self.ctx.tag_columns.len() + 2);
1532        project_exprs.push(self.create_time_index_column_expr()?);
1533        project_exprs.push(time_expr);
1534        project_exprs.extend(self.create_tag_column_exprs()?);
1535
1536        LogicalPlanBuilder::from(normalize)
1537            .project(project_exprs)
1538            .context(DataFusionPlanningSnafu)?
1539            .build()
1540            .context(DataFusionPlanningSnafu)
1541    }
1542
1543    async fn prom_matrix_selector_to_plan(
1544        &mut self,
1545        matrix_selector: &MatrixSelector,
1546    ) -> Result<LogicalPlan> {
1547        let MatrixSelector { vs, range } = matrix_selector;
1548        let VectorSelector {
1549            name,
1550            offset,
1551            matchers,
1552            ..
1553        } = vs;
1554        let matchers = self.preprocess_label_matchers(matchers, name)?;
1555        ensure!(!range.is_zero(), ZeroRangeSelectorSnafu);
1556        let range_ms = range.as_millis() as _;
1557        self.ctx.range = Some(range_ms);
1558
1559        // Some functions like rate may require special fields in the RangeManipulate plan
1560        // so we can't skip RangeManipulate.
1561        let normalize = match self.setup_context().await? {
1562            Some(empty_plan) => empty_plan,
1563            None => {
1564                self.selector_to_series_normalize_plan(offset, matchers, true)
1565                    .await?
1566            }
1567        };
1568        let manipulate = RangeManipulate::new(
1569            self.ctx.start,
1570            self.ctx.end,
1571            self.ctx.interval,
1572            // TODO(ruihang): convert via Timestamp datatypes to support different time units
1573            range_ms,
1574            self.ctx
1575                .time_index_column
1576                .clone()
1577                .expect("time index should be set in `setup_context`"),
1578            self.ctx.field_columns.clone(),
1579            normalize,
1580        )
1581        .context(DataFusionPlanningSnafu)?;
1582
1583        Ok(LogicalPlan::Extension(Extension {
1584            node: Arc::new(manipulate),
1585        }))
1586    }
1587
1588    async fn prom_call_expr_to_plan(
1589        &mut self,
1590        query_engine_state: &QueryEngineState,
1591        call_expr: &Call,
1592    ) -> Result<LogicalPlan> {
1593        let Call { func, args } = call_expr;
1594        // some special functions that are not expression but a plan
1595        match func.name {
1596            SPECIAL_HISTOGRAM_QUANTILE => {
1597                return self.create_histogram_plan(args, query_engine_state).await;
1598            }
1599            SPECIAL_VECTOR_FUNCTION => return self.create_vector_plan(args).await,
1600            SCALAR_FUNCTION => return self.create_scalar_plan(args, query_engine_state).await,
1601            SPECIAL_ABSENT_FUNCTION => {
1602                return self.create_absent_plan(args, query_engine_state).await;
1603            }
1604            _ => {}
1605        }
1606
1607        // transform function arguments
1608        let args = self.create_function_args(&args.args)?;
1609        let input = if let Some(prom_expr) = &args.input {
1610            self.prom_expr_to_plan_inner(prom_expr, func.name == "timestamp", query_engine_state)
1611                .await?
1612        } else {
1613            self.ctx.time_index_column = Some(SPECIAL_TIME_FUNCTION.to_string());
1614            self.ctx.reset_table_name_and_schema();
1615            self.ctx.tag_columns = vec![];
1616            self.ctx.field_columns = vec![DEFAULT_FIELD_COLUMN.to_string()];
1617            LogicalPlan::Extension(Extension {
1618                node: Arc::new(
1619                    EmptyMetric::new(
1620                        self.ctx.start,
1621                        self.ctx.end,
1622                        self.ctx.interval,
1623                        SPECIAL_TIME_FUNCTION.to_string(),
1624                        DEFAULT_FIELD_COLUMN.to_string(),
1625                        None,
1626                    )
1627                    .context(DataFusionPlanningSnafu)?,
1628                ),
1629            })
1630        };
1631        let (mut func_exprs, new_tags) =
1632            self.create_function_expr(func, args.literals.clone(), query_engine_state)?;
1633        func_exprs.insert(0, self.create_time_index_column_expr()?);
1634        func_exprs.extend_from_slice(&self.create_tag_column_exprs()?);
1635        if let Some(tsid_col) =
1636            Self::optional_tsid_projection(input.schema(), None, self.ctx.use_tsid)
1637        {
1638            func_exprs.push(tsid_col);
1639        }
1640
1641        let builder = LogicalPlanBuilder::from(input)
1642            .project(func_exprs)
1643            .context(DataFusionPlanningSnafu)?
1644            .filter(self.create_empty_values_filter_expr()?)
1645            .context(DataFusionPlanningSnafu)?;
1646
1647        let builder = match func.name {
1648            "sort" => builder
1649                .sort(self.create_field_columns_sort_exprs(true))
1650                .context(DataFusionPlanningSnafu)?,
1651            "sort_desc" => builder
1652                .sort(self.create_field_columns_sort_exprs(false))
1653                .context(DataFusionPlanningSnafu)?,
1654            "sort_by_label" => builder
1655                .sort(Self::create_sort_exprs_by_tags(
1656                    func.name,
1657                    args.literals,
1658                    true,
1659                )?)
1660                .context(DataFusionPlanningSnafu)?,
1661            "sort_by_label_desc" => builder
1662                .sort(Self::create_sort_exprs_by_tags(
1663                    func.name,
1664                    args.literals,
1665                    false,
1666                )?)
1667                .context(DataFusionPlanningSnafu)?,
1668
1669            _ => builder,
1670        };
1671
1672        // Update context tags after building plan
1673        // We can't push them before planning, because they won't exist until projection.
1674        for tag in new_tags {
1675            self.ctx.tag_columns.push(tag);
1676        }
1677
1678        let plan = builder.build().context(DataFusionPlanningSnafu)?;
1679        common_telemetry::debug!("Created PromQL function plan: {plan:?} for {call_expr:?}");
1680
1681        Ok(plan)
1682    }
1683
1684    async fn prom_ext_expr_to_plan(
1685        &mut self,
1686        query_engine_state: &QueryEngineState,
1687        ext_expr: &promql_parser::parser::ast::Extension,
1688    ) -> Result<LogicalPlan> {
1689        // let promql_parser::parser::ast::Extension { expr } = ext_expr;
1690        let expr = &ext_expr.expr;
1691        let children = expr.children();
1692        let plan = self
1693            .prom_expr_to_plan(&children[0], query_engine_state)
1694            .await?;
1695        // Wrapper for the explanation/analyze of the existing plan
1696        // https://docs.rs/datafusion-expr/latest/datafusion_expr/logical_plan/builder/struct.LogicalPlanBuilder.html#method.explain
1697        // if `analyze` is true, runs the actual plan and produces
1698        // information about metrics during run.
1699        // if `verbose` is true, prints out additional details when VERBOSE keyword is specified
1700        match expr.name() {
1701            ANALYZE_NODE_NAME => LogicalPlanBuilder::from(plan)
1702                .explain(false, true)
1703                .unwrap()
1704                .build()
1705                .context(DataFusionPlanningSnafu),
1706            ANALYZE_VERBOSE_NODE_NAME => LogicalPlanBuilder::from(plan)
1707                .explain(true, true)
1708                .unwrap()
1709                .build()
1710                .context(DataFusionPlanningSnafu),
1711            EXPLAIN_NODE_NAME => LogicalPlanBuilder::from(plan)
1712                .explain(false, false)
1713                .unwrap()
1714                .build()
1715                .context(DataFusionPlanningSnafu),
1716            EXPLAIN_VERBOSE_NODE_NAME => LogicalPlanBuilder::from(plan)
1717                .explain(true, false)
1718                .unwrap()
1719                .build()
1720                .context(DataFusionPlanningSnafu),
1721            ALIAS_NODE_NAME => {
1722                let alias = expr
1723                    .as_any()
1724                    .downcast_ref::<AliasExpr>()
1725                    .context(UnexpectedPlanExprSnafu {
1726                        desc: "Expected AliasExpr",
1727                    })?
1728                    .alias
1729                    .clone();
1730                self.apply_alias(plan, alias)
1731            }
1732            _ => LogicalPlanBuilder::empty(true)
1733                .build()
1734                .context(DataFusionPlanningSnafu),
1735        }
1736    }
1737
1738    /// Extract metric name from `__name__` matcher and set it into [PromPlannerContext].
1739    /// Returns a new [Matchers] that doesn't contain metric name matcher.
1740    ///
1741    /// Each call to this function means new selector is started. Thus, the context will be reset
1742    /// at first.
1743    ///
1744    /// Name rule:
1745    /// - if `name` is some, then the matchers MUST NOT contain `__name__` matcher.
1746    /// - if `name` is none, then the matchers MAY contain NONE OR MULTIPLE `__name__` matchers.
1747    #[allow(clippy::mutable_key_type)]
1748    fn preprocess_label_matchers(
1749        &mut self,
1750        label_matchers: &Matchers,
1751        name: &Option<String>,
1752    ) -> Result<Matchers> {
1753        self.ctx.reset();
1754
1755        let metric_name;
1756        if let Some(name) = name.clone() {
1757            metric_name = Some(name);
1758            ensure!(
1759                label_matchers.find_matchers(METRIC_NAME).is_empty(),
1760                MultipleMetricMatchersSnafu
1761            );
1762        } else {
1763            let mut matches = label_matchers.find_matchers(METRIC_NAME);
1764            ensure!(!matches.is_empty(), NoMetricMatcherSnafu);
1765            ensure!(matches.len() == 1, MultipleMetricMatchersSnafu);
1766            ensure!(
1767                matches[0].op == MatchOp::Equal,
1768                UnsupportedMatcherOpSnafu {
1769                    matcher_op: matches[0].op.to_string(),
1770                    matcher: METRIC_NAME
1771                }
1772            );
1773            metric_name = matches.pop().map(|m| m.value);
1774        }
1775
1776        self.ctx.table_name = metric_name;
1777
1778        let mut matchers = HashSet::new();
1779        for matcher in &label_matchers.matchers {
1780            // TODO(ruihang): support other metric match ops
1781            if matcher.name == FIELD_COLUMN_MATCHER {
1782                self.ctx
1783                    .field_column_matcher
1784                    .get_or_insert_default()
1785                    .push(matcher.clone());
1786            } else if matcher.name == SCHEMA_COLUMN_MATCHER || matcher.name == DB_COLUMN_MATCHER {
1787                ensure!(
1788                    matcher.op == MatchOp::Equal,
1789                    UnsupportedMatcherOpSnafu {
1790                        matcher: matcher.name.clone(),
1791                        matcher_op: matcher.op.to_string(),
1792                    }
1793                );
1794                self.ctx.schema_name = Some(matcher.value.clone());
1795            } else if matcher.name != METRIC_NAME {
1796                self.ctx.selector_matcher.push(matcher.clone());
1797                let _ = matchers.insert(matcher.clone());
1798            }
1799        }
1800
1801        Ok(Matchers::new(matchers.into_iter().collect()))
1802    }
1803
1804    async fn selector_to_series_normalize_plan(
1805        &mut self,
1806        offset: &Option<Offset>,
1807        label_matchers: Matchers,
1808        is_range_selector: bool,
1809    ) -> Result<LogicalPlan> {
1810        // make table scan plan
1811        let table_ref = self.table_ref()?;
1812        let mut table_scan = self.create_table_scan_plan(table_ref.clone()).await?;
1813        let table_schema = table_scan.schema();
1814
1815        // make filter exprs
1816        let offset_duration = match offset {
1817            Some(Offset::Pos(duration)) => duration.as_millis() as Millisecond,
1818            Some(Offset::Neg(duration)) => -(duration.as_millis() as Millisecond),
1819            None => 0,
1820        };
1821        let mut scan_filters = Self::matchers_to_expr(label_matchers.clone(), table_schema)?;
1822        if let Some(time_index_filter) = self.build_time_index_filter(offset_duration)? {
1823            scan_filters.push(time_index_filter);
1824        }
1825        table_scan = LogicalPlanBuilder::from(table_scan)
1826            .filter(conjunction(scan_filters).unwrap()) // Safety: `scan_filters` is not empty.
1827            .context(DataFusionPlanningSnafu)?
1828            .build()
1829            .context(DataFusionPlanningSnafu)?;
1830
1831        // make a projection plan if there is any `__field__` matcher
1832        if let Some(field_matchers) = &self.ctx.field_column_matcher {
1833            let col_set = self.ctx.field_columns.iter().collect::<HashSet<_>>();
1834            // opt-in set
1835            let mut result_set = HashSet::new();
1836            // opt-out set
1837            let mut reverse_set = HashSet::new();
1838            for matcher in field_matchers {
1839                match &matcher.op {
1840                    MatchOp::Equal => {
1841                        if col_set.contains(&matcher.value) {
1842                            let _ = result_set.insert(matcher.value.clone());
1843                        } else {
1844                            return Err(ColumnNotFoundSnafu {
1845                                col: matcher.value.clone(),
1846                            }
1847                            .build());
1848                        }
1849                    }
1850                    MatchOp::NotEqual => {
1851                        if col_set.contains(&matcher.value) {
1852                            let _ = reverse_set.insert(matcher.value.clone());
1853                        } else {
1854                            return Err(ColumnNotFoundSnafu {
1855                                col: matcher.value.clone(),
1856                            }
1857                            .build());
1858                        }
1859                    }
1860                    MatchOp::Re(regex) => {
1861                        for col in &self.ctx.field_columns {
1862                            if regex.is_match(col) {
1863                                let _ = result_set.insert(col.clone());
1864                            }
1865                        }
1866                    }
1867                    MatchOp::NotRe(regex) => {
1868                        for col in &self.ctx.field_columns {
1869                            if regex.is_match(col) {
1870                                let _ = reverse_set.insert(col.clone());
1871                            }
1872                        }
1873                    }
1874                }
1875            }
1876            // merge two set
1877            if result_set.is_empty() {
1878                result_set = col_set.into_iter().cloned().collect();
1879            }
1880            for col in reverse_set {
1881                let _ = result_set.remove(&col);
1882            }
1883
1884            // mask the field columns in context using computed result set
1885            self.ctx.field_columns = self
1886                .ctx
1887                .field_columns
1888                .drain(..)
1889                .filter(|col| result_set.contains(col))
1890                .collect();
1891
1892            let exprs = result_set
1893                .into_iter()
1894                .map(|col| DfExpr::Column(Column::new_unqualified(col)))
1895                .chain(self.create_tag_column_exprs()?)
1896                .chain(
1897                    self.ctx
1898                        .use_tsid
1899                        .then_some(DfExpr::Column(Column::new_unqualified(
1900                            DATA_SCHEMA_TSID_COLUMN_NAME,
1901                        ))),
1902                )
1903                .chain(Some(self.create_time_index_column_expr()?))
1904                .collect::<Vec<_>>();
1905
1906            // reuse this variable for simplicity
1907            table_scan = LogicalPlanBuilder::from(table_scan)
1908                .project(exprs)
1909                .context(DataFusionPlanningSnafu)?
1910                .build()
1911                .context(DataFusionPlanningSnafu)?;
1912        }
1913
1914        // make sort plan
1915        let series_key_columns = if self.ctx.use_tsid {
1916            vec![DATA_SCHEMA_TSID_COLUMN_NAME.to_string()]
1917        } else {
1918            self.ctx.tag_columns.clone()
1919        };
1920
1921        let sort_exprs = if self.ctx.use_tsid {
1922            vec![
1923                DfExpr::Column(Column::from_name(DATA_SCHEMA_TSID_COLUMN_NAME)).sort(true, true),
1924                self.create_time_index_column_expr()?.sort(true, true),
1925            ]
1926        } else {
1927            self.create_tag_and_time_index_column_sort_exprs()?
1928        };
1929
1930        let sort_plan = LogicalPlanBuilder::from(table_scan)
1931            .sort(sort_exprs)
1932            .context(DataFusionPlanningSnafu)?
1933            .build()
1934            .context(DataFusionPlanningSnafu)?;
1935
1936        // make divide plan
1937        let time_index_column =
1938            self.ctx
1939                .time_index_column
1940                .clone()
1941                .with_context(|| TimeIndexNotFoundSnafu {
1942                    table: table_ref.to_string(),
1943                })?;
1944        let divide_plan = LogicalPlan::Extension(Extension {
1945            node: Arc::new(SeriesDivide::new(
1946                series_key_columns.clone(),
1947                time_index_column,
1948                sort_plan,
1949            )),
1950        });
1951
1952        // make series_normalize plan
1953        if !is_range_selector && offset_duration == 0 {
1954            return Ok(divide_plan);
1955        }
1956        let series_normalize = SeriesNormalize::new(
1957            offset_duration,
1958            self.ctx
1959                .time_index_column
1960                .clone()
1961                .with_context(|| TimeIndexNotFoundSnafu {
1962                    table: table_ref.to_quoted_string(),
1963                })?,
1964            is_range_selector,
1965            series_key_columns,
1966            divide_plan,
1967        );
1968        let logical_plan = LogicalPlan::Extension(Extension {
1969            node: Arc::new(series_normalize),
1970        });
1971
1972        Ok(logical_plan)
1973    }
1974
1975    /// Convert [LabelModifier] to [Column] exprs for aggregation.
1976    /// Timestamp column and tag columns will be included.
1977    ///
1978    /// # Side effect
1979    ///
1980    /// This method will also change the tag columns in ctx if `update_ctx` is true.
1981    fn agg_modifier_to_col(
1982        &mut self,
1983        input_schema: &DFSchemaRef,
1984        modifier: &Option<LabelModifier>,
1985        update_ctx: bool,
1986    ) -> Result<Vec<DfExpr>> {
1987        match modifier {
1988            None => {
1989                if update_ctx {
1990                    self.ctx.tag_columns.clear();
1991                }
1992                Ok(vec![self.create_time_index_column_expr()?])
1993            }
1994            Some(LabelModifier::Include(labels)) => {
1995                if update_ctx {
1996                    self.ctx.tag_columns.clear();
1997                }
1998                let mut exprs = Vec::with_capacity(labels.labels.len());
1999                for label in &labels.labels {
2000                    if is_metric_engine_internal_column(label) {
2001                        continue;
2002                    }
2003                    // nonexistence label will be ignored
2004                    if let Some(column_name) = Self::find_case_sensitive_column(input_schema, label)
2005                    {
2006                        exprs.push(DfExpr::Column(Column::from_name(column_name.clone())));
2007
2008                        if update_ctx {
2009                            // update the tag columns in context
2010                            self.ctx.tag_columns.push(column_name);
2011                        }
2012                    }
2013                }
2014                // add timestamp column
2015                exprs.push(self.create_time_index_column_expr()?);
2016
2017                Ok(exprs)
2018            }
2019            Some(LabelModifier::Exclude(labels)) => {
2020                let mut all_fields = input_schema
2021                    .fields()
2022                    .iter()
2023                    .map(|f| f.name())
2024                    .collect::<BTreeSet<_>>();
2025
2026                // Exclude metric engine internal columns (not PromQL labels) from the implicit
2027                // "without" label set.
2028                all_fields.retain(|col| !is_metric_engine_internal_column(col.as_str()));
2029
2030                // remove "without"-ed fields
2031                // nonexistence label will be ignored
2032                for label in &labels.labels {
2033                    let _ = all_fields.remove(label);
2034                }
2035
2036                // remove time index and value fields
2037                if let Some(time_index) = &self.ctx.time_index_column {
2038                    let _ = all_fields.remove(time_index);
2039                }
2040                for value in &self.ctx.field_columns {
2041                    let _ = all_fields.remove(value);
2042                }
2043
2044                if update_ctx {
2045                    // change the tag columns in context
2046                    self.ctx.tag_columns = all_fields.iter().map(|col| (*col).clone()).collect();
2047                }
2048
2049                // collect remaining fields and convert to col expr
2050                let mut exprs = all_fields
2051                    .into_iter()
2052                    .map(|c| DfExpr::Column(Column::from(c)))
2053                    .collect::<Vec<_>>();
2054
2055                // add timestamp column
2056                exprs.push(self.create_time_index_column_expr()?);
2057
2058                Ok(exprs)
2059            }
2060        }
2061    }
2062
2063    // TODO(ruihang): ignore `MetricNameLabel` (`__name__`) matcher
2064    pub fn matchers_to_expr(
2065        label_matchers: Matchers,
2066        table_schema: &DFSchemaRef,
2067    ) -> Result<Vec<DfExpr>> {
2068        let mut exprs = Vec::with_capacity(label_matchers.matchers.len());
2069        for matcher in label_matchers.matchers {
2070            if matcher.name == SCHEMA_COLUMN_MATCHER
2071                || matcher.name == DB_COLUMN_MATCHER
2072                || matcher.name == FIELD_COLUMN_MATCHER
2073            {
2074                continue;
2075            }
2076
2077            let column_name = Self::find_case_sensitive_column(table_schema, matcher.name.as_str());
2078            let col = if let Some(column_name) = column_name {
2079                DfExpr::Column(Column::from_name(column_name))
2080            } else {
2081                DfExpr::Literal(ScalarValue::Utf8(Some(String::new())), None)
2082                    .alias(matcher.name.clone())
2083            };
2084            let lit = DfExpr::Literal(ScalarValue::Utf8(Some(matcher.value)), None);
2085            let expr = match matcher.op {
2086                MatchOp::Equal => col.eq(lit),
2087                MatchOp::NotEqual => col.not_eq(lit),
2088                MatchOp::Re(re) => {
2089                    // TODO(ruihang): a more programmatic way to handle this in datafusion
2090
2091                    // This is a hack to handle `.+` and `.*`, and is not strictly correct
2092                    // `.` doesn't match newline (`\n`). Given this is in PromQL context,
2093                    // most of the time it's fine.
2094                    if re.as_str() == "^(?:.*)$" {
2095                        continue;
2096                    }
2097                    if re.as_str() == "^(?:.+)$" {
2098                        col.not_eq(DfExpr::Literal(
2099                            ScalarValue::Utf8(Some(String::new())),
2100                            None,
2101                        ))
2102                    } else {
2103                        DfExpr::BinaryExpr(BinaryExpr {
2104                            left: Box::new(col),
2105                            op: Operator::RegexMatch,
2106                            right: Box::new(DfExpr::Literal(
2107                                ScalarValue::Utf8(Some(re.as_str().to_string())),
2108                                None,
2109                            )),
2110                        })
2111                    }
2112                }
2113                MatchOp::NotRe(re) => {
2114                    if re.as_str() == "^(?:.*)$" {
2115                        DfExpr::Literal(ScalarValue::Boolean(Some(false)), None)
2116                    } else if re.as_str() == "^(?:.+)$" {
2117                        col.eq(DfExpr::Literal(
2118                            ScalarValue::Utf8(Some(String::new())),
2119                            None,
2120                        ))
2121                    } else {
2122                        DfExpr::BinaryExpr(BinaryExpr {
2123                            left: Box::new(col),
2124                            op: Operator::RegexNotMatch,
2125                            right: Box::new(DfExpr::Literal(
2126                                ScalarValue::Utf8(Some(re.as_str().to_string())),
2127                                None,
2128                            )),
2129                        })
2130                    }
2131                }
2132            };
2133            exprs.push(expr);
2134        }
2135
2136        Ok(exprs)
2137    }
2138
2139    fn find_case_sensitive_column(schema: &DFSchemaRef, column: &str) -> Option<String> {
2140        if is_metric_engine_internal_column(column) {
2141            return None;
2142        }
2143        schema
2144            .fields()
2145            .iter()
2146            .find(|field| field.name() == column)
2147            .map(|field| field.name().clone())
2148    }
2149
2150    fn table_from_source(&self, source: &Arc<dyn TableSource>) -> Result<table::TableRef> {
2151        Ok(source
2152            .as_any()
2153            .downcast_ref::<DefaultTableSource>()
2154            .context(UnknownTableSnafu)?
2155            .table_provider
2156            .as_any()
2157            .downcast_ref::<DfTableProviderAdapter>()
2158            .context(UnknownTableSnafu)?
2159            .table())
2160    }
2161
2162    fn table_ref(&self) -> Result<TableReference> {
2163        let table_name = self
2164            .ctx
2165            .table_name
2166            .clone()
2167            .context(TableNameNotFoundSnafu)?;
2168
2169        // set schema name if `__schema__` is given
2170        let table_ref = if let Some(schema_name) = &self.ctx.schema_name {
2171            TableReference::partial(schema_name.as_str(), table_name.as_str())
2172        } else {
2173            TableReference::bare(table_name.as_str())
2174        };
2175
2176        Ok(table_ref)
2177    }
2178
2179    fn build_time_index_filter(&self, offset_duration: i64) -> Result<Option<DfExpr>> {
2180        let start = self.ctx.start;
2181        let end = self.ctx.end;
2182        if end < start {
2183            return InvalidTimeRangeSnafu { start, end }.fail();
2184        }
2185        let lookback_delta = self.ctx.lookback_delta;
2186        let range = self.ctx.range.unwrap_or_default();
2187        let interval = self.ctx.interval;
2188        let time_index_expr = self.create_time_index_column_expr()?;
2189        let num_points = (end - start) / interval;
2190
2191        // Prometheus semantics:
2192        // - Instant selector lookback: (eval_ts - lookback_delta, eval_ts]
2193        // - Range selector:           (eval_ts - range, eval_ts]
2194        //
2195        // So samples positioned exactly at the lower boundary must be excluded. We align the scan
2196        // lower bound with Prometheus by shifting it forward by 1ms (millisecond granularity),
2197        // while still using a `>=` filter.
2198        let selector_window = if range == 0 { lookback_delta } else { range };
2199        let lower_exclusive_adjustment = if selector_window > 0 { 1 } else { 0 };
2200
2201        // Scan a continuous time range
2202        if (end - start) / interval > MAX_SCATTER_POINTS || interval <= INTERVAL_1H {
2203            let single_time_range = time_index_expr
2204                .clone()
2205                .gt_eq(DfExpr::Literal(
2206                    ScalarValue::TimestampMillisecond(
2207                        Some(
2208                            self.ctx.start - offset_duration - selector_window
2209                                + lower_exclusive_adjustment,
2210                        ),
2211                        None,
2212                    ),
2213                    None,
2214                ))
2215                .and(time_index_expr.lt_eq(DfExpr::Literal(
2216                    ScalarValue::TimestampMillisecond(Some(self.ctx.end - offset_duration), None),
2217                    None,
2218                )));
2219            return Ok(Some(single_time_range));
2220        }
2221
2222        // Otherwise scan scatter ranges separately
2223        let mut filters = Vec::with_capacity(num_points as usize + 1);
2224        for timestamp in (start..=end).step_by(interval as usize) {
2225            filters.push(
2226                time_index_expr
2227                    .clone()
2228                    .gt_eq(DfExpr::Literal(
2229                        ScalarValue::TimestampMillisecond(
2230                            Some(
2231                                timestamp - offset_duration - selector_window
2232                                    + lower_exclusive_adjustment,
2233                            ),
2234                            None,
2235                        ),
2236                        None,
2237                    ))
2238                    .and(time_index_expr.clone().lt_eq(DfExpr::Literal(
2239                        ScalarValue::TimestampMillisecond(Some(timestamp - offset_duration), None),
2240                        None,
2241                    ))),
2242            )
2243        }
2244
2245        Ok(filters.into_iter().reduce(DfExpr::or))
2246    }
2247
2248    /// Create a table scan plan and a filter plan with given filter.
2249    ///
2250    /// # Panic
2251    /// If the filter is empty
2252    async fn create_table_scan_plan(&mut self, table_ref: TableReference) -> Result<LogicalPlan> {
2253        let provider = self
2254            .table_provider
2255            .resolve_table(table_ref.clone())
2256            .await
2257            .context(CatalogSnafu)?;
2258
2259        let logical_table = self.table_from_source(&provider)?;
2260
2261        // Try to rewrite the table scan to physical table scan if possible.
2262        let mut maybe_phy_table_ref = table_ref.clone();
2263        let mut scan_provider = provider;
2264        let mut table_id_filter: Option<u32> = None;
2265
2266        // If it's a metric engine logical table, scan its physical table directly and filter by
2267        // `__table_id = logical_table_id` to get access to internal columns like `__tsid`.
2268        if logical_table.table_info().meta.engine == METRIC_ENGINE_NAME
2269            && let Some(physical_table_name) = logical_table
2270                .table_info()
2271                .meta
2272                .options
2273                .extra_options
2274                .get(LOGICAL_TABLE_METADATA_KEY)
2275        {
2276            let physical_table_ref = if let Some(schema_name) = &self.ctx.schema_name {
2277                TableReference::partial(schema_name.as_str(), physical_table_name.as_str())
2278            } else {
2279                TableReference::bare(physical_table_name.as_str())
2280            };
2281
2282            let physical_provider = match self
2283                .table_provider
2284                .resolve_table(physical_table_ref.clone())
2285                .await
2286            {
2287                Ok(provider) => provider,
2288                Err(e) if e.status_code() == StatusCode::TableNotFound => {
2289                    // Fall back to scanning the logical table. It still works, but without
2290                    // `__tsid` optimization.
2291                    scan_provider.clone()
2292                }
2293                Err(e) => return Err(e).context(CatalogSnafu),
2294            };
2295
2296            if !Arc::ptr_eq(&physical_provider, &scan_provider) {
2297                // Only rewrite when internal columns exist in physical schema.
2298                let physical_table = self.table_from_source(&physical_provider)?;
2299
2300                let has_table_id = physical_table
2301                    .schema()
2302                    .column_schema_by_name(DATA_SCHEMA_TABLE_ID_COLUMN_NAME)
2303                    .is_some();
2304                let has_tsid = physical_table
2305                    .schema()
2306                    .column_schema_by_name(DATA_SCHEMA_TSID_COLUMN_NAME)
2307                    .is_some_and(|col| matches!(col.data_type, ConcreteDataType::UInt64(_)));
2308
2309                if has_table_id && has_tsid {
2310                    scan_provider = physical_provider;
2311                    maybe_phy_table_ref = physical_table_ref;
2312                    table_id_filter = Some(logical_table.table_info().ident.table_id);
2313                }
2314            }
2315        }
2316
2317        let scan_table = self.table_from_source(&scan_provider)?;
2318
2319        let use_tsid = table_id_filter.is_some()
2320            && scan_table
2321                .schema()
2322                .column_schema_by_name(DATA_SCHEMA_TSID_COLUMN_NAME)
2323                .is_some_and(|col| matches!(col.data_type, ConcreteDataType::UInt64(_)));
2324        self.ctx.use_tsid = use_tsid;
2325
2326        let all_table_tags = self.ctx.tag_columns.clone();
2327
2328        let scan_tag_columns = if use_tsid {
2329            let mut scan_tags = self.ctx.tag_columns.clone();
2330            for matcher in &self.ctx.selector_matcher {
2331                if is_metric_engine_internal_column(&matcher.name) {
2332                    continue;
2333                }
2334                if all_table_tags.iter().any(|tag| tag == &matcher.name) {
2335                    scan_tags.push(matcher.name.clone());
2336                }
2337            }
2338            scan_tags.sort_unstable();
2339            scan_tags.dedup();
2340            scan_tags
2341        } else {
2342            self.ctx.tag_columns.clone()
2343        };
2344
2345        let is_time_index_ms = scan_table
2346            .schema()
2347            .timestamp_column()
2348            .with_context(|| TimeIndexNotFoundSnafu {
2349                table: maybe_phy_table_ref.to_quoted_string(),
2350            })?
2351            .data_type
2352            == ConcreteDataType::timestamp_millisecond_datatype();
2353
2354        let scan_projection = if table_id_filter.is_some() {
2355            let mut required_columns = HashSet::new();
2356            required_columns.insert(DATA_SCHEMA_TABLE_ID_COLUMN_NAME.to_string());
2357            required_columns.insert(self.ctx.time_index_column.clone().with_context(|| {
2358                TimeIndexNotFoundSnafu {
2359                    table: maybe_phy_table_ref.to_quoted_string(),
2360                }
2361            })?);
2362            for col in &scan_tag_columns {
2363                required_columns.insert(col.clone());
2364            }
2365            for col in &self.ctx.field_columns {
2366                required_columns.insert(col.clone());
2367            }
2368            if use_tsid {
2369                required_columns.insert(DATA_SCHEMA_TSID_COLUMN_NAME.to_string());
2370            }
2371
2372            let arrow_schema = scan_table.schema().arrow_schema().clone();
2373            Some(
2374                arrow_schema
2375                    .fields()
2376                    .iter()
2377                    .enumerate()
2378                    .filter(|(_, field)| required_columns.contains(field.name().as_str()))
2379                    .map(|(idx, _)| idx)
2380                    .collect::<Vec<_>>(),
2381            )
2382        } else {
2383            None
2384        };
2385
2386        let mut scan_plan =
2387            LogicalPlanBuilder::scan(maybe_phy_table_ref.clone(), scan_provider, scan_projection)
2388                .context(DataFusionPlanningSnafu)?
2389                .build()
2390                .context(DataFusionPlanningSnafu)?;
2391
2392        if let Some(table_id) = table_id_filter {
2393            scan_plan = LogicalPlanBuilder::from(scan_plan)
2394                .filter(
2395                    DfExpr::Column(Column::from_name(DATA_SCHEMA_TABLE_ID_COLUMN_NAME))
2396                        .eq(lit(table_id)),
2397                )
2398                .context(DataFusionPlanningSnafu)?
2399                .alias(table_ref.clone()) // rename the relation back to logical table's name after filtering
2400                .context(DataFusionPlanningSnafu)?
2401                .build()
2402                .context(DataFusionPlanningSnafu)?;
2403        }
2404
2405        if !is_time_index_ms {
2406            // cast to ms if time_index not in Millisecond precision
2407            let expr: Vec<_> = self
2408                .create_field_column_exprs()?
2409                .into_iter()
2410                .chain(
2411                    scan_tag_columns
2412                        .iter()
2413                        .map(|tag| DfExpr::Column(Column::from_name(tag))),
2414                )
2415                .chain(self.ctx.use_tsid.then_some(DfExpr::Column(Column::new(
2416                    Some(table_ref.clone()),
2417                    DATA_SCHEMA_TSID_COLUMN_NAME.to_string(),
2418                ))))
2419                .chain(Some(DfExpr::Alias(Alias {
2420                    expr: Box::new(DfExpr::Cast(Cast {
2421                        expr: Box::new(self.create_time_index_column_expr()?),
2422                        data_type: ArrowDataType::Timestamp(ArrowTimeUnit::Millisecond, None),
2423                    })),
2424                    relation: Some(table_ref.clone()),
2425                    name: self
2426                        .ctx
2427                        .time_index_column
2428                        .as_ref()
2429                        .with_context(|| TimeIndexNotFoundSnafu {
2430                            table: table_ref.to_quoted_string(),
2431                        })?
2432                        .clone(),
2433                    metadata: None,
2434                })))
2435                .collect::<Vec<_>>();
2436            scan_plan = LogicalPlanBuilder::from(scan_plan)
2437                .project(expr)
2438                .context(DataFusionPlanningSnafu)?
2439                .build()
2440                .context(DataFusionPlanningSnafu)?;
2441        } else if table_id_filter.is_some() {
2442            // Drop the internal `__table_id` column after filtering.
2443            let project_exprs = self
2444                .create_field_column_exprs()?
2445                .into_iter()
2446                .chain(
2447                    scan_tag_columns
2448                        .iter()
2449                        .map(|tag| DfExpr::Column(Column::from_name(tag))),
2450                )
2451                .chain(
2452                    self.ctx
2453                        .use_tsid
2454                        .then_some(DfExpr::Column(Column::from_name(
2455                            DATA_SCHEMA_TSID_COLUMN_NAME,
2456                        ))),
2457                )
2458                .chain(Some(self.create_time_index_column_expr()?))
2459                .collect::<Vec<_>>();
2460
2461            scan_plan = LogicalPlanBuilder::from(scan_plan)
2462                .project(project_exprs)
2463                .context(DataFusionPlanningSnafu)?
2464                .build()
2465                .context(DataFusionPlanningSnafu)?;
2466        }
2467
2468        let result = LogicalPlanBuilder::from(scan_plan)
2469            .build()
2470            .context(DataFusionPlanningSnafu)?;
2471        Ok(result)
2472    }
2473
2474    fn collect_row_key_tag_columns_from_plan(
2475        &self,
2476        plan: &LogicalPlan,
2477    ) -> Result<BTreeSet<String>> {
2478        fn walk(
2479            planner: &PromPlanner,
2480            plan: &LogicalPlan,
2481            out: &mut BTreeSet<String>,
2482        ) -> Result<()> {
2483            // Derived PromQL plans may contain non-Greptime scans without row-key metadata.
2484            if let LogicalPlan::TableScan(scan) = plan
2485                && let Ok(table) = planner.table_from_source(&scan.source)
2486            {
2487                for col in table.table_info().meta.row_key_column_names() {
2488                    if col != DATA_SCHEMA_TABLE_ID_COLUMN_NAME
2489                        && col != DATA_SCHEMA_TSID_COLUMN_NAME
2490                        && !is_metric_engine_internal_column(col)
2491                    {
2492                        out.insert(col.clone());
2493                    }
2494                }
2495            }
2496
2497            for input in plan.inputs() {
2498                walk(planner, input, out)?;
2499            }
2500            Ok(())
2501        }
2502
2503        let mut out = BTreeSet::new();
2504        walk(self, plan, &mut out)?;
2505        Ok(out)
2506    }
2507
2508    fn ensure_tag_columns_available(
2509        &self,
2510        plan: LogicalPlan,
2511        required_tags: &BTreeSet<String>,
2512    ) -> Result<LogicalPlan> {
2513        if required_tags.is_empty() {
2514            return Ok(plan);
2515        }
2516
2517        struct Rewriter {
2518            required_tags: BTreeSet<String>,
2519        }
2520
2521        impl TreeNodeRewriter for Rewriter {
2522            type Node = LogicalPlan;
2523
2524            fn f_up(
2525                &mut self,
2526                node: Self::Node,
2527            ) -> datafusion_common::Result<Transformed<Self::Node>> {
2528                match node {
2529                    LogicalPlan::TableScan(scan) => {
2530                        let schema = scan.source.schema();
2531                        let mut projection = match scan.projection.clone() {
2532                            Some(p) => p,
2533                            None => {
2534                                // Scanning all columns already covers required tags.
2535                                return Ok(Transformed::no(LogicalPlan::TableScan(scan)));
2536                            }
2537                        };
2538
2539                        let mut changed = false;
2540                        for tag in &self.required_tags {
2541                            if let Some((idx, _)) = schema
2542                                .fields()
2543                                .iter()
2544                                .enumerate()
2545                                .find(|(_, field)| field.name() == tag)
2546                                && !projection.contains(&idx)
2547                            {
2548                                projection.push(idx);
2549                                changed = true;
2550                            }
2551                        }
2552
2553                        if !changed {
2554                            return Ok(Transformed::no(LogicalPlan::TableScan(scan)));
2555                        }
2556
2557                        projection.sort_unstable();
2558                        projection.dedup();
2559
2560                        let new_scan = TableScan::try_new(
2561                            scan.table_name.clone(),
2562                            scan.source.clone(),
2563                            Some(projection),
2564                            scan.filters,
2565                            scan.fetch,
2566                        )?;
2567                        Ok(Transformed::yes(LogicalPlan::TableScan(new_scan)))
2568                    }
2569                    LogicalPlan::Projection(proj) => {
2570                        let input_schema = proj.input.schema();
2571
2572                        let existing = proj
2573                            .schema
2574                            .fields()
2575                            .iter()
2576                            .map(|f| f.name().as_str())
2577                            .collect::<HashSet<_>>();
2578
2579                        let mut expr = proj.expr.clone();
2580                        let mut has_changed = false;
2581                        for tag in &self.required_tags {
2582                            if existing.contains(tag.as_str()) {
2583                                continue;
2584                            }
2585
2586                            if let Some(idx) = input_schema.index_of_column_by_name(None, tag) {
2587                                expr.push(DfExpr::Column(Column::from(
2588                                    input_schema.qualified_field(idx),
2589                                )));
2590                                has_changed = true;
2591                            }
2592                        }
2593
2594                        if !has_changed {
2595                            return Ok(Transformed::no(LogicalPlan::Projection(proj)));
2596                        }
2597
2598                        let new_proj = Projection::try_new(expr, proj.input)?;
2599                        Ok(Transformed::yes(LogicalPlan::Projection(new_proj)))
2600                    }
2601                    other => Ok(Transformed::no(other)),
2602                }
2603            }
2604        }
2605
2606        let mut rewriter = Rewriter {
2607            required_tags: required_tags.clone(),
2608        };
2609        let rewritten = plan
2610            .rewrite(&mut rewriter)
2611            .context(DataFusionPlanningSnafu)?;
2612        Ok(rewritten.data)
2613    }
2614
2615    fn refresh_tag_columns_from_schema(&mut self, schema: &DFSchemaRef) {
2616        let time_index = self.ctx.time_index_column.as_deref();
2617        let field_columns = self.ctx.field_columns.iter().collect::<HashSet<_>>();
2618
2619        let mut tags = schema
2620            .fields()
2621            .iter()
2622            .map(|f| f.name())
2623            .filter(|name| Some(name.as_str()) != time_index)
2624            .filter(|name| !field_columns.contains(name))
2625            .filter(|name| !is_metric_engine_internal_column(name))
2626            .cloned()
2627            .collect::<Vec<_>>();
2628        tags.sort_unstable();
2629        tags.dedup();
2630        self.ctx.tag_columns = tags;
2631    }
2632
2633    /// Setup [PromPlannerContext]'s state fields.
2634    ///
2635    /// Returns a logical plan for an empty metric.
2636    async fn setup_context(&mut self) -> Result<Option<LogicalPlan>> {
2637        let table_ref = self.table_ref()?;
2638        let source = match self.table_provider.resolve_table(table_ref.clone()).await {
2639            Err(e) if e.status_code() == StatusCode::TableNotFound => {
2640                let plan = self.setup_context_for_empty_metric()?;
2641                return Ok(Some(plan));
2642            }
2643            res => res.context(CatalogSnafu)?,
2644        };
2645        let table = self.table_from_source(&source)?;
2646
2647        // set time index column name
2648        let time_index = table
2649            .schema()
2650            .timestamp_column()
2651            .with_context(|| TimeIndexNotFoundSnafu {
2652                table: table_ref.to_quoted_string(),
2653            })?
2654            .name
2655            .clone();
2656        self.ctx.time_index_column = Some(time_index);
2657
2658        // set values columns
2659        let values = table
2660            .table_info()
2661            .meta
2662            .field_column_names()
2663            .cloned()
2664            .collect();
2665        self.ctx.field_columns = values;
2666
2667        // set primary key (tag) columns
2668        let tags = table
2669            .table_info()
2670            .meta
2671            .row_key_column_names()
2672            .filter(|col| {
2673                // remove metric engine's internal columns
2674                col != &DATA_SCHEMA_TABLE_ID_COLUMN_NAME && col != &DATA_SCHEMA_TSID_COLUMN_NAME
2675            })
2676            .cloned()
2677            .collect();
2678        self.ctx.tag_columns = tags;
2679
2680        self.ctx.use_tsid = false;
2681
2682        Ok(None)
2683    }
2684
2685    /// Setup [PromPlannerContext]'s state fields for a non existent table
2686    /// without any rows.
2687    fn setup_context_for_empty_metric(&mut self) -> Result<LogicalPlan> {
2688        self.ctx.time_index_column = Some(SPECIAL_TIME_FUNCTION.to_string());
2689        self.ctx.reset_table_name_and_schema();
2690        self.ctx.tag_columns = vec![];
2691        self.ctx.field_columns = vec![DEFAULT_FIELD_COLUMN.to_string()];
2692        self.ctx.use_tsid = false;
2693
2694        // The table doesn't have any data, so we set start to 0 and end to -1.
2695        let plan = LogicalPlan::Extension(Extension {
2696            node: Arc::new(
2697                EmptyMetric::new(
2698                    0,
2699                    -1,
2700                    self.ctx.interval,
2701                    SPECIAL_TIME_FUNCTION.to_string(),
2702                    DEFAULT_FIELD_COLUMN.to_string(),
2703                    Some(lit(0.0f64)),
2704                )
2705                .context(DataFusionPlanningSnafu)?,
2706            ),
2707        });
2708        Ok(plan)
2709    }
2710
2711    // TODO(ruihang): insert column expr
2712    fn create_function_args(&self, args: &[Box<PromExpr>]) -> Result<FunctionArgs> {
2713        let mut result = FunctionArgs::default();
2714
2715        for arg in args {
2716            // First try to parse as literal expression (including binary expressions like 100.0 + 3.0)
2717            if let Some(expr) = Self::try_build_literal_expr(arg) {
2718                result.literals.push(expr);
2719            } else {
2720                // If not a literal, treat as vector input
2721                match arg.as_ref() {
2722                    PromExpr::Subquery(_)
2723                    | PromExpr::VectorSelector(_)
2724                    | PromExpr::MatrixSelector(_)
2725                    | PromExpr::Extension(_)
2726                    | PromExpr::Aggregate(_)
2727                    | PromExpr::Paren(_)
2728                    | PromExpr::Call(_)
2729                    | PromExpr::Binary(_)
2730                    | PromExpr::Unary(_) => {
2731                        if result.input.replace(*arg.clone()).is_some() {
2732                            MultipleVectorSnafu { expr: *arg.clone() }.fail()?;
2733                        }
2734                    }
2735
2736                    _ => {
2737                        let expr = Self::get_param_as_literal_expr(&Some(arg.clone()), None, None)?;
2738                        result.literals.push(expr);
2739                    }
2740                }
2741            }
2742        }
2743
2744        Ok(result)
2745    }
2746
2747    /// Creates function expressions for projection and returns the expressions and new tags.
2748    ///
2749    /// # Side Effects
2750    ///
2751    /// This method will update [PromPlannerContext]'s fields and tags if needed.
2752    fn create_function_expr(
2753        &mut self,
2754        func: &Function,
2755        other_input_exprs: Vec<DfExpr>,
2756        query_engine_state: &QueryEngineState,
2757    ) -> Result<(Vec<DfExpr>, Vec<String>)> {
2758        // TODO(ruihang): check function args list
2759        let mut other_input_exprs: VecDeque<DfExpr> = other_input_exprs.into();
2760
2761        // TODO(ruihang): set this according to in-param list
2762        let field_column_pos = 0;
2763        let mut exprs = Vec::with_capacity(self.ctx.field_columns.len());
2764        // New labels after executing the function, e.g. `label_replace` etc.
2765        let mut new_tags = vec![];
2766        let scalar_func = match func.name {
2767            "increase" => ScalarFunc::ExtrapolateUdf(
2768                Arc::new(Increase::scalar_udf()),
2769                self.ctx.range.context(ExpectRangeSelectorSnafu)?,
2770            ),
2771            "rate" => ScalarFunc::ExtrapolateUdf(
2772                Arc::new(Rate::scalar_udf()),
2773                self.ctx.range.context(ExpectRangeSelectorSnafu)?,
2774            ),
2775            "delta" => ScalarFunc::ExtrapolateUdf(
2776                Arc::new(Delta::scalar_udf()),
2777                self.ctx.range.context(ExpectRangeSelectorSnafu)?,
2778            ),
2779            "idelta" => ScalarFunc::Udf(Arc::new(IDelta::<false>::scalar_udf())),
2780            "irate" => ScalarFunc::Udf(Arc::new(IDelta::<true>::scalar_udf())),
2781            "resets" => ScalarFunc::Udf(Arc::new(Resets::scalar_udf())),
2782            "changes" => ScalarFunc::Udf(Arc::new(Changes::scalar_udf())),
2783            "deriv" => ScalarFunc::Udf(Arc::new(Deriv::scalar_udf())),
2784            "avg_over_time" => ScalarFunc::Udf(Arc::new(AvgOverTime::scalar_udf())),
2785            "min_over_time" => ScalarFunc::Udf(Arc::new(MinOverTime::scalar_udf())),
2786            "max_over_time" => ScalarFunc::Udf(Arc::new(MaxOverTime::scalar_udf())),
2787            "sum_over_time" => ScalarFunc::Udf(Arc::new(SumOverTime::scalar_udf())),
2788            "count_over_time" => ScalarFunc::Udf(Arc::new(CountOverTime::scalar_udf())),
2789            "last_over_time" => ScalarFunc::Udf(Arc::new(LastOverTime::scalar_udf())),
2790            "absent_over_time" => ScalarFunc::Udf(Arc::new(AbsentOverTime::scalar_udf())),
2791            "present_over_time" => ScalarFunc::Udf(Arc::new(PresentOverTime::scalar_udf())),
2792            "stddev_over_time" => ScalarFunc::Udf(Arc::new(StddevOverTime::scalar_udf())),
2793            "stdvar_over_time" => ScalarFunc::Udf(Arc::new(StdvarOverTime::scalar_udf())),
2794            "quantile_over_time" => ScalarFunc::Udf(Arc::new(QuantileOverTime::scalar_udf())),
2795            "predict_linear" => {
2796                other_input_exprs[0] = DfExpr::Cast(Cast {
2797                    expr: Box::new(other_input_exprs[0].clone()),
2798                    data_type: ArrowDataType::Int64,
2799                });
2800                ScalarFunc::Udf(Arc::new(PredictLinear::scalar_udf()))
2801            }
2802            "double_exponential_smoothing" | "holt_winters" => {
2803                ScalarFunc::Udf(Arc::new(DoubleExponentialSmoothing::scalar_udf()))
2804            }
2805            "time" => {
2806                exprs.push(build_special_time_expr(
2807                    self.ctx.time_index_column.as_ref().unwrap(),
2808                ));
2809                ScalarFunc::GeneratedExpr
2810            }
2811            "minute" => {
2812                // date_part('minute', time_index)
2813                let expr = self.date_part_on_time_index("minute")?;
2814                exprs.push(expr);
2815                ScalarFunc::GeneratedExpr
2816            }
2817            "hour" => {
2818                // date_part('hour', time_index)
2819                let expr = self.date_part_on_time_index("hour")?;
2820                exprs.push(expr);
2821                ScalarFunc::GeneratedExpr
2822            }
2823            "month" => {
2824                // date_part('month', time_index)
2825                let expr = self.date_part_on_time_index("month")?;
2826                exprs.push(expr);
2827                ScalarFunc::GeneratedExpr
2828            }
2829            "year" => {
2830                // date_part('year', time_index)
2831                let expr = self.date_part_on_time_index("year")?;
2832                exprs.push(expr);
2833                ScalarFunc::GeneratedExpr
2834            }
2835            "day_of_month" => {
2836                // date_part('day', time_index)
2837                let expr = self.date_part_on_time_index("day")?;
2838                exprs.push(expr);
2839                ScalarFunc::GeneratedExpr
2840            }
2841            "day_of_week" => {
2842                // date_part('dow', time_index)
2843                let expr = self.date_part_on_time_index("dow")?;
2844                exprs.push(expr);
2845                ScalarFunc::GeneratedExpr
2846            }
2847            "day_of_year" => {
2848                // date_part('doy', time_index)
2849                let expr = self.date_part_on_time_index("doy")?;
2850                exprs.push(expr);
2851                ScalarFunc::GeneratedExpr
2852            }
2853            "days_in_month" => {
2854                // date_part(
2855                //     'days',
2856                //     (date_trunc('month', <TIME INDEX>::date) + interval '1 month - 1 day')
2857                // );
2858                let day_lit_expr = "day".lit();
2859                let month_lit_expr = "month".lit();
2860                let interval_1month_lit_expr =
2861                    DfExpr::Literal(ScalarValue::IntervalYearMonth(Some(1)), None);
2862                let interval_1day_lit_expr = DfExpr::Literal(
2863                    ScalarValue::IntervalDayTime(Some(IntervalDayTime::new(1, 0))),
2864                    None,
2865                );
2866                let the_1month_minus_1day_expr = DfExpr::BinaryExpr(BinaryExpr {
2867                    left: Box::new(interval_1month_lit_expr),
2868                    op: Operator::Minus,
2869                    right: Box::new(interval_1day_lit_expr),
2870                });
2871                let date_trunc_expr = DfExpr::ScalarFunction(ScalarFunction {
2872                    func: datafusion_functions::datetime::date_trunc(),
2873                    args: vec![month_lit_expr, self.create_time_index_column_expr()?],
2874                });
2875                let date_trunc_plus_interval_expr = DfExpr::BinaryExpr(BinaryExpr {
2876                    left: Box::new(date_trunc_expr),
2877                    op: Operator::Plus,
2878                    right: Box::new(the_1month_minus_1day_expr),
2879                });
2880                let date_part_expr = DfExpr::ScalarFunction(ScalarFunction {
2881                    func: datafusion_functions::datetime::date_part(),
2882                    args: vec![day_lit_expr, date_trunc_plus_interval_expr],
2883                });
2884
2885                exprs.push(date_part_expr);
2886                ScalarFunc::GeneratedExpr
2887            }
2888
2889            "label_join" => {
2890                self.ctx.use_tsid = false;
2891                let (concat_expr, dst_label) = Self::build_concat_labels_expr(
2892                    &mut other_input_exprs,
2893                    &self.ctx,
2894                    query_engine_state,
2895                )?;
2896
2897                // Reserve the current field columns except the `dst_label`.
2898                for value in &self.ctx.field_columns {
2899                    if *value != dst_label {
2900                        let expr = DfExpr::Column(Column::from_name(value));
2901                        exprs.push(expr);
2902                    }
2903                }
2904
2905                // Remove it from tag columns if exists to avoid duplicated column names
2906                self.ctx.tag_columns.retain(|tag| *tag != dst_label);
2907                new_tags.push(dst_label);
2908                // Add the new label expr to evaluate
2909                exprs.push(concat_expr);
2910
2911                ScalarFunc::GeneratedExpr
2912            }
2913            "label_replace" => {
2914                self.ctx.use_tsid = false;
2915                if let Some((replace_expr, dst_label)) = self
2916                    .build_regexp_replace_label_expr(&mut other_input_exprs, query_engine_state)?
2917                {
2918                    // Reserve the current field columns except the `dst_label`.
2919                    for value in &self.ctx.field_columns {
2920                        if *value != dst_label {
2921                            let expr = DfExpr::Column(Column::from_name(value));
2922                            exprs.push(expr);
2923                        }
2924                    }
2925
2926                    ensure!(
2927                        !self.ctx.tag_columns.contains(&dst_label),
2928                        SameLabelSetSnafu
2929                    );
2930                    new_tags.push(dst_label);
2931                    // Add the new label expr to evaluate
2932                    exprs.push(replace_expr);
2933                } else {
2934                    // Keep the current field columns
2935                    for value in &self.ctx.field_columns {
2936                        let expr = DfExpr::Column(Column::from_name(value));
2937                        exprs.push(expr);
2938                    }
2939                }
2940
2941                ScalarFunc::GeneratedExpr
2942            }
2943            "sort" | "sort_desc" | "sort_by_label" | "sort_by_label_desc" | "timestamp" => {
2944                // These functions are not expression but a part of plan,
2945                // they are processed by `prom_call_expr_to_plan`.
2946                for value in &self.ctx.field_columns {
2947                    let expr = DfExpr::Column(Column::from_name(value));
2948                    exprs.push(expr);
2949                }
2950
2951                ScalarFunc::GeneratedExpr
2952            }
2953            "round" => {
2954                if other_input_exprs.is_empty() {
2955                    other_input_exprs.push_front(0.0f64.lit());
2956                }
2957                ScalarFunc::DataFusionUdf(Arc::new(Round::scalar_udf()))
2958            }
2959            "rad" => ScalarFunc::DataFusionBuiltin(datafusion::functions::math::radians()),
2960            "deg" => ScalarFunc::DataFusionBuiltin(datafusion::functions::math::degrees()),
2961            "sgn" => ScalarFunc::DataFusionBuiltin(datafusion::functions::math::signum()),
2962            "pi" => {
2963                // pi functions doesn't accepts any arguments, needs special processing
2964                let fn_expr = DfExpr::ScalarFunction(ScalarFunction {
2965                    func: datafusion::functions::math::pi(),
2966                    args: vec![],
2967                });
2968                exprs.push(fn_expr);
2969
2970                ScalarFunc::GeneratedExpr
2971            }
2972            _ => {
2973                if let Some(f) = query_engine_state
2974                    .session_state()
2975                    .scalar_functions()
2976                    .get(func.name)
2977                {
2978                    ScalarFunc::DataFusionBuiltin(f.clone())
2979                } else if let Some(factory) = query_engine_state.scalar_function(func.name) {
2980                    let func_state = query_engine_state.function_state();
2981                    let query_ctx = self.table_provider.query_ctx();
2982
2983                    ScalarFunc::DataFusionUdf(Arc::new(factory.provide(FunctionContext {
2984                        state: func_state,
2985                        query_ctx: query_ctx.clone(),
2986                    })))
2987                } else if let Some(f) = datafusion_functions::math::functions()
2988                    .iter()
2989                    .find(|f| f.name() == func.name)
2990                {
2991                    ScalarFunc::DataFusionUdf(f.clone())
2992                } else {
2993                    return UnsupportedExprSnafu {
2994                        name: func.name.to_string(),
2995                    }
2996                    .fail();
2997                }
2998            }
2999        };
3000
3001        for value in &self.ctx.field_columns {
3002            let col_expr = DfExpr::Column(Column::from_name(value));
3003
3004            match scalar_func.clone() {
3005                ScalarFunc::DataFusionBuiltin(func) => {
3006                    other_input_exprs.insert(field_column_pos, col_expr);
3007                    let fn_expr = DfExpr::ScalarFunction(ScalarFunction {
3008                        func,
3009                        args: other_input_exprs.clone().into(),
3010                    });
3011                    exprs.push(fn_expr);
3012                    let _ = other_input_exprs.remove(field_column_pos);
3013                }
3014                ScalarFunc::DataFusionUdf(func) => {
3015                    let args = itertools::chain!(
3016                        other_input_exprs.iter().take(field_column_pos).cloned(),
3017                        std::iter::once(col_expr),
3018                        other_input_exprs.iter().skip(field_column_pos).cloned()
3019                    )
3020                    .collect_vec();
3021                    exprs.push(DfExpr::ScalarFunction(ScalarFunction { func, args }))
3022                }
3023                ScalarFunc::Udf(func) => {
3024                    let ts_range_expr = DfExpr::Column(Column::from_name(
3025                        RangeManipulate::build_timestamp_range_name(
3026                            self.ctx.time_index_column.as_ref().unwrap(),
3027                        ),
3028                    ));
3029                    other_input_exprs.insert(field_column_pos, ts_range_expr);
3030                    other_input_exprs.insert(field_column_pos + 1, col_expr);
3031                    let fn_expr = DfExpr::ScalarFunction(ScalarFunction {
3032                        func,
3033                        args: other_input_exprs.clone().into(),
3034                    });
3035                    exprs.push(fn_expr);
3036                    let _ = other_input_exprs.remove(field_column_pos + 1);
3037                    let _ = other_input_exprs.remove(field_column_pos);
3038                }
3039                ScalarFunc::ExtrapolateUdf(func, range_length) => {
3040                    let ts_range_expr = DfExpr::Column(Column::from_name(
3041                        RangeManipulate::build_timestamp_range_name(
3042                            self.ctx.time_index_column.as_ref().unwrap(),
3043                        ),
3044                    ));
3045                    other_input_exprs.insert(field_column_pos, ts_range_expr);
3046                    other_input_exprs.insert(field_column_pos + 1, col_expr);
3047                    other_input_exprs
3048                        .insert(field_column_pos + 2, self.create_time_index_column_expr()?);
3049                    other_input_exprs.push_back(lit(range_length));
3050                    let fn_expr = DfExpr::ScalarFunction(ScalarFunction {
3051                        func,
3052                        args: other_input_exprs.clone().into(),
3053                    });
3054                    exprs.push(fn_expr);
3055                    let _ = other_input_exprs.pop_back();
3056                    let _ = other_input_exprs.remove(field_column_pos + 2);
3057                    let _ = other_input_exprs.remove(field_column_pos + 1);
3058                    let _ = other_input_exprs.remove(field_column_pos);
3059                }
3060                ScalarFunc::GeneratedExpr => {}
3061            }
3062        }
3063
3064        // Update value columns' name, and alias them to remove qualifiers
3065        // For label functions such as `label_join`, `label_replace`, etc.,
3066        // we keep the fields unchanged.
3067        if !matches!(func.name, "label_join" | "label_replace") {
3068            let mut new_field_columns = Vec::with_capacity(exprs.len());
3069
3070            exprs = exprs
3071                .into_iter()
3072                .map(|expr| {
3073                    let display_name = expr.schema_name().to_string();
3074                    new_field_columns.push(display_name.clone());
3075                    Ok(expr.alias(display_name))
3076                })
3077                .collect::<std::result::Result<Vec<_>, _>>()
3078                .context(DataFusionPlanningSnafu)?;
3079
3080            self.ctx.field_columns = new_field_columns;
3081        }
3082
3083        Ok((exprs, new_tags))
3084    }
3085
3086    /// Validate label name according to Prometheus specification.
3087    /// Label names must match the regex: [a-zA-Z_][a-zA-Z0-9_]*
3088    /// Additionally, label names starting with double underscores are reserved for internal use.
3089    fn validate_label_name(label_name: &str) -> Result<()> {
3090        // Check if label name starts with double underscores (reserved)
3091        if label_name.starts_with("__") {
3092            return InvalidDestinationLabelNameSnafu { label_name }.fail();
3093        }
3094        // Check if label name matches the required pattern
3095        if !LABEL_NAME_REGEX.is_match(label_name) {
3096            return InvalidDestinationLabelNameSnafu { label_name }.fail();
3097        }
3098
3099        Ok(())
3100    }
3101
3102    /// Build expr for `label_replace` function
3103    fn build_regexp_replace_label_expr(
3104        &self,
3105        other_input_exprs: &mut VecDeque<DfExpr>,
3106        query_engine_state: &QueryEngineState,
3107    ) -> Result<Option<(DfExpr, String)>> {
3108        // label_replace(vector, dst_label, replacement, src_label, regex)
3109        let dst_label = match other_input_exprs.pop_front() {
3110            Some(DfExpr::Literal(ScalarValue::Utf8(Some(d)), _)) => d,
3111            other => UnexpectedPlanExprSnafu {
3112                desc: format!("expected dst_label string literal, but found {:?}", other),
3113            }
3114            .fail()?,
3115        };
3116
3117        // Validate the destination label name
3118        Self::validate_label_name(&dst_label)?;
3119        let replacement = match other_input_exprs.pop_front() {
3120            Some(DfExpr::Literal(ScalarValue::Utf8(Some(r)), _)) => r,
3121            other => UnexpectedPlanExprSnafu {
3122                desc: format!("expected replacement string literal, but found {:?}", other),
3123            }
3124            .fail()?,
3125        };
3126        let src_label = match other_input_exprs.pop_front() {
3127            Some(DfExpr::Literal(ScalarValue::Utf8(Some(s)), None)) => s,
3128            other => UnexpectedPlanExprSnafu {
3129                desc: format!("expected src_label string literal, but found {:?}", other),
3130            }
3131            .fail()?,
3132        };
3133
3134        let regex = match other_input_exprs.pop_front() {
3135            Some(DfExpr::Literal(ScalarValue::Utf8(Some(r)), None)) => r,
3136            other => UnexpectedPlanExprSnafu {
3137                desc: format!("expected regex string literal, but found {:?}", other),
3138            }
3139            .fail()?,
3140        };
3141
3142        // Validate the regex before using it
3143        // doc: https://prometheus.io/docs/prometheus/latest/querying/functions/#label_replace
3144        regex::Regex::new(&regex).map_err(|_| {
3145            InvalidRegularExpressionSnafu {
3146                regex: regex.clone(),
3147            }
3148            .build()
3149        })?;
3150
3151        // If the src_label exists and regex is empty, keep everything unchanged.
3152        if self.ctx.tag_columns.contains(&src_label) && regex.is_empty() {
3153            return Ok(None);
3154        }
3155
3156        // If the src_label doesn't exists, and
3157        if !self.ctx.tag_columns.contains(&src_label) {
3158            if replacement.is_empty() {
3159                // the replacement is empty, keep everything unchanged.
3160                return Ok(None);
3161            } else {
3162                // the replacement is not empty, always adds dst_label with replacement value.
3163                return Ok(Some((
3164                    // alias literal `replacement` as dst_label
3165                    lit(replacement).alias(&dst_label),
3166                    dst_label,
3167                )));
3168            }
3169        }
3170
3171        // Preprocess the regex:
3172        // https://github.com/prometheus/prometheus/blob/d902abc50d6652ba8fe9a81ff8e5cce936114eba/promql/functions.go#L1575C32-L1575C37
3173        let regex = format!("^(?s:{regex})$");
3174
3175        let session_state = query_engine_state.session_state();
3176        let func = session_state
3177            .scalar_functions()
3178            .get("regexp_replace")
3179            .context(UnsupportedExprSnafu {
3180                name: "regexp_replace",
3181            })?;
3182
3183        // regexp_replace(src_label, regex, replacement)
3184        let args = vec![
3185            if src_label.is_empty() {
3186                DfExpr::Literal(ScalarValue::Utf8(Some(String::new())), None)
3187            } else {
3188                DfExpr::Column(Column::from_name(src_label))
3189            },
3190            DfExpr::Literal(ScalarValue::Utf8(Some(regex)), None),
3191            DfExpr::Literal(ScalarValue::Utf8(Some(replacement)), None),
3192        ];
3193
3194        Ok(Some((
3195            DfExpr::ScalarFunction(ScalarFunction {
3196                func: func.clone(),
3197                args,
3198            })
3199            .alias(&dst_label),
3200            dst_label,
3201        )))
3202    }
3203
3204    /// Build expr for `label_join` function
3205    fn build_concat_labels_expr(
3206        other_input_exprs: &mut VecDeque<DfExpr>,
3207        ctx: &PromPlannerContext,
3208        query_engine_state: &QueryEngineState,
3209    ) -> Result<(DfExpr, String)> {
3210        // label_join(vector, dst_label, separator, src_label_1, src_label_2, ...)
3211
3212        let dst_label = match other_input_exprs.pop_front() {
3213            Some(DfExpr::Literal(ScalarValue::Utf8(Some(d)), _)) => d,
3214            other => UnexpectedPlanExprSnafu {
3215                desc: format!("expected dst_label string literal, but found {:?}", other),
3216            }
3217            .fail()?,
3218        };
3219        let separator = match other_input_exprs.pop_front() {
3220            Some(DfExpr::Literal(ScalarValue::Utf8(Some(d)), _)) => d,
3221            other => UnexpectedPlanExprSnafu {
3222                desc: format!("expected separator string literal, but found {:?}", other),
3223            }
3224            .fail()?,
3225        };
3226
3227        // Create a set of available columns (tag columns + field columns + time index column)
3228        let available_columns: HashSet<&str> = ctx
3229            .tag_columns
3230            .iter()
3231            .chain(ctx.field_columns.iter())
3232            .chain(ctx.time_index_column.as_ref())
3233            .map(|s| s.as_str())
3234            .collect();
3235
3236        let src_labels = other_input_exprs
3237            .iter()
3238            .map(|expr| {
3239                // Cast source label into column or null literal
3240                match expr {
3241                    DfExpr::Literal(ScalarValue::Utf8(Some(label)), None) => {
3242                        if label.is_empty() {
3243                            Ok(DfExpr::Literal(ScalarValue::Null, None))
3244                        } else if available_columns.contains(label.as_str()) {
3245                            // Label exists in the table schema
3246                            Ok(DfExpr::Column(Column::from_name(label)))
3247                        } else {
3248                            // Label doesn't exist, treat as empty string (null)
3249                            Ok(DfExpr::Literal(ScalarValue::Null, None))
3250                        }
3251                    }
3252                    other => UnexpectedPlanExprSnafu {
3253                        desc: format!(
3254                            "expected source label string literal, but found {:?}",
3255                            other
3256                        ),
3257                    }
3258                    .fail(),
3259                }
3260            })
3261            .collect::<Result<Vec<_>>>()?;
3262        ensure!(
3263            !src_labels.is_empty(),
3264            FunctionInvalidArgumentSnafu {
3265                fn_name: "label_join"
3266            }
3267        );
3268
3269        let session_state = query_engine_state.session_state();
3270        let func = session_state
3271            .scalar_functions()
3272            .get("concat_ws")
3273            .context(UnsupportedExprSnafu { name: "concat_ws" })?;
3274
3275        // concat_ws(separator, src_label_1, src_label_2, ...) as dst_label
3276        let mut args = Vec::with_capacity(1 + src_labels.len());
3277        args.push(DfExpr::Literal(ScalarValue::Utf8(Some(separator)), None));
3278        args.extend(src_labels);
3279
3280        Ok((
3281            DfExpr::ScalarFunction(ScalarFunction {
3282                func: func.clone(),
3283                args,
3284            })
3285            .alias(&dst_label),
3286            dst_label,
3287        ))
3288    }
3289
3290    fn create_time_index_column_expr(&self) -> Result<DfExpr> {
3291        Ok(DfExpr::Column(Column::from_name(
3292            self.ctx
3293                .time_index_column
3294                .clone()
3295                .with_context(|| TimeIndexNotFoundSnafu { table: "unknown" })?,
3296        )))
3297    }
3298
3299    fn create_tag_column_exprs(&self) -> Result<Vec<DfExpr>> {
3300        let mut result = Vec::with_capacity(self.ctx.tag_columns.len());
3301        for tag in &self.ctx.tag_columns {
3302            let expr = DfExpr::Column(Column::from_name(tag));
3303            result.push(expr);
3304        }
3305        Ok(result)
3306    }
3307
3308    fn create_field_column_exprs(&self) -> Result<Vec<DfExpr>> {
3309        let mut result = Vec::with_capacity(self.ctx.field_columns.len());
3310        for field in &self.ctx.field_columns {
3311            let expr = DfExpr::Column(Column::from_name(field));
3312            result.push(expr);
3313        }
3314        Ok(result)
3315    }
3316
3317    fn create_tag_and_time_index_column_sort_exprs(&self) -> Result<Vec<SortExpr>> {
3318        let mut result = self
3319            .ctx
3320            .tag_columns
3321            .iter()
3322            .map(|col| DfExpr::Column(Column::from_name(col)).sort(true, true))
3323            .collect::<Vec<_>>();
3324        result.push(self.create_time_index_column_expr()?.sort(true, true));
3325        Ok(result)
3326    }
3327
3328    fn create_field_columns_sort_exprs(&self, asc: bool) -> Vec<SortExpr> {
3329        self.ctx
3330            .field_columns
3331            .iter()
3332            .map(|col| DfExpr::Column(Column::from_name(col)).sort(asc, true))
3333            .collect::<Vec<_>>()
3334    }
3335
3336    fn create_sort_exprs_by_tags(
3337        func: &str,
3338        tags: Vec<DfExpr>,
3339        asc: bool,
3340    ) -> Result<Vec<SortExpr>> {
3341        ensure!(
3342            !tags.is_empty(),
3343            FunctionInvalidArgumentSnafu { fn_name: func }
3344        );
3345
3346        tags.iter()
3347            .map(|col| match col {
3348                DfExpr::Literal(ScalarValue::Utf8(Some(label)), _) => {
3349                    Ok(DfExpr::Column(Column::from_name(label)).sort(asc, false))
3350                }
3351                other => UnexpectedPlanExprSnafu {
3352                    desc: format!("expected label string literal, but found {:?}", other),
3353                }
3354                .fail(),
3355            })
3356            .collect::<Result<Vec<_>>>()
3357    }
3358
3359    fn create_empty_values_filter_expr(&self) -> Result<DfExpr> {
3360        let mut exprs = Vec::with_capacity(self.ctx.field_columns.len());
3361        for value in &self.ctx.field_columns {
3362            let expr = DfExpr::Column(Column::from_name(value)).is_not_null();
3363            exprs.push(expr);
3364        }
3365
3366        // This error context should be computed lazily: the planner may set `ctx.table_name` to
3367        // `None` for derived expressions (e.g. after projecting the LHS of a vector-vector
3368        // comparison filter). Eagerly calling `table_ref()?` here can turn a valid plan into
3369        // a `TableNameNotFound` error even when `conjunction(exprs)` succeeds.
3370        conjunction(exprs).with_context(|| ValueNotFoundSnafu {
3371            table: self
3372                .table_ref()
3373                .map(|t| t.to_quoted_string())
3374                .unwrap_or_else(|_| "unknown".to_string()),
3375        })
3376    }
3377
3378    /// Creates a set of DataFusion `DfExpr::AggregateFunction` expressions for each value column using the specified aggregate function.
3379    ///
3380    /// # Side Effects
3381    ///
3382    /// This method modifies the value columns in the context by replacing them with the new columns
3383    /// created by the aggregate function application.
3384    ///
3385    /// # Returns
3386    ///
3387    /// Returns a tuple of `(aggregate_expressions, previous_field_expressions)` where:
3388    /// - `aggregate_expressions`: Expressions that apply the aggregate function to the original fields
3389    /// - `previous_field_expressions`: Original field expressions before aggregation. This is non-empty
3390    ///   only when the operation is `count_values`, as this operation requires preserving the original
3391    ///   values for grouping.
3392    ///
3393    fn create_aggregate_exprs(
3394        &mut self,
3395        op: TokenType,
3396        param: &Option<Box<PromExpr>>,
3397        input_plan: &LogicalPlan,
3398    ) -> Result<(Vec<DfExpr>, Vec<DfExpr>)> {
3399        let mut non_col_args = Vec::new();
3400        let is_group_agg = op.id() == token::T_GROUP;
3401        if is_group_agg {
3402            ensure!(
3403                self.ctx.field_columns.len() == 1,
3404                MultiFieldsNotSupportedSnafu {
3405                    operator: "group()"
3406                }
3407            );
3408        }
3409        let aggr = match op.id() {
3410            token::T_SUM => sum_udaf(),
3411            token::T_QUANTILE => {
3412                let q =
3413                    Self::get_param_as_literal_expr(param, Some(op), Some(ArrowDataType::Float64))?;
3414                non_col_args.push(q);
3415                quantile_udaf()
3416            }
3417            token::T_AVG => avg_udaf(),
3418            token::T_COUNT_VALUES | token::T_COUNT => count_udaf(),
3419            token::T_MIN => min_udaf(),
3420            token::T_MAX => max_udaf(),
3421            // PromQL's `group()` aggregator produces 1 for each group.
3422            // Use `max(1.0)` (per-group) to match semantics and output type (Float64).
3423            token::T_GROUP => max_udaf(),
3424            token::T_STDDEV => stddev_pop_udaf(),
3425            token::T_STDVAR => var_pop_udaf(),
3426            token::T_TOPK | token::T_BOTTOMK => UnsupportedExprSnafu {
3427                name: format!("{op:?}"),
3428            }
3429            .fail()?,
3430            _ => UnexpectedTokenSnafu { token: op }.fail()?,
3431        };
3432
3433        // perform aggregate operation to each value column
3434        let exprs: Vec<DfExpr> = self
3435            .ctx
3436            .field_columns
3437            .iter()
3438            .map(|col| {
3439                if is_group_agg {
3440                    aggr.call(vec![lit(1_f64)])
3441                } else {
3442                    non_col_args.push(DfExpr::Column(Column::from_name(col)));
3443                    let expr = aggr.call(non_col_args.clone());
3444                    non_col_args.pop();
3445                    expr
3446                }
3447            })
3448            .collect::<Vec<_>>();
3449
3450        // if the aggregator is `count_values`, it must be grouped by current fields.
3451        let prev_field_exprs = if op.id() == token::T_COUNT_VALUES {
3452            let prev_field_exprs: Vec<_> = self
3453                .ctx
3454                .field_columns
3455                .iter()
3456                .map(|col| DfExpr::Column(Column::from_name(col)))
3457                .collect();
3458
3459            ensure!(
3460                self.ctx.field_columns.len() == 1,
3461                UnsupportedExprSnafu {
3462                    name: "count_values on multi-value input"
3463                }
3464            );
3465
3466            prev_field_exprs
3467        } else {
3468            vec![]
3469        };
3470
3471        // update value column name according to the aggregators,
3472        let mut new_field_columns = Vec::with_capacity(self.ctx.field_columns.len());
3473
3474        let normalized_exprs =
3475            normalize_cols(exprs.iter().cloned(), input_plan).context(DataFusionPlanningSnafu)?;
3476        for expr in normalized_exprs {
3477            new_field_columns.push(expr.schema_name().to_string());
3478        }
3479        self.ctx.field_columns = new_field_columns;
3480
3481        Ok((exprs, prev_field_exprs))
3482    }
3483
3484    fn get_param_value_as_str(op: TokenType, param: &Option<Box<PromExpr>>) -> Result<&str> {
3485        let param = param
3486            .as_deref()
3487            .with_context(|| FunctionInvalidArgumentSnafu {
3488                fn_name: op.to_string(),
3489            })?;
3490        let PromExpr::StringLiteral(StringLiteral { val }) = param else {
3491            return FunctionInvalidArgumentSnafu {
3492                fn_name: op.to_string(),
3493            }
3494            .fail();
3495        };
3496
3497        Ok(val)
3498    }
3499
3500    fn get_param_as_literal_expr(
3501        param: &Option<Box<PromExpr>>,
3502        op: Option<TokenType>,
3503        expected_type: Option<ArrowDataType>,
3504    ) -> Result<DfExpr> {
3505        let prom_param = param.as_deref().with_context(|| {
3506            if let Some(op) = op {
3507                FunctionInvalidArgumentSnafu {
3508                    fn_name: op.to_string(),
3509                }
3510            } else {
3511                FunctionInvalidArgumentSnafu {
3512                    fn_name: "unknown".to_string(),
3513                }
3514            }
3515        })?;
3516
3517        let expr = Self::try_build_literal_expr(prom_param).with_context(|| {
3518            if let Some(op) = op {
3519                FunctionInvalidArgumentSnafu {
3520                    fn_name: op.to_string(),
3521                }
3522            } else {
3523                FunctionInvalidArgumentSnafu {
3524                    fn_name: "unknown".to_string(),
3525                }
3526            }
3527        })?;
3528
3529        // check if the type is expected
3530        if let Some(expected_type) = expected_type {
3531            // literal should not have reference to column
3532            let expr_type = expr
3533                .get_type(&DFSchema::empty())
3534                .context(DataFusionPlanningSnafu)?;
3535            if expected_type != expr_type {
3536                return FunctionInvalidArgumentSnafu {
3537                    fn_name: format!("expected {expected_type:?}, but found {expr_type:?}"),
3538                }
3539                .fail();
3540            }
3541        }
3542
3543        Ok(expr)
3544    }
3545
3546    /// Create [DfExpr::WindowFunction] expr for each value column with given window function.
3547    ///
3548    fn create_window_exprs(
3549        &mut self,
3550        op: TokenType,
3551        group_exprs: Vec<DfExpr>,
3552        input_plan: &LogicalPlan,
3553    ) -> Result<Vec<DfExpr>> {
3554        ensure!(
3555            self.ctx.field_columns.len() == 1,
3556            UnsupportedExprSnafu {
3557                name: "topk or bottomk on multi-value input"
3558            }
3559        );
3560
3561        assert!(matches!(op.id(), token::T_TOPK | token::T_BOTTOMK));
3562
3563        let asc = matches!(op.id(), token::T_BOTTOMK);
3564
3565        let tag_sort_exprs = self
3566            .create_tag_column_exprs()?
3567            .into_iter()
3568            .map(|expr| expr.sort(asc, true));
3569
3570        // perform window operation to each value column
3571        let exprs: Vec<DfExpr> = self
3572            .ctx
3573            .field_columns
3574            .iter()
3575            .map(|col| {
3576                let mut sort_exprs = Vec::with_capacity(self.ctx.tag_columns.len() + 1);
3577                // Order by value in the specific order
3578                sort_exprs.push(DfExpr::Column(Column::from(col)).sort(asc, true));
3579                // Then tags if the values are equal,
3580                // Try to ensure the relative stability of the output results.
3581                sort_exprs.extend(tag_sort_exprs.clone());
3582
3583                DfExpr::WindowFunction(Box::new(WindowFunction {
3584                    fun: WindowFunctionDefinition::WindowUDF(Arc::new(RowNumber::new().into())),
3585                    params: WindowFunctionParams {
3586                        args: vec![],
3587                        partition_by: group_exprs.clone(),
3588                        order_by: sort_exprs,
3589                        window_frame: WindowFrame::new(Some(true)),
3590                        null_treatment: None,
3591                        distinct: false,
3592                        filter: None,
3593                    },
3594                }))
3595            })
3596            .collect();
3597
3598        let normalized_exprs =
3599            normalize_cols(exprs.iter().cloned(), input_plan).context(DataFusionPlanningSnafu)?;
3600        Ok(normalized_exprs)
3601    }
3602
3603    /// Try to build a [f64] from [PromExpr].
3604    #[deprecated(
3605        note = "use `Self::get_param_as_literal_expr` instead. This is only for `create_histogram_plan`"
3606    )]
3607    fn try_build_float_literal(expr: &PromExpr) -> Option<f64> {
3608        match expr {
3609            PromExpr::NumberLiteral(NumberLiteral { val }) => Some(*val),
3610            PromExpr::Paren(ParenExpr { expr }) => Self::try_build_float_literal(expr),
3611            PromExpr::Unary(UnaryExpr { expr, .. }) => {
3612                Self::try_build_float_literal(expr).map(|f| -f)
3613            }
3614            PromExpr::StringLiteral(_)
3615            | PromExpr::Binary(_)
3616            | PromExpr::VectorSelector(_)
3617            | PromExpr::MatrixSelector(_)
3618            | PromExpr::Call(_)
3619            | PromExpr::Extension(_)
3620            | PromExpr::Aggregate(_)
3621            | PromExpr::Subquery(_) => None,
3622        }
3623    }
3624
3625    /// Create a [SPECIAL_HISTOGRAM_QUANTILE] plan.
3626    async fn create_histogram_plan(
3627        &mut self,
3628        args: &PromFunctionArgs,
3629        query_engine_state: &QueryEngineState,
3630    ) -> Result<LogicalPlan> {
3631        if args.args.len() != 2 {
3632            return FunctionInvalidArgumentSnafu {
3633                fn_name: SPECIAL_HISTOGRAM_QUANTILE.to_string(),
3634            }
3635            .fail();
3636        }
3637        #[allow(deprecated)]
3638        let phi = Self::try_build_float_literal(&args.args[0]).with_context(|| {
3639            FunctionInvalidArgumentSnafu {
3640                fn_name: SPECIAL_HISTOGRAM_QUANTILE.to_string(),
3641            }
3642        })?;
3643
3644        let input = args.args[1].as_ref().clone();
3645        let input_plan = self.prom_expr_to_plan(&input, query_engine_state).await?;
3646        // `histogram_quantile` folds buckets across `le`, so `__tsid` (which includes `le`) is not
3647        // a stable series identifier anymore. Also, HistogramFold infers label columns from the
3648        // input schema and must not treat `__tsid` as a label column.
3649        let input_plan = self.strip_tsid_column(input_plan)?;
3650        self.ctx.use_tsid = false;
3651
3652        if !self.ctx.has_le_tag() {
3653            // Return empty result instead of error when 'le' column is not found
3654            // This handles the case when histogram metrics don't exist
3655            return Ok(LogicalPlan::EmptyRelation(
3656                datafusion::logical_expr::EmptyRelation {
3657                    produce_one_row: false,
3658                    schema: Arc::new(DFSchema::empty()),
3659                },
3660            ));
3661        }
3662        let time_index_column =
3663            self.ctx
3664                .time_index_column
3665                .clone()
3666                .with_context(|| TimeIndexNotFoundSnafu {
3667                    table: self.ctx.table_name.clone().unwrap_or_default(),
3668                })?;
3669        // FIXME(ruihang): support multi fields
3670        let field_column = self
3671            .ctx
3672            .field_columns
3673            .first()
3674            .with_context(|| FunctionInvalidArgumentSnafu {
3675                fn_name: SPECIAL_HISTOGRAM_QUANTILE.to_string(),
3676            })?
3677            .clone();
3678        // remove le column from tag columns
3679        self.ctx.tag_columns.retain(|col| col != LE_COLUMN_NAME);
3680
3681        Ok(LogicalPlan::Extension(Extension {
3682            node: Arc::new(
3683                HistogramFold::new(
3684                    LE_COLUMN_NAME.to_string(),
3685                    field_column,
3686                    time_index_column,
3687                    phi,
3688                    input_plan,
3689                )
3690                .context(DataFusionPlanningSnafu)?,
3691            ),
3692        }))
3693    }
3694
3695    /// Create a [SPECIAL_VECTOR_FUNCTION] plan
3696    async fn create_vector_plan(&mut self, args: &PromFunctionArgs) -> Result<LogicalPlan> {
3697        if args.args.len() != 1 {
3698            return FunctionInvalidArgumentSnafu {
3699                fn_name: SPECIAL_VECTOR_FUNCTION.to_string(),
3700            }
3701            .fail();
3702        }
3703        let lit = Self::get_param_as_literal_expr(&Some(args.args[0].clone()), None, None)?;
3704
3705        // reuse `SPECIAL_TIME_FUNCTION` as name of time index column
3706        self.ctx.time_index_column = Some(SPECIAL_TIME_FUNCTION.to_string());
3707        self.ctx.reset_table_name_and_schema();
3708        self.ctx.tag_columns = vec![];
3709        self.ctx.field_columns = vec![greptime_value().to_string()];
3710        Ok(LogicalPlan::Extension(Extension {
3711            node: Arc::new(
3712                EmptyMetric::new(
3713                    self.ctx.start,
3714                    self.ctx.end,
3715                    self.ctx.interval,
3716                    SPECIAL_TIME_FUNCTION.to_string(),
3717                    greptime_value().to_string(),
3718                    Some(lit),
3719                )
3720                .context(DataFusionPlanningSnafu)?,
3721            ),
3722        }))
3723    }
3724
3725    /// Create a [SCALAR_FUNCTION] plan
3726    async fn create_scalar_plan(
3727        &mut self,
3728        args: &PromFunctionArgs,
3729        query_engine_state: &QueryEngineState,
3730    ) -> Result<LogicalPlan> {
3731        ensure!(
3732            args.len() == 1,
3733            FunctionInvalidArgumentSnafu {
3734                fn_name: SCALAR_FUNCTION
3735            }
3736        );
3737        let input = self
3738            .prom_expr_to_plan(&args.args[0], query_engine_state)
3739            .await?;
3740        ensure!(
3741            self.ctx.field_columns.len() == 1,
3742            MultiFieldsNotSupportedSnafu {
3743                operator: SCALAR_FUNCTION
3744            },
3745        );
3746        let scalar_plan = LogicalPlan::Extension(Extension {
3747            node: Arc::new(
3748                ScalarCalculate::new(
3749                    self.ctx.start,
3750                    self.ctx.end,
3751                    self.ctx.interval,
3752                    input,
3753                    self.ctx.time_index_column.as_ref().unwrap(),
3754                    &self.ctx.tag_columns,
3755                    &self.ctx.field_columns[0],
3756                    self.ctx.table_name.as_deref(),
3757                )
3758                .context(PromqlPlanNodeSnafu)?,
3759            ),
3760        });
3761        // scalar plan have no tag columns
3762        self.ctx.tag_columns.clear();
3763        self.ctx.field_columns.clear();
3764        self.ctx
3765            .field_columns
3766            .push(scalar_plan.schema().field(1).name().clone());
3767        Ok(scalar_plan)
3768    }
3769
3770    /// Create a [SPECIAL_ABSENT_FUNCTION] plan
3771    async fn create_absent_plan(
3772        &mut self,
3773        args: &PromFunctionArgs,
3774        query_engine_state: &QueryEngineState,
3775    ) -> Result<LogicalPlan> {
3776        if args.args.len() != 1 {
3777            return FunctionInvalidArgumentSnafu {
3778                fn_name: SPECIAL_ABSENT_FUNCTION.to_string(),
3779            }
3780            .fail();
3781        }
3782        let input = self
3783            .prom_expr_to_plan(&args.args[0], query_engine_state)
3784            .await?;
3785
3786        let time_index_expr = self.create_time_index_column_expr()?;
3787        let first_field_expr =
3788            self.create_field_column_exprs()?
3789                .pop()
3790                .with_context(|| ValueNotFoundSnafu {
3791                    table: self.ctx.table_name.clone().unwrap_or_default(),
3792                })?;
3793        let first_value_expr = first_value(first_field_expr, vec![]);
3794
3795        let ordered_aggregated_input = LogicalPlanBuilder::from(input)
3796            .aggregate(
3797                vec![time_index_expr.clone()],
3798                vec![first_value_expr.clone()],
3799            )
3800            .context(DataFusionPlanningSnafu)?
3801            .sort(vec![time_index_expr.sort(true, false)])
3802            .context(DataFusionPlanningSnafu)?
3803            .build()
3804            .context(DataFusionPlanningSnafu)?;
3805
3806        let fake_labels = self
3807            .ctx
3808            .selector_matcher
3809            .iter()
3810            .filter_map(|matcher| match matcher.op {
3811                MatchOp::Equal => Some((matcher.name.clone(), matcher.value.clone())),
3812                _ => None,
3813            })
3814            .collect::<Vec<_>>();
3815
3816        // Create the absent plan
3817        let absent_plan = LogicalPlan::Extension(Extension {
3818            node: Arc::new(
3819                Absent::try_new(
3820                    self.ctx.start,
3821                    self.ctx.end,
3822                    self.ctx.interval,
3823                    self.ctx.time_index_column.as_ref().unwrap().clone(),
3824                    self.ctx.field_columns[0].clone(),
3825                    fake_labels,
3826                    ordered_aggregated_input,
3827                )
3828                .context(DataFusionPlanningSnafu)?,
3829            ),
3830        });
3831
3832        Ok(absent_plan)
3833    }
3834
3835    /// Try to build a DataFusion Literal Expression from PromQL Expr, return
3836    /// `None` if the input is not a literal expression.
3837    fn try_build_literal_expr(expr: &PromExpr) -> Option<DfExpr> {
3838        match expr {
3839            PromExpr::NumberLiteral(NumberLiteral { val }) => Some(val.lit()),
3840            PromExpr::StringLiteral(StringLiteral { val }) => Some(val.lit()),
3841            PromExpr::VectorSelector(_)
3842            | PromExpr::MatrixSelector(_)
3843            | PromExpr::Extension(_)
3844            | PromExpr::Aggregate(_)
3845            | PromExpr::Subquery(_) => None,
3846            PromExpr::Call(Call { func, .. }) => {
3847                if func.name == SPECIAL_TIME_FUNCTION {
3848                    // For time() function, don't treat it as a literal
3849                    // Let it be handled as a regular function call
3850                    None
3851                } else {
3852                    None
3853                }
3854            }
3855            PromExpr::Paren(ParenExpr { expr }) => Self::try_build_literal_expr(expr),
3856            // TODO(ruihang): support Unary operator
3857            PromExpr::Unary(UnaryExpr { expr, .. }) => Self::try_build_literal_expr(expr),
3858            PromExpr::Binary(PromBinaryExpr {
3859                lhs,
3860                rhs,
3861                op,
3862                modifier,
3863            }) => {
3864                let lhs = Self::try_build_literal_expr(lhs)?;
3865                let rhs = Self::try_build_literal_expr(rhs)?;
3866                let is_comparison_op = Self::is_token_a_comparison_op(*op);
3867                let expr_builder = Self::prom_token_to_binary_expr_builder(*op).ok()?;
3868                let expr = expr_builder(lhs, rhs).ok()?;
3869
3870                let should_return_bool = if let Some(m) = modifier {
3871                    m.return_bool
3872                } else {
3873                    false
3874                };
3875                if is_comparison_op && should_return_bool {
3876                    Some(DfExpr::Cast(Cast {
3877                        expr: Box::new(expr),
3878                        data_type: ArrowDataType::Float64,
3879                    }))
3880                } else {
3881                    Some(expr)
3882                }
3883            }
3884        }
3885    }
3886
3887    fn try_build_special_time_expr_with_context(&self, expr: &PromExpr) -> Option<DfExpr> {
3888        match expr {
3889            PromExpr::Call(Call { func, .. }) => {
3890                if func.name == SPECIAL_TIME_FUNCTION
3891                    && let Some(time_index_col) = self.ctx.time_index_column.as_ref()
3892                {
3893                    Some(build_special_time_expr(time_index_col))
3894                } else {
3895                    None
3896                }
3897            }
3898            _ => None,
3899        }
3900    }
3901
3902    /// Return a lambda to build binary expression from token.
3903    /// Because some binary operator are function in DataFusion like `atan2` or `^`.
3904    #[allow(clippy::type_complexity)]
3905    fn prom_token_to_binary_expr_builder(
3906        token: TokenType,
3907    ) -> Result<Box<dyn Fn(DfExpr, DfExpr) -> Result<DfExpr>>> {
3908        let cast_float = |expr| {
3909            if matches!(
3910                &expr,
3911                DfExpr::Cast(Cast {
3912                    data_type: ArrowDataType::Float64,
3913                    ..
3914                })
3915            ) || matches!(&expr, DfExpr::Literal(ScalarValue::Float64(_), _))
3916            {
3917                expr
3918            } else {
3919                DfExpr::Cast(Cast {
3920                    expr: Box::new(expr),
3921                    data_type: ArrowDataType::Float64,
3922                })
3923            }
3924        };
3925        match token.id() {
3926            token::T_ADD => Ok(Box::new(move |lhs, rhs| {
3927                Ok(cast_float(lhs) + cast_float(rhs))
3928            })),
3929            token::T_SUB => Ok(Box::new(move |lhs, rhs| {
3930                Ok(cast_float(lhs) - cast_float(rhs))
3931            })),
3932            token::T_MUL => Ok(Box::new(move |lhs, rhs| {
3933                Ok(cast_float(lhs) * cast_float(rhs))
3934            })),
3935            token::T_DIV => Ok(Box::new(move |lhs, rhs| {
3936                Ok(cast_float(lhs) / cast_float(rhs))
3937            })),
3938            token::T_MOD => Ok(Box::new(move |lhs: DfExpr, rhs| {
3939                Ok(cast_float(lhs) % cast_float(rhs))
3940            })),
3941            token::T_EQLC => Ok(Box::new(|lhs, rhs| Ok(lhs.eq(rhs)))),
3942            token::T_NEQ => Ok(Box::new(|lhs, rhs| Ok(lhs.not_eq(rhs)))),
3943            token::T_GTR => Ok(Box::new(|lhs, rhs| Ok(lhs.gt(rhs)))),
3944            token::T_LSS => Ok(Box::new(|lhs, rhs| Ok(lhs.lt(rhs)))),
3945            token::T_GTE => Ok(Box::new(|lhs, rhs| Ok(lhs.gt_eq(rhs)))),
3946            token::T_LTE => Ok(Box::new(|lhs, rhs| Ok(lhs.lt_eq(rhs)))),
3947            token::T_POW => Ok(Box::new(move |lhs, rhs| {
3948                Ok(DfExpr::ScalarFunction(ScalarFunction {
3949                    func: datafusion_functions::math::power(),
3950                    args: vec![cast_float(lhs), cast_float(rhs)],
3951                }))
3952            })),
3953            token::T_ATAN2 => Ok(Box::new(move |lhs, rhs| {
3954                Ok(DfExpr::ScalarFunction(ScalarFunction {
3955                    func: datafusion_functions::math::atan2(),
3956                    args: vec![cast_float(lhs), cast_float(rhs)],
3957                }))
3958            })),
3959            _ => UnexpectedTokenSnafu { token }.fail(),
3960        }
3961    }
3962
3963    /// Check if the given op is a [comparison operator](https://prometheus.io/docs/prometheus/latest/querying/operators/#comparison-binary-operators).
3964    fn is_token_a_comparison_op(token: TokenType) -> bool {
3965        matches!(
3966            token.id(),
3967            token::T_EQLC
3968                | token::T_NEQ
3969                | token::T_GTR
3970                | token::T_LSS
3971                | token::T_GTE
3972                | token::T_LTE
3973        )
3974    }
3975
3976    /// Check if the given op is a set operator (UNION, INTERSECT and EXCEPT in SQL).
3977    fn is_token_a_set_op(token: TokenType) -> bool {
3978        matches!(
3979            token.id(),
3980            token::T_LAND // INTERSECT
3981                | token::T_LOR // UNION
3982                | token::T_LUNLESS // EXCEPT
3983        )
3984    }
3985
3986    fn align_binary_field_columns<'a>(
3987        left_field_columns: &'a [String],
3988        right_field_columns: &'a [String],
3989    ) -> (Vec<String>, Vec<(&'a String, &'a String)>) {
3990        let field_pairs = left_field_columns
3991            .iter()
3992            .zip(right_field_columns.iter())
3993            .collect::<Vec<_>>();
3994        let output_field_columns = field_pairs
3995            .iter()
3996            .map(|(left_col_name, _)| (*left_col_name).clone())
3997            .collect();
3998        (output_field_columns, field_pairs)
3999    }
4000
4001    fn plan_has_tsid_column(plan: &LogicalPlan) -> bool {
4002        plan.schema()
4003            .fields()
4004            .iter()
4005            .any(|field| field.name() == DATA_SCHEMA_TSID_COLUMN_NAME)
4006    }
4007
4008    fn optional_tsid_projection(
4009        schema: &DFSchemaRef,
4010        table_ref: Option<&TableReference>,
4011        keep_tsid: bool,
4012    ) -> Option<DfExpr> {
4013        keep_tsid.then_some(()).and_then(|_| {
4014            schema
4015                .qualified_field_with_name(table_ref, DATA_SCHEMA_TSID_COLUMN_NAME)
4016                .ok()
4017                .map(|field| DfExpr::Column(field.into()))
4018        })
4019    }
4020
4021    fn binary_join_key_columns(
4022        &self,
4023        left_schema: &DFSchemaRef,
4024        right_schema: &DFSchemaRef,
4025        left_context: &PromPlannerContext,
4026        right_context: &PromPlannerContext,
4027        only_join_time_index: bool,
4028        modifier: &Option<BinModifier>,
4029    ) -> Result<(BTreeSet<String>, BTreeSet<String>, bool)> {
4030        let has_tsid = |schema: &DFSchemaRef| {
4031            schema
4032                .fields()
4033                .iter()
4034                .any(|field| field.name() == DATA_SCHEMA_TSID_COLUMN_NAME)
4035        };
4036        let use_tsid_join = !only_join_time_index
4037            && self.binary_modifier_preserves_tsid_join_key(left_context, right_context, modifier)
4038            && left_context.use_tsid
4039            && right_context.use_tsid
4040            && has_tsid(left_schema)
4041            && has_tsid(right_schema);
4042
4043        let (mut left_tag_columns, mut right_tag_columns) = if use_tsid_join {
4044            (
4045                BTreeSet::from([DATA_SCHEMA_TSID_COLUMN_NAME.to_string()]),
4046                BTreeSet::from([DATA_SCHEMA_TSID_COLUMN_NAME.to_string()]),
4047            )
4048        } else {
4049            if only_join_time_index {
4050                (BTreeSet::new(), BTreeSet::new())
4051            } else {
4052                (
4053                    left_context
4054                        .tag_columns
4055                        .iter()
4056                        .cloned()
4057                        .collect::<BTreeSet<_>>(),
4058                    right_context
4059                        .tag_columns
4060                        .iter()
4061                        .cloned()
4062                        .collect::<BTreeSet<_>>(),
4063                )
4064            }
4065        };
4066
4067        if !use_tsid_join
4068            && let Some(modifier) = modifier
4069            && let Some(matching) = &modifier.matching
4070        {
4071            match matching {
4072                LabelModifier::Include(on) => {
4073                    let mask = on.labels.iter().cloned().collect::<BTreeSet<_>>();
4074                    left_tag_columns = left_tag_columns.intersection(&mask).cloned().collect();
4075                    right_tag_columns = right_tag_columns.intersection(&mask).cloned().collect();
4076                }
4077                LabelModifier::Exclude(ignoring) => {
4078                    for label in &ignoring.labels {
4079                        let _ = left_tag_columns.remove(label);
4080                        let _ = right_tag_columns.remove(label);
4081                    }
4082                }
4083            }
4084        }
4085
4086        let force_empty_join =
4087            !use_tsid_join && !only_join_time_index && left_tag_columns != right_tag_columns;
4088        if force_empty_join {
4089            let common_tag_columns = left_tag_columns
4090                .intersection(&right_tag_columns)
4091                .cloned()
4092                .collect::<BTreeSet<_>>();
4093            left_tag_columns = common_tag_columns.clone();
4094            right_tag_columns = common_tag_columns;
4095        }
4096
4097        Ok((left_tag_columns, right_tag_columns, force_empty_join))
4098    }
4099
4100    fn binary_modifier_preserves_tsid_join_key(
4101        &self,
4102        left_context: &PromPlannerContext,
4103        right_context: &PromPlannerContext,
4104        modifier: &Option<BinModifier>,
4105    ) -> bool {
4106        let Some(modifier) = modifier else {
4107            return true;
4108        };
4109
4110        if !matches!(modifier.card, VectorMatchCardinality::OneToOne) {
4111            return false;
4112        }
4113
4114        match &modifier.matching {
4115            None => true,
4116            Some(LabelModifier::Exclude(ignoring)) => ignoring.labels.iter().all(|label| {
4117                !left_context.tag_columns.contains(label)
4118                    && !right_context.tag_columns.contains(label)
4119            }),
4120            Some(LabelModifier::Include(on)) => {
4121                let on_labels = on.labels.iter().cloned().collect::<BTreeSet<_>>();
4122                let left_labels = left_context
4123                    .tag_columns
4124                    .iter()
4125                    .cloned()
4126                    .collect::<BTreeSet<_>>();
4127                let right_labels = right_context
4128                    .tag_columns
4129                    .iter()
4130                    .cloned()
4131                    .collect::<BTreeSet<_>>();
4132
4133                on_labels == left_labels && on_labels == right_labels
4134            }
4135        }
4136    }
4137
4138    /// Build a inner join on time index column and tag columns to concat two logical plans.
4139    /// When `only_join_time_index == true` we only join on the time index, because these two plan may not have the same tag columns
4140    #[allow(clippy::too_many_arguments)]
4141    fn join_on_non_field_columns(
4142        &self,
4143        left: LogicalPlan,
4144        right: LogicalPlan,
4145        left_table_ref: TableReference,
4146        right_table_ref: TableReference,
4147        left_time_index_column: Option<String>,
4148        right_time_index_column: Option<String>,
4149        only_join_time_index: bool,
4150        modifier: &Option<BinModifier>,
4151        left_context: &PromPlannerContext,
4152        right_context: &PromPlannerContext,
4153    ) -> Result<LogicalPlan> {
4154        let (mut left_tag_columns, mut right_tag_columns, force_empty_join) = self
4155            .binary_join_key_columns(
4156                left.schema(),
4157                right.schema(),
4158                left_context,
4159                right_context,
4160                only_join_time_index,
4161                modifier,
4162            )?;
4163
4164        // push time index column if it exists
4165        if let (Some(left_time_index_column), Some(right_time_index_column)) =
4166            (left_time_index_column, right_time_index_column)
4167        {
4168            left_tag_columns.insert(left_time_index_column);
4169            right_tag_columns.insert(right_time_index_column);
4170        }
4171
4172        let right = LogicalPlanBuilder::from(right)
4173            .alias(right_table_ref)
4174            .context(DataFusionPlanningSnafu)?
4175            .build()
4176            .context(DataFusionPlanningSnafu)?;
4177
4178        // Inner Join on time index column to concat two operator
4179        LogicalPlanBuilder::from(left)
4180            .alias(left_table_ref)
4181            .context(DataFusionPlanningSnafu)?
4182            .join_detailed(
4183                right,
4184                JoinType::Inner,
4185                (
4186                    left_tag_columns
4187                        .into_iter()
4188                        .map(Column::from_name)
4189                        .collect::<Vec<_>>(),
4190                    right_tag_columns
4191                        .into_iter()
4192                        .map(Column::from_name)
4193                        .collect::<Vec<_>>(),
4194                ),
4195                force_empty_join.then_some(lit(false)),
4196                NullEquality::NullEqualsNull,
4197            )
4198            .context(DataFusionPlanningSnafu)?
4199            .build()
4200            .context(DataFusionPlanningSnafu)
4201    }
4202
4203    /// Build a set operator (AND/OR/UNLESS)
4204    fn set_op_on_non_field_columns(
4205        &mut self,
4206        left: LogicalPlan,
4207        mut right: LogicalPlan,
4208        left_context: PromPlannerContext,
4209        right_context: PromPlannerContext,
4210        op: TokenType,
4211        modifier: &Option<BinModifier>,
4212    ) -> Result<LogicalPlan> {
4213        let mut left_tag_col_set = left_context
4214            .tag_columns
4215            .iter()
4216            .cloned()
4217            .collect::<HashSet<_>>();
4218        let mut right_tag_col_set = right_context
4219            .tag_columns
4220            .iter()
4221            .cloned()
4222            .collect::<HashSet<_>>();
4223
4224        if matches!(op.id(), token::T_LOR) {
4225            return self.or_operator(
4226                left,
4227                right,
4228                left_tag_col_set,
4229                right_tag_col_set,
4230                left_context,
4231                right_context,
4232                modifier,
4233            );
4234        }
4235
4236        // apply modifier
4237        if let Some(modifier) = modifier {
4238            // one-to-many and many-to-one are not supported
4239            ensure!(
4240                matches!(
4241                    modifier.card,
4242                    VectorMatchCardinality::OneToOne | VectorMatchCardinality::ManyToMany
4243                ),
4244                UnsupportedVectorMatchSnafu {
4245                    name: modifier.card.clone(),
4246                },
4247            );
4248            // apply label modifier
4249            if let Some(matching) = &modifier.matching {
4250                match matching {
4251                    // keeps columns mentioned in `on`
4252                    LabelModifier::Include(on) => {
4253                        let mask = on.labels.iter().cloned().collect::<HashSet<_>>();
4254                        left_tag_col_set = left_tag_col_set.intersection(&mask).cloned().collect();
4255                        right_tag_col_set =
4256                            right_tag_col_set.intersection(&mask).cloned().collect();
4257                    }
4258                    // removes columns memtioned in `ignoring`
4259                    LabelModifier::Exclude(ignoring) => {
4260                        // doesn't check existence of label
4261                        for label in &ignoring.labels {
4262                            let _ = left_tag_col_set.remove(label);
4263                            let _ = right_tag_col_set.remove(label);
4264                        }
4265                    }
4266                }
4267            }
4268        }
4269        // ensure two sides have the same tag columns
4270        if !matches!(op.id(), token::T_LOR) {
4271            ensure!(
4272                left_tag_col_set == right_tag_col_set,
4273                CombineTableColumnMismatchSnafu {
4274                    left: left_tag_col_set.into_iter().collect::<Vec<_>>(),
4275                    right: right_tag_col_set.into_iter().collect::<Vec<_>>(),
4276                }
4277            )
4278        };
4279        let left_time_index = left_context.time_index_column.clone().unwrap();
4280        let right_time_index = right_context.time_index_column.clone().unwrap();
4281        let join_keys = left_tag_col_set
4282            .iter()
4283            .cloned()
4284            .chain([left_time_index.clone()])
4285            .collect::<Vec<_>>();
4286        self.ctx.time_index_column = Some(left_time_index.clone());
4287        self.ctx.use_tsid = left_context.use_tsid;
4288
4289        // alias right time index column if necessary
4290        if left_context.time_index_column != right_context.time_index_column {
4291            let right_project_exprs = right
4292                .schema()
4293                .fields()
4294                .iter()
4295                .map(|field| {
4296                    if field.name() == &right_time_index {
4297                        DfExpr::Column(Column::from_name(&right_time_index)).alias(&left_time_index)
4298                    } else {
4299                        DfExpr::Column(Column::from_name(field.name()))
4300                    }
4301                })
4302                .collect::<Vec<_>>();
4303
4304            right = LogicalPlanBuilder::from(right)
4305                .project(right_project_exprs)
4306                .context(DataFusionPlanningSnafu)?
4307                .build()
4308                .context(DataFusionPlanningSnafu)?;
4309        }
4310
4311        ensure!(
4312            left_context.field_columns.len() == 1,
4313            MultiFieldsNotSupportedSnafu {
4314                operator: "AND operator"
4315            }
4316        );
4317        // Update the field column in context.
4318        // The AND/UNLESS operator only keep the field column in left input.
4319        let left_field_col = left_context.field_columns.first().unwrap();
4320        self.ctx.field_columns = vec![left_field_col.clone()];
4321
4322        // Generate join plan.
4323        // All set operations in PromQL are "distinct"
4324        match op.id() {
4325            token::T_LAND => LogicalPlanBuilder::from(left)
4326                .distinct()
4327                .context(DataFusionPlanningSnafu)?
4328                .join_detailed(
4329                    right,
4330                    JoinType::LeftSemi,
4331                    (join_keys.clone(), join_keys),
4332                    None,
4333                    NullEquality::NullEqualsNull,
4334                )
4335                .context(DataFusionPlanningSnafu)?
4336                .build()
4337                .context(DataFusionPlanningSnafu),
4338            token::T_LUNLESS => LogicalPlanBuilder::from(left)
4339                .distinct()
4340                .context(DataFusionPlanningSnafu)?
4341                .join_detailed(
4342                    right,
4343                    JoinType::LeftAnti,
4344                    (join_keys.clone(), join_keys),
4345                    None,
4346                    NullEquality::NullEqualsNull,
4347                )
4348                .context(DataFusionPlanningSnafu)?
4349                .build()
4350                .context(DataFusionPlanningSnafu),
4351            token::T_LOR => {
4352                // OR is handled at the beginning of this function, as it cannot
4353                // be expressed using JOIN like AND and UNLESS.
4354                unreachable!()
4355            }
4356            _ => UnexpectedTokenSnafu { token: op }.fail(),
4357        }
4358    }
4359
4360    // TODO(ruihang): change function name
4361    #[allow(clippy::too_many_arguments)]
4362    fn or_operator(
4363        &mut self,
4364        left: LogicalPlan,
4365        right: LogicalPlan,
4366        left_tag_cols_set: HashSet<String>,
4367        right_tag_cols_set: HashSet<String>,
4368        left_context: PromPlannerContext,
4369        right_context: PromPlannerContext,
4370        modifier: &Option<BinModifier>,
4371    ) -> Result<LogicalPlan> {
4372        // checks
4373        ensure!(
4374            left_context.field_columns.len() == right_context.field_columns.len(),
4375            CombineTableColumnMismatchSnafu {
4376                left: left_context.field_columns.clone(),
4377                right: right_context.field_columns.clone()
4378            }
4379        );
4380        ensure!(
4381            left_context.field_columns.len() == 1,
4382            MultiFieldsNotSupportedSnafu {
4383                operator: "OR operator"
4384            }
4385        );
4386
4387        // prepare hash sets
4388        let all_tags = left_tag_cols_set
4389            .union(&right_tag_cols_set)
4390            .cloned()
4391            .collect::<HashSet<_>>();
4392        let tags_not_in_left = all_tags
4393            .difference(&left_tag_cols_set)
4394            .cloned()
4395            .collect::<Vec<_>>();
4396        let tags_not_in_right = all_tags
4397            .difference(&right_tag_cols_set)
4398            .cloned()
4399            .collect::<Vec<_>>();
4400        let left_qualifier = left.schema().qualified_field(0).0.cloned();
4401        let right_qualifier = right.schema().qualified_field(0).0.cloned();
4402        let left_qualifier_string = left_qualifier
4403            .as_ref()
4404            .map(|l| l.to_string())
4405            .unwrap_or_default();
4406        let right_qualifier_string = right_qualifier
4407            .as_ref()
4408            .map(|r| r.to_string())
4409            .unwrap_or_default();
4410        let left_time_index_column =
4411            left_context
4412                .time_index_column
4413                .clone()
4414                .with_context(|| TimeIndexNotFoundSnafu {
4415                    table: left_qualifier_string.clone(),
4416                })?;
4417        let right_time_index_column =
4418            right_context
4419                .time_index_column
4420                .clone()
4421                .with_context(|| TimeIndexNotFoundSnafu {
4422                    table: right_qualifier_string.clone(),
4423                })?;
4424        // Take the name of first field column. The length is checked above.
4425        let left_field_col = left_context.field_columns.first().unwrap();
4426        let right_field_col = right_context.field_columns.first().unwrap();
4427        let left_field = left
4428            .schema()
4429            .iter()
4430            .find(|(_, field)| field.name() == left_field_col)
4431            .map(|(qualifier, field)| (qualifier.cloned(), field.data_type().clone()))
4432            .with_context(|| ColumnNotFoundSnafu {
4433                col: left_field_col.clone(),
4434            })?;
4435        let right_field = right
4436            .schema()
4437            .iter()
4438            .find(|(_, field)| field.name() == right_field_col)
4439            .map(|(qualifier, field)| (qualifier.cloned(), field.data_type().clone()))
4440            .with_context(|| ColumnNotFoundSnafu {
4441                col: right_field_col.clone(),
4442            })?;
4443        let target_field_type = if left_field.1 == right_field.1 {
4444            left_field.1.clone()
4445        } else if matches!(
4446            left_field.1,
4447            ArrowDataType::Int8
4448                | ArrowDataType::Int16
4449                | ArrowDataType::Int32
4450                | ArrowDataType::Int64
4451                | ArrowDataType::UInt8
4452                | ArrowDataType::UInt16
4453                | ArrowDataType::UInt32
4454                | ArrowDataType::UInt64
4455                | ArrowDataType::Float32
4456                | ArrowDataType::Float64
4457        ) && matches!(
4458            right_field.1,
4459            ArrowDataType::Int8
4460                | ArrowDataType::Int16
4461                | ArrowDataType::Int32
4462                | ArrowDataType::Int64
4463                | ArrowDataType::UInt8
4464                | ArrowDataType::UInt16
4465                | ArrowDataType::UInt32
4466                | ArrowDataType::UInt64
4467                | ArrowDataType::Float32
4468                | ArrowDataType::Float64
4469        ) {
4470            ArrowDataType::Float64
4471        } else {
4472            return UnexpectedPlanExprSnafu {
4473                desc: format!(
4474                    "OR value fields have incompatible types: {:?} and {:?}",
4475                    left_field.1, right_field.1
4476                ),
4477            }
4478            .fail();
4479        };
4480        let left_has_tsid = left
4481            .schema()
4482            .fields()
4483            .iter()
4484            .any(|field| field.name() == DATA_SCHEMA_TSID_COLUMN_NAME);
4485        let right_has_tsid = right
4486            .schema()
4487            .fields()
4488            .iter()
4489            .any(|field| field.name() == DATA_SCHEMA_TSID_COLUMN_NAME);
4490
4491        // step 0: fill all columns in output schema
4492        let mut all_columns_set = left
4493            .schema()
4494            .fields()
4495            .iter()
4496            .chain(right.schema().fields().iter())
4497            .map(|field| field.name().clone())
4498            .collect::<HashSet<_>>();
4499        // Keep `__tsid` only when both sides contain it, otherwise it may break schema alignment
4500        // (e.g. `unknown_metric or some_metric`).
4501        if !(left_has_tsid && right_has_tsid) {
4502            all_columns_set.remove(DATA_SCHEMA_TSID_COLUMN_NAME);
4503        }
4504        // remove time index column
4505        all_columns_set.remove(&left_time_index_column);
4506        all_columns_set.remove(&right_time_index_column);
4507        // remove field column in the right
4508        if left_field_col != right_field_col {
4509            all_columns_set.remove(right_field_col);
4510        }
4511        let mut all_columns = all_columns_set.into_iter().collect::<Vec<_>>();
4512        // sort to ensure the generated schema is not volatile
4513        all_columns.sort_unstable();
4514        // use left time index column name as the result time index column name
4515        all_columns.insert(0, left_time_index_column.clone());
4516        let mut occupied_column_names = left
4517            .schema()
4518            .fields()
4519            .iter()
4520            .chain(right.schema().fields().iter())
4521            .map(|field| field.name().clone())
4522            .collect::<HashSet<_>>();
4523
4524        // step 1: align schema using project, fill non-exist columns with null
4525        let left_proj_exprs = all_columns.iter().map(|col| {
4526            if col == left_field_col && left_field.1 != target_field_type {
4527                DfExpr::Cast(Cast {
4528                    expr: Box::new(DfExpr::Column(Column::new(
4529                        left_field.0.clone(),
4530                        left_field_col,
4531                    ))),
4532                    data_type: target_field_type.clone(),
4533                })
4534                .alias(left_field_col.clone())
4535            } else if tags_not_in_left.contains(col) {
4536                DfExpr::Literal(ScalarValue::Utf8(None), None).alias(col.clone())
4537            } else {
4538                DfExpr::Column(Column::new(None::<String>, col))
4539            }
4540        });
4541        let right_time_index_expr = DfExpr::Column(Column::new(
4542            right_qualifier.clone(),
4543            right_time_index_column,
4544        ))
4545        .alias(left_time_index_column.clone());
4546        // The field column in right side may not have qualifier (it may be removed by join operation),
4547        // so we need to find it from the schema.
4548        // `skip(1)` to skip the time index column
4549        let right_proj_exprs_without_time_index = all_columns.iter().skip(1).map(|col| {
4550            // expr
4551            if col == left_field_col {
4552                let expr = DfExpr::Column(Column::new(right_field.0.clone(), right_field_col));
4553                if right_field.1 != target_field_type {
4554                    DfExpr::Cast(Cast {
4555                        expr: Box::new(expr),
4556                        data_type: target_field_type.clone(),
4557                    })
4558                    .alias(left_field_col.clone())
4559                } else if left_field_col != right_field_col {
4560                    expr.alias(left_field_col.clone())
4561                } else {
4562                    expr
4563                }
4564            } else if tags_not_in_right.contains(col) {
4565                DfExpr::Literal(ScalarValue::Utf8(None), None).alias(col.clone())
4566            } else {
4567                DfExpr::Column(Column::new(None::<String>, col))
4568            }
4569        });
4570        let right_proj_exprs = [right_time_index_expr]
4571            .into_iter()
4572            .chain(right_proj_exprs_without_time_index);
4573
4574        let left_projected = LogicalPlanBuilder::from(left)
4575            .project(left_proj_exprs)
4576            .context(DataFusionPlanningSnafu)?
4577            .alias(left_qualifier_string.clone())
4578            .context(DataFusionPlanningSnafu)?
4579            .build()
4580            .context(DataFusionPlanningSnafu)?;
4581        let right_projected = LogicalPlanBuilder::from(right)
4582            .project(right_proj_exprs)
4583            .context(DataFusionPlanningSnafu)?
4584            .alias(right_qualifier_string.clone())
4585            .context(DataFusionPlanningSnafu)?
4586            .build()
4587            .context(DataFusionPlanningSnafu)?;
4588
4589        // step 2: compute match columns
4590        let mut match_columns = if let Some(modifier) = modifier
4591            && let Some(matching) = &modifier.matching
4592        {
4593            match matching {
4594                // keeps columns mentioned in `on`
4595                LabelModifier::Include(on) => on.labels.clone(),
4596                // removes columns memtioned in `ignoring`
4597                LabelModifier::Exclude(ignoring) => {
4598                    let ignoring = ignoring.labels.iter().cloned().collect::<HashSet<_>>();
4599                    all_tags.difference(&ignoring).cloned().collect()
4600                }
4601            }
4602        } else {
4603            all_tags.iter().cloned().collect()
4604        };
4605        // sort to ensure the generated plan is not volatile
4606        match_columns.sort_unstable();
4607        match_columns.dedup();
4608        occupied_column_names.extend(
4609            left_projected
4610                .schema()
4611                .fields()
4612                .iter()
4613                .chain(right_projected.schema().fields().iter())
4614                .map(|field| field.name().clone()),
4615        );
4616
4617        let visible_schema = left_projected.schema().clone();
4618        let visible_left_exprs = left_projected
4619            .schema()
4620            .iter()
4621            .map(|(qualifier, field)| {
4622                DfExpr::Column(Column::new(qualifier.cloned(), field.name().clone()))
4623            })
4624            .collect::<Vec<_>>();
4625        let visible_right_exprs = right_projected
4626            .schema()
4627            .iter()
4628            .map(|(qualifier, field)| {
4629                DfExpr::Column(Column::new(qualifier.cloned(), field.name().clone()))
4630            })
4631            .collect::<Vec<_>>();
4632        let mut left_match_exprs = Vec::with_capacity(match_columns.len());
4633        let mut right_match_exprs = Vec::with_capacity(match_columns.len());
4634        let mut next_internal_column = 0;
4635
4636        for label in &match_columns {
4637            let left_field = if left_tag_cols_set.contains(label) {
4638                Some(
4639                    left_projected
4640                        .schema()
4641                        .iter()
4642                        .find(|(_, field)| field.name() == label)
4643                        .map(|(qualifier, field)| (qualifier.cloned(), field.data_type().clone()))
4644                        .with_context(|| ColumnNotFoundSnafu { col: label.clone() })?,
4645                )
4646            } else {
4647                None
4648            };
4649            let right_field = if right_tag_cols_set.contains(label) {
4650                Some(
4651                    right_projected
4652                        .schema()
4653                        .iter()
4654                        .find(|(_, field)| field.name() == label)
4655                        .map(|(qualifier, field)| (qualifier.cloned(), field.data_type().clone()))
4656                        .with_context(|| ColumnNotFoundSnafu { col: label.clone() })?,
4657                )
4658            } else {
4659                None
4660            };
4661            let data_type = match (left_field.as_ref(), right_field.as_ref()) {
4662                (Some((_, left_type)), Some((_, right_type))) if left_type == right_type => {
4663                    left_type.clone()
4664                }
4665                (Some((_, left_type)), Some((_, right_type))) => {
4666                    return UnexpectedPlanExprSnafu {
4667                        desc: format!(
4668                            "OR match label {label} has incompatible types: {left_type:?} and {right_type:?}"
4669                        ),
4670                    }
4671                    .fail();
4672                }
4673                (Some((_, data_type)), None) | (None, Some((_, data_type))) => data_type.clone(),
4674                (None, None) => ArrowDataType::Utf8,
4675            };
4676            let empty = match data_type {
4677                ArrowDataType::Utf8 => ScalarValue::Utf8(Some(String::new())),
4678                ArrowDataType::LargeUtf8 => ScalarValue::LargeUtf8(Some(String::new())),
4679                _ => {
4680                    return UnexpectedPlanExprSnafu {
4681                        desc: format!("OR match label {label} must be a string"),
4682                    }
4683                    .fail();
4684                }
4685            };
4686            let internal_name = loop {
4687                let name = format!("__promql_or_match_{next_internal_column}");
4688                next_internal_column += 1;
4689                if occupied_column_names.insert(name.clone()) {
4690                    break name;
4691                }
4692            };
4693            let normalize = |field: Option<(Option<TableReference>, ArrowDataType)>| {
4694                let expr = if let Some((qualifier, _)) = field {
4695                    DfExpr::ScalarFunction(ScalarFunction {
4696                        func: coalesce(),
4697                        args: vec![
4698                            DfExpr::Column(Column::new(qualifier, label.clone())),
4699                            DfExpr::Literal(empty.clone(), None),
4700                        ],
4701                    })
4702                } else {
4703                    DfExpr::Literal(empty.clone(), None)
4704                };
4705                expr.alias(internal_name.clone())
4706            };
4707            left_match_exprs.push(normalize(left_field));
4708            right_match_exprs.push(normalize(right_field));
4709        }
4710
4711        let left_augmented = LogicalPlanBuilder::from(left_projected)
4712            .project(visible_left_exprs.into_iter().chain(left_match_exprs))
4713            .context(DataFusionPlanningSnafu)?
4714            .build()
4715            .context(DataFusionPlanningSnafu)?;
4716        let right_augmented = LogicalPlanBuilder::from(right_projected)
4717            .project(visible_right_exprs.into_iter().chain(right_match_exprs))
4718            .context(DataFusionPlanningSnafu)?
4719            .build()
4720            .context(DataFusionPlanningSnafu)?;
4721
4722        // step 3: build `UnionDistinctOn` with normalized internal match keys.
4723        let visible_field_count = visible_schema.fields().len();
4724        let compare_key_indices =
4725            (visible_field_count..visible_field_count + match_columns.len()).collect::<Vec<_>>();
4726        let (time_qualifier, _) = visible_schema
4727            .iter()
4728            .find(|(_, field)| field.name() == &left_time_index_column)
4729            .with_context(|| TimeIndexNotFoundSnafu {
4730                table: left_qualifier_string.clone(),
4731            })?;
4732        let ts_col_idx = left_augmented
4733            .schema()
4734            .iter()
4735            .position(|(qualifier, field)| {
4736                qualifier == time_qualifier && field.name() == &left_time_index_column
4737            })
4738            .with_context(|| TimeIndexNotFoundSnafu {
4739                table: left_qualifier_string.clone(),
4740            })?;
4741        let union_distinct_on = UnionDistinctOn::try_new(
4742            left_augmented,
4743            right_augmented,
4744            compare_key_indices,
4745            ts_col_idx,
4746        )
4747        .context(DataFusionPlanningSnafu)?;
4748        let augmented_result = LogicalPlan::Extension(Extension {
4749            node: Arc::new(union_distinct_on),
4750        });
4751        let result = LogicalPlanBuilder::from(augmented_result)
4752            .project(visible_schema.iter().map(|(qualifier, field)| {
4753                DfExpr::Column(Column::new(qualifier.cloned(), field.name().clone()))
4754            }))
4755            .context(DataFusionPlanningSnafu)?
4756            .build()
4757            .context(DataFusionPlanningSnafu)?;
4758
4759        // step 4: update context
4760        let output_field_col = left_field_col.clone();
4761        let mut output_context = left_context;
4762        let mut visible_tags = all_tags.into_iter().collect::<Vec<_>>();
4763        visible_tags.sort_unstable();
4764        output_context.time_index_column = Some(left_time_index_column);
4765        output_context.tag_columns = visible_tags;
4766        output_context.field_columns = vec![output_field_col];
4767        output_context.use_tsid = left_has_tsid && right_has_tsid;
4768        self.ctx = output_context;
4769
4770        Ok(result)
4771    }
4772
4773    /// Build a projection that project and perform operation expr for every value columns.
4774    /// Non-value columns (tag and timestamp) will be preserved in the projection.
4775    ///
4776    /// # Side effect
4777    ///
4778    /// This function will update the value columns in the context. Those new column names
4779    /// don't contains qualifier.
4780    fn projection_for_each_field_column<F>(
4781        &mut self,
4782        input: LogicalPlan,
4783        name_to_expr: F,
4784    ) -> Result<LogicalPlan>
4785    where
4786        F: FnMut(&String) -> Result<DfExpr>,
4787    {
4788        let table_ref = self.ctx.table_name.clone().map(TableReference::bare);
4789        let non_field_columns_iter = self
4790            .ctx
4791            .tag_columns
4792            .iter()
4793            .chain(self.ctx.time_index_column.iter())
4794            .map(|col| Ok(DfExpr::Column(Column::new(table_ref.clone(), col))));
4795        let tsid_iter =
4796            Self::optional_tsid_projection(input.schema(), table_ref.as_ref(), self.ctx.use_tsid)
4797                .into_iter()
4798                .map(Ok);
4799
4800        // build computation exprs
4801        let result_field_columns = self
4802            .ctx
4803            .field_columns
4804            .iter()
4805            .map(name_to_expr)
4806            .collect::<Result<Vec<_>>>()?;
4807
4808        // alias the computation exprs to remove qualifier
4809        self.ctx.field_columns = result_field_columns
4810            .iter()
4811            .map(|expr| expr.schema_name().to_string())
4812            .collect();
4813        let field_columns_iter = result_field_columns
4814            .into_iter()
4815            .zip(self.ctx.field_columns.iter())
4816            .map(|(expr, name)| Ok(DfExpr::Alias(Alias::new(expr, None::<String>, name))));
4817
4818        // chain non-field columns (unchanged) and field columns (applied computation then alias)
4819        let project_fields = non_field_columns_iter
4820            .chain(tsid_iter)
4821            .chain(field_columns_iter)
4822            .collect::<Result<Vec<_>>>()?;
4823
4824        LogicalPlanBuilder::from(input)
4825            .project(project_fields)
4826            .context(DataFusionPlanningSnafu)?
4827            .build()
4828            .context(DataFusionPlanningSnafu)
4829    }
4830
4831    /// Build a filter plan that filter on value column. Notice that only one value column
4832    /// is expected.
4833    fn filter_on_field_column<F>(
4834        &self,
4835        input: LogicalPlan,
4836        mut name_to_expr: F,
4837    ) -> Result<LogicalPlan>
4838    where
4839        F: FnMut(&String) -> Result<DfExpr>,
4840    {
4841        ensure!(
4842            self.ctx.field_columns.len() == 1,
4843            UnsupportedExprSnafu {
4844                name: "filter on multi-value input"
4845            }
4846        );
4847
4848        let field_column_filter = name_to_expr(&self.ctx.field_columns[0])?;
4849
4850        LogicalPlanBuilder::from(input)
4851            .filter(field_column_filter)
4852            .context(DataFusionPlanningSnafu)?
4853            .build()
4854            .context(DataFusionPlanningSnafu)
4855    }
4856
4857    /// Generate an expr like `date_part("hour", <TIME_INDEX>)`. Caller should ensure the
4858    /// time index column in context is set
4859    fn date_part_on_time_index(&self, date_part: &str) -> Result<DfExpr> {
4860        let input_expr = datafusion::logical_expr::col(
4861            self.ctx
4862                .time_index_column
4863                .as_ref()
4864                // table name doesn't matters here
4865                .with_context(|| TimeIndexNotFoundSnafu {
4866                    table: "<doesn't matter>",
4867                })?,
4868        );
4869        let fn_expr = DfExpr::ScalarFunction(ScalarFunction {
4870            func: datafusion_functions::datetime::date_part(),
4871            args: vec![date_part.lit(), input_expr],
4872        });
4873        Ok(fn_expr)
4874    }
4875
4876    fn strip_tsid_column(&self, plan: LogicalPlan) -> Result<LogicalPlan> {
4877        let schema = plan.schema();
4878        if !schema
4879            .fields()
4880            .iter()
4881            .any(|field| field.name() == DATA_SCHEMA_TSID_COLUMN_NAME)
4882        {
4883            return Ok(plan);
4884        }
4885
4886        // Preserve column qualifiers so downstream plan nodes can keep referencing
4887        // the columns by their original qualified names.
4888        let project_exprs = schema
4889            .iter()
4890            .filter(|(_, field)| field.name() != DATA_SCHEMA_TSID_COLUMN_NAME)
4891            .map(|(qualifier, field)| {
4892                DfExpr::Column(Column::new(qualifier.cloned(), field.name().clone()))
4893            })
4894            .collect::<Vec<_>>();
4895
4896        LogicalPlanBuilder::from(plan)
4897            .project(project_exprs)
4898            .context(DataFusionPlanningSnafu)?
4899            .build()
4900            .context(DataFusionPlanningSnafu)
4901    }
4902
4903    /// Apply an alias to the query result by adding a projection with the alias name
4904    fn apply_alias(&mut self, plan: LogicalPlan, alias_name: String) -> Result<LogicalPlan> {
4905        let fields_expr = self.create_field_column_exprs()?;
4906
4907        // TODO(dennis): how to support multi-value aliasing?
4908        ensure!(
4909            fields_expr.len() == 1,
4910            UnsupportedExprSnafu {
4911                name: "alias on multi-value result"
4912            }
4913        );
4914
4915        let project_fields = fields_expr
4916            .into_iter()
4917            .map(|expr| expr.alias(&alias_name))
4918            .chain(self.create_tag_column_exprs()?)
4919            .chain(Some(self.create_time_index_column_expr()?));
4920
4921        LogicalPlanBuilder::from(plan)
4922            .project(project_fields)
4923            .context(DataFusionPlanningSnafu)?
4924            .build()
4925            .context(DataFusionPlanningSnafu)
4926    }
4927}
4928
4929#[derive(Default, Debug)]
4930struct FunctionArgs {
4931    input: Option<PromExpr>,
4932    literals: Vec<DfExpr>,
4933}
4934
4935/// Represents different types of scalar functions supported in PromQL expressions.
4936/// Each variant defines how the function should be processed and what arguments it expects.
4937#[derive(Debug, Clone)]
4938enum ScalarFunc {
4939    /// DataFusion's registered(including built-in) scalar functions (e.g., abs, sqrt, round, clamp).
4940    /// These are passed through directly to DataFusion's execution engine.
4941    /// Processing: Simple argument insertion at the specified position.
4942    DataFusionBuiltin(Arc<ScalarUdfDef>),
4943    /// User-defined functions registered in DataFusion's function registry.
4944    /// Similar to DataFusionBuiltin but for custom functions not built into DataFusion.
4945    /// Processing: Direct pass-through with argument positioning.
4946    DataFusionUdf(Arc<ScalarUdfDef>),
4947    /// PromQL-specific functions that operate on time series data with temporal context.
4948    /// These functions require both timestamp ranges and values to perform calculations.
4949    /// Processing: Automatically injects timestamp_range and value columns as first arguments.
4950    /// Examples: idelta, irate, resets, changes, deriv, *_over_time function
4951    Udf(Arc<ScalarUdfDef>),
4952    /// PromQL functions requiring extrapolation calculations with explicit range information.
4953    /// These functions need to know the time range length to perform rate calculations.
4954    /// The second field contains the range length in milliseconds.
4955    /// Processing: Injects timestamp_range, value, time_index columns and appends range_length.
4956    /// Examples: increase, rate, delta
4957    // TODO(ruihang): maybe merge with Udf later
4958    ExtrapolateUdf(Arc<ScalarUdfDef>, i64),
4959    /// Functions that generate expressions directly without external UDF calls.
4960    /// The expression is constructed during function matching and requires no additional processing.
4961    /// Examples: time(), minute(), hour(), month(), year() and other date/time extractors
4962    GeneratedExpr,
4963}
4964
4965#[cfg(test)]
4966mod test {
4967    use std::time::{Duration, UNIX_EPOCH};
4968
4969    use catalog::RegisterTableRequest;
4970    use catalog::memory::{MemoryCatalogManager, new_memory_catalog_manager};
4971    use common_base::Plugins;
4972    use common_catalog::consts::{DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME};
4973    use common_query::prelude::greptime_timestamp;
4974    use common_query::test_util::DummyDecoder;
4975    use datafusion::arrow::array::{
4976        Array, Float64Array, Int64Array, StringArray, TimestampMillisecondArray,
4977    };
4978    use datafusion::arrow::datatypes::{Field, Schema as ArrowSchema};
4979    use datafusion::arrow::record_batch::RecordBatch;
4980    use datafusion::catalog::{CatalogProvider, MemoryCatalogProvider, MemorySchemaProvider};
4981    use datafusion::datasource::memory::MemorySourceConfig;
4982    use datafusion::datasource::source::DataSourceExec;
4983    use datafusion::datasource::{MemTable, provider_as_source};
4984    use datafusion::execution::context::SessionContext;
4985    use datafusion::logical_expr::Extension;
4986    use datatypes::prelude::ConcreteDataType;
4987    use datatypes::schema::{ColumnSchema, Schema};
4988    use promql_parser::label::Labels;
4989    use promql_parser::parser;
4990    use session::context::QueryContext;
4991    use substrait::{DFLogicalSubstraitConvertor, SubstraitPlan};
4992    use table::metadata::{TableInfoBuilder, TableMetaBuilder};
4993    use table::test_util::EmptyTable;
4994
4995    use super::*;
4996    use crate::QueryEngineContext;
4997    use crate::options::QueryOptions;
4998    use crate::parser::QueryLanguageParser;
4999    use crate::query_engine::DefaultSerializer;
5000
5001    fn find_instant_manipulate(plan: &LogicalPlan) -> Option<&InstantManipulate> {
5002        if let LogicalPlan::Extension(Extension { node }) = plan
5003            && let Some(instant_manipulate) = node.as_any().downcast_ref::<InstantManipulate>()
5004        {
5005            return Some(instant_manipulate);
5006        }
5007
5008        plan.inputs().into_iter().find_map(find_instant_manipulate)
5009    }
5010
5011    fn build_query_engine_state() -> QueryEngineState {
5012        QueryEngineState::new(
5013            new_memory_catalog_manager().unwrap(),
5014            None,
5015            None,
5016            None,
5017            None,
5018            None,
5019            false,
5020            Plugins::default(),
5021            QueryOptions::default(),
5022        )
5023    }
5024
5025    async fn build_optimized_promql_plan(
5026        table_provider: DfTableSourceProvider,
5027        eval_stmt: &EvalStmt,
5028    ) -> LogicalPlan {
5029        let state = build_query_engine_state();
5030        let raw_plan = PromPlanner::stmt_to_plan(table_provider, eval_stmt, &state)
5031            .await
5032            .unwrap();
5033        let context = QueryEngineContext::new(state.session_state(), QueryContext::arc());
5034        state
5035            .optimize_by_extension_rules(raw_plan, &context)
5036            .unwrap()
5037    }
5038
5039    async fn build_optimized_tsid_plan(
5040        query: &str,
5041        num_tag: usize,
5042        num_field: usize,
5043        end_secs: u64,
5044        lookback_secs: u64,
5045    ) -> String {
5046        let eval_stmt = EvalStmt {
5047            expr: parser::parse(query).unwrap(),
5048            start: UNIX_EPOCH,
5049            end: UNIX_EPOCH
5050                .checked_add(Duration::from_secs(end_secs))
5051                .unwrap(),
5052            interval: Duration::from_secs(5),
5053            lookback_delta: Duration::from_secs(lookback_secs),
5054        };
5055        let table_provider = build_test_table_provider_with_tsid(
5056            &[(DEFAULT_SCHEMA_NAME.to_string(), "some_metric".to_string())],
5057            num_tag,
5058            num_field,
5059        )
5060        .await;
5061
5062        build_optimized_promql_plan(table_provider, &eval_stmt)
5063            .await
5064            .display_indent_schema()
5065            .to_string()
5066    }
5067
5068    async fn assert_nested_count_rewrite_applies(query: &str, expected_outer_agg: &str) {
5069        let plan_str = build_optimized_tsid_plan(query, 2, 1, 100_000, 1).await;
5070
5071        assert!(plan_str.contains("PromSeriesDivide: tags=[\"__tsid\"]"));
5072        assert!(plan_str.contains("Projection: some_metric.timestamp, some_metric.tag_0"));
5073        assert!(plan_str.contains("Distinct:"));
5074        assert!(plan_str.contains(expected_outer_agg), "{plan_str}");
5075        assert!(!plan_str.contains("PromSeriesDivide: tags=[\"tag_0\"]"));
5076    }
5077
5078    async fn assert_nested_count_rewrite_missing(query: &str, num_tag: usize, lookback_secs: u64) {
5079        let plan_str = build_optimized_tsid_plan(query, num_tag, 1, 100_000, lookback_secs).await;
5080        assert!(!plan_str.contains("Distinct:"), "{plan_str}");
5081    }
5082
5083    fn build_eval_stmt(expr: &str) -> EvalStmt {
5084        EvalStmt {
5085            expr: parser::parse(expr).unwrap(),
5086            start: UNIX_EPOCH,
5087            end: UNIX_EPOCH
5088                .checked_add(Duration::from_secs(100_000))
5089                .unwrap(),
5090            interval: Duration::from_secs(5),
5091            lookback_delta: Duration::from_secs(1),
5092        }
5093    }
5094
5095    enum DirectOrValue {
5096        Float64(f64),
5097        Int64(i64),
5098        Utf8(&'static str),
5099    }
5100
5101    impl DirectOrValue {
5102        fn data_type(&self) -> ArrowDataType {
5103            match self {
5104                Self::Float64(_) => ArrowDataType::Float64,
5105                Self::Int64(_) => ArrowDataType::Int64,
5106                Self::Utf8(_) => ArrowDataType::Utf8,
5107            }
5108        }
5109        fn array(&self) -> Arc<dyn Array> {
5110            match self {
5111                Self::Float64(v) => Arc::new(Float64Array::from(vec![*v])),
5112                Self::Int64(v) => Arc::new(Int64Array::from(vec![*v])),
5113                Self::Utf8(v) => Arc::new(StringArray::from(vec![*v])),
5114            }
5115        }
5116    }
5117
5118    struct DirectOrSource {
5119        name: &'static str,
5120        empty: bool,
5121        timestamp: i64,
5122        tags: Vec<(&'static str, Option<&'static str>)>,
5123        value: DirectOrValue,
5124    }
5125
5126    fn source(
5127        name: &'static str,
5128        empty: bool,
5129        timestamp: i64,
5130        tags: Vec<(&'static str, Option<&'static str>)>,
5131        value: DirectOrValue,
5132    ) -> DirectOrSource {
5133        DirectOrSource {
5134            name,
5135            empty,
5136            timestamp,
5137            tags,
5138            value,
5139        }
5140    }
5141
5142    fn tagged_source(
5143        name: &'static str,
5144        empty: bool,
5145        tag: (&'static str, Option<&'static str>),
5146        value: DirectOrValue,
5147    ) -> DirectOrSource {
5148        source(name, empty, 1, vec![("job", Some("job")), tag], value)
5149    }
5150
5151    fn job_source(name: &'static str, value: DirectOrValue) -> DirectOrSource {
5152        source(name, true, 1, vec![("job", Some("job"))], value)
5153    }
5154
5155    fn table(source: &DirectOrSource) -> Arc<MemTable> {
5156        let mut fields = vec![Field::new(
5157            "ts",
5158            ArrowDataType::Timestamp(ArrowTimeUnit::Millisecond, None),
5159            false,
5160        )];
5161        fields.extend(
5162            source
5163                .tags
5164                .iter()
5165                .map(|(name, _)| Field::new(*name, ArrowDataType::Utf8, true)),
5166        );
5167        fields.push(Field::new("v", source.value.data_type(), true));
5168        let schema = Arc::new(ArrowSchema::new(fields));
5169        let partitions = if source.empty {
5170            vec![vec![]]
5171        } else {
5172            let mut columns: Vec<Arc<dyn Array>> =
5173                vec![Arc::new(TimestampMillisecondArray::from(vec![
5174                    source.timestamp,
5175                ]))];
5176            columns.extend(
5177                source
5178                    .tags
5179                    .iter()
5180                    .map(|(_, value)| Arc::new(StringArray::from(vec![*value])) as Arc<dyn Array>),
5181            );
5182            columns.push(source.value.array());
5183            vec![vec![RecordBatch::try_new(schema.clone(), columns).unwrap()]]
5184        };
5185        Arc::new(MemTable::try_new(schema, partitions).unwrap())
5186    }
5187
5188    fn scan(source: &DirectOrSource) -> LogicalPlan {
5189        LogicalPlanBuilder::scan(source.name, provider_as_source(table(source)), None)
5190            .unwrap()
5191            .build()
5192            .unwrap()
5193    }
5194
5195    fn direct_or_context(qualifier: &str, tags: &[&str], field: &str) -> PromPlannerContext {
5196        PromPlannerContext {
5197            table_name: Some(qualifier.to_string()),
5198            time_index_column: Some("ts".to_string()),
5199            field_columns: vec![field.to_string()],
5200            tag_columns: tags.iter().map(|tag| (*tag).to_string()).collect(),
5201            ..Default::default()
5202        }
5203    }
5204
5205    fn or_modifier(expr: &str) -> Option<BinModifier> {
5206        let PromExpr::Binary(expr) = parser::parse(expr).unwrap() else {
5207            unreachable!()
5208        };
5209        expr.modifier
5210    }
5211
5212    async fn plan_direct_or(
5213        left: LogicalPlan,
5214        right: LogicalPlan,
5215        left_context: PromPlannerContext,
5216        right_context: PromPlannerContext,
5217        modifier: &Option<BinModifier>,
5218    ) -> LogicalPlan {
5219        let table_provider = build_test_table_provider_with_fields(
5220            &[(DEFAULT_SCHEMA_NAME.to_string(), "dummy".to_string())],
5221            &[],
5222        )
5223        .await;
5224        let mut planner = PromPlanner {
5225            table_provider,
5226            ctx: PromPlannerContext::default(),
5227        };
5228        planner
5229            .or_operator(
5230                left,
5231                right,
5232                left_context.tag_columns.iter().cloned().collect(),
5233                right_context.tag_columns.iter().cloned().collect(),
5234                left_context,
5235                right_context,
5236                modifier,
5237            )
5238            .unwrap()
5239    }
5240
5241    async fn execute(
5242        plan: LogicalPlan,
5243        state: &QueryEngineState,
5244    ) -> (LogicalPlan, Vec<RecordBatch>) {
5245        let context = QueryEngineContext::new(state.session_state(), QueryContext::arc());
5246        let optimized = state.optimize_by_extension_rules(plan, &context).unwrap();
5247        let physical = state
5248            .session_state()
5249            .create_physical_plan(&optimized)
5250            .await
5251            .unwrap();
5252        let batches =
5253            datafusion::physical_plan::collect(physical, state.session_state().task_ctx())
5254                .await
5255                .unwrap();
5256        (optimized, batches)
5257    }
5258
5259    async fn run(
5260        left: &DirectOrSource,
5261        right: &DirectOrSource,
5262        left_context: PromPlannerContext,
5263        right_context: PromPlannerContext,
5264        modifier: &Option<BinModifier>,
5265    ) -> (LogicalPlan, Vec<RecordBatch>) {
5266        let plan = plan_direct_or(
5267            scan(left),
5268            scan(right),
5269            left_context,
5270            right_context,
5271            modifier,
5272        )
5273        .await;
5274        execute(plan, &build_query_engine_state()).await
5275    }
5276
5277    fn assert_no_internal_or_keys(schema: &DFSchema) {
5278        assert!(
5279            schema
5280                .fields()
5281                .iter()
5282                .all(|field| !field.name().starts_with("__promql_or_match_")),
5283            "{schema:?}"
5284        );
5285    }
5286
5287    fn values(batches: &[RecordBatch], column: &str) -> Vec<f64> {
5288        batches
5289            .iter()
5290            .flat_map(|batch| {
5291                batch
5292                    .column_by_name(column)
5293                    .unwrap()
5294                    .as_any()
5295                    .downcast_ref::<Float64Array>()
5296                    .unwrap()
5297                    .iter()
5298                    .flatten()
5299            })
5300            .collect()
5301    }
5302
5303    fn rows(batches: &[RecordBatch]) -> Vec<(f64, Option<String>)> {
5304        let mut rows = batches
5305            .iter()
5306            .flat_map(|batch| {
5307                let values = batch
5308                    .column_by_name("v")
5309                    .unwrap()
5310                    .as_any()
5311                    .downcast_ref::<Float64Array>()
5312                    .unwrap();
5313                let labels = batch
5314                    .column_by_name("k")
5315                    .map(|column| column.as_any().downcast_ref::<StringArray>().unwrap());
5316                (0..batch.num_rows()).map(move |i| {
5317                    (
5318                        values.value(i),
5319                        labels.and_then(|labels| {
5320                            (!labels.is_null(i)).then(|| labels.value(i).to_string())
5321                        }),
5322                    )
5323                })
5324            })
5325            .collect::<Vec<_>>();
5326        rows.sort_by(|left, right| left.0.total_cmp(&right.0));
5327        rows
5328    }
5329
5330    fn matrix_source(
5331        name: &'static str,
5332        k: Option<Option<&'static str>>,
5333        timestamp: i64,
5334        value: f64,
5335    ) -> DirectOrSource {
5336        let mut tags = vec![("job", Some("job"))];
5337        if let Some(k) = k {
5338            tags.push(("k", k));
5339        }
5340        source(name, false, timestamp, tags, DirectOrValue::Float64(value))
5341    }
5342
5343    fn matrix_context(name: &str, k: Option<Option<&str>>) -> PromPlannerContext {
5344        direct_or_context(
5345            name,
5346            if k.is_some() { &["job", "k"] } else { &["job"] },
5347            "v",
5348        )
5349    }
5350
5351    async fn build_test_table_provider(
5352        table_name_tuples: &[(String, String)],
5353        num_tag: usize,
5354        num_field: usize,
5355    ) -> DfTableSourceProvider {
5356        let catalog_list = MemoryCatalogManager::with_default_setup();
5357        for (schema_name, table_name) in table_name_tuples {
5358            let mut columns = vec![];
5359            for i in 0..num_tag {
5360                columns.push(ColumnSchema::new(
5361                    format!("tag_{i}"),
5362                    ConcreteDataType::string_datatype(),
5363                    false,
5364                ));
5365            }
5366            columns.push(
5367                ColumnSchema::new(
5368                    "timestamp".to_string(),
5369                    ConcreteDataType::timestamp_millisecond_datatype(),
5370                    false,
5371                )
5372                .with_time_index(true),
5373            );
5374            for i in 0..num_field {
5375                columns.push(ColumnSchema::new(
5376                    format!("field_{i}"),
5377                    ConcreteDataType::float64_datatype(),
5378                    true,
5379                ));
5380            }
5381            let schema = Arc::new(Schema::new(columns));
5382            let table_meta = TableMetaBuilder::empty()
5383                .schema(schema)
5384                .primary_key_indices((0..num_tag).collect())
5385                .value_indices((num_tag + 1..num_tag + 1 + num_field).collect())
5386                .next_column_id(1024)
5387                .build()
5388                .unwrap();
5389            let table_info = TableInfoBuilder::default()
5390                .name(table_name.clone())
5391                .meta(table_meta)
5392                .build()
5393                .unwrap();
5394            let table = EmptyTable::from_table_info(&table_info);
5395
5396            assert!(
5397                catalog_list
5398                    .register_table_sync(RegisterTableRequest {
5399                        catalog: DEFAULT_CATALOG_NAME.to_string(),
5400                        schema: schema_name.clone(),
5401                        table_name: table_name.clone(),
5402                        table_id: 1024,
5403                        table,
5404                    })
5405                    .is_ok()
5406            );
5407        }
5408
5409        DfTableSourceProvider::new(
5410            catalog_list,
5411            false,
5412            QueryContext::arc(),
5413            DummyDecoder::arc(),
5414            false,
5415        )
5416    }
5417
5418    async fn build_test_table_provider_with_tsid(
5419        table_name_tuples: &[(String, String)],
5420        num_tag: usize,
5421        num_field: usize,
5422    ) -> DfTableSourceProvider {
5423        let table_specs = table_name_tuples
5424            .iter()
5425            .map(|(schema_name, table_name)| ((schema_name.clone(), table_name.clone()), num_field))
5426            .collect::<Vec<_>>();
5427        build_test_table_provider_with_tsid_fields(&table_specs, num_tag).await
5428    }
5429
5430    async fn build_test_table_provider_with_tsid_fields(
5431        table_specs: &[((String, String), usize)],
5432        num_tag: usize,
5433    ) -> DfTableSourceProvider {
5434        let table_specs = table_specs
5435            .iter()
5436            .map(|(table_name_tuple, num_field)| (table_name_tuple.clone(), num_tag, *num_field))
5437            .collect::<Vec<_>>();
5438        build_test_table_provider_with_tsid_tag_fields(&table_specs).await
5439    }
5440
5441    async fn build_test_table_provider_with_tsid_tag_fields(
5442        table_specs: &[((String, String), usize, usize)],
5443    ) -> DfTableSourceProvider {
5444        let catalog_list = MemoryCatalogManager::with_default_setup();
5445
5446        let physical_table_name = "phy";
5447        let physical_table_id = 999u32;
5448        let physical_num_tag = table_specs
5449            .iter()
5450            .map(|(_, num_tag, _)| *num_tag)
5451            .max()
5452            .unwrap_or(0);
5453        let physical_num_field = table_specs
5454            .iter()
5455            .map(|(_, _, num_field)| *num_field)
5456            .max()
5457            .unwrap_or(0);
5458
5459        // Register a metric engine physical table with internal columns.
5460        {
5461            let mut columns = vec![
5462                ColumnSchema::new(
5463                    DATA_SCHEMA_TABLE_ID_COLUMN_NAME.to_string(),
5464                    ConcreteDataType::uint32_datatype(),
5465                    false,
5466                ),
5467                ColumnSchema::new(
5468                    DATA_SCHEMA_TSID_COLUMN_NAME.to_string(),
5469                    ConcreteDataType::uint64_datatype(),
5470                    false,
5471                ),
5472            ];
5473            for i in 0..physical_num_tag {
5474                columns.push(ColumnSchema::new(
5475                    format!("tag_{i}"),
5476                    ConcreteDataType::string_datatype(),
5477                    false,
5478                ));
5479            }
5480            columns.push(
5481                ColumnSchema::new(
5482                    "timestamp".to_string(),
5483                    ConcreteDataType::timestamp_millisecond_datatype(),
5484                    false,
5485                )
5486                .with_time_index(true),
5487            );
5488            for i in 0..physical_num_field {
5489                columns.push(ColumnSchema::new(
5490                    format!("field_{i}"),
5491                    ConcreteDataType::float64_datatype(),
5492                    true,
5493                ));
5494            }
5495
5496            let schema = Arc::new(Schema::new(columns));
5497            let primary_key_indices = (0..(2 + physical_num_tag)).collect::<Vec<_>>();
5498            let table_meta = TableMetaBuilder::empty()
5499                .schema(schema)
5500                .primary_key_indices(primary_key_indices)
5501                .value_indices(
5502                    (2 + physical_num_tag..2 + physical_num_tag + 1 + physical_num_field).collect(),
5503                )
5504                .engine(METRIC_ENGINE_NAME.to_string())
5505                .next_column_id(1024)
5506                .build()
5507                .unwrap();
5508            let table_info = TableInfoBuilder::default()
5509                .table_id(physical_table_id)
5510                .name(physical_table_name)
5511                .meta(table_meta)
5512                .build()
5513                .unwrap();
5514            let table = EmptyTable::from_table_info(&table_info);
5515
5516            assert!(
5517                catalog_list
5518                    .register_table_sync(RegisterTableRequest {
5519                        catalog: DEFAULT_CATALOG_NAME.to_string(),
5520                        schema: DEFAULT_SCHEMA_NAME.to_string(),
5521                        table_name: physical_table_name.to_string(),
5522                        table_id: physical_table_id,
5523                        table,
5524                    })
5525                    .is_ok()
5526            );
5527        }
5528
5529        // Register metric engine logical tables without `__tsid`, referencing the physical table.
5530        for (idx, ((schema_name, table_name), num_tag, num_field)) in table_specs.iter().enumerate()
5531        {
5532            let mut columns = vec![];
5533            for i in 0..*num_tag {
5534                columns.push(ColumnSchema::new(
5535                    format!("tag_{i}"),
5536                    ConcreteDataType::string_datatype(),
5537                    false,
5538                ));
5539            }
5540            columns.push(
5541                ColumnSchema::new(
5542                    "timestamp".to_string(),
5543                    ConcreteDataType::timestamp_millisecond_datatype(),
5544                    false,
5545                )
5546                .with_time_index(true),
5547            );
5548            for i in 0..*num_field {
5549                columns.push(ColumnSchema::new(
5550                    format!("field_{i}"),
5551                    ConcreteDataType::float64_datatype(),
5552                    true,
5553                ));
5554            }
5555
5556            let schema = Arc::new(Schema::new(columns));
5557            let mut options = table::requests::TableOptions::default();
5558            options.extra_options.insert(
5559                LOGICAL_TABLE_METADATA_KEY.to_string(),
5560                physical_table_name.to_string(),
5561            );
5562            let table_id = 1024u32 + idx as u32;
5563            let table_meta = TableMetaBuilder::empty()
5564                .schema(schema)
5565                .primary_key_indices((0..*num_tag).collect())
5566                .value_indices((*num_tag + 1..*num_tag + 1 + *num_field).collect())
5567                .engine(METRIC_ENGINE_NAME.to_string())
5568                .options(options)
5569                .next_column_id(1024)
5570                .build()
5571                .unwrap();
5572            let table_info = TableInfoBuilder::default()
5573                .table_id(table_id)
5574                .name(table_name.clone())
5575                .meta(table_meta)
5576                .build()
5577                .unwrap();
5578            let table = EmptyTable::from_table_info(&table_info);
5579
5580            assert!(
5581                catalog_list
5582                    .register_table_sync(RegisterTableRequest {
5583                        catalog: DEFAULT_CATALOG_NAME.to_string(),
5584                        schema: schema_name.clone(),
5585                        table_name: table_name.clone(),
5586                        table_id,
5587                        table,
5588                    })
5589                    .is_ok()
5590            );
5591        }
5592
5593        DfTableSourceProvider::new(
5594            catalog_list,
5595            false,
5596            QueryContext::arc(),
5597            DummyDecoder::arc(),
5598            false,
5599        )
5600    }
5601
5602    async fn build_test_table_provider_with_fields(
5603        table_name_tuples: &[(String, String)],
5604        tags: &[&str],
5605    ) -> DfTableSourceProvider {
5606        let catalog_list = MemoryCatalogManager::with_default_setup();
5607        for (schema_name, table_name) in table_name_tuples {
5608            let mut columns = vec![];
5609            let num_tag = tags.len();
5610            for tag in tags {
5611                columns.push(ColumnSchema::new(
5612                    tag.to_string(),
5613                    ConcreteDataType::string_datatype(),
5614                    false,
5615                ));
5616            }
5617            columns.push(
5618                ColumnSchema::new(
5619                    greptime_timestamp().to_string(),
5620                    ConcreteDataType::timestamp_millisecond_datatype(),
5621                    false,
5622                )
5623                .with_time_index(true),
5624            );
5625            columns.push(ColumnSchema::new(
5626                greptime_value().to_string(),
5627                ConcreteDataType::float64_datatype(),
5628                true,
5629            ));
5630            let schema = Arc::new(Schema::new(columns));
5631            let table_meta = TableMetaBuilder::empty()
5632                .schema(schema)
5633                .primary_key_indices((0..num_tag).collect())
5634                .next_column_id(1024)
5635                .build()
5636                .unwrap();
5637            let table_info = TableInfoBuilder::default()
5638                .name(table_name.clone())
5639                .meta(table_meta)
5640                .build()
5641                .unwrap();
5642            let table = EmptyTable::from_table_info(&table_info);
5643
5644            assert!(
5645                catalog_list
5646                    .register_table_sync(RegisterTableRequest {
5647                        catalog: DEFAULT_CATALOG_NAME.to_string(),
5648                        schema: schema_name.clone(),
5649                        table_name: table_name.clone(),
5650                        table_id: 1024,
5651                        table,
5652                    })
5653                    .is_ok()
5654            );
5655        }
5656
5657        DfTableSourceProvider::new(
5658            catalog_list,
5659            false,
5660            QueryContext::arc(),
5661            DummyDecoder::arc(),
5662            false,
5663        )
5664    }
5665
5666    // {
5667    //     input: `abs(some_metric{foo!="bar"})`,
5668    //     expected: &Call{
5669    //         Func: MustGetFunction("abs"),
5670    //         Args: Expressions{
5671    //             &VectorSelector{
5672    //                 Name: "some_metric",
5673    //                 LabelMatchers: []*labels.Matcher{
5674    //                     MustLabelMatcher(labels.MatchNotEqual, "foo", "bar"),
5675    //                     MustLabelMatcher(labels.MatchEqual, model.MetricNameLabel, "some_metric"),
5676    //                 },
5677    //             },
5678    //         },
5679    //     },
5680    // },
5681    async fn do_single_instant_function_call(fn_name: &'static str, plan_name: &str) {
5682        let prom_expr =
5683            parser::parse(&format!("{fn_name}(some_metric{{tag_0!=\"bar\"}})")).unwrap();
5684        let eval_stmt = EvalStmt {
5685            expr: prom_expr,
5686            start: UNIX_EPOCH,
5687            end: UNIX_EPOCH
5688                .checked_add(Duration::from_secs(100_000))
5689                .unwrap(),
5690            interval: Duration::from_secs(5),
5691            lookback_delta: Duration::from_secs(1),
5692        };
5693
5694        let table_provider = build_test_table_provider(
5695            &[(DEFAULT_SCHEMA_NAME.to_string(), "some_metric".to_string())],
5696            1,
5697            1,
5698        )
5699        .await;
5700        let plan =
5701            PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
5702                .await
5703                .unwrap();
5704
5705        let expected = String::from(
5706            "Filter: TEMPLATE(field_0) IS NOT NULL [timestamp:Timestamp(ms), TEMPLATE(field_0):Float64;N, tag_0:Utf8]\
5707            \n  Projection: some_metric.timestamp, TEMPLATE(some_metric.field_0) AS TEMPLATE(field_0), some_metric.tag_0 [timestamp:Timestamp(ms), TEMPLATE(field_0):Float64;N, tag_0:Utf8]\
5708            \n    PromInstantManipulate: range=[0..100000000], lookback=[1000], interval=[5000], time index=[timestamp] [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
5709            \n      PromSeriesDivide: tags=[\"tag_0\"] [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
5710            \n        Sort: some_metric.tag_0 ASC NULLS FIRST, some_metric.timestamp ASC NULLS FIRST [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
5711	            \n          Filter: some_metric.tag_0 != Utf8(\"bar\") AND some_metric.timestamp >= TimestampMillisecond(-999, None) AND some_metric.timestamp <= TimestampMillisecond(100000000, None) [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
5712            \n            TableScan: some_metric [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]"
5713        ).replace("TEMPLATE", plan_name);
5714
5715        assert_eq!(plan.display_indent_schema().to_string(), expected);
5716    }
5717
5718    #[tokio::test]
5719    async fn single_abs() {
5720        do_single_instant_function_call("abs", "abs").await;
5721    }
5722
5723    #[tokio::test]
5724    #[should_panic]
5725    async fn single_absent() {
5726        do_single_instant_function_call("absent", "").await;
5727    }
5728
5729    #[tokio::test]
5730    async fn single_ceil() {
5731        do_single_instant_function_call("ceil", "ceil").await;
5732    }
5733
5734    #[tokio::test]
5735    async fn single_exp() {
5736        do_single_instant_function_call("exp", "exp").await;
5737    }
5738
5739    #[tokio::test]
5740    async fn single_ln() {
5741        do_single_instant_function_call("ln", "ln").await;
5742    }
5743
5744    #[tokio::test]
5745    async fn single_log2() {
5746        do_single_instant_function_call("log2", "log2").await;
5747    }
5748
5749    #[tokio::test]
5750    async fn single_log10() {
5751        do_single_instant_function_call("log10", "log10").await;
5752    }
5753
5754    #[tokio::test]
5755    #[should_panic]
5756    async fn single_scalar() {
5757        do_single_instant_function_call("scalar", "").await;
5758    }
5759
5760    #[tokio::test]
5761    #[should_panic]
5762    async fn single_sgn() {
5763        do_single_instant_function_call("sgn", "").await;
5764    }
5765
5766    #[tokio::test]
5767    #[should_panic]
5768    async fn single_sort() {
5769        do_single_instant_function_call("sort", "").await;
5770    }
5771
5772    #[tokio::test]
5773    #[should_panic]
5774    async fn single_sort_desc() {
5775        do_single_instant_function_call("sort_desc", "").await;
5776    }
5777
5778    #[tokio::test]
5779    async fn single_sqrt() {
5780        do_single_instant_function_call("sqrt", "sqrt").await;
5781    }
5782
5783    #[tokio::test]
5784    #[should_panic]
5785    async fn single_timestamp() {
5786        do_single_instant_function_call("timestamp", "").await;
5787    }
5788
5789    #[tokio::test]
5790    async fn single_acos() {
5791        do_single_instant_function_call("acos", "acos").await;
5792    }
5793
5794    #[tokio::test]
5795    #[should_panic]
5796    async fn single_acosh() {
5797        do_single_instant_function_call("acosh", "").await;
5798    }
5799
5800    #[tokio::test]
5801    async fn single_asin() {
5802        do_single_instant_function_call("asin", "asin").await;
5803    }
5804
5805    #[tokio::test]
5806    #[should_panic]
5807    async fn single_asinh() {
5808        do_single_instant_function_call("asinh", "").await;
5809    }
5810
5811    #[tokio::test]
5812    async fn single_atan() {
5813        do_single_instant_function_call("atan", "atan").await;
5814    }
5815
5816    #[tokio::test]
5817    #[should_panic]
5818    async fn single_atanh() {
5819        do_single_instant_function_call("atanh", "").await;
5820    }
5821
5822    #[tokio::test]
5823    async fn single_cos() {
5824        do_single_instant_function_call("cos", "cos").await;
5825    }
5826
5827    #[tokio::test]
5828    #[should_panic]
5829    async fn single_cosh() {
5830        do_single_instant_function_call("cosh", "").await;
5831    }
5832
5833    #[tokio::test]
5834    async fn single_sin() {
5835        do_single_instant_function_call("sin", "sin").await;
5836    }
5837
5838    #[tokio::test]
5839    #[should_panic]
5840    async fn single_sinh() {
5841        do_single_instant_function_call("sinh", "").await;
5842    }
5843
5844    #[tokio::test]
5845    async fn single_tan() {
5846        do_single_instant_function_call("tan", "tan").await;
5847    }
5848
5849    #[tokio::test]
5850    #[should_panic]
5851    async fn single_tanh() {
5852        do_single_instant_function_call("tanh", "").await;
5853    }
5854
5855    #[tokio::test]
5856    #[should_panic]
5857    async fn single_deg() {
5858        do_single_instant_function_call("deg", "").await;
5859    }
5860
5861    #[tokio::test]
5862    #[should_panic]
5863    async fn single_rad() {
5864        do_single_instant_function_call("rad", "").await;
5865    }
5866
5867    // {
5868    //     input: "avg by (foo)(some_metric)",
5869    //     expected: &AggregateExpr{
5870    //         Op: AVG,
5871    //         Expr: &VectorSelector{
5872    //             Name: "some_metric",
5873    //             LabelMatchers: []*labels.Matcher{
5874    //                 MustLabelMatcher(labels.MatchEqual, model.MetricNameLabel, "some_metric"),
5875    //             },
5876    //             PosRange: PositionRange{
5877    //                 Start: 13,
5878    //                 End:   24,
5879    //             },
5880    //         },
5881    //         Grouping: []string{"foo"},
5882    //         PosRange: PositionRange{
5883    //             Start: 0,
5884    //             End:   25,
5885    //         },
5886    //     },
5887    // },
5888    async fn do_aggregate_expr_plan(fn_name: &str, plan_name: &str) {
5889        let prom_expr = parser::parse(&format!(
5890            "{fn_name} by (tag_1)(some_metric{{tag_0!=\"bar\"}})",
5891        ))
5892        .unwrap();
5893        let mut eval_stmt = EvalStmt {
5894            expr: prom_expr,
5895            start: UNIX_EPOCH,
5896            end: UNIX_EPOCH
5897                .checked_add(Duration::from_secs(100_000))
5898                .unwrap(),
5899            interval: Duration::from_secs(5),
5900            lookback_delta: Duration::from_secs(1),
5901        };
5902
5903        // test group by
5904        let table_provider = build_test_table_provider(
5905            &[(DEFAULT_SCHEMA_NAME.to_string(), "some_metric".to_string())],
5906            2,
5907            2,
5908        )
5909        .await;
5910        let plan =
5911            PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
5912                .await
5913                .unwrap();
5914        let expected_no_without = String::from(
5915            "Sort: some_metric.tag_1 ASC NULLS LAST, some_metric.timestamp ASC NULLS LAST [tag_1:Utf8, timestamp:Timestamp(ms), TEMPLATE(some_metric.field_0):Float64;N, TEMPLATE(some_metric.field_1):Float64;N]\
5916            \n  Aggregate: groupBy=[[some_metric.tag_1, some_metric.timestamp]], aggr=[[TEMPLATE(some_metric.field_0), TEMPLATE(some_metric.field_1)]] [tag_1:Utf8, timestamp:Timestamp(ms), TEMPLATE(some_metric.field_0):Float64;N, TEMPLATE(some_metric.field_1):Float64;N]\
5917            \n    PromInstantManipulate: range=[0..100000000], lookback=[1000], interval=[5000], time index=[timestamp] [tag_0:Utf8, tag_1:Utf8, timestamp:Timestamp(ms), field_0:Float64;N, field_1:Float64;N]\
5918            \n      PromSeriesDivide: tags=[\"tag_0\", \"tag_1\"] [tag_0:Utf8, tag_1:Utf8, timestamp:Timestamp(ms), field_0:Float64;N, field_1:Float64;N]\
5919            \n        Sort: some_metric.tag_0 ASC NULLS FIRST, some_metric.tag_1 ASC NULLS FIRST, some_metric.timestamp ASC NULLS FIRST [tag_0:Utf8, tag_1:Utf8, timestamp:Timestamp(ms), field_0:Float64;N, field_1:Float64;N]\
5920            \n          Filter: some_metric.tag_0 != Utf8(\"bar\") AND some_metric.timestamp >= TimestampMillisecond(-999, None) AND some_metric.timestamp <= TimestampMillisecond(100000000, None) [tag_0:Utf8, tag_1:Utf8, timestamp:Timestamp(ms), field_0:Float64;N, field_1:Float64;N]\
5921            \n            TableScan: some_metric [tag_0:Utf8, tag_1:Utf8, timestamp:Timestamp(ms), field_0:Float64;N, field_1:Float64;N]"
5922        ).replace("TEMPLATE", plan_name);
5923        assert_eq!(
5924            plan.display_indent_schema().to_string(),
5925            expected_no_without
5926        );
5927
5928        // test group without
5929        if let PromExpr::Aggregate(AggregateExpr { modifier, .. }) = &mut eval_stmt.expr {
5930            *modifier = Some(LabelModifier::Exclude(Labels {
5931                labels: vec![String::from("tag_1")].into_iter().collect(),
5932            }));
5933        }
5934        let table_provider = build_test_table_provider(
5935            &[(DEFAULT_SCHEMA_NAME.to_string(), "some_metric".to_string())],
5936            2,
5937            2,
5938        )
5939        .await;
5940        let plan =
5941            PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
5942                .await
5943                .unwrap();
5944        let expected_without = String::from(
5945            "Sort: some_metric.tag_0 ASC NULLS LAST, some_metric.timestamp ASC NULLS LAST [tag_0:Utf8, timestamp:Timestamp(ms), TEMPLATE(some_metric.field_0):Float64;N, TEMPLATE(some_metric.field_1):Float64;N]\
5946            \n  Aggregate: groupBy=[[some_metric.tag_0, some_metric.timestamp]], aggr=[[TEMPLATE(some_metric.field_0), TEMPLATE(some_metric.field_1)]] [tag_0:Utf8, timestamp:Timestamp(ms), TEMPLATE(some_metric.field_0):Float64;N, TEMPLATE(some_metric.field_1):Float64;N]\
5947            \n    PromInstantManipulate: range=[0..100000000], lookback=[1000], interval=[5000], time index=[timestamp] [tag_0:Utf8, tag_1:Utf8, timestamp:Timestamp(ms), field_0:Float64;N, field_1:Float64;N]\
5948            \n      PromSeriesDivide: tags=[\"tag_0\", \"tag_1\"] [tag_0:Utf8, tag_1:Utf8, timestamp:Timestamp(ms), field_0:Float64;N, field_1:Float64;N]\
5949            \n        Sort: some_metric.tag_0 ASC NULLS FIRST, some_metric.tag_1 ASC NULLS FIRST, some_metric.timestamp ASC NULLS FIRST [tag_0:Utf8, tag_1:Utf8, timestamp:Timestamp(ms), field_0:Float64;N, field_1:Float64;N]\
5950            \n          Filter: some_metric.tag_0 != Utf8(\"bar\") AND some_metric.timestamp >= TimestampMillisecond(-999, None) AND some_metric.timestamp <= TimestampMillisecond(100000000, None) [tag_0:Utf8, tag_1:Utf8, timestamp:Timestamp(ms), field_0:Float64;N, field_1:Float64;N]\
5951            \n            TableScan: some_metric [tag_0:Utf8, tag_1:Utf8, timestamp:Timestamp(ms), field_0:Float64;N, field_1:Float64;N]"
5952        ).replace("TEMPLATE", plan_name);
5953        assert_eq!(plan.display_indent_schema().to_string(), expected_without);
5954    }
5955
5956    #[tokio::test]
5957    async fn aggregate_sum() {
5958        do_aggregate_expr_plan("sum", "sum").await;
5959    }
5960
5961    #[tokio::test]
5962    async fn tsid_is_used_for_series_divide_when_available() {
5963        let prom_expr = parser::parse("some_metric").unwrap();
5964        let eval_stmt = EvalStmt {
5965            expr: prom_expr,
5966            start: UNIX_EPOCH,
5967            end: UNIX_EPOCH
5968                .checked_add(Duration::from_secs(100_000))
5969                .unwrap(),
5970            interval: Duration::from_secs(5),
5971            lookback_delta: Duration::from_secs(1),
5972        };
5973
5974        let table_provider = build_test_table_provider_with_tsid(
5975            &[(DEFAULT_SCHEMA_NAME.to_string(), "some_metric".to_string())],
5976            1,
5977            1,
5978        )
5979        .await;
5980        let plan =
5981            PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
5982                .await
5983                .unwrap();
5984
5985        let plan_str = plan.display_indent_schema().to_string();
5986        assert!(plan_str.contains("PromSeriesDivide: tags=[\"__tsid\"]"));
5987        assert!(plan_str.contains("__tsid ASC NULLS FIRST"));
5988        assert!(
5989            !plan
5990                .schema()
5991                .fields()
5992                .iter()
5993                .any(|field| field.name() == DATA_SCHEMA_TSID_COLUMN_NAME)
5994        );
5995
5996        let manipulate = find_instant_manipulate(&plan).unwrap();
5997        let exec = manipulate.to_execution_plan(Arc::new(DataSourceExec::new(Arc::new(
5998            MemorySourceConfig::try_new(&[], Arc::new(ArrowSchema::empty()), None).unwrap(),
5999        ))));
6000        assert!(format!("{exec:?}").contains("reuse_tsid_column: true"));
6001    }
6002
6003    #[tokio::test]
6004    async fn default_binary_join_uses_tsid_when_available() {
6005        let eval_stmt = build_eval_stmt("some_metric / some_alt_metric");
6006
6007        let table_provider = build_test_table_provider_with_tsid(
6008            &[
6009                (DEFAULT_SCHEMA_NAME.to_string(), "some_metric".to_string()),
6010                (
6011                    DEFAULT_SCHEMA_NAME.to_string(),
6012                    "some_alt_metric".to_string(),
6013                ),
6014            ],
6015            1,
6016            1,
6017        )
6018        .await;
6019        let plan =
6020            PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
6021                .await
6022                .unwrap();
6023
6024        let plan_str = plan.display_indent_schema().to_string();
6025        assert!(
6026            plan_str.contains("some_metric.__tsid = some_alt_metric.__tsid"),
6027            "{plan_str}"
6028        );
6029        assert!(
6030            !plan_str.contains("some_metric.tag_0 = some_alt_metric.tag_0"),
6031            "{plan_str}"
6032        );
6033    }
6034
6035    #[tokio::test]
6036    async fn reject_binary_fill_modifiers() {
6037        let state = build_query_engine_state();
6038
6039        for query in [
6040            "some_metric + fill(0) some_alt_metric",
6041            "some_metric + fill_left(0) some_alt_metric",
6042            "some_metric + fill_right(0) some_alt_metric",
6043            "(some_metric + fill(0) some_alt_metric) + some_metric",
6044        ] {
6045            let eval_stmt = build_eval_stmt(query);
6046            let table_provider = build_test_table_provider(&[], 0, 0).await;
6047            let err = PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &state)
6048                .await
6049                .unwrap_err();
6050
6051            assert!(
6052                matches!(
6053                    &err,
6054                    crate::promql::error::Error::UnsupportedExpr { name, .. }
6055                        if name == "PromQL fill modifiers"
6056                ),
6057                "{err}"
6058            );
6059        }
6060    }
6061
6062    #[tokio::test]
6063    async fn timestamp_binary_join_falls_back_when_tsid_is_projected_out() {
6064        for query in [
6065            "timestamp(some_metric) / some_metric",
6066            "some_metric / timestamp(some_metric)",
6067        ] {
6068            let eval_stmt = build_eval_stmt(query);
6069
6070            let table_provider = build_test_table_provider_with_tsid(
6071                &[(DEFAULT_SCHEMA_NAME.to_string(), "some_metric".to_string())],
6072                1,
6073                1,
6074            )
6075            .await;
6076            let plan =
6077                PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
6078                    .await
6079                    .unwrap();
6080
6081            let plan_str = plan.display_indent_schema().to_string();
6082            assert!(!plan_str.contains("__tsid ="), "{query}: {plan_str}");
6083            assert!(
6084                plan_str.contains("lhs.tag_0 = rhs.tag_0"),
6085                "{query}: {plan_str}"
6086            );
6087            assert!(
6088                !plan
6089                    .schema()
6090                    .fields()
6091                    .iter()
6092                    .any(|field| field.name() == DATA_SCHEMA_TSID_COLUMN_NAME),
6093                "{query}: {plan_str}"
6094            );
6095        }
6096    }
6097
6098    #[tokio::test]
6099    async fn timestamp_binary_join_rejects_default_matching_on_mismatched_labels() {
6100        let eval_stmt = build_eval_stmt("timestamp(left_host_job) / right_by_job");
6101
6102        let table_provider = build_test_table_provider_with_tsid_tag_fields(&[
6103            (
6104                (DEFAULT_SCHEMA_NAME.to_string(), "left_host_job".to_string()),
6105                2,
6106                1,
6107            ),
6108            (
6109                (DEFAULT_SCHEMA_NAME.to_string(), "right_by_job".to_string()),
6110                1,
6111                1,
6112            ),
6113        ])
6114        .await;
6115        let plan =
6116            PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
6117                .await
6118                .unwrap();
6119        let plan_str = plan.display_indent_schema().to_string();
6120
6121        assert!(
6122            plan_str.contains("Boolean(false)") || plan_str.contains("false"),
6123            "{plan_str}"
6124        );
6125    }
6126
6127    #[tokio::test]
6128    async fn tsid_is_preserved_for_nested_default_binary_joins() {
6129        let eval_stmt = build_eval_stmt("(some_metric - some_alt_metric) / some_third_metric");
6130
6131        let table_provider = build_test_table_provider_with_tsid(
6132            &[
6133                (DEFAULT_SCHEMA_NAME.to_string(), "some_metric".to_string()),
6134                (
6135                    DEFAULT_SCHEMA_NAME.to_string(),
6136                    "some_alt_metric".to_string(),
6137                ),
6138                (
6139                    DEFAULT_SCHEMA_NAME.to_string(),
6140                    "some_third_metric".to_string(),
6141                ),
6142            ],
6143            1,
6144            1,
6145        )
6146        .await;
6147        let plan =
6148            PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
6149                .await
6150                .unwrap();
6151
6152        let plan_str = plan.display_indent_schema().to_string();
6153        assert_eq!(plan_str.matches("__tsid =").count(), 2, "{plan_str}");
6154        assert!(!plan_str.contains("tag_0 ="), "{plan_str}");
6155    }
6156
6157    #[tokio::test]
6158    async fn repeated_tsid_binary_operand_reuses_leaf_plan() {
6159        let eval_stmt = build_eval_stmt("((some_metric - some_alt_metric) / some_metric) * 100");
6160
6161        let table_provider = build_test_table_provider_with_tsid(
6162            &[
6163                (DEFAULT_SCHEMA_NAME.to_string(), "some_metric".to_string()),
6164                (
6165                    DEFAULT_SCHEMA_NAME.to_string(),
6166                    "some_alt_metric".to_string(),
6167                ),
6168            ],
6169            1,
6170            1,
6171        )
6172        .await;
6173        let plan =
6174            PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
6175                .await
6176                .unwrap();
6177
6178        let plan_str = plan.display_indent_schema().to_string();
6179        assert_eq!(plan_str.matches("__tsid =").count(), 1, "{plan_str}");
6180        assert_eq!(
6181            plan_str
6182                .matches("Filter: phy.__table_id = UInt32(1024)")
6183                .count(),
6184            1,
6185            "{plan_str}"
6186        );
6187        assert_eq!(
6188            plan_str.matches("PromInstantManipulate").count(),
6189            2,
6190            "{plan_str}"
6191        );
6192        assert!(!plan_str.contains("tag_0 ="), "{plan_str}");
6193    }
6194
6195    #[tokio::test]
6196    async fn repeated_tsid_binary_operand_reuses_shorter_field_side() {
6197        let eval_stmt =
6198            build_eval_stmt("((two_field_metric - one_field_metric) / one_field_metric) * 100");
6199
6200        let table_provider = build_test_table_provider_with_tsid_fields(
6201            &[
6202                (
6203                    (
6204                        DEFAULT_SCHEMA_NAME.to_string(),
6205                        "two_field_metric".to_string(),
6206                    ),
6207                    2,
6208                ),
6209                (
6210                    (
6211                        DEFAULT_SCHEMA_NAME.to_string(),
6212                        "one_field_metric".to_string(),
6213                    ),
6214                    1,
6215                ),
6216            ],
6217            1,
6218        )
6219        .await;
6220        let plan =
6221            PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
6222                .await
6223                .unwrap();
6224
6225        let field_names = plan
6226            .schema()
6227            .fields()
6228            .iter()
6229            .map(|field| field.name().clone())
6230            .collect::<Vec<_>>();
6231        let value_columns = field_names
6232            .iter()
6233            .filter(|name| {
6234                *name != "tag_0" && *name != "timestamp" && *name != DATA_SCHEMA_TSID_COLUMN_NAME
6235            })
6236            .count();
6237        assert_eq!(value_columns, 1, "{field_names:?}");
6238        let plan_str = plan.display_indent_schema().to_string();
6239        assert_eq!(plan_str.matches("__tsid =").count(), 1, "{plan_str}");
6240        assert_eq!(
6241            plan_str
6242                .matches("Filter: phy.__table_id = UInt32(1025)")
6243                .count(),
6244            1,
6245            "{plan_str}"
6246        );
6247        assert!(!plan_str.contains("tag_0 ="), "{plan_str}");
6248    }
6249
6250    #[tokio::test]
6251    async fn binary_island_reuses_self_operand_without_join() {
6252        let eval_stmt = build_eval_stmt("some_metric / some_metric");
6253
6254        let table_provider = build_test_table_provider_with_tsid(
6255            &[(DEFAULT_SCHEMA_NAME.to_string(), "some_metric".to_string())],
6256            1,
6257            1,
6258        )
6259        .await;
6260        let plan =
6261            PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
6262                .await
6263                .unwrap();
6264
6265        let plan_str = plan.display_indent_schema().to_string();
6266        assert_eq!(plan_str.matches("__tsid =").count(), 0, "{plan_str}");
6267        assert_eq!(
6268            plan_str
6269                .matches("Filter: phy.__table_id = UInt32(1024)")
6270                .count(),
6271            1,
6272            "{plan_str}"
6273        );
6274        assert_eq!(
6275            plan_str.matches("PromInstantManipulate").count(),
6276            1,
6277            "{plan_str}"
6278        );
6279    }
6280
6281    #[tokio::test]
6282    async fn binary_island_reuses_leaf_across_two_branches() {
6283        let eval_stmt =
6284            build_eval_stmt("(some_metric + some_alt_metric) / (some_metric + third_metric)");
6285
6286        let table_provider = build_test_table_provider_with_tsid(
6287            &[
6288                (DEFAULT_SCHEMA_NAME.to_string(), "some_metric".to_string()),
6289                (
6290                    DEFAULT_SCHEMA_NAME.to_string(),
6291                    "some_alt_metric".to_string(),
6292                ),
6293                (DEFAULT_SCHEMA_NAME.to_string(), "third_metric".to_string()),
6294            ],
6295            1,
6296            1,
6297        )
6298        .await;
6299        let plan =
6300            PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
6301                .await
6302                .unwrap();
6303
6304        let plan_str = plan.display_indent_schema().to_string();
6305        assert_eq!(plan_str.matches("__tsid =").count(), 2, "{plan_str}");
6306        assert_eq!(
6307            plan_str
6308                .matches("Filter: phy.__table_id = UInt32(1024)")
6309                .count(),
6310            1,
6311            "{plan_str}"
6312        );
6313        assert_eq!(
6314            plan_str.matches("PromInstantManipulate").count(),
6315            3,
6316            "{plan_str}"
6317        );
6318    }
6319
6320    #[tokio::test]
6321    async fn binary_island_generated_alias_avoids_user_column_names() {
6322        let eval_stmt = build_eval_stmt("(some_metric + some_alt_metric) / some_metric");
6323
6324        let table_provider = build_test_table_provider_with_fields(
6325            &[
6326                (DEFAULT_SCHEMA_NAME.to_string(), "some_metric".to_string()),
6327                (
6328                    DEFAULT_SCHEMA_NAME.to_string(),
6329                    "some_alt_metric".to_string(),
6330                ),
6331            ],
6332            &["prom_v0", "__prom_v0"],
6333        )
6334        .await;
6335        let plan =
6336            PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
6337                .await
6338                .unwrap();
6339
6340        let field_names = plan.schema().field_names();
6341        assert!(field_names.iter().any(|name| name.ends_with(".prom_v0")));
6342        assert!(field_names.iter().any(|name| name.ends_with(".__prom_v0")));
6343
6344        let plan_str = plan.display_indent_schema().to_string();
6345        assert!(plan_str.contains("SubqueryAlias: __prom_v0"), "{plan_str}");
6346        assert_eq!(
6347            plan_str.matches("PromInstantManipulate").count(),
6348            2,
6349            "{plan_str}"
6350        );
6351    }
6352
6353    #[tokio::test]
6354    async fn binary_island_clears_qualifier_for_nested_unary_projection() {
6355        let eval_stmt = build_eval_stmt("-((some_metric + some_alt_metric) / some_metric)");
6356
6357        let table_provider = build_test_table_provider_with_tsid(
6358            &[
6359                (DEFAULT_SCHEMA_NAME.to_string(), "some_metric".to_string()),
6360                (
6361                    DEFAULT_SCHEMA_NAME.to_string(),
6362                    "some_alt_metric".to_string(),
6363                ),
6364            ],
6365            1,
6366            1,
6367        )
6368        .await;
6369        let plan =
6370            PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
6371                .await
6372                .unwrap();
6373
6374        let plan_str = plan.display_indent_schema().to_string();
6375        assert_eq!(plan_str.matches("__tsid =").count(), 1, "{plan_str}");
6376        assert_eq!(
6377            plan_str.matches("PromInstantManipulate").count(),
6378            2,
6379            "{plan_str}"
6380        );
6381    }
6382
6383    #[tokio::test]
6384    async fn binary_island_keeps_distinct_matcher_leaves() {
6385        let eval_stmt = build_eval_stmt(
6386            "(some_metric{tag_0=\"foo\"} + some_alt_metric) / some_metric{tag_0=\"bar\"}",
6387        );
6388
6389        let table_provider = build_test_table_provider_with_tsid(
6390            &[
6391                (DEFAULT_SCHEMA_NAME.to_string(), "some_metric".to_string()),
6392                (
6393                    DEFAULT_SCHEMA_NAME.to_string(),
6394                    "some_alt_metric".to_string(),
6395                ),
6396            ],
6397            1,
6398            1,
6399        )
6400        .await;
6401        let plan =
6402            PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
6403                .await
6404                .unwrap();
6405
6406        let plan_str = plan.display_indent_schema().to_string();
6407        assert_eq!(plan_str.matches("__tsid =").count(), 2, "{plan_str}");
6408        assert_eq!(
6409            plan_str.matches("PromInstantManipulate").count(),
6410            3,
6411            "{plan_str}"
6412        );
6413    }
6414
6415    #[tokio::test]
6416    async fn binary_island_keeps_offset_leaves_distinct() {
6417        let eval_stmt = build_eval_stmt("(some_metric offset 5m + some_alt_metric) / some_metric");
6418
6419        let table_provider = build_test_table_provider_with_tsid(
6420            &[
6421                (DEFAULT_SCHEMA_NAME.to_string(), "some_metric".to_string()),
6422                (
6423                    DEFAULT_SCHEMA_NAME.to_string(),
6424                    "some_alt_metric".to_string(),
6425                ),
6426            ],
6427            1,
6428            1,
6429        )
6430        .await;
6431        let plan =
6432            PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
6433                .await
6434                .unwrap();
6435
6436        let plan_str = plan.display_indent_schema().to_string();
6437        assert_eq!(plan_str.matches("__tsid =").count(), 2, "{plan_str}");
6438        assert_eq!(
6439            plan_str.matches("PromInstantManipulate").count(),
6440            3,
6441            "{plan_str}"
6442        );
6443    }
6444
6445    #[tokio::test]
6446    async fn binary_island_falls_back_for_group_modifier() {
6447        let eval_stmt = build_eval_stmt(
6448            "(some_metric + ignoring(tag_0) group_left some_alt_metric) / some_metric",
6449        );
6450
6451        let table_provider = build_test_table_provider_with_tsid(
6452            &[
6453                (DEFAULT_SCHEMA_NAME.to_string(), "some_metric".to_string()),
6454                (
6455                    DEFAULT_SCHEMA_NAME.to_string(),
6456                    "some_alt_metric".to_string(),
6457                ),
6458            ],
6459            1,
6460            1,
6461        )
6462        .await;
6463        let plan =
6464            PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
6465                .await
6466                .unwrap();
6467
6468        let plan_str = plan.display_indent_schema().to_string();
6469        assert_eq!(
6470            plan_str.matches("PromInstantManipulate").count(),
6471            3,
6472            "{plan_str}"
6473        );
6474    }
6475
6476    #[tokio::test]
6477    async fn binary_island_falls_back_for_comparison_filter() {
6478        let eval_stmt = build_eval_stmt("(some_metric > some_alt_metric) / some_metric");
6479
6480        let table_provider = build_test_table_provider_with_tsid(
6481            &[
6482                (DEFAULT_SCHEMA_NAME.to_string(), "some_metric".to_string()),
6483                (
6484                    DEFAULT_SCHEMA_NAME.to_string(),
6485                    "some_alt_metric".to_string(),
6486                ),
6487            ],
6488            1,
6489            1,
6490        )
6491        .await;
6492        let plan =
6493            PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
6494                .await
6495                .unwrap();
6496
6497        let plan_str = plan.display_indent_schema().to_string();
6498        assert_eq!(plan_str.matches("__tsid =").count(), 2, "{plan_str}");
6499        assert_eq!(
6500            plan_str.matches("PromInstantManipulate").count(),
6501            3,
6502            "{plan_str}"
6503        );
6504    }
6505
6506    #[tokio::test]
6507    async fn tsid_binary_join_uses_shorter_field_side() {
6508        let eval_stmt = build_eval_stmt("one_field_metric / two_field_metric");
6509
6510        let table_provider = build_test_table_provider_with_tsid_fields(
6511            &[
6512                (
6513                    (
6514                        DEFAULT_SCHEMA_NAME.to_string(),
6515                        "one_field_metric".to_string(),
6516                    ),
6517                    1,
6518                ),
6519                (
6520                    (
6521                        DEFAULT_SCHEMA_NAME.to_string(),
6522                        "two_field_metric".to_string(),
6523                    ),
6524                    2,
6525                ),
6526            ],
6527            1,
6528        )
6529        .await;
6530        let plan =
6531            PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
6532                .await
6533                .unwrap();
6534
6535        let field_names = plan
6536            .schema()
6537            .fields()
6538            .iter()
6539            .map(|field| field.name().clone())
6540            .collect::<Vec<_>>();
6541        let value_columns = field_names
6542            .iter()
6543            .filter(|name| {
6544                *name != "tag_0" && *name != "timestamp" && *name != DATA_SCHEMA_TSID_COLUMN_NAME
6545            })
6546            .count();
6547        assert_eq!(value_columns, 1, "{field_names:?}");
6548    }
6549
6550    #[tokio::test]
6551    async fn comparison_binary_join_uses_shorter_field_side() {
6552        let eval_stmt = build_eval_stmt("two_field_metric > one_field_metric");
6553
6554        let table_provider = build_test_table_provider_with_tsid_fields(
6555            &[
6556                (
6557                    (
6558                        DEFAULT_SCHEMA_NAME.to_string(),
6559                        "two_field_metric".to_string(),
6560                    ),
6561                    2,
6562                ),
6563                (
6564                    (
6565                        DEFAULT_SCHEMA_NAME.to_string(),
6566                        "one_field_metric".to_string(),
6567                    ),
6568                    1,
6569                ),
6570            ],
6571            1,
6572        )
6573        .await;
6574        let plan =
6575            PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
6576                .await
6577                .unwrap();
6578
6579        let field_names = plan
6580            .schema()
6581            .fields()
6582            .iter()
6583            .map(|field| field.name().clone())
6584            .collect::<Vec<_>>();
6585        assert!(
6586            field_names.iter().any(|name| name == "field_0"),
6587            "{field_names:?}"
6588        );
6589        assert!(
6590            !field_names.iter().any(|name| name == "field_1"),
6591            "{field_names:?}"
6592        );
6593    }
6594
6595    #[tokio::test]
6596    async fn label_matching_modifier_disables_tsid_binary_join() {
6597        let eval_stmt = build_eval_stmt("some_metric / ignoring(tag_0) some_alt_metric");
6598
6599        let table_provider = build_test_table_provider_with_tsid(
6600            &[
6601                (DEFAULT_SCHEMA_NAME.to_string(), "some_metric".to_string()),
6602                (
6603                    DEFAULT_SCHEMA_NAME.to_string(),
6604                    "some_alt_metric".to_string(),
6605                ),
6606            ],
6607            2,
6608            1,
6609        )
6610        .await;
6611        let plan =
6612            PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
6613                .await
6614                .unwrap();
6615
6616        let plan_str = plan.display_indent_schema().to_string();
6617        assert!(!plan_str.contains("__tsid ="), "{plan_str}");
6618        assert!(
6619            plan_str.contains("some_metric.tag_1 = some_alt_metric.tag_1"),
6620            "{plan_str}"
6621        );
6622    }
6623
6624    #[tokio::test]
6625    async fn ignoring_absent_label_keeps_tsid_binary_join() {
6626        let eval_stmt = build_eval_stmt("some_metric / ignoring(missing) some_alt_metric");
6627
6628        let table_provider = build_test_table_provider_with_tsid(
6629            &[
6630                (DEFAULT_SCHEMA_NAME.to_string(), "some_metric".to_string()),
6631                (
6632                    DEFAULT_SCHEMA_NAME.to_string(),
6633                    "some_alt_metric".to_string(),
6634                ),
6635            ],
6636            2,
6637            1,
6638        )
6639        .await;
6640        let plan =
6641            PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
6642                .await
6643                .unwrap();
6644
6645        let plan_str = plan.display_indent_schema().to_string();
6646        assert!(
6647            plan_str.contains("some_metric.__tsid = some_alt_metric.__tsid"),
6648            "{plan_str}"
6649        );
6650        assert!(!plan_str.contains("tag_0 ="), "{plan_str}");
6651        assert!(!plan_str.contains("tag_1 ="), "{plan_str}");
6652    }
6653
6654    #[tokio::test]
6655    async fn range_function_keeps_tsid_for_absent_ignoring_binary_join() {
6656        let eval_stmt =
6657            build_eval_stmt("rate(some_metric[5m]) / ignoring(missing) some_alt_metric");
6658
6659        let table_provider = build_test_table_provider_with_tsid(
6660            &[
6661                (DEFAULT_SCHEMA_NAME.to_string(), "some_metric".to_string()),
6662                (
6663                    DEFAULT_SCHEMA_NAME.to_string(),
6664                    "some_alt_metric".to_string(),
6665                ),
6666            ],
6667            2,
6668            1,
6669        )
6670        .await;
6671        let plan =
6672            PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
6673                .await
6674                .unwrap();
6675
6676        let plan_str = plan.display_indent_schema().to_string();
6677        assert!(
6678            plan_str.contains("some_metric.__tsid = some_alt_metric.__tsid"),
6679            "{plan_str}"
6680        );
6681        assert!(!plan_str.contains("tag_0 ="), "{plan_str}");
6682        assert!(!plan_str.contains("tag_1 ="), "{plan_str}");
6683    }
6684
6685    #[tokio::test]
6686    async fn on_full_label_set_keeps_tsid_binary_join() {
6687        let eval_stmt = build_eval_stmt("some_metric / on(tag_0, tag_1) some_alt_metric");
6688
6689        let table_provider = build_test_table_provider_with_tsid(
6690            &[
6691                (DEFAULT_SCHEMA_NAME.to_string(), "some_metric".to_string()),
6692                (
6693                    DEFAULT_SCHEMA_NAME.to_string(),
6694                    "some_alt_metric".to_string(),
6695                ),
6696            ],
6697            2,
6698            1,
6699        )
6700        .await;
6701        let plan =
6702            PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
6703                .await
6704                .unwrap();
6705
6706        let plan_str = plan.display_indent_schema().to_string();
6707        assert!(
6708            plan_str.contains("some_metric.__tsid = some_alt_metric.__tsid"),
6709            "{plan_str}"
6710        );
6711        assert!(!plan_str.contains("tag_0 ="), "{plan_str}");
6712        assert!(!plan_str.contains("tag_1 ="), "{plan_str}");
6713    }
6714
6715    #[tokio::test]
6716    async fn on_partial_label_set_disables_tsid_binary_join() {
6717        let eval_stmt = build_eval_stmt("some_metric / on(tag_0) some_alt_metric");
6718
6719        let table_provider = build_test_table_provider_with_tsid(
6720            &[
6721                (DEFAULT_SCHEMA_NAME.to_string(), "some_metric".to_string()),
6722                (
6723                    DEFAULT_SCHEMA_NAME.to_string(),
6724                    "some_alt_metric".to_string(),
6725                ),
6726            ],
6727            2,
6728            1,
6729        )
6730        .await;
6731        let plan =
6732            PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
6733                .await
6734                .unwrap();
6735
6736        let plan_str = plan.display_indent_schema().to_string();
6737        assert!(!plan_str.contains("__tsid ="), "{plan_str}");
6738        assert!(
6739            plan_str.contains("some_metric.tag_0 = some_alt_metric.tag_0"),
6740            "{plan_str}"
6741        );
6742        assert!(!plan_str.contains("tag_1 ="), "{plan_str}");
6743    }
6744
6745    #[tokio::test]
6746    async fn on_label_set_must_cover_both_sides_to_use_tsid_binary_join() {
6747        let eval_stmt = build_eval_stmt("some_metric / on(tag_0) some_alt_metric");
6748
6749        let table_provider = build_test_table_provider_with_tsid_tag_fields(&[
6750            (
6751                (DEFAULT_SCHEMA_NAME.to_string(), "some_metric".to_string()),
6752                2,
6753                1,
6754            ),
6755            (
6756                (
6757                    DEFAULT_SCHEMA_NAME.to_string(),
6758                    "some_alt_metric".to_string(),
6759                ),
6760                1,
6761                1,
6762            ),
6763        ])
6764        .await;
6765        let plan =
6766            PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
6767                .await
6768                .unwrap();
6769
6770        let plan_str = plan.display_indent_schema().to_string();
6771        assert!(!plan_str.contains("__tsid ="), "{plan_str}");
6772        assert!(
6773            plan_str.contains("some_metric.tag_0 = some_alt_metric.tag_0"),
6774            "{plan_str}"
6775        );
6776        assert!(!plan_str.contains("tag_1 ="), "{plan_str}");
6777    }
6778
6779    #[tokio::test]
6780    async fn comparison_binary_join_uses_tsid_and_keeps_it_in_filtered_result() {
6781        let eval_stmt = build_eval_stmt("some_metric > some_alt_metric");
6782
6783        let table_provider = build_test_table_provider_with_tsid(
6784            &[
6785                (DEFAULT_SCHEMA_NAME.to_string(), "some_metric".to_string()),
6786                (
6787                    DEFAULT_SCHEMA_NAME.to_string(),
6788                    "some_alt_metric".to_string(),
6789                ),
6790            ],
6791            2,
6792            1,
6793        )
6794        .await;
6795        let mut planner = PromPlanner {
6796            table_provider,
6797            ctx: PromPlannerContext::from_eval_stmt(&eval_stmt),
6798        };
6799        let plan = planner
6800            .prom_expr_to_plan(&eval_stmt.expr, &build_query_engine_state())
6801            .await
6802            .unwrap();
6803
6804        let plan_str = plan.display_indent_schema().to_string();
6805        assert!(
6806            plan_str.contains("some_metric.__tsid = some_alt_metric.__tsid"),
6807            "{plan_str}"
6808        );
6809        assert!(
6810            plan.schema()
6811                .fields()
6812                .iter()
6813                .any(|field| field.name() == DATA_SCHEMA_TSID_COLUMN_NAME),
6814            "{plan_str}"
6815        );
6816        assert!(planner.ctx.use_tsid, "{plan_str}");
6817    }
6818
6819    #[tokio::test]
6820    async fn comparison_bool_binary_join_uses_tsid_when_available() {
6821        let eval_stmt = build_eval_stmt("some_metric > bool some_alt_metric");
6822
6823        let table_provider = build_test_table_provider_with_tsid(
6824            &[
6825                (DEFAULT_SCHEMA_NAME.to_string(), "some_metric".to_string()),
6826                (
6827                    DEFAULT_SCHEMA_NAME.to_string(),
6828                    "some_alt_metric".to_string(),
6829                ),
6830            ],
6831            2,
6832            1,
6833        )
6834        .await;
6835        let plan =
6836            PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
6837                .await
6838                .unwrap();
6839
6840        let plan_str = plan.display_indent_schema().to_string();
6841        assert!(
6842            plan_str.contains("some_metric.__tsid = some_alt_metric.__tsid"),
6843            "{plan_str}"
6844        );
6845        assert!(!plan_str.contains("tag_0 ="), "{plan_str}");
6846        assert!(!plan_str.contains("tag_1 ="), "{plan_str}");
6847    }
6848
6849    #[tokio::test]
6850    async fn scalar_count_count_range_keeps_full_window() {
6851        let plan_str = build_optimized_tsid_plan(
6852            "scalar(count(count(some_metric) by (tag_0)))",
6853            1,
6854            1,
6855            100_000,
6856            1,
6857        )
6858        .await;
6859        assert!(plan_str.contains("ScalarCalculate: tags=[]"));
6860        assert!(plan_str.contains("PromInstantManipulate: range=[0..100000000]"));
6861        assert!(!plan_str.contains("PromInstantManipulate: range=[99999000..99999000]"));
6862    }
6863
6864    #[tokio::test]
6865    async fn scalar_count_count_rewrite_applies_inside_binary_expr_for_tsid_input() {
6866        let plan_str = build_optimized_tsid_plan(
6867            "sum(irate(some_metric[1h])) / scalar(count(count(some_metric) by (tag_0)))",
6868            2,
6869            1,
6870            10,
6871            300,
6872        )
6873        .await;
6874        assert!(plan_str.contains("Distinct:"), "{plan_str}");
6875    }
6876
6877    #[tokio::test]
6878    async fn nested_count_rewrite_keeps_full_series_key_with_tsid_input() {
6879        assert_nested_count_rewrite_applies(
6880            "count(count(some_metric) by (tag_0))",
6881            "Aggregate: groupBy=[[some_metric.timestamp]], aggr=[[count(Int64(1)) AS count(count(some_metric.field_0))]]"
6882        )
6883        .await;
6884    }
6885
6886    #[tokio::test]
6887    async fn nested_sum_count_rewrite_keeps_full_series_key_with_tsid_input() {
6888        assert_nested_count_rewrite_applies(
6889            "count(sum(some_metric) by (tag_0))",
6890            "Aggregate: groupBy=[[some_metric.timestamp]], aggr=[[count(Int64(1)) AS count(sum(some_metric.field_0))]]"
6891        )
6892        .await;
6893    }
6894
6895    #[tokio::test]
6896    async fn nested_supported_inner_aggs_rewrite_apply_for_tsid_input() {
6897        for (query, expected_outer_agg) in [
6898            (
6899                "count(avg(some_metric) by (tag_0))",
6900                "Aggregate: groupBy=[[some_metric.timestamp]], aggr=[[count(Int64(1)) AS count(avg(some_metric.field_0))]]",
6901            ),
6902            (
6903                "count(min(some_metric) by (tag_0))",
6904                "Aggregate: groupBy=[[some_metric.timestamp]], aggr=[[count(Int64(1)) AS count(min(some_metric.field_0))]]",
6905            ),
6906            (
6907                "count(max(some_metric) by (tag_0))",
6908                "Aggregate: groupBy=[[some_metric.timestamp]], aggr=[[count(Int64(1)) AS count(max(some_metric.field_0))]]",
6909            ),
6910            (
6911                "count(stddev(some_metric) by (tag_0))",
6912                "Aggregate: groupBy=[[some_metric.timestamp]], aggr=[[count(Int64(1)) AS count(stddev_pop(some_metric.field_0))]]",
6913            ),
6914            (
6915                "count(stdvar(some_metric) by (tag_0))",
6916                "Aggregate: groupBy=[[some_metric.timestamp]], aggr=[[count(Int64(1)) AS count(var_pop(some_metric.field_0))]]",
6917            ),
6918        ] {
6919            assert_nested_count_rewrite_applies(query, expected_outer_agg).await;
6920        }
6921    }
6922
6923    #[tokio::test]
6924    async fn nested_non_count_inner_aggs_rewrite_filter_null_values_for_tsid_input() {
6925        let count_plan =
6926            build_optimized_tsid_plan("count(count(some_metric) by (tag_0))", 2, 1, 100_000, 1)
6927                .await;
6928        assert!(
6929            !count_plan.contains("some_metric.field_0 IS NOT NULL"),
6930            "{count_plan}"
6931        );
6932
6933        for query in [
6934            "count(sum(some_metric) by (tag_0))",
6935            "count(avg(some_metric) by (tag_0))",
6936            "count(min(some_metric) by (tag_0))",
6937            "count(max(some_metric) by (tag_0))",
6938            "count(stddev(some_metric) by (tag_0))",
6939            "count(stdvar(some_metric) by (tag_0))",
6940        ] {
6941            let plan_str = build_optimized_tsid_plan(query, 2, 1, 100_000, 1).await;
6942            assert!(
6943                plan_str.contains("Filter: some_metric.field_0 IS NOT NULL"),
6944                "{query}: {plan_str}"
6945            );
6946        }
6947    }
6948
6949    #[tokio::test]
6950    async fn nested_unsupported_or_non_direct_inner_aggs_do_not_rewrite() {
6951        assert_nested_count_rewrite_missing("count(group(some_metric) by (tag_0))", 2, 1).await;
6952        assert_nested_count_rewrite_missing(
6953            "count(sum(irate(some_metric[1h])) by (tag_0))",
6954            2,
6955            300,
6956        )
6957        .await;
6958    }
6959
6960    #[tokio::test]
6961    async fn physical_table_name_is_not_leaked_in_plan() {
6962        let prom_expr = parser::parse("some_metric").unwrap();
6963        let eval_stmt = EvalStmt {
6964            expr: prom_expr,
6965            start: UNIX_EPOCH,
6966            end: UNIX_EPOCH
6967                .checked_add(Duration::from_secs(100_000))
6968                .unwrap(),
6969            interval: Duration::from_secs(5),
6970            lookback_delta: Duration::from_secs(1),
6971        };
6972
6973        let table_provider = build_test_table_provider_with_tsid(
6974            &[(DEFAULT_SCHEMA_NAME.to_string(), "some_metric".to_string())],
6975            1,
6976            1,
6977        )
6978        .await;
6979        let plan =
6980            PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
6981                .await
6982                .unwrap();
6983
6984        let plan_str = plan.display_indent_schema().to_string();
6985        assert!(plan_str.contains("TableScan: phy"), "{plan}");
6986        assert!(plan_str.contains("SubqueryAlias: some_metric"));
6987        assert!(plan_str.contains("Filter: phy.__table_id = UInt32(1024)"));
6988        assert!(!plan_str.contains("TableScan: some_metric"));
6989    }
6990
6991    #[tokio::test]
6992    async fn sum_without_does_not_group_by_tsid() {
6993        let prom_expr = parser::parse("sum without (tag_0) (some_metric)").unwrap();
6994        let eval_stmt = EvalStmt {
6995            expr: prom_expr,
6996            start: UNIX_EPOCH,
6997            end: UNIX_EPOCH
6998                .checked_add(Duration::from_secs(100_000))
6999                .unwrap(),
7000            interval: Duration::from_secs(5),
7001            lookback_delta: Duration::from_secs(1),
7002        };
7003
7004        let table_provider = build_test_table_provider_with_tsid(
7005            &[(DEFAULT_SCHEMA_NAME.to_string(), "some_metric".to_string())],
7006            1,
7007            1,
7008        )
7009        .await;
7010        let plan =
7011            PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
7012                .await
7013                .unwrap();
7014
7015        let plan_str = plan.display_indent_schema().to_string();
7016        assert!(plan_str.contains("PromSeriesDivide: tags=[\"__tsid\"]"));
7017
7018        let aggr_line = plan_str
7019            .lines()
7020            .find(|line| line.contains("Aggregate: groupBy="))
7021            .unwrap();
7022        assert!(!aggr_line.contains(DATA_SCHEMA_TSID_COLUMN_NAME));
7023    }
7024
7025    #[tokio::test]
7026    async fn topk_without_does_not_partition_by_tsid() {
7027        let prom_expr = parser::parse("topk without (tag_0) (1, some_metric)").unwrap();
7028        let eval_stmt = EvalStmt {
7029            expr: prom_expr,
7030            start: UNIX_EPOCH,
7031            end: UNIX_EPOCH
7032                .checked_add(Duration::from_secs(100_000))
7033                .unwrap(),
7034            interval: Duration::from_secs(5),
7035            lookback_delta: Duration::from_secs(1),
7036        };
7037
7038        let table_provider = build_test_table_provider_with_tsid(
7039            &[(DEFAULT_SCHEMA_NAME.to_string(), "some_metric".to_string())],
7040            1,
7041            1,
7042        )
7043        .await;
7044        let plan =
7045            PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
7046                .await
7047                .unwrap();
7048
7049        let plan_str = plan.display_indent_schema().to_string();
7050        assert!(plan_str.contains("PromSeriesDivide: tags=[\"__tsid\"]"));
7051
7052        let window_line = plan_str
7053            .lines()
7054            .find(|line| line.contains("WindowAggr: windowExpr=[[row_number()"))
7055            .unwrap();
7056        let partition_by = window_line
7057            .split("PARTITION BY [")
7058            .nth(1)
7059            .and_then(|s| s.split("] ORDER BY").next())
7060            .unwrap();
7061        assert!(!partition_by.contains(DATA_SCHEMA_TSID_COLUMN_NAME));
7062    }
7063
7064    #[tokio::test]
7065    async fn sum_by_does_not_group_by_tsid() {
7066        let prom_expr = parser::parse("sum by (__tsid) (some_metric)").unwrap();
7067        let eval_stmt = EvalStmt {
7068            expr: prom_expr,
7069            start: UNIX_EPOCH,
7070            end: UNIX_EPOCH
7071                .checked_add(Duration::from_secs(100_000))
7072                .unwrap(),
7073            interval: Duration::from_secs(5),
7074            lookback_delta: Duration::from_secs(1),
7075        };
7076
7077        let table_provider = build_test_table_provider_with_tsid(
7078            &[(DEFAULT_SCHEMA_NAME.to_string(), "some_metric".to_string())],
7079            1,
7080            1,
7081        )
7082        .await;
7083        let plan =
7084            PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
7085                .await
7086                .unwrap();
7087
7088        let plan_str = plan.display_indent_schema().to_string();
7089        assert!(plan_str.contains("PromSeriesDivide: tags=[\"__tsid\"]"));
7090
7091        let aggr_line = plan_str
7092            .lines()
7093            .find(|line| line.contains("Aggregate: groupBy="))
7094            .unwrap();
7095        assert!(!aggr_line.contains(DATA_SCHEMA_TSID_COLUMN_NAME));
7096    }
7097
7098    #[tokio::test]
7099    async fn aggregate_over_binary_time_function_expr() {
7100        for op in ["sum", "min", "max", "avg"] {
7101            let prom_expr = parser::parse(&format!(
7102                "{op} by (tag_0, tag_1, tag_2) (time() - some_metric)"
7103            ))
7104            .unwrap();
7105            let eval_stmt = EvalStmt {
7106                expr: prom_expr,
7107                start: UNIX_EPOCH,
7108                end: UNIX_EPOCH
7109                    .checked_add(Duration::from_secs(100_000))
7110                    .unwrap(),
7111                interval: Duration::from_secs(5),
7112                lookback_delta: Duration::from_secs(1),
7113            };
7114
7115            let table_provider = build_test_table_provider_with_tsid(
7116                &[(DEFAULT_SCHEMA_NAME.to_string(), "some_metric".to_string())],
7117                3,
7118                1,
7119            )
7120            .await;
7121            let plan =
7122                PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
7123                    .await
7124                    .unwrap();
7125
7126            let plan_str = plan.display_indent_schema().to_string();
7127            let aggr_line = plan_str
7128                .lines()
7129                .find(|line| line.contains("Aggregate: groupBy="))
7130                .unwrap();
7131            assert!(aggr_line.contains(op), "{plan_str}");
7132            assert!(aggr_line.contains("first_value"), "{plan_str}");
7133            assert!(
7134                !plan
7135                    .schema()
7136                    .fields()
7137                    .iter()
7138                    .any(|field| { field.name() == DATA_SCHEMA_TSID_COLUMN_NAME })
7139            );
7140        }
7141    }
7142
7143    #[tokio::test]
7144    async fn topk_by_does_not_partition_by_tsid() {
7145        let prom_expr = parser::parse("topk by (__tsid) (1, some_metric)").unwrap();
7146        let eval_stmt = EvalStmt {
7147            expr: prom_expr,
7148            start: UNIX_EPOCH,
7149            end: UNIX_EPOCH
7150                .checked_add(Duration::from_secs(100_000))
7151                .unwrap(),
7152            interval: Duration::from_secs(5),
7153            lookback_delta: Duration::from_secs(1),
7154        };
7155
7156        let table_provider = build_test_table_provider_with_tsid(
7157            &[(DEFAULT_SCHEMA_NAME.to_string(), "some_metric".to_string())],
7158            1,
7159            1,
7160        )
7161        .await;
7162        let plan =
7163            PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
7164                .await
7165                .unwrap();
7166
7167        let plan_str = plan.display_indent_schema().to_string();
7168        assert!(plan_str.contains("PromSeriesDivide: tags=[\"__tsid\"]"));
7169
7170        let window_line = plan_str
7171            .lines()
7172            .find(|line| line.contains("WindowAggr: windowExpr=[[row_number()"))
7173            .unwrap();
7174        let partition_by = window_line
7175            .split("PARTITION BY [")
7176            .nth(1)
7177            .and_then(|s| s.split("] ORDER BY").next())
7178            .unwrap();
7179        assert!(!partition_by.contains(DATA_SCHEMA_TSID_COLUMN_NAME));
7180    }
7181
7182    #[tokio::test]
7183    async fn selector_matcher_on_tsid_does_not_use_internal_column() {
7184        let prom_expr = parser::parse(r#"some_metric{__tsid="123"}"#).unwrap();
7185        let eval_stmt = EvalStmt {
7186            expr: prom_expr,
7187            start: UNIX_EPOCH,
7188            end: UNIX_EPOCH
7189                .checked_add(Duration::from_secs(100_000))
7190                .unwrap(),
7191            interval: Duration::from_secs(5),
7192            lookback_delta: Duration::from_secs(1),
7193        };
7194
7195        let table_provider = build_test_table_provider_with_tsid(
7196            &[(DEFAULT_SCHEMA_NAME.to_string(), "some_metric".to_string())],
7197            1,
7198            1,
7199        )
7200        .await;
7201        let plan =
7202            PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
7203                .await
7204                .unwrap();
7205
7206        fn collect_filter_cols(plan: &LogicalPlan, out: &mut HashSet<Column>) {
7207            if let LogicalPlan::Filter(filter) = plan {
7208                datafusion_expr::utils::expr_to_columns(&filter.predicate, out).unwrap();
7209            }
7210            for input in plan.inputs() {
7211                collect_filter_cols(input, out);
7212            }
7213        }
7214
7215        let mut filter_cols = HashSet::new();
7216        collect_filter_cols(&plan, &mut filter_cols);
7217        assert!(
7218            !filter_cols
7219                .iter()
7220                .any(|c| c.name == DATA_SCHEMA_TSID_COLUMN_NAME)
7221        );
7222    }
7223
7224    #[tokio::test]
7225    async fn tsid_is_not_used_when_physical_table_is_missing() {
7226        let prom_expr = parser::parse("some_metric").unwrap();
7227        let eval_stmt = EvalStmt {
7228            expr: prom_expr,
7229            start: UNIX_EPOCH,
7230            end: UNIX_EPOCH
7231                .checked_add(Duration::from_secs(100_000))
7232                .unwrap(),
7233            interval: Duration::from_secs(5),
7234            lookback_delta: Duration::from_secs(1),
7235        };
7236
7237        let catalog_list = MemoryCatalogManager::with_default_setup();
7238
7239        // Register a metric engine logical table referencing a missing physical table.
7240        let mut columns = vec![ColumnSchema::new(
7241            "tag_0".to_string(),
7242            ConcreteDataType::string_datatype(),
7243            false,
7244        )];
7245        columns.push(
7246            ColumnSchema::new(
7247                "timestamp".to_string(),
7248                ConcreteDataType::timestamp_millisecond_datatype(),
7249                false,
7250            )
7251            .with_time_index(true),
7252        );
7253        columns.push(ColumnSchema::new(
7254            "field_0".to_string(),
7255            ConcreteDataType::float64_datatype(),
7256            true,
7257        ));
7258        let schema = Arc::new(Schema::new(columns));
7259        let mut options = table::requests::TableOptions::default();
7260        options
7261            .extra_options
7262            .insert(LOGICAL_TABLE_METADATA_KEY.to_string(), "phy".to_string());
7263        let table_meta = TableMetaBuilder::empty()
7264            .schema(schema)
7265            .primary_key_indices(vec![0])
7266            .value_indices(vec![2])
7267            .engine(METRIC_ENGINE_NAME.to_string())
7268            .options(options)
7269            .next_column_id(1024)
7270            .build()
7271            .unwrap();
7272        let table_info = TableInfoBuilder::default()
7273            .table_id(1024)
7274            .name("some_metric")
7275            .meta(table_meta)
7276            .build()
7277            .unwrap();
7278        let table = EmptyTable::from_table_info(&table_info);
7279        catalog_list
7280            .register_table_sync(RegisterTableRequest {
7281                catalog: DEFAULT_CATALOG_NAME.to_string(),
7282                schema: DEFAULT_SCHEMA_NAME.to_string(),
7283                table_name: "some_metric".to_string(),
7284                table_id: 1024,
7285                table,
7286            })
7287            .unwrap();
7288
7289        let table_provider = DfTableSourceProvider::new(
7290            catalog_list,
7291            false,
7292            QueryContext::arc(),
7293            DummyDecoder::arc(),
7294            false,
7295        );
7296
7297        let plan =
7298            PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
7299                .await
7300                .unwrap();
7301
7302        let plan_str = plan.display_indent_schema().to_string();
7303        assert!(plan_str.contains("PromSeriesDivide: tags=[\"tag_0\"]"));
7304        assert!(!plan_str.contains("PromSeriesDivide: tags=[\"__tsid\"]"));
7305    }
7306
7307    #[tokio::test]
7308    async fn tsid_is_carried_only_when_aggregate_preserves_label_set() {
7309        let prom_expr = parser::parse("sum by (tag_0) (some_metric)").unwrap();
7310        let eval_stmt = EvalStmt {
7311            expr: prom_expr,
7312            start: UNIX_EPOCH,
7313            end: UNIX_EPOCH
7314                .checked_add(Duration::from_secs(100_000))
7315                .unwrap(),
7316            interval: Duration::from_secs(5),
7317            lookback_delta: Duration::from_secs(1),
7318        };
7319
7320        let table_provider = build_test_table_provider_with_tsid(
7321            &[(DEFAULT_SCHEMA_NAME.to_string(), "some_metric".to_string())],
7322            1,
7323            1,
7324        )
7325        .await;
7326        let plan =
7327            PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
7328                .await
7329                .unwrap();
7330
7331        let plan_str = plan.display_indent_schema().to_string();
7332        assert!(plan_str.contains("first_value") && plan_str.contains("__tsid"));
7333        assert!(
7334            !plan
7335                .schema()
7336                .fields()
7337                .iter()
7338                .any(|field| field.name() == DATA_SCHEMA_TSID_COLUMN_NAME)
7339        );
7340
7341        // Merging aggregate: label set is reduced, tsid should not be carried.
7342        let prom_expr = parser::parse("sum(some_metric)").unwrap();
7343        let eval_stmt = EvalStmt {
7344            expr: prom_expr,
7345            start: UNIX_EPOCH,
7346            end: UNIX_EPOCH
7347                .checked_add(Duration::from_secs(100_000))
7348                .unwrap(),
7349            interval: Duration::from_secs(5),
7350            lookback_delta: Duration::from_secs(1),
7351        };
7352        let table_provider = build_test_table_provider_with_tsid(
7353            &[(DEFAULT_SCHEMA_NAME.to_string(), "some_metric".to_string())],
7354            1,
7355            1,
7356        )
7357        .await;
7358        let plan =
7359            PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
7360                .await
7361                .unwrap();
7362        let plan_str = plan.display_indent_schema().to_string();
7363        assert!(!plan_str.contains("first_value"));
7364    }
7365
7366    #[tokio::test]
7367    async fn or_operator_with_unknown_metric_does_not_require_tsid() {
7368        let prom_expr = parser::parse("unknown_metric or some_metric").unwrap();
7369        let eval_stmt = EvalStmt {
7370            expr: prom_expr,
7371            start: UNIX_EPOCH,
7372            end: UNIX_EPOCH
7373                .checked_add(Duration::from_secs(100_000))
7374                .unwrap(),
7375            interval: Duration::from_secs(5),
7376            lookback_delta: Duration::from_secs(1),
7377        };
7378
7379        let table_provider = build_test_table_provider_with_tsid(
7380            &[(DEFAULT_SCHEMA_NAME.to_string(), "some_metric".to_string())],
7381            1,
7382            1,
7383        )
7384        .await;
7385
7386        let plan =
7387            PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
7388                .await
7389                .unwrap();
7390
7391        assert!(
7392            !plan
7393                .schema()
7394                .fields()
7395                .iter()
7396                .any(|field| field.name() == DATA_SCHEMA_TSID_COLUMN_NAME)
7397        );
7398    }
7399
7400    #[tokio::test]
7401    async fn aggregate_avg() {
7402        do_aggregate_expr_plan("avg", "avg").await;
7403    }
7404
7405    #[tokio::test]
7406    #[should_panic] // output type doesn't match
7407    async fn aggregate_count() {
7408        do_aggregate_expr_plan("count", "count").await;
7409    }
7410
7411    #[tokio::test]
7412    async fn aggregate_min() {
7413        do_aggregate_expr_plan("min", "min").await;
7414    }
7415
7416    #[tokio::test]
7417    async fn aggregate_max() {
7418        do_aggregate_expr_plan("max", "max").await;
7419    }
7420
7421    #[tokio::test]
7422    async fn aggregate_group() {
7423        // Regression test for `group()` aggregator.
7424        // PromQL: sum(group by (cluster)(kubernetes_build_info{service="kubernetes",job="apiserver"}))
7425        // should be plannable, and `group()` should produce constant 1 for each group.
7426        let prom_expr = parser::parse(
7427            "sum(group by (cluster)(kubernetes_build_info{service=\"kubernetes\",job=\"apiserver\"}))",
7428        )
7429        .unwrap();
7430        let eval_stmt = EvalStmt {
7431            expr: prom_expr,
7432            start: UNIX_EPOCH,
7433            end: UNIX_EPOCH
7434                .checked_add(Duration::from_secs(100_000))
7435                .unwrap(),
7436            interval: Duration::from_secs(5),
7437            lookback_delta: Duration::from_secs(1),
7438        };
7439
7440        let table_provider = build_test_table_provider_with_fields(
7441            &[(
7442                DEFAULT_SCHEMA_NAME.to_string(),
7443                "kubernetes_build_info".to_string(),
7444            )],
7445            &["cluster", "service", "job"],
7446        )
7447        .await;
7448        let plan =
7449            PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
7450                .await
7451                .unwrap();
7452
7453        let plan_str = plan.display_indent_schema().to_string();
7454        assert!(plan_str.contains("max(Float64(1"));
7455    }
7456
7457    #[tokio::test]
7458    async fn aggregate_stddev() {
7459        do_aggregate_expr_plan("stddev", "stddev_pop").await;
7460    }
7461
7462    #[tokio::test]
7463    async fn aggregate_stdvar() {
7464        do_aggregate_expr_plan("stdvar", "var_pop").await;
7465    }
7466
7467    // TODO(ruihang): add range fn tests once exprs are ready.
7468
7469    // {
7470    //     input: "some_metric{tag_0="foo"} + some_metric{tag_0="bar"}",
7471    //     expected: &BinaryExpr{
7472    //         Op: ADD,
7473    //         LHS: &VectorSelector{
7474    //             Name: "a",
7475    //             LabelMatchers: []*labels.Matcher{
7476    //                     MustLabelMatcher(labels.MatchEqual, "tag_0", "foo"),
7477    //                     MustLabelMatcher(labels.MatchEqual, model.MetricNameLabel, "some_metric"),
7478    //             },
7479    //         },
7480    //         RHS: &VectorSelector{
7481    //             Name: "sum",
7482    //             LabelMatchers: []*labels.Matcher{
7483    //                     MustLabelMatcher(labels.MatchxEqual, "tag_0", "bar"),
7484    //                     MustLabelMatcher(labels.MatchEqual, model.MetricNameLabel, "some_metric"),
7485    //             },
7486    //         },
7487    //         VectorMatching: &VectorMatching{},
7488    //     },
7489    // },
7490    #[tokio::test]
7491    async fn binary_op_column_column() {
7492        let prom_expr =
7493            parser::parse(r#"some_metric{tag_0="foo"} + some_metric{tag_0="bar"}"#).unwrap();
7494        let eval_stmt = EvalStmt {
7495            expr: prom_expr,
7496            start: UNIX_EPOCH,
7497            end: UNIX_EPOCH
7498                .checked_add(Duration::from_secs(100_000))
7499                .unwrap(),
7500            interval: Duration::from_secs(5),
7501            lookback_delta: Duration::from_secs(1),
7502        };
7503
7504        let table_provider = build_test_table_provider(
7505            &[(DEFAULT_SCHEMA_NAME.to_string(), "some_metric".to_string())],
7506            1,
7507            1,
7508        )
7509        .await;
7510        let plan =
7511            PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
7512                .await
7513                .unwrap();
7514
7515        let expected = String::from(
7516            "Projection: rhs.tag_0, rhs.timestamp, CAST(lhs.field_0 AS Float64) + CAST(rhs.field_0 AS Float64) AS lhs.field_0 + rhs.field_0 [tag_0:Utf8, timestamp:Timestamp(ms), lhs.field_0 + rhs.field_0:Float64;N]\
7517            \n  Inner Join: lhs.tag_0 = rhs.tag_0, lhs.timestamp = rhs.timestamp [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N, tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
7518            \n    SubqueryAlias: lhs [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
7519            \n      PromInstantManipulate: range=[0..100000000], lookback=[1000], interval=[5000], time index=[timestamp] [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
7520            \n        PromSeriesDivide: tags=[\"tag_0\"] [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
7521            \n          Sort: some_metric.tag_0 ASC NULLS FIRST, some_metric.timestamp ASC NULLS FIRST [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
7522            \n            Filter: some_metric.tag_0 = Utf8(\"foo\") AND some_metric.timestamp >= TimestampMillisecond(-999, None) AND some_metric.timestamp <= TimestampMillisecond(100000000, None) [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
7523            \n              TableScan: some_metric [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
7524            \n    SubqueryAlias: rhs [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
7525            \n      PromInstantManipulate: range=[0..100000000], lookback=[1000], interval=[5000], time index=[timestamp] [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
7526            \n        PromSeriesDivide: tags=[\"tag_0\"] [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
7527            \n          Sort: some_metric.tag_0 ASC NULLS FIRST, some_metric.timestamp ASC NULLS FIRST [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
7528            \n            Filter: some_metric.tag_0 = Utf8(\"bar\") AND some_metric.timestamp >= TimestampMillisecond(-999, None) AND some_metric.timestamp <= TimestampMillisecond(100000000, None) [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
7529            \n              TableScan: some_metric [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]",
7530        );
7531
7532        assert_eq!(plan.display_indent_schema().to_string(), expected);
7533    }
7534
7535    async fn indie_query_plan_compare<T: AsRef<str>>(query: &str, expected: T) {
7536        let prom_expr = parser::parse(query).unwrap();
7537        let eval_stmt = EvalStmt {
7538            expr: prom_expr,
7539            start: UNIX_EPOCH,
7540            end: UNIX_EPOCH
7541                .checked_add(Duration::from_secs(100_000))
7542                .unwrap(),
7543            interval: Duration::from_secs(5),
7544            lookback_delta: Duration::from_secs(1),
7545        };
7546
7547        let table_provider = build_test_table_provider(
7548            &[
7549                (DEFAULT_SCHEMA_NAME.to_string(), "some_metric".to_string()),
7550                (
7551                    "greptime_private".to_string(),
7552                    "some_alt_metric".to_string(),
7553                ),
7554            ],
7555            1,
7556            1,
7557        )
7558        .await;
7559        let plan =
7560            PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
7561                .await
7562                .unwrap();
7563
7564        assert_eq!(plan.display_indent_schema().to_string(), expected.as_ref());
7565    }
7566
7567    #[tokio::test]
7568    async fn binary_op_literal_column() {
7569        let query = r#"1 + some_metric{tag_0="bar"}"#;
7570        let expected = String::from(
7571            "Projection: some_metric.tag_0, some_metric.timestamp, Float64(1) + CAST(some_metric.field_0 AS Float64) AS Float64(1) + field_0 [tag_0:Utf8, timestamp:Timestamp(ms), Float64(1) + field_0:Float64;N]\
7572            \n  PromInstantManipulate: range=[0..100000000], lookback=[1000], interval=[5000], time index=[timestamp] [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
7573            \n    PromSeriesDivide: tags=[\"tag_0\"] [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
7574            \n      Sort: some_metric.tag_0 ASC NULLS FIRST, some_metric.timestamp ASC NULLS FIRST [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
7575            \n        Filter: some_metric.tag_0 = Utf8(\"bar\") AND some_metric.timestamp >= TimestampMillisecond(-999, None) AND some_metric.timestamp <= TimestampMillisecond(100000000, None) [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
7576            \n          TableScan: some_metric [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]",
7577        );
7578
7579        indie_query_plan_compare(query, expected).await;
7580    }
7581
7582    #[tokio::test]
7583    async fn binary_op_literal_literal() {
7584        let query = r#"1 + 1"#;
7585        let expected = r#"EmptyMetric: range=[0..100000000], interval=[5000] [time:Timestamp(ms), value:Float64;N]
7586  TableScan: dummy [time:Timestamp(ms), value:Float64;N]"#;
7587        indie_query_plan_compare(query, expected).await;
7588    }
7589
7590    #[tokio::test]
7591    async fn simple_bool_grammar() {
7592        let query = "some_metric != bool 1.2345";
7593        let expected = String::from(
7594            "Projection: some_metric.tag_0, some_metric.timestamp, CAST(some_metric.field_0 != Float64(1.2345) AS Float64) AS field_0 != Float64(1.2345) [tag_0:Utf8, timestamp:Timestamp(ms), field_0 != Float64(1.2345):Float64;N]\
7595            \n  PromInstantManipulate: range=[0..100000000], lookback=[1000], interval=[5000], time index=[timestamp] [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
7596            \n    PromSeriesDivide: tags=[\"tag_0\"] [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
7597            \n      Sort: some_metric.tag_0 ASC NULLS FIRST, some_metric.timestamp ASC NULLS FIRST [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
7598            \n        Filter: some_metric.timestamp >= TimestampMillisecond(-999, None) AND some_metric.timestamp <= TimestampMillisecond(100000000, None) [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
7599            \n          TableScan: some_metric [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]",
7600        );
7601
7602        indie_query_plan_compare(query, expected).await;
7603    }
7604
7605    #[tokio::test]
7606    async fn bool_with_additional_arithmetic() {
7607        let query = "some_metric + (1 == bool 2)";
7608        let expected = String::from(
7609            "Projection: some_metric.tag_0, some_metric.timestamp, CAST(some_metric.field_0 AS Float64) + CAST(Float64(1) = Float64(2) AS Float64) AS field_0 + Float64(1) = Float64(2) [tag_0:Utf8, timestamp:Timestamp(ms), field_0 + Float64(1) = Float64(2):Float64;N]\
7610            \n  PromInstantManipulate: range=[0..100000000], lookback=[1000], interval=[5000], time index=[timestamp] [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
7611            \n    PromSeriesDivide: tags=[\"tag_0\"] [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
7612            \n      Sort: some_metric.tag_0 ASC NULLS FIRST, some_metric.timestamp ASC NULLS FIRST [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
7613            \n        Filter: some_metric.timestamp >= TimestampMillisecond(-999, None) AND some_metric.timestamp <= TimestampMillisecond(100000000, None) [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
7614            \n          TableScan: some_metric [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]",
7615        );
7616
7617        indie_query_plan_compare(query, expected).await;
7618    }
7619
7620    #[tokio::test]
7621    async fn simple_unary() {
7622        let query = "-some_metric";
7623        let expected = String::from(
7624            "Projection: some_metric.tag_0, some_metric.timestamp, (- some_metric.field_0) AS (- field_0) [tag_0:Utf8, timestamp:Timestamp(ms), (- field_0):Float64;N]\
7625            \n  PromInstantManipulate: range=[0..100000000], lookback=[1000], interval=[5000], time index=[timestamp] [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
7626            \n    PromSeriesDivide: tags=[\"tag_0\"] [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
7627            \n      Sort: some_metric.tag_0 ASC NULLS FIRST, some_metric.timestamp ASC NULLS FIRST [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
7628            \n        Filter: some_metric.timestamp >= TimestampMillisecond(-999, None) AND some_metric.timestamp <= TimestampMillisecond(100000000, None) [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
7629            \n          TableScan: some_metric [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]",
7630        );
7631
7632        indie_query_plan_compare(query, expected).await;
7633    }
7634
7635    #[tokio::test]
7636    async fn increase_aggr() {
7637        let query = "increase(some_metric[5m])";
7638        let expected = String::from(
7639            "Filter: prom_increase(timestamp_range,field_0,timestamp,Int64(300000)) IS NOT NULL [timestamp:Timestamp(ms), prom_increase(timestamp_range,field_0,timestamp,Int64(300000)):Float64;N, tag_0:Utf8]\
7640            \n  Projection: some_metric.timestamp, prom_increase(timestamp_range, field_0, some_metric.timestamp, Int64(300000)) AS prom_increase(timestamp_range,field_0,timestamp,Int64(300000)), some_metric.tag_0 [timestamp:Timestamp(ms), prom_increase(timestamp_range,field_0,timestamp,Int64(300000)):Float64;N, tag_0:Utf8]\
7641            \n    PromRangeManipulate: req range=[0..100000000], interval=[5000], eval range=[300000], time index=[timestamp], values=[\"field_0\"] [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Dictionary(Int64, Float64);N, timestamp_range:Dictionary(Int64, Timestamp(ms))]\
7642            \n      PromSeriesNormalize: offset=[0], time index=[timestamp], filter NaN: [true] [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
7643            \n        PromSeriesDivide: tags=[\"tag_0\"] [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
7644            \n          Sort: some_metric.tag_0 ASC NULLS FIRST, some_metric.timestamp ASC NULLS FIRST [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
7645            \n            Filter: some_metric.timestamp >= TimestampMillisecond(-299999, None) AND some_metric.timestamp <= TimestampMillisecond(100000000, None) [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
7646            \n              TableScan: some_metric [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]",
7647        );
7648
7649        indie_query_plan_compare(query, expected).await;
7650    }
7651
7652    #[tokio::test]
7653    async fn less_filter_on_value() {
7654        let query = "some_metric < 1.2345";
7655        let expected = String::from(
7656            "Filter: some_metric.field_0 < Float64(1.2345) [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
7657            \n  PromInstantManipulate: range=[0..100000000], lookback=[1000], interval=[5000], time index=[timestamp] [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
7658            \n    PromSeriesDivide: tags=[\"tag_0\"] [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
7659            \n      Sort: some_metric.tag_0 ASC NULLS FIRST, some_metric.timestamp ASC NULLS FIRST [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
7660            \n        Filter: some_metric.timestamp >= TimestampMillisecond(-999, None) AND some_metric.timestamp <= TimestampMillisecond(100000000, None) [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
7661            \n          TableScan: some_metric [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]",
7662        );
7663
7664        indie_query_plan_compare(query, expected).await;
7665    }
7666
7667    #[tokio::test]
7668    async fn count_over_time() {
7669        let query = "count_over_time(some_metric[5m])";
7670        let expected = String::from(
7671            "Filter: prom_count_over_time(timestamp_range,field_0) IS NOT NULL [timestamp:Timestamp(ms), prom_count_over_time(timestamp_range,field_0):Float64;N, tag_0:Utf8]\
7672            \n  Projection: some_metric.timestamp, prom_count_over_time(timestamp_range, field_0) AS prom_count_over_time(timestamp_range,field_0), some_metric.tag_0 [timestamp:Timestamp(ms), prom_count_over_time(timestamp_range,field_0):Float64;N, tag_0:Utf8]\
7673            \n    PromRangeManipulate: req range=[0..100000000], interval=[5000], eval range=[300000], time index=[timestamp], values=[\"field_0\"] [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Dictionary(Int64, Float64);N, timestamp_range:Dictionary(Int64, Timestamp(ms))]\
7674            \n      PromSeriesNormalize: offset=[0], time index=[timestamp], filter NaN: [true] [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
7675            \n        PromSeriesDivide: tags=[\"tag_0\"] [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
7676            \n          Sort: some_metric.tag_0 ASC NULLS FIRST, some_metric.timestamp ASC NULLS FIRST [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
7677            \n            Filter: some_metric.timestamp >= TimestampMillisecond(-299999, None) AND some_metric.timestamp <= TimestampMillisecond(100000000, None) [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
7678            \n              TableScan: some_metric [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]",
7679        );
7680
7681        indie_query_plan_compare(query, expected).await;
7682    }
7683
7684    /// The outer `PromRangeManipulate` from a subquery must be preceded by
7685    /// `Sort` + `PromSeriesDivide`.
7686    #[tokio::test]
7687    async fn count_over_time_subquery() {
7688        let query = "count_over_time(some_metric[10m:1m])";
7689        let expected = String::from(
7690            "Filter: prom_count_over_time(timestamp_range,field_0) IS NOT NULL [timestamp:Timestamp(ms), prom_count_over_time(timestamp_range,field_0):Float64;N, tag_0:Utf8]\
7691            \n  Projection: some_metric.timestamp, prom_count_over_time(timestamp_range, field_0) AS prom_count_over_time(timestamp_range,field_0), some_metric.tag_0 [timestamp:Timestamp(ms), prom_count_over_time(timestamp_range,field_0):Float64;N, tag_0:Utf8]\
7692            \n    PromRangeManipulate: req range=[0..100000000], interval=[5000], eval range=[600000], time index=[timestamp], values=[\"field_0\"] [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Dictionary(Int64, Float64);N, timestamp_range:Dictionary(Int64, Timestamp(ms))]\
7693            \n      PromSeriesDivide: tags=[\"tag_0\"] [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
7694            \n        Sort: some_metric.tag_0 ASC NULLS FIRST, some_metric.timestamp ASC NULLS FIRST [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
7695            \n          PromInstantManipulate: range=[-540000..100000000], lookback=[1000], interval=[60000], time index=[timestamp] [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
7696            \n            PromSeriesDivide: tags=[\"tag_0\"] [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
7697            \n              Sort: some_metric.tag_0 ASC NULLS FIRST, some_metric.timestamp ASC NULLS FIRST [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
7698            \n                Filter: some_metric.timestamp >= TimestampMillisecond(-540999, None) AND some_metric.timestamp <= TimestampMillisecond(100000000, None) [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
7699            \n                  TableScan: some_metric [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]",
7700        );
7701        indie_query_plan_compare(query, expected).await;
7702    }
7703
7704    #[tokio::test]
7705    async fn test_hash_join() {
7706        let mut eval_stmt = EvalStmt {
7707            expr: PromExpr::NumberLiteral(NumberLiteral { val: 1.0 }),
7708            start: UNIX_EPOCH,
7709            end: UNIX_EPOCH
7710                .checked_add(Duration::from_secs(100_000))
7711                .unwrap(),
7712            interval: Duration::from_secs(5),
7713            lookback_delta: Duration::from_secs(1),
7714        };
7715
7716        let case = r#"http_server_requests_seconds_sum{uri="/accounts/login"} / ignoring(kubernetes_pod_name,kubernetes_namespace) http_server_requests_seconds_count{uri="/accounts/login"}"#;
7717
7718        let prom_expr = parser::parse(case).unwrap();
7719        eval_stmt.expr = prom_expr;
7720        let table_provider = build_test_table_provider_with_fields(
7721            &[
7722                (
7723                    DEFAULT_SCHEMA_NAME.to_string(),
7724                    "http_server_requests_seconds_sum".to_string(),
7725                ),
7726                (
7727                    DEFAULT_SCHEMA_NAME.to_string(),
7728                    "http_server_requests_seconds_count".to_string(),
7729                ),
7730            ],
7731            &["uri", "kubernetes_namespace", "kubernetes_pod_name"],
7732        )
7733        .await;
7734        // Should be ok
7735        let plan =
7736            PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
7737                .await
7738                .unwrap();
7739        let expected = "Projection: http_server_requests_seconds_count.uri, http_server_requests_seconds_count.kubernetes_namespace, http_server_requests_seconds_count.kubernetes_pod_name, http_server_requests_seconds_count.greptime_timestamp, CAST(http_server_requests_seconds_sum.greptime_value AS Float64) / CAST(http_server_requests_seconds_count.greptime_value AS Float64) AS http_server_requests_seconds_sum.greptime_value / http_server_requests_seconds_count.greptime_value\
7740            \n  Inner Join: http_server_requests_seconds_sum.greptime_timestamp = http_server_requests_seconds_count.greptime_timestamp, http_server_requests_seconds_sum.uri = http_server_requests_seconds_count.uri\
7741            \n    SubqueryAlias: http_server_requests_seconds_sum\
7742            \n      PromInstantManipulate: range=[0..100000000], lookback=[1000], interval=[5000], time index=[greptime_timestamp]\
7743            \n        PromSeriesDivide: tags=[\"uri\", \"kubernetes_namespace\", \"kubernetes_pod_name\"]\
7744            \n          Sort: http_server_requests_seconds_sum.uri ASC NULLS FIRST, http_server_requests_seconds_sum.kubernetes_namespace ASC NULLS FIRST, http_server_requests_seconds_sum.kubernetes_pod_name ASC NULLS FIRST, http_server_requests_seconds_sum.greptime_timestamp ASC NULLS FIRST\
7745            \n            Filter: http_server_requests_seconds_sum.uri = Utf8(\"/accounts/login\") AND http_server_requests_seconds_sum.greptime_timestamp >= TimestampMillisecond(-999, None) AND http_server_requests_seconds_sum.greptime_timestamp <= TimestampMillisecond(100000000, None)\
7746            \n              TableScan: http_server_requests_seconds_sum\
7747            \n    SubqueryAlias: http_server_requests_seconds_count\
7748            \n      PromInstantManipulate: range=[0..100000000], lookback=[1000], interval=[5000], time index=[greptime_timestamp]\
7749            \n        PromSeriesDivide: tags=[\"uri\", \"kubernetes_namespace\", \"kubernetes_pod_name\"]\
7750            \n          Sort: http_server_requests_seconds_count.uri ASC NULLS FIRST, http_server_requests_seconds_count.kubernetes_namespace ASC NULLS FIRST, http_server_requests_seconds_count.kubernetes_pod_name ASC NULLS FIRST, http_server_requests_seconds_count.greptime_timestamp ASC NULLS FIRST\
7751            \n            Filter: http_server_requests_seconds_count.uri = Utf8(\"/accounts/login\") AND http_server_requests_seconds_count.greptime_timestamp >= TimestampMillisecond(-999, None) AND http_server_requests_seconds_count.greptime_timestamp <= TimestampMillisecond(100000000, None)\
7752            \n              TableScan: http_server_requests_seconds_count";
7753        assert_eq!(plan.to_string(), expected);
7754    }
7755
7756    #[tokio::test]
7757    async fn test_nested_histogram_quantile() {
7758        let mut eval_stmt = EvalStmt {
7759            expr: PromExpr::NumberLiteral(NumberLiteral { val: 1.0 }),
7760            start: UNIX_EPOCH,
7761            end: UNIX_EPOCH
7762                .checked_add(Duration::from_secs(100_000))
7763                .unwrap(),
7764            interval: Duration::from_secs(5),
7765            lookback_delta: Duration::from_secs(1),
7766        };
7767
7768        let case = r#"label_replace(histogram_quantile(0.99, sum by(pod, le, path, code) (rate(greptime_servers_grpc_requests_elapsed_bucket{container="frontend"}[1m0s]))), "pod_new", "$1", "pod", "greptimedb-frontend-[0-9a-z]*-(.*)")"#;
7769
7770        let prom_expr = parser::parse(case).unwrap();
7771        eval_stmt.expr = prom_expr;
7772        let table_provider = build_test_table_provider_with_fields(
7773            &[(
7774                DEFAULT_SCHEMA_NAME.to_string(),
7775                "greptime_servers_grpc_requests_elapsed_bucket".to_string(),
7776            )],
7777            &["pod", "le", "path", "code", "container"],
7778        )
7779        .await;
7780        // Should be ok
7781        let _ = PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
7782            .await
7783            .unwrap();
7784    }
7785
7786    #[tokio::test]
7787    async fn test_histogram_quantile_binary_op() {
7788        let mut eval_stmt = EvalStmt {
7789            expr: PromExpr::NumberLiteral(NumberLiteral { val: 1.0 }),
7790            start: UNIX_EPOCH,
7791            end: UNIX_EPOCH
7792                .checked_add(Duration::from_secs(100_000))
7793                .unwrap(),
7794            interval: Duration::from_secs(5),
7795            lookback_delta: Duration::from_secs(1),
7796        };
7797
7798        // Arithmetic applied to a histogram_quantile() result. Regression for #8144:
7799        // HistogramFold used to drop the input column qualifiers, so the binary-op
7800        // projection failed to resolve the qualified tag column.
7801        let case = r#"histogram_quantile(0.5, sum by (le, pod) (rate(http_request_duration_seconds_bucket[5m]))) + 0"#;
7802
7803        let prom_expr = parser::parse(case).unwrap();
7804        eval_stmt.expr = prom_expr;
7805        let table_provider = build_test_table_provider_with_fields(
7806            &[(
7807                DEFAULT_SCHEMA_NAME.to_string(),
7808                "http_request_duration_seconds_bucket".to_string(),
7809            )],
7810            &["pod", "le"],
7811        )
7812        .await;
7813        // Should plan without a "No field named ..." error.
7814        let _ = PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
7815            .await
7816            .unwrap();
7817    }
7818
7819    #[tokio::test]
7820    async fn test_parse_and_operator() {
7821        let mut eval_stmt = EvalStmt {
7822            expr: PromExpr::NumberLiteral(NumberLiteral { val: 1.0 }),
7823            start: UNIX_EPOCH,
7824            end: UNIX_EPOCH
7825                .checked_add(Duration::from_secs(100_000))
7826                .unwrap(),
7827            interval: Duration::from_secs(5),
7828            lookback_delta: Duration::from_secs(1),
7829        };
7830
7831        let cases = [
7832            r#"count (max by (persistentvolumeclaim,namespace) (kubelet_volume_stats_used_bytes{namespace=~".+"} ) and (max by (persistentvolumeclaim,namespace) (kubelet_volume_stats_used_bytes{namespace=~".+"} )) / (max by (persistentvolumeclaim,namespace) (kubelet_volume_stats_capacity_bytes{namespace=~".+"} )) >= (80 / 100)) or vector (0)"#,
7833            r#"count (max by (persistentvolumeclaim,namespace) (kubelet_volume_stats_used_bytes{namespace=~".+"} ) unless (max by (persistentvolumeclaim,namespace) (kubelet_volume_stats_used_bytes{namespace=~".+"} )) / (max by (persistentvolumeclaim,namespace) (kubelet_volume_stats_capacity_bytes{namespace=~".+"} )) >= (80 / 100)) or vector (0)"#,
7834        ];
7835
7836        for case in cases {
7837            let prom_expr = parser::parse(case).unwrap();
7838            eval_stmt.expr = prom_expr;
7839            let table_provider = build_test_table_provider_with_fields(
7840                &[
7841                    (
7842                        DEFAULT_SCHEMA_NAME.to_string(),
7843                        "kubelet_volume_stats_used_bytes".to_string(),
7844                    ),
7845                    (
7846                        DEFAULT_SCHEMA_NAME.to_string(),
7847                        "kubelet_volume_stats_capacity_bytes".to_string(),
7848                    ),
7849                ],
7850                &["namespace", "persistentvolumeclaim"],
7851            )
7852            .await;
7853            // Should be ok
7854            let _ =
7855                PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
7856                    .await
7857                    .unwrap();
7858        }
7859    }
7860
7861    #[tokio::test]
7862    async fn test_nested_binary_op() {
7863        let mut eval_stmt = EvalStmt {
7864            expr: PromExpr::NumberLiteral(NumberLiteral { val: 1.0 }),
7865            start: UNIX_EPOCH,
7866            end: UNIX_EPOCH
7867                .checked_add(Duration::from_secs(100_000))
7868                .unwrap(),
7869            interval: Duration::from_secs(5),
7870            lookback_delta: Duration::from_secs(1),
7871        };
7872
7873        let case = r#"sum(rate(nginx_ingress_controller_requests{job=~".*"}[2m])) -
7874        (
7875            sum(rate(nginx_ingress_controller_requests{namespace=~".*"}[2m]))
7876            or
7877            vector(0)
7878        )"#;
7879
7880        let prom_expr = parser::parse(case).unwrap();
7881        eval_stmt.expr = prom_expr;
7882        let table_provider = build_test_table_provider_with_fields(
7883            &[(
7884                DEFAULT_SCHEMA_NAME.to_string(),
7885                "nginx_ingress_controller_requests".to_string(),
7886            )],
7887            &["namespace", "job"],
7888        )
7889        .await;
7890        // Should be ok
7891        let _ = PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
7892            .await
7893            .unwrap();
7894    }
7895
7896    #[tokio::test]
7897    async fn test_parse_or_operator() {
7898        let mut eval_stmt = EvalStmt {
7899            expr: PromExpr::NumberLiteral(NumberLiteral { val: 1.0 }),
7900            start: UNIX_EPOCH,
7901            end: UNIX_EPOCH
7902                .checked_add(Duration::from_secs(100_000))
7903                .unwrap(),
7904            interval: Duration::from_secs(5),
7905            lookback_delta: Duration::from_secs(1),
7906        };
7907
7908        let case = r#"
7909        sum(rate(sysstat{tenant_name=~"tenant1",cluster_name=~"cluster1"}[120s])) by (cluster_name,tenant_name) /
7910        (sum(sysstat{tenant_name=~"tenant1",cluster_name=~"cluster1"}) by (cluster_name,tenant_name) * 100)
7911            or
7912        200 * sum(sysstat{tenant_name=~"tenant1",cluster_name=~"cluster1"}) by (cluster_name,tenant_name) /
7913        sum(sysstat{tenant_name=~"tenant1",cluster_name=~"cluster1"}) by (cluster_name,tenant_name)"#;
7914
7915        let table_provider = build_test_table_provider_with_fields(
7916            &[(DEFAULT_SCHEMA_NAME.to_string(), "sysstat".to_string())],
7917            &["tenant_name", "cluster_name"],
7918        )
7919        .await;
7920        eval_stmt.expr = parser::parse(case).unwrap();
7921        let _ = PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
7922            .await
7923            .unwrap();
7924
7925        let case = r#"sum(delta(sysstat{tenant_name=~"sys",cluster_name=~"cluster1"}[2m])/120) by (cluster_name,tenant_name) /
7926            (sum(delta(sysstat{tenant_name=~"sys",cluster_name=~"cluster1"}[2m])/120) by (cluster_name,tenant_name) *1000) +
7927            sum(delta(sysstat{tenant_name=~"sys",cluster_name=~"cluster1"}[2m])/120) by (cluster_name,tenant_name) /
7928            (sum(delta(sysstat{tenant_name=~"sys",cluster_name=~"cluster1"}[2m])/120) by (cluster_name,tenant_name) *1000) >= 0
7929            or
7930            sum(delta(sysstat{tenant_name=~"sys",cluster_name=~"cluster1"}[2m])/120) by (cluster_name,tenant_name) /
7931            (sum(delta(sysstat{tenant_name=~"sys",cluster_name=~"cluster1"}[2m])/120) by (cluster_name,tenant_name) *1000) >= 0
7932            or
7933            sum(delta(sysstat{tenant_name=~"sys",cluster_name=~"cluster1"}[2m])/120) by (cluster_name,tenant_name) /
7934            (sum(delta(sysstat{tenant_name=~"sys",cluster_name=~"cluster1"}[2m])/120) by (cluster_name,tenant_name) *1000) >= 0"#;
7935        let table_provider = build_test_table_provider_with_fields(
7936            &[(DEFAULT_SCHEMA_NAME.to_string(), "sysstat".to_string())],
7937            &["tenant_name", "cluster_name"],
7938        )
7939        .await;
7940        eval_stmt.expr = parser::parse(case).unwrap();
7941        let _ = PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
7942            .await
7943            .unwrap();
7944
7945        let case = r#"(sum(background_waitevent_cnt{tenant_name=~"sys",cluster_name=~"cluster1"}) by (cluster_name,tenant_name) +
7946            sum(foreground_waitevent_cnt{tenant_name=~"sys",cluster_name=~"cluster1"}) by (cluster_name,tenant_name)) or
7947            (sum(background_waitevent_cnt{tenant_name=~"sys",cluster_name=~"cluster1"}) by (cluster_name,tenant_name)) or
7948            (sum(foreground_waitevent_cnt{tenant_name=~"sys",cluster_name=~"cluster1"}) by (cluster_name,tenant_name))"#;
7949        let table_provider = build_test_table_provider_with_fields(
7950            &[
7951                (
7952                    DEFAULT_SCHEMA_NAME.to_string(),
7953                    "background_waitevent_cnt".to_string(),
7954                ),
7955                (
7956                    DEFAULT_SCHEMA_NAME.to_string(),
7957                    "foreground_waitevent_cnt".to_string(),
7958                ),
7959            ],
7960            &["tenant_name", "cluster_name"],
7961        )
7962        .await;
7963        eval_stmt.expr = parser::parse(case).unwrap();
7964        let _ = PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
7965            .await
7966            .unwrap();
7967
7968        let case = r#"avg(node_load1{cluster_name=~"cluster1"}) by (cluster_name,host_name) or max(container_cpu_load_average_10s{cluster_name=~"cluster1"}) by (cluster_name,host_name) * 100 / max(container_spec_cpu_quota{cluster_name=~"cluster1"}) by (cluster_name,host_name)"#;
7969        let table_provider = build_test_table_provider_with_fields(
7970            &[
7971                (DEFAULT_SCHEMA_NAME.to_string(), "node_load1".to_string()),
7972                (
7973                    DEFAULT_SCHEMA_NAME.to_string(),
7974                    "container_cpu_load_average_10s".to_string(),
7975                ),
7976                (
7977                    DEFAULT_SCHEMA_NAME.to_string(),
7978                    "container_spec_cpu_quota".to_string(),
7979                ),
7980            ],
7981            &["cluster_name", "host_name"],
7982        )
7983        .await;
7984        eval_stmt.expr = parser::parse(case).unwrap();
7985        let _ = PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
7986            .await
7987            .unwrap();
7988    }
7989
7990    #[tokio::test]
7991    async fn value_matcher() {
7992        // template
7993        let mut eval_stmt = EvalStmt {
7994            expr: PromExpr::NumberLiteral(NumberLiteral { val: 1.0 }),
7995            start: UNIX_EPOCH,
7996            end: UNIX_EPOCH
7997                .checked_add(Duration::from_secs(100_000))
7998                .unwrap(),
7999            interval: Duration::from_secs(5),
8000            lookback_delta: Duration::from_secs(1),
8001        };
8002
8003        let cases = [
8004            // single equal matcher
8005            (
8006                r#"some_metric{__field__="field_1"}"#,
8007                vec![
8008                    "some_metric.field_1",
8009                    "some_metric.tag_0",
8010                    "some_metric.tag_1",
8011                    "some_metric.tag_2",
8012                    "some_metric.timestamp",
8013                ],
8014            ),
8015            // two equal matchers
8016            (
8017                r#"some_metric{__field__="field_1", __field__="field_0"}"#,
8018                vec![
8019                    "some_metric.field_0",
8020                    "some_metric.field_1",
8021                    "some_metric.tag_0",
8022                    "some_metric.tag_1",
8023                    "some_metric.tag_2",
8024                    "some_metric.timestamp",
8025                ],
8026            ),
8027            // single not_eq matcher
8028            (
8029                r#"some_metric{__field__!="field_1"}"#,
8030                vec![
8031                    "some_metric.field_0",
8032                    "some_metric.field_2",
8033                    "some_metric.tag_0",
8034                    "some_metric.tag_1",
8035                    "some_metric.tag_2",
8036                    "some_metric.timestamp",
8037                ],
8038            ),
8039            // two not_eq matchers
8040            (
8041                r#"some_metric{__field__!="field_1", __field__!="field_2"}"#,
8042                vec![
8043                    "some_metric.field_0",
8044                    "some_metric.tag_0",
8045                    "some_metric.tag_1",
8046                    "some_metric.tag_2",
8047                    "some_metric.timestamp",
8048                ],
8049            ),
8050            // equal and not_eq matchers (no conflict)
8051            (
8052                r#"some_metric{__field__="field_1", __field__!="field_0"}"#,
8053                vec![
8054                    "some_metric.field_1",
8055                    "some_metric.tag_0",
8056                    "some_metric.tag_1",
8057                    "some_metric.tag_2",
8058                    "some_metric.timestamp",
8059                ],
8060            ),
8061            // equal and not_eq matchers (conflict)
8062            (
8063                r#"some_metric{__field__="field_2", __field__!="field_2"}"#,
8064                vec![
8065                    "some_metric.tag_0",
8066                    "some_metric.tag_1",
8067                    "some_metric.tag_2",
8068                    "some_metric.timestamp",
8069                ],
8070            ),
8071            // single regex eq matcher
8072            (
8073                r#"some_metric{__field__=~"field_1|field_2"}"#,
8074                vec![
8075                    "some_metric.field_1",
8076                    "some_metric.field_2",
8077                    "some_metric.tag_0",
8078                    "some_metric.tag_1",
8079                    "some_metric.tag_2",
8080                    "some_metric.timestamp",
8081                ],
8082            ),
8083            // single regex not_eq matcher
8084            (
8085                r#"some_metric{__field__!~"field_1|field_2"}"#,
8086                vec![
8087                    "some_metric.field_0",
8088                    "some_metric.tag_0",
8089                    "some_metric.tag_1",
8090                    "some_metric.tag_2",
8091                    "some_metric.timestamp",
8092                ],
8093            ),
8094        ];
8095
8096        for case in cases {
8097            let prom_expr = parser::parse(case.0).unwrap();
8098            eval_stmt.expr = prom_expr;
8099            let table_provider = build_test_table_provider(
8100                &[(DEFAULT_SCHEMA_NAME.to_string(), "some_metric".to_string())],
8101                3,
8102                3,
8103            )
8104            .await;
8105            let plan =
8106                PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
8107                    .await
8108                    .unwrap();
8109            let mut fields = plan.schema().field_names();
8110            let mut expected = case.1.into_iter().map(String::from).collect::<Vec<_>>();
8111            fields.sort();
8112            expected.sort();
8113            assert_eq!(fields, expected, "case: {:?}", case.0);
8114        }
8115
8116        let bad_cases = [
8117            r#"some_metric{__field__="nonexistent"}"#,
8118            r#"some_metric{__field__!="nonexistent"}"#,
8119        ];
8120
8121        for case in bad_cases {
8122            let prom_expr = parser::parse(case).unwrap();
8123            eval_stmt.expr = prom_expr;
8124            let table_provider = build_test_table_provider(
8125                &[(DEFAULT_SCHEMA_NAME.to_string(), "some_metric".to_string())],
8126                3,
8127                3,
8128            )
8129            .await;
8130            let plan =
8131                PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
8132                    .await;
8133            assert!(plan.is_err(), "case: {:?}", case);
8134        }
8135    }
8136
8137    #[tokio::test]
8138    async fn custom_schema() {
8139        let query = "some_alt_metric{__schema__=\"greptime_private\"}";
8140        let expected = String::from(
8141            "PromInstantManipulate: range=[0..100000000], lookback=[1000], interval=[5000], time index=[timestamp] [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
8142            \n  PromSeriesDivide: tags=[\"tag_0\"] [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
8143            \n    Sort: greptime_private.some_alt_metric.tag_0 ASC NULLS FIRST, greptime_private.some_alt_metric.timestamp ASC NULLS FIRST [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
8144            \n      Filter: greptime_private.some_alt_metric.timestamp >= TimestampMillisecond(-999, None) AND greptime_private.some_alt_metric.timestamp <= TimestampMillisecond(100000000, None) [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
8145            \n        TableScan: greptime_private.some_alt_metric [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]",
8146        );
8147
8148        indie_query_plan_compare(query, expected).await;
8149
8150        let query = "some_alt_metric{__database__=\"greptime_private\"}";
8151        let expected = String::from(
8152            "PromInstantManipulate: range=[0..100000000], lookback=[1000], interval=[5000], time index=[timestamp] [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
8153            \n  PromSeriesDivide: tags=[\"tag_0\"] [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
8154            \n    Sort: greptime_private.some_alt_metric.tag_0 ASC NULLS FIRST, greptime_private.some_alt_metric.timestamp ASC NULLS FIRST [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
8155            \n      Filter: greptime_private.some_alt_metric.timestamp >= TimestampMillisecond(-999, None) AND greptime_private.some_alt_metric.timestamp <= TimestampMillisecond(100000000, None) [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
8156            \n        TableScan: greptime_private.some_alt_metric [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]",
8157        );
8158
8159        indie_query_plan_compare(query, expected).await;
8160
8161        let query = "some_alt_metric{__schema__=\"greptime_private\"} / some_metric";
8162        let expected = String::from(
8163            "Projection: some_metric.tag_0, some_metric.timestamp, CAST(greptime_private.some_alt_metric.field_0 AS Float64) / CAST(some_metric.field_0 AS Float64) AS greptime_private.some_alt_metric.field_0 / some_metric.field_0 [tag_0:Utf8, timestamp:Timestamp(ms), greptime_private.some_alt_metric.field_0 / some_metric.field_0:Float64;N]\
8164            \n  Inner Join: greptime_private.some_alt_metric.tag_0 = some_metric.tag_0, greptime_private.some_alt_metric.timestamp = some_metric.timestamp [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N, tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
8165            \n    SubqueryAlias: greptime_private.some_alt_metric [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
8166            \n      PromInstantManipulate: range=[0..100000000], lookback=[1000], interval=[5000], time index=[timestamp] [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
8167            \n        PromSeriesDivide: tags=[\"tag_0\"] [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
8168            \n          Sort: greptime_private.some_alt_metric.tag_0 ASC NULLS FIRST, greptime_private.some_alt_metric.timestamp ASC NULLS FIRST [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
8169            \n            Filter: greptime_private.some_alt_metric.timestamp >= TimestampMillisecond(-999, None) AND greptime_private.some_alt_metric.timestamp <= TimestampMillisecond(100000000, None) [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
8170            \n              TableScan: greptime_private.some_alt_metric [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
8171            \n    SubqueryAlias: some_metric [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
8172            \n      PromInstantManipulate: range=[0..100000000], lookback=[1000], interval=[5000], time index=[timestamp] [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
8173            \n        PromSeriesDivide: tags=[\"tag_0\"] [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
8174            \n          Sort: some_metric.tag_0 ASC NULLS FIRST, some_metric.timestamp ASC NULLS FIRST [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
8175            \n            Filter: some_metric.timestamp >= TimestampMillisecond(-999, None) AND some_metric.timestamp <= TimestampMillisecond(100000000, None) [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
8176            \n              TableScan: some_metric [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]",
8177        );
8178
8179        indie_query_plan_compare(query, expected).await;
8180    }
8181
8182    #[tokio::test]
8183    async fn only_equals_is_supported_for_special_matcher() {
8184        let queries = &[
8185            "some_alt_metric{__schema__!=\"greptime_private\"}",
8186            "some_alt_metric{__schema__=~\"lalala\"}",
8187            "some_alt_metric{__database__!=\"greptime_private\"}",
8188            "some_alt_metric{__database__=~\"lalala\"}",
8189        ];
8190
8191        for query in queries {
8192            let prom_expr = parser::parse(query).unwrap();
8193            let eval_stmt = EvalStmt {
8194                expr: prom_expr,
8195                start: UNIX_EPOCH,
8196                end: UNIX_EPOCH
8197                    .checked_add(Duration::from_secs(100_000))
8198                    .unwrap(),
8199                interval: Duration::from_secs(5),
8200                lookback_delta: Duration::from_secs(1),
8201            };
8202
8203            let table_provider = build_test_table_provider(
8204                &[
8205                    (DEFAULT_SCHEMA_NAME.to_string(), "some_metric".to_string()),
8206                    (
8207                        "greptime_private".to_string(),
8208                        "some_alt_metric".to_string(),
8209                    ),
8210                ],
8211                1,
8212                1,
8213            )
8214            .await;
8215
8216            let plan =
8217                PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
8218                    .await;
8219            assert!(plan.is_err(), "query: {:?}", query);
8220        }
8221    }
8222
8223    #[tokio::test]
8224    async fn test_non_ms_precision() {
8225        let catalog_list = MemoryCatalogManager::with_default_setup();
8226        let columns = vec![
8227            ColumnSchema::new(
8228                "tag".to_string(),
8229                ConcreteDataType::string_datatype(),
8230                false,
8231            ),
8232            ColumnSchema::new(
8233                "timestamp".to_string(),
8234                ConcreteDataType::timestamp_nanosecond_datatype(),
8235                false,
8236            )
8237            .with_time_index(true),
8238            ColumnSchema::new(
8239                "field".to_string(),
8240                ConcreteDataType::float64_datatype(),
8241                true,
8242            ),
8243        ];
8244        let schema = Arc::new(Schema::new(columns));
8245        let table_meta = TableMetaBuilder::empty()
8246            .schema(schema)
8247            .primary_key_indices(vec![0])
8248            .value_indices(vec![2])
8249            .next_column_id(1024)
8250            .build()
8251            .unwrap();
8252        let table_info = TableInfoBuilder::default()
8253            .name("metrics".to_string())
8254            .meta(table_meta)
8255            .build()
8256            .unwrap();
8257        let table = EmptyTable::from_table_info(&table_info);
8258        assert!(
8259            catalog_list
8260                .register_table_sync(RegisterTableRequest {
8261                    catalog: DEFAULT_CATALOG_NAME.to_string(),
8262                    schema: DEFAULT_SCHEMA_NAME.to_string(),
8263                    table_name: "metrics".to_string(),
8264                    table_id: 1024,
8265                    table,
8266                })
8267                .is_ok()
8268        );
8269
8270        let plan = PromPlanner::stmt_to_plan(
8271            DfTableSourceProvider::new(
8272                catalog_list.clone(),
8273                false,
8274                QueryContext::arc(),
8275                DummyDecoder::arc(),
8276                true,
8277            ),
8278            &EvalStmt {
8279                expr: parser::parse("metrics{tag = \"1\"}").unwrap(),
8280                start: UNIX_EPOCH,
8281                end: UNIX_EPOCH
8282                    .checked_add(Duration::from_secs(100_000))
8283                    .unwrap(),
8284                interval: Duration::from_secs(5),
8285                lookback_delta: Duration::from_secs(1),
8286            },
8287            &build_query_engine_state(),
8288        )
8289        .await
8290        .unwrap();
8291        assert_eq!(
8292            plan.display_indent_schema().to_string(),
8293            "PromInstantManipulate: range=[0..100000000], lookback=[1000], interval=[5000], time index=[timestamp] [field:Float64;N, tag:Utf8, timestamp:Timestamp(ms)]\
8294            \n  PromSeriesDivide: tags=[\"tag\"] [field:Float64;N, tag:Utf8, timestamp:Timestamp(ms)]\
8295            \n    Sort: metrics.tag ASC NULLS FIRST, metrics.timestamp ASC NULLS FIRST [field:Float64;N, tag:Utf8, timestamp:Timestamp(ms)]\
8296            \n      Filter: metrics.tag = Utf8(\"1\") AND metrics.timestamp >= TimestampMillisecond(-999, None) AND metrics.timestamp <= TimestampMillisecond(100000000, None) [field:Float64;N, tag:Utf8, timestamp:Timestamp(ms)]\
8297            \n        Projection: metrics.field, metrics.tag, CAST(metrics.timestamp AS Timestamp(ms)) AS timestamp [field:Float64;N, tag:Utf8, timestamp:Timestamp(ms)]\
8298            \n          TableScan: metrics [tag:Utf8, timestamp:Timestamp(ns), field:Float64;N]"
8299        );
8300        let plan = PromPlanner::stmt_to_plan(
8301            DfTableSourceProvider::new(
8302                catalog_list.clone(),
8303                false,
8304                QueryContext::arc(),
8305                DummyDecoder::arc(),
8306                true,
8307            ),
8308            &EvalStmt {
8309                expr: parser::parse("avg_over_time(metrics{tag = \"1\"}[5s])").unwrap(),
8310                start: UNIX_EPOCH,
8311                end: UNIX_EPOCH
8312                    .checked_add(Duration::from_secs(100_000))
8313                    .unwrap(),
8314                interval: Duration::from_secs(5),
8315                lookback_delta: Duration::from_secs(1),
8316            },
8317            &build_query_engine_state(),
8318        )
8319        .await
8320        .unwrap();
8321        assert_eq!(
8322            plan.display_indent_schema().to_string(),
8323            "Filter: prom_avg_over_time(timestamp_range,field) IS NOT NULL [timestamp:Timestamp(ms), prom_avg_over_time(timestamp_range,field):Float64;N, tag:Utf8]\
8324            \n  Projection: metrics.timestamp, prom_avg_over_time(timestamp_range, field) AS prom_avg_over_time(timestamp_range,field), metrics.tag [timestamp:Timestamp(ms), prom_avg_over_time(timestamp_range,field):Float64;N, tag:Utf8]\
8325            \n    PromRangeManipulate: req range=[0..100000000], interval=[5000], eval range=[5000], time index=[timestamp], values=[\"field\"] [field:Dictionary(Int64, Float64);N, tag:Utf8, timestamp:Timestamp(ms), timestamp_range:Dictionary(Int64, Timestamp(ms))]\
8326            \n      PromSeriesNormalize: offset=[0], time index=[timestamp], filter NaN: [true] [field:Float64;N, tag:Utf8, timestamp:Timestamp(ms)]\
8327            \n        PromSeriesDivide: tags=[\"tag\"] [field:Float64;N, tag:Utf8, timestamp:Timestamp(ms)]\
8328            \n          Sort: metrics.tag ASC NULLS FIRST, metrics.timestamp ASC NULLS FIRST [field:Float64;N, tag:Utf8, timestamp:Timestamp(ms)]\
8329            \n            Filter: metrics.tag = Utf8(\"1\") AND metrics.timestamp >= TimestampMillisecond(-4999, None) AND metrics.timestamp <= TimestampMillisecond(100000000, None) [field:Float64;N, tag:Utf8, timestamp:Timestamp(ms)]\
8330            \n              Projection: metrics.field, metrics.tag, CAST(metrics.timestamp AS Timestamp(ms)) AS timestamp [field:Float64;N, tag:Utf8, timestamp:Timestamp(ms)]\
8331            \n                TableScan: metrics [tag:Utf8, timestamp:Timestamp(ns), field:Float64;N]"
8332        );
8333    }
8334
8335    #[tokio::test]
8336    async fn test_nonexistent_label() {
8337        // template
8338        let mut eval_stmt = EvalStmt {
8339            expr: PromExpr::NumberLiteral(NumberLiteral { val: 1.0 }),
8340            start: UNIX_EPOCH,
8341            end: UNIX_EPOCH
8342                .checked_add(Duration::from_secs(100_000))
8343                .unwrap(),
8344            interval: Duration::from_secs(5),
8345            lookback_delta: Duration::from_secs(1),
8346        };
8347
8348        let case = r#"some_metric{nonexistent="hi"}"#;
8349        let prom_expr = parser::parse(case).unwrap();
8350        eval_stmt.expr = prom_expr;
8351        let table_provider = build_test_table_provider(
8352            &[(DEFAULT_SCHEMA_NAME.to_string(), "some_metric".to_string())],
8353            3,
8354            3,
8355        )
8356        .await;
8357        // Should be ok
8358        let _ = PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
8359            .await
8360            .unwrap();
8361    }
8362
8363    #[tokio::test]
8364    async fn test_label_join() {
8365        let prom_expr = parser::parse(
8366            "label_join(up{tag_0='api-server'}, 'foo', ',', 'tag_1', 'tag_2', 'tag_3')",
8367        )
8368        .unwrap();
8369        let eval_stmt = EvalStmt {
8370            expr: prom_expr,
8371            start: UNIX_EPOCH,
8372            end: UNIX_EPOCH
8373                .checked_add(Duration::from_secs(100_000))
8374                .unwrap(),
8375            interval: Duration::from_secs(5),
8376            lookback_delta: Duration::from_secs(1),
8377        };
8378
8379        let table_provider =
8380            build_test_table_provider(&[(DEFAULT_SCHEMA_NAME.to_string(), "up".to_string())], 4, 1)
8381                .await;
8382        let plan =
8383            PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
8384                .await
8385                .unwrap();
8386
8387        let expected = r#"
8388Filter: up.field_0 IS NOT NULL [timestamp:Timestamp(ms), field_0:Float64;N, foo:Utf8;N, tag_0:Utf8, tag_1:Utf8, tag_2:Utf8, tag_3:Utf8]
8389  Projection: up.timestamp, up.field_0, concat_ws(Utf8(","), up.tag_1, up.tag_2, up.tag_3) AS foo, up.tag_0, up.tag_1, up.tag_2, up.tag_3 [timestamp:Timestamp(ms), field_0:Float64;N, foo:Utf8;N, tag_0:Utf8, tag_1:Utf8, tag_2:Utf8, tag_3:Utf8]
8390    PromInstantManipulate: range=[0..100000000], lookback=[1000], interval=[5000], time index=[timestamp] [tag_0:Utf8, tag_1:Utf8, tag_2:Utf8, tag_3:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]
8391      PromSeriesDivide: tags=["tag_0", "tag_1", "tag_2", "tag_3"] [tag_0:Utf8, tag_1:Utf8, tag_2:Utf8, tag_3:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]
8392        Sort: up.tag_0 ASC NULLS FIRST, up.tag_1 ASC NULLS FIRST, up.tag_2 ASC NULLS FIRST, up.tag_3 ASC NULLS FIRST, up.timestamp ASC NULLS FIRST [tag_0:Utf8, tag_1:Utf8, tag_2:Utf8, tag_3:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]
8393          Filter: up.tag_0 = Utf8("api-server") AND up.timestamp >= TimestampMillisecond(-999, None) AND up.timestamp <= TimestampMillisecond(100000000, None) [tag_0:Utf8, tag_1:Utf8, tag_2:Utf8, tag_3:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]
8394            TableScan: up [tag_0:Utf8, tag_1:Utf8, tag_2:Utf8, tag_3:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]"#;
8395
8396        let ret = plan.display_indent_schema().to_string();
8397        assert_eq!(format!("\n{ret}"), expected, "\n{}", ret);
8398    }
8399
8400    #[tokio::test]
8401    async fn test_label_replace() {
8402        let prom_expr = parser::parse(
8403            "label_replace(up{tag_0=\"a:c\"}, \"foo\", \"$1\", \"tag_0\", \"(.*):.*\")",
8404        )
8405        .unwrap();
8406        let eval_stmt = EvalStmt {
8407            expr: prom_expr,
8408            start: UNIX_EPOCH,
8409            end: UNIX_EPOCH
8410                .checked_add(Duration::from_secs(100_000))
8411                .unwrap(),
8412            interval: Duration::from_secs(5),
8413            lookback_delta: Duration::from_secs(1),
8414        };
8415
8416        let table_provider =
8417            build_test_table_provider(&[(DEFAULT_SCHEMA_NAME.to_string(), "up".to_string())], 1, 1)
8418                .await;
8419        let plan =
8420            PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
8421                .await
8422                .unwrap();
8423
8424        let expected = r#"
8425Filter: up.field_0 IS NOT NULL [timestamp:Timestamp(ms), field_0:Float64;N, foo:Utf8;N, tag_0:Utf8]
8426  Projection: up.timestamp, up.field_0, regexp_replace(up.tag_0, Utf8("^(?s:(.*):.*)$"), Utf8("$1")) AS foo, up.tag_0 [timestamp:Timestamp(ms), field_0:Float64;N, foo:Utf8;N, tag_0:Utf8]
8427    PromInstantManipulate: range=[0..100000000], lookback=[1000], interval=[5000], time index=[timestamp] [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]
8428      PromSeriesDivide: tags=["tag_0"] [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]
8429        Sort: up.tag_0 ASC NULLS FIRST, up.timestamp ASC NULLS FIRST [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]
8430          Filter: up.tag_0 = Utf8("a:c") AND up.timestamp >= TimestampMillisecond(-999, None) AND up.timestamp <= TimestampMillisecond(100000000, None) [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]
8431            TableScan: up [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]"#;
8432
8433        let ret = plan.display_indent_schema().to_string();
8434        assert_eq!(format!("\n{ret}"), expected, "\n{}", ret);
8435    }
8436
8437    #[tokio::test]
8438    async fn test_matchers_to_expr() {
8439        let mut eval_stmt = EvalStmt {
8440            expr: PromExpr::NumberLiteral(NumberLiteral { val: 1.0 }),
8441            start: UNIX_EPOCH,
8442            end: UNIX_EPOCH
8443                .checked_add(Duration::from_secs(100_000))
8444                .unwrap(),
8445            interval: Duration::from_secs(5),
8446            lookback_delta: Duration::from_secs(1),
8447        };
8448        let case =
8449            r#"sum(prometheus_tsdb_head_series{tag_1=~"(10.0.160.237:8080|10.0.160.237:9090)"})"#;
8450
8451        let prom_expr = parser::parse(case).unwrap();
8452        eval_stmt.expr = prom_expr;
8453        let table_provider = build_test_table_provider(
8454            &[(
8455                DEFAULT_SCHEMA_NAME.to_string(),
8456                "prometheus_tsdb_head_series".to_string(),
8457            )],
8458            3,
8459            3,
8460        )
8461        .await;
8462        let plan =
8463            PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
8464                .await
8465                .unwrap();
8466        let expected = "Sort: prometheus_tsdb_head_series.timestamp ASC NULLS LAST [timestamp:Timestamp(ms), sum(prometheus_tsdb_head_series.field_0):Float64;N, sum(prometheus_tsdb_head_series.field_1):Float64;N, sum(prometheus_tsdb_head_series.field_2):Float64;N]\
8467        \n  Aggregate: groupBy=[[prometheus_tsdb_head_series.timestamp]], aggr=[[sum(prometheus_tsdb_head_series.field_0), sum(prometheus_tsdb_head_series.field_1), sum(prometheus_tsdb_head_series.field_2)]] [timestamp:Timestamp(ms), sum(prometheus_tsdb_head_series.field_0):Float64;N, sum(prometheus_tsdb_head_series.field_1):Float64;N, sum(prometheus_tsdb_head_series.field_2):Float64;N]\
8468        \n    PromInstantManipulate: range=[0..100000000], lookback=[1000], interval=[5000], time index=[timestamp] [tag_0:Utf8, tag_1:Utf8, tag_2:Utf8, timestamp:Timestamp(ms), field_0:Float64;N, field_1:Float64;N, field_2:Float64;N]\
8469        \n      PromSeriesDivide: tags=[\"tag_0\", \"tag_1\", \"tag_2\"] [tag_0:Utf8, tag_1:Utf8, tag_2:Utf8, timestamp:Timestamp(ms), field_0:Float64;N, field_1:Float64;N, field_2:Float64;N]\
8470        \n        Sort: prometheus_tsdb_head_series.tag_0 ASC NULLS FIRST, prometheus_tsdb_head_series.tag_1 ASC NULLS FIRST, prometheus_tsdb_head_series.tag_2 ASC NULLS FIRST, prometheus_tsdb_head_series.timestamp ASC NULLS FIRST [tag_0:Utf8, tag_1:Utf8, tag_2:Utf8, timestamp:Timestamp(ms), field_0:Float64;N, field_1:Float64;N, field_2:Float64;N]\
8471        \n          Filter: prometheus_tsdb_head_series.tag_1 ~ Utf8(\"^(?:(10.0.160.237:8080|10.0.160.237:9090))$\") AND prometheus_tsdb_head_series.timestamp >= TimestampMillisecond(-999, None) AND prometheus_tsdb_head_series.timestamp <= TimestampMillisecond(100000000, None) [tag_0:Utf8, tag_1:Utf8, tag_2:Utf8, timestamp:Timestamp(ms), field_0:Float64;N, field_1:Float64;N, field_2:Float64;N]\
8472        \n            TableScan: prometheus_tsdb_head_series [tag_0:Utf8, tag_1:Utf8, tag_2:Utf8, timestamp:Timestamp(ms), field_0:Float64;N, field_1:Float64;N, field_2:Float64;N]";
8473        assert_eq!(plan.display_indent_schema().to_string(), expected);
8474    }
8475
8476    #[tokio::test]
8477    async fn test_topk_expr() {
8478        let mut eval_stmt = EvalStmt {
8479            expr: PromExpr::NumberLiteral(NumberLiteral { val: 1.0 }),
8480            start: UNIX_EPOCH,
8481            end: UNIX_EPOCH
8482                .checked_add(Duration::from_secs(100_000))
8483                .unwrap(),
8484            interval: Duration::from_secs(5),
8485            lookback_delta: Duration::from_secs(1),
8486        };
8487        let case = r#"topk(10, sum(prometheus_tsdb_head_series{ip=~"(10.0.160.237:8080|10.0.160.237:9090)"}) by (ip))"#;
8488
8489        let prom_expr = parser::parse(case).unwrap();
8490        eval_stmt.expr = prom_expr;
8491        let table_provider = build_test_table_provider_with_fields(
8492            &[
8493                (
8494                    DEFAULT_SCHEMA_NAME.to_string(),
8495                    "prometheus_tsdb_head_series".to_string(),
8496                ),
8497                (
8498                    DEFAULT_SCHEMA_NAME.to_string(),
8499                    "http_server_requests_seconds_count".to_string(),
8500                ),
8501            ],
8502            &["ip"],
8503        )
8504        .await;
8505
8506        let plan =
8507            PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
8508                .await
8509                .unwrap();
8510        let expected = "Projection: sum(prometheus_tsdb_head_series.greptime_value), prometheus_tsdb_head_series.ip, prometheus_tsdb_head_series.greptime_timestamp [sum(prometheus_tsdb_head_series.greptime_value):Float64;N, ip:Utf8, greptime_timestamp:Timestamp(ms)]\
8511        \n  Sort: prometheus_tsdb_head_series.greptime_timestamp ASC NULLS LAST, row_number() PARTITION BY [prometheus_tsdb_head_series.greptime_timestamp] ORDER BY [sum(prometheus_tsdb_head_series.greptime_value) DESC NULLS FIRST, prometheus_tsdb_head_series.ip DESC NULLS FIRST] ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ASC NULLS LAST [ip:Utf8, greptime_timestamp:Timestamp(ms), sum(prometheus_tsdb_head_series.greptime_value):Float64;N, row_number() PARTITION BY [prometheus_tsdb_head_series.greptime_timestamp] ORDER BY [sum(prometheus_tsdb_head_series.greptime_value) DESC NULLS FIRST, prometheus_tsdb_head_series.ip DESC NULLS FIRST] ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW:UInt64]\
8512        \n    Filter: row_number() PARTITION BY [prometheus_tsdb_head_series.greptime_timestamp] ORDER BY [sum(prometheus_tsdb_head_series.greptime_value) DESC NULLS FIRST, prometheus_tsdb_head_series.ip DESC NULLS FIRST] ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW <= Float64(10) [ip:Utf8, greptime_timestamp:Timestamp(ms), sum(prometheus_tsdb_head_series.greptime_value):Float64;N, row_number() PARTITION BY [prometheus_tsdb_head_series.greptime_timestamp] ORDER BY [sum(prometheus_tsdb_head_series.greptime_value) DESC NULLS FIRST, prometheus_tsdb_head_series.ip DESC NULLS FIRST] ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW:UInt64]\
8513        \n      WindowAggr: windowExpr=[[row_number() PARTITION BY [prometheus_tsdb_head_series.greptime_timestamp] ORDER BY [sum(prometheus_tsdb_head_series.greptime_value) DESC NULLS FIRST, prometheus_tsdb_head_series.ip DESC NULLS FIRST] ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]] [ip:Utf8, greptime_timestamp:Timestamp(ms), sum(prometheus_tsdb_head_series.greptime_value):Float64;N, row_number() PARTITION BY [prometheus_tsdb_head_series.greptime_timestamp] ORDER BY [sum(prometheus_tsdb_head_series.greptime_value) DESC NULLS FIRST, prometheus_tsdb_head_series.ip DESC NULLS FIRST] ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW:UInt64]\
8514        \n        Sort: prometheus_tsdb_head_series.ip ASC NULLS LAST, prometheus_tsdb_head_series.greptime_timestamp ASC NULLS LAST [ip:Utf8, greptime_timestamp:Timestamp(ms), sum(prometheus_tsdb_head_series.greptime_value):Float64;N]\
8515        \n          Aggregate: groupBy=[[prometheus_tsdb_head_series.ip, prometheus_tsdb_head_series.greptime_timestamp]], aggr=[[sum(prometheus_tsdb_head_series.greptime_value)]] [ip:Utf8, greptime_timestamp:Timestamp(ms), sum(prometheus_tsdb_head_series.greptime_value):Float64;N]\
8516        \n            PromInstantManipulate: range=[0..100000000], lookback=[1000], interval=[5000], time index=[greptime_timestamp] [ip:Utf8, greptime_timestamp:Timestamp(ms), greptime_value:Float64;N]\
8517        \n              PromSeriesDivide: tags=[\"ip\"] [ip:Utf8, greptime_timestamp:Timestamp(ms), greptime_value:Float64;N]\
8518        \n                Sort: prometheus_tsdb_head_series.ip ASC NULLS FIRST, prometheus_tsdb_head_series.greptime_timestamp ASC NULLS FIRST [ip:Utf8, greptime_timestamp:Timestamp(ms), greptime_value:Float64;N]\
8519        \n                  Filter: prometheus_tsdb_head_series.ip ~ Utf8(\"^(?:(10.0.160.237:8080|10.0.160.237:9090))$\") AND prometheus_tsdb_head_series.greptime_timestamp >= TimestampMillisecond(-999, None) AND prometheus_tsdb_head_series.greptime_timestamp <= TimestampMillisecond(100000000, None) [ip:Utf8, greptime_timestamp:Timestamp(ms), greptime_value:Float64;N]\
8520        \n                    TableScan: prometheus_tsdb_head_series [ip:Utf8, greptime_timestamp:Timestamp(ms), greptime_value:Float64;N]";
8521
8522        assert_eq!(plan.display_indent_schema().to_string(), expected);
8523    }
8524
8525    #[tokio::test]
8526    async fn test_count_values_expr() {
8527        let mut eval_stmt = EvalStmt {
8528            expr: PromExpr::NumberLiteral(NumberLiteral { val: 1.0 }),
8529            start: UNIX_EPOCH,
8530            end: UNIX_EPOCH
8531                .checked_add(Duration::from_secs(100_000))
8532                .unwrap(),
8533            interval: Duration::from_secs(5),
8534            lookback_delta: Duration::from_secs(1),
8535        };
8536        let case = r#"count_values('series', prometheus_tsdb_head_series{ip=~"(10.0.160.237:8080|10.0.160.237:9090)"}) by (ip)"#;
8537
8538        let prom_expr = parser::parse(case).unwrap();
8539        eval_stmt.expr = prom_expr;
8540        let table_provider = build_test_table_provider_with_fields(
8541            &[
8542                (
8543                    DEFAULT_SCHEMA_NAME.to_string(),
8544                    "prometheus_tsdb_head_series".to_string(),
8545                ),
8546                (
8547                    DEFAULT_SCHEMA_NAME.to_string(),
8548                    "http_server_requests_seconds_count".to_string(),
8549                ),
8550            ],
8551            &["ip"],
8552        )
8553        .await;
8554
8555        let plan =
8556            PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
8557                .await
8558                .unwrap();
8559        let expected = "Projection: count(prometheus_tsdb_head_series.greptime_value), prometheus_tsdb_head_series.ip, prometheus_tsdb_head_series.greptime_timestamp, series [count(prometheus_tsdb_head_series.greptime_value):Int64, ip:Utf8, greptime_timestamp:Timestamp(ms), series:Float64;N]\
8560        \n  Sort: prometheus_tsdb_head_series.ip ASC NULLS LAST, prometheus_tsdb_head_series.greptime_timestamp ASC NULLS LAST, prometheus_tsdb_head_series.greptime_value ASC NULLS LAST [count(prometheus_tsdb_head_series.greptime_value):Int64, ip:Utf8, greptime_timestamp:Timestamp(ms), series:Float64;N, greptime_value:Float64;N]\
8561        \n    Projection: count(prometheus_tsdb_head_series.greptime_value), prometheus_tsdb_head_series.ip, prometheus_tsdb_head_series.greptime_timestamp, prometheus_tsdb_head_series.greptime_value AS series, prometheus_tsdb_head_series.greptime_value [count(prometheus_tsdb_head_series.greptime_value):Int64, ip:Utf8, greptime_timestamp:Timestamp(ms), series:Float64;N, greptime_value:Float64;N]\
8562        \n      Aggregate: groupBy=[[prometheus_tsdb_head_series.ip, prometheus_tsdb_head_series.greptime_timestamp, prometheus_tsdb_head_series.greptime_value]], aggr=[[count(prometheus_tsdb_head_series.greptime_value)]] [ip:Utf8, greptime_timestamp:Timestamp(ms), greptime_value:Float64;N, count(prometheus_tsdb_head_series.greptime_value):Int64]\
8563        \n        PromInstantManipulate: range=[0..100000000], lookback=[1000], interval=[5000], time index=[greptime_timestamp] [ip:Utf8, greptime_timestamp:Timestamp(ms), greptime_value:Float64;N]\
8564        \n          PromSeriesDivide: tags=[\"ip\"] [ip:Utf8, greptime_timestamp:Timestamp(ms), greptime_value:Float64;N]\
8565        \n            Sort: prometheus_tsdb_head_series.ip ASC NULLS FIRST, prometheus_tsdb_head_series.greptime_timestamp ASC NULLS FIRST [ip:Utf8, greptime_timestamp:Timestamp(ms), greptime_value:Float64;N]\
8566        \n              Filter: prometheus_tsdb_head_series.ip ~ Utf8(\"^(?:(10.0.160.237:8080|10.0.160.237:9090))$\") AND prometheus_tsdb_head_series.greptime_timestamp >= TimestampMillisecond(-999, None) AND prometheus_tsdb_head_series.greptime_timestamp <= TimestampMillisecond(100000000, None) [ip:Utf8, greptime_timestamp:Timestamp(ms), greptime_value:Float64;N]\
8567        \n                TableScan: prometheus_tsdb_head_series [ip:Utf8, greptime_timestamp:Timestamp(ms), greptime_value:Float64;N]";
8568
8569        assert_eq!(plan.display_indent_schema().to_string(), expected);
8570    }
8571
8572    #[tokio::test]
8573    async fn test_value_alias() {
8574        let mut eval_stmt = EvalStmt {
8575            expr: PromExpr::NumberLiteral(NumberLiteral { val: 1.0 }),
8576            start: UNIX_EPOCH,
8577            end: UNIX_EPOCH
8578                .checked_add(Duration::from_secs(100_000))
8579                .unwrap(),
8580            interval: Duration::from_secs(5),
8581            lookback_delta: Duration::from_secs(1),
8582        };
8583        let case = r#"count_values('series', prometheus_tsdb_head_series{ip=~"(10.0.160.237:8080|10.0.160.237:9090)"}) by (ip)"#;
8584
8585        let prom_expr = parser::parse(case).unwrap();
8586        eval_stmt.expr = prom_expr;
8587        eval_stmt = QueryLanguageParser::apply_alias_extension(eval_stmt, "my_series");
8588        let table_provider = build_test_table_provider_with_fields(
8589            &[
8590                (
8591                    DEFAULT_SCHEMA_NAME.to_string(),
8592                    "prometheus_tsdb_head_series".to_string(),
8593                ),
8594                (
8595                    DEFAULT_SCHEMA_NAME.to_string(),
8596                    "http_server_requests_seconds_count".to_string(),
8597                ),
8598            ],
8599            &["ip"],
8600        )
8601        .await;
8602
8603        let plan =
8604            PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
8605                .await
8606                .unwrap();
8607        let expected = r#"
8608Projection: count(prometheus_tsdb_head_series.greptime_value) AS my_series, prometheus_tsdb_head_series.ip, prometheus_tsdb_head_series.greptime_timestamp [my_series:Int64, ip:Utf8, greptime_timestamp:Timestamp(ms)]
8609  Projection: count(prometheus_tsdb_head_series.greptime_value), prometheus_tsdb_head_series.ip, prometheus_tsdb_head_series.greptime_timestamp, series [count(prometheus_tsdb_head_series.greptime_value):Int64, ip:Utf8, greptime_timestamp:Timestamp(ms), series:Float64;N]
8610    Sort: prometheus_tsdb_head_series.ip ASC NULLS LAST, prometheus_tsdb_head_series.greptime_timestamp ASC NULLS LAST, prometheus_tsdb_head_series.greptime_value ASC NULLS LAST [count(prometheus_tsdb_head_series.greptime_value):Int64, ip:Utf8, greptime_timestamp:Timestamp(ms), series:Float64;N, greptime_value:Float64;N]
8611      Projection: count(prometheus_tsdb_head_series.greptime_value), prometheus_tsdb_head_series.ip, prometheus_tsdb_head_series.greptime_timestamp, prometheus_tsdb_head_series.greptime_value AS series, prometheus_tsdb_head_series.greptime_value [count(prometheus_tsdb_head_series.greptime_value):Int64, ip:Utf8, greptime_timestamp:Timestamp(ms), series:Float64;N, greptime_value:Float64;N]
8612        Aggregate: groupBy=[[prometheus_tsdb_head_series.ip, prometheus_tsdb_head_series.greptime_timestamp, prometheus_tsdb_head_series.greptime_value]], aggr=[[count(prometheus_tsdb_head_series.greptime_value)]] [ip:Utf8, greptime_timestamp:Timestamp(ms), greptime_value:Float64;N, count(prometheus_tsdb_head_series.greptime_value):Int64]
8613          PromInstantManipulate: range=[0..100000000], lookback=[1000], interval=[5000], time index=[greptime_timestamp] [ip:Utf8, greptime_timestamp:Timestamp(ms), greptime_value:Float64;N]
8614            PromSeriesDivide: tags=["ip"] [ip:Utf8, greptime_timestamp:Timestamp(ms), greptime_value:Float64;N]
8615              Sort: prometheus_tsdb_head_series.ip ASC NULLS FIRST, prometheus_tsdb_head_series.greptime_timestamp ASC NULLS FIRST [ip:Utf8, greptime_timestamp:Timestamp(ms), greptime_value:Float64;N]
8616                Filter: prometheus_tsdb_head_series.ip ~ Utf8("^(?:(10.0.160.237:8080|10.0.160.237:9090))$") AND prometheus_tsdb_head_series.greptime_timestamp >= TimestampMillisecond(-999, None) AND prometheus_tsdb_head_series.greptime_timestamp <= TimestampMillisecond(100000000, None) [ip:Utf8, greptime_timestamp:Timestamp(ms), greptime_value:Float64;N]
8617                  TableScan: prometheus_tsdb_head_series [ip:Utf8, greptime_timestamp:Timestamp(ms), greptime_value:Float64;N]"#;
8618        assert_eq!(format!("\n{}", plan.display_indent_schema()), expected);
8619    }
8620
8621    #[tokio::test]
8622    async fn test_quantile_expr() {
8623        let mut eval_stmt = EvalStmt {
8624            expr: PromExpr::NumberLiteral(NumberLiteral { val: 1.0 }),
8625            start: UNIX_EPOCH,
8626            end: UNIX_EPOCH
8627                .checked_add(Duration::from_secs(100_000))
8628                .unwrap(),
8629            interval: Duration::from_secs(5),
8630            lookback_delta: Duration::from_secs(1),
8631        };
8632        let case = r#"quantile(0.3, sum(prometheus_tsdb_head_series{ip=~"(10.0.160.237:8080|10.0.160.237:9090)"}) by (ip))"#;
8633
8634        let prom_expr = parser::parse(case).unwrap();
8635        eval_stmt.expr = prom_expr;
8636        let table_provider = build_test_table_provider_with_fields(
8637            &[
8638                (
8639                    DEFAULT_SCHEMA_NAME.to_string(),
8640                    "prometheus_tsdb_head_series".to_string(),
8641                ),
8642                (
8643                    DEFAULT_SCHEMA_NAME.to_string(),
8644                    "http_server_requests_seconds_count".to_string(),
8645                ),
8646            ],
8647            &["ip"],
8648        )
8649        .await;
8650
8651        let plan =
8652            PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
8653                .await
8654                .unwrap();
8655        let expected = "Sort: prometheus_tsdb_head_series.greptime_timestamp ASC NULLS LAST [greptime_timestamp:Timestamp(ms), quantile(Float64(0.3),sum(prometheus_tsdb_head_series.greptime_value)):Float64;N]\
8656        \n  Aggregate: groupBy=[[prometheus_tsdb_head_series.greptime_timestamp]], aggr=[[quantile(Float64(0.3), sum(prometheus_tsdb_head_series.greptime_value))]] [greptime_timestamp:Timestamp(ms), quantile(Float64(0.3),sum(prometheus_tsdb_head_series.greptime_value)):Float64;N]\
8657        \n    Sort: prometheus_tsdb_head_series.ip ASC NULLS LAST, prometheus_tsdb_head_series.greptime_timestamp ASC NULLS LAST [ip:Utf8, greptime_timestamp:Timestamp(ms), sum(prometheus_tsdb_head_series.greptime_value):Float64;N]\
8658        \n      Aggregate: groupBy=[[prometheus_tsdb_head_series.ip, prometheus_tsdb_head_series.greptime_timestamp]], aggr=[[sum(prometheus_tsdb_head_series.greptime_value)]] [ip:Utf8, greptime_timestamp:Timestamp(ms), sum(prometheus_tsdb_head_series.greptime_value):Float64;N]\
8659        \n        PromInstantManipulate: range=[0..100000000], lookback=[1000], interval=[5000], time index=[greptime_timestamp] [ip:Utf8, greptime_timestamp:Timestamp(ms), greptime_value:Float64;N]\
8660        \n          PromSeriesDivide: tags=[\"ip\"] [ip:Utf8, greptime_timestamp:Timestamp(ms), greptime_value:Float64;N]\
8661        \n            Sort: prometheus_tsdb_head_series.ip ASC NULLS FIRST, prometheus_tsdb_head_series.greptime_timestamp ASC NULLS FIRST [ip:Utf8, greptime_timestamp:Timestamp(ms), greptime_value:Float64;N]\
8662        \n              Filter: prometheus_tsdb_head_series.ip ~ Utf8(\"^(?:(10.0.160.237:8080|10.0.160.237:9090))$\") AND prometheus_tsdb_head_series.greptime_timestamp >= TimestampMillisecond(-999, None) AND prometheus_tsdb_head_series.greptime_timestamp <= TimestampMillisecond(100000000, None) [ip:Utf8, greptime_timestamp:Timestamp(ms), greptime_value:Float64;N]\
8663        \n                TableScan: prometheus_tsdb_head_series [ip:Utf8, greptime_timestamp:Timestamp(ms), greptime_value:Float64;N]";
8664
8665        assert_eq!(plan.display_indent_schema().to_string(), expected);
8666    }
8667
8668    #[tokio::test]
8669    async fn test_or_not_exists_table_label() {
8670        let state = build_query_engine_state();
8671        let provider = build_test_table_provider_with_fields(
8672            &[(DEFAULT_SCHEMA_NAME.to_string(), "normal_metric".to_string())],
8673            &["job"],
8674        )
8675        .await;
8676        let raw = PromPlanner::stmt_to_plan(
8677            provider,
8678            &build_eval_stmt(r#"missing_metric or on(absent_label) normal_metric"#),
8679            &state,
8680        )
8681        .await
8682        .unwrap();
8683        assert!(
8684            raw.display_indent_schema()
8685                .to_string()
8686                .contains("__promql_or_match_0@")
8687        );
8688        let (optimized, batches) = execute(raw, &state).await;
8689        assert_no_internal_or_keys(optimized.schema());
8690        assert!(batches.iter().all(|batch| {
8691            batch
8692                .schema()
8693                .fields()
8694                .iter()
8695                .all(|field| !field.name().starts_with("__promql_or_match_"))
8696        }));
8697    }
8698
8699    #[tokio::test]
8700    async fn test_histogram_quantile_missing_le_column() {
8701        let mut eval_stmt = EvalStmt {
8702            expr: PromExpr::NumberLiteral(NumberLiteral { val: 1.0 }),
8703            start: UNIX_EPOCH,
8704            end: UNIX_EPOCH
8705                .checked_add(Duration::from_secs(100_000))
8706                .unwrap(),
8707            interval: Duration::from_secs(5),
8708            lookback_delta: Duration::from_secs(1),
8709        };
8710
8711        // Test case: histogram_quantile with a table that doesn't have 'le' column
8712        let case = r#"histogram_quantile(0.99, sum by(pod,instance,le) (rate(non_existent_histogram_bucket{instance=~"xxx"}[1m])))"#;
8713
8714        let prom_expr = parser::parse(case).unwrap();
8715        eval_stmt.expr = prom_expr;
8716
8717        // Create a table provider with a table that doesn't have 'le' column
8718        let table_provider = build_test_table_provider_with_fields(
8719            &[(
8720                DEFAULT_SCHEMA_NAME.to_string(),
8721                "non_existent_histogram_bucket".to_string(),
8722            )],
8723            &["pod", "instance"], // Note: no 'le' column
8724        )
8725        .await;
8726
8727        // Should return empty result instead of error
8728        let result =
8729            PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state())
8730                .await;
8731
8732        // This should succeed now (returning empty result) instead of failing with "Cannot find column le"
8733        assert!(
8734            result.is_ok(),
8735            "Expected successful plan creation with empty result, but got error: {:?}",
8736            result.err()
8737        );
8738
8739        // Verify that the result is an EmptyRelation
8740        let plan = result.unwrap();
8741        match plan {
8742            LogicalPlan::EmptyRelation(_) => {
8743                // This is what we expect
8744            }
8745            _ => panic!("Expected EmptyRelation, but got: {:?}", plan),
8746        }
8747    }
8748
8749    #[tokio::test]
8750    async fn test_direct_or_normalizes_missing_match_labels() {
8751        type Case<'a> = (
8752            Option<Option<&'a str>>,
8753            Option<Option<&'a str>>,
8754            i64,
8755            i64,
8756            &'a [(f64, Option<&'a str>)],
8757        );
8758
8759        let modifier = or_modifier("lhs or on(k) rhs");
8760        #[rustfmt::skip]
8761        let cases: &[Case<'_>] = &[
8762            (None, None, 1, 1, &[(1.0, None)]),
8763            (None, Some(Some("")), 1, 1, &[(1.0, None)]),
8764            (Some(Some("")), None, 1, 1, &[(1.0, Some(""))]),
8765            (None, Some(Some("r")), 1, 1, &[(1.0, None), (2.0, Some("r"))]),
8766            (Some(Some("l")), None, 1, 1, &[(1.0, Some("l")), (2.0, None)]),
8767            (Some(None), Some(Some("")), 1, 1, &[(1.0, None)]),
8768            (Some(None), Some(Some("r")), 1, 1, &[(1.0, None), (2.0, Some("r"))]),
8769            (Some(Some("same")), Some(Some("same")), 1, 2, &[(1.0, Some("same")), (2.0, Some("same"))]),
8770        ];
8771        for &(left, right, left_ts, right_ts, expected) in cases {
8772            let (optimized, batches) = run(
8773                &matrix_source("lhs", left, left_ts, 1.0),
8774                &matrix_source("rhs", right, right_ts, 2.0),
8775                matrix_context("lhs", left),
8776                matrix_context("rhs", right),
8777                &modifier,
8778            )
8779            .await;
8780            assert_no_internal_or_keys(optimized.schema());
8781            assert_eq!(
8782                rows(&batches),
8783                expected
8784                    .iter()
8785                    .map(|(value, label)| (*value, label.map(str::to_string)))
8786                    .collect::<Vec<_>>()
8787            );
8788        }
8789    }
8790
8791    #[tokio::test]
8792    async fn test_direct_or_match_modifiers() {
8793        for (modifier, left, right, expected) in [
8794            (None, "left", "right", 2),
8795            (or_modifier("lhs or on(k) rhs"), "same", "same", 1),
8796            (or_modifier("lhs or on() rhs"), "left", "right", 1),
8797            (or_modifier("lhs or ignoring(k) rhs"), "left", "right", 1),
8798        ] {
8799            let (_, batches) = run(
8800                &matrix_source("lhs", Some(Some(left)), 1, 1.0),
8801                &matrix_source("rhs", Some(Some(right)), 1, 2.0),
8802                direct_or_context("lhs", &["job", "k"], "v"),
8803                direct_or_context("rhs", &["job", "k"], "v"),
8804                &modifier,
8805            )
8806            .await;
8807            assert_eq!(
8808                batches.iter().map(RecordBatch::num_rows).sum::<usize>(),
8809                expected
8810            );
8811        }
8812    }
8813
8814    #[tokio::test]
8815    async fn test_direct_or_nested_projection_uses_left_context() {
8816        let left = matrix_source("lhs", Some(Some("k")), 1, 1.0);
8817        let right = matrix_source("rhs", Some(Some("k")), 1, 2.0);
8818        let raw = plan_direct_or(
8819            scan(&left),
8820            scan(&right),
8821            direct_or_context("lhs", &["job", "k"], "v"),
8822            direct_or_context("rhs", &["job", "k"], "v"),
8823            &or_modifier("lhs or on(k) rhs"),
8824        )
8825        .await;
8826        assert!(raw.schema().iter().any(|(qualifier, field)| {
8827            qualifier.as_ref().is_some_and(|q| q.to_string() == "lhs") && field.name() == "v"
8828        }));
8829        let nested = LogicalPlanBuilder::from(raw)
8830            .project(vec![
8831                DfExpr::BinaryExpr(BinaryExpr {
8832                    left: Box::new(DfExpr::Column(Column::new(
8833                        Some(TableReference::bare("lhs")),
8834                        "v",
8835                    ))),
8836                    op: Operator::Plus,
8837                    right: Box::new(lit(1.0)),
8838                })
8839                .alias("v_plus"),
8840            ])
8841            .unwrap()
8842            .build()
8843            .unwrap();
8844        let (_, batches) = execute(nested, &build_query_engine_state()).await;
8845        assert_eq!(values(&batches, "v_plus"), vec![2.0]);
8846    }
8847
8848    #[tokio::test]
8849    async fn test_direct_or_skips_user_internal_key_name() {
8850        const USER_TAG: &str = "__promql_or_match_0";
8851        let left = tagged_source(
8852            "lhs",
8853            false,
8854            (USER_TAG, Some("left")),
8855            DirectOrValue::Float64(1.0),
8856        );
8857        let right = tagged_source(
8858            "rhs",
8859            false,
8860            (USER_TAG, Some("right")),
8861            DirectOrValue::Float64(2.0),
8862        );
8863        let raw = plan_direct_or(
8864            scan(&left),
8865            scan(&right),
8866            direct_or_context("lhs", &["job", USER_TAG], "v"),
8867            direct_or_context("rhs", &["job", USER_TAG], "v"),
8868            &or_modifier("lhs or on(missing_label) rhs"),
8869        )
8870        .await;
8871        assert!(
8872            raw.display_indent_schema()
8873                .to_string()
8874                .contains("__promql_or_match_1@")
8875        );
8876        let (_, batches) = execute(raw, &build_query_engine_state()).await;
8877        assert!(
8878            batches
8879                .iter()
8880                .all(|batch| batch.column_by_name(USER_TAG).is_some())
8881        );
8882    }
8883
8884    #[tokio::test]
8885    async fn test_direct_or_substrait_round_trip_with_normalized_key() {
8886        let state = build_query_engine_state();
8887        let ctx = SessionContext::new_with_state(state.session_state());
8888        let catalog = Arc::new(MemoryCatalogProvider::new());
8889        catalog
8890            .register_schema("public", Arc::new(MemorySchemaProvider::new()))
8891            .unwrap();
8892        ctx.register_catalog("datafusion", catalog);
8893        let left = matrix_source("lhs", Some(Some("")), 1, 1.0);
8894        let right = matrix_source("rhs", None, 1, 2.0);
8895        ctx.register_table(
8896            TableReference::full("datafusion", "public", "lhs"),
8897            table(&left),
8898        )
8899        .unwrap();
8900        ctx.register_table(
8901            TableReference::full("datafusion", "public", "rhs"),
8902            table(&right),
8903        )
8904        .unwrap();
8905        let raw = plan_direct_or(
8906            ctx.table("datafusion.public.lhs")
8907                .await
8908                .unwrap()
8909                .into_unoptimized_plan(),
8910            ctx.table("datafusion.public.rhs")
8911                .await
8912                .unwrap()
8913                .into_unoptimized_plan(),
8914            direct_or_context("lhs", &["job", "k"], "v"),
8915            direct_or_context("rhs", &["job"], "v"),
8916            &or_modifier("lhs or on(k) rhs"),
8917        )
8918        .await;
8919        let decoded = DFLogicalSubstraitConvertor
8920            .decode(
8921                DFLogicalSubstraitConvertor
8922                    .encode(&raw, DefaultSerializer)
8923                    .unwrap(),
8924                ctx.state(),
8925            )
8926            .await
8927            .unwrap();
8928        let (optimized, batches) = execute(decoded, &state).await;
8929        assert_no_internal_or_keys(optimized.schema());
8930        assert!(batches.iter().all(|batch| {
8931            batch
8932                .schema()
8933                .fields()
8934                .iter()
8935                .all(|field| !field.name().starts_with("__promql_or_match_"))
8936        }));
8937        assert_eq!(values(&batches, "v"), vec![1.0]);
8938    }
8939
8940    #[tokio::test]
8941    async fn test_direct_or_numeric_value_types() {
8942        let left = tagged_source("lhs", true, ("k", Some("lhs")), DirectOrValue::Int64(0));
8943        let right = tagged_source(
8944            "rhs",
8945            false,
8946            ("k", Some("rhs")),
8947            DirectOrValue::Float64(0.5),
8948        );
8949        let (optimized, batches) = run(
8950            &left,
8951            &right,
8952            direct_or_context("lhs", &["job", "k"], "v"),
8953            direct_or_context("rhs", &["job", "k"], "v"),
8954            &or_modifier("lhs or on(k) rhs"),
8955        )
8956        .await;
8957        assert_eq!(
8958            optimized
8959                .schema()
8960                .field_with_name(None, "v")
8961                .unwrap()
8962                .data_type(),
8963            &ArrowDataType::Float64
8964        );
8965        assert_eq!(values(&batches, "v"), vec![0.5]);
8966        let provider = build_test_table_provider_with_fields(
8967            &[(DEFAULT_SCHEMA_NAME.to_string(), "dummy".to_string())],
8968            &[],
8969        )
8970        .await;
8971        let mut planner = PromPlanner {
8972            table_provider: provider,
8973            ctx: PromPlannerContext::default(),
8974        };
8975        let left_context = direct_or_context("lhs", &["job"], "v");
8976        let right_context = direct_or_context("rhs", &["job"], "v");
8977        let error = planner
8978            .or_operator(
8979                scan(&job_source("lhs", DirectOrValue::Utf8("x"))),
8980                scan(&job_source("rhs", DirectOrValue::Float64(1.0))),
8981                left_context.tag_columns.iter().cloned().collect(),
8982                right_context.tag_columns.iter().cloned().collect(),
8983                left_context,
8984                right_context,
8985                &or_modifier("lhs or on() rhs"),
8986            )
8987            .unwrap_err();
8988        assert!(
8989            error
8990                .to_string()
8991                .contains("OR value fields have incompatible types")
8992        );
8993    }
8994}