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
15mod at_modifier;
16mod function_plans;
17mod island;
18mod matching_filters;
19
20mod set_operator;
21
22use std::collections::{BTreeSet, HashSet, VecDeque};
23use std::sync::Arc;
24use std::time::UNIX_EPOCH;
25
26use arrow::datatypes::IntervalDayTime;
27use async_recursion::async_recursion;
28use catalog::table_source::DfTableSourceProvider;
29use common_error::ext::ErrorExt;
30use common_error::status_code::StatusCode;
31use common_function::function::FunctionContext;
32use common_query::native_histogram::native_histogram_value_type;
33use common_query::prelude::{
34    GREPTIME_TEMPORALITY_DELTA, OTLP_AGGREGATION_TEMPORALITY_LABEL, greptime_native_histogram,
35    greptime_value,
36};
37use common_query::promql_annotations::PromqlAnnotationCollector;
38use datafusion::common::DFSchemaRef;
39use datafusion::datasource::DefaultTableSource;
40use datafusion::functions_aggregate::average::avg_udaf;
41use datafusion::functions_aggregate::count::count_udaf;
42use datafusion::functions_aggregate::expr_fn::first_value;
43use datafusion::functions_aggregate::min_max::{max_udaf, min_udaf};
44use datafusion::functions_aggregate::stddev::stddev_pop_udaf;
45use datafusion::functions_aggregate::sum::sum_udaf;
46use datafusion::functions_aggregate::variance::var_pop_udaf;
47use datafusion::functions_window::row_number::RowNumber;
48use datafusion::logical_expr::expr::{Alias, ScalarFunction, WindowFunction};
49use datafusion::logical_expr::expr_rewriter::normalize_cols;
50use datafusion::logical_expr::{
51    BinaryExpr, Cast, Extension, LogicalPlan, LogicalPlanBuilder, Operator,
52    ScalarUDF as ScalarUdfDef, WindowFrame, WindowFunctionDefinition,
53};
54use datafusion::prelude as df_prelude;
55use datafusion::prelude::{Column, Expr as DfExpr, JoinType};
56use datafusion::scalar::ScalarValue;
57use datafusion_common::{DFSchema, NullEquality, TableReference};
58use datafusion_expr::expr::WindowFunctionParams;
59use datafusion_expr::expr_fn::when;
60use datafusion_expr::utils::{conjunction, disjunction};
61use datafusion_expr::{ExprSchemable, Literal, SortExpr, TableSource, ident, lit};
62use datafusion_functions::core::coalesce;
63use datatypes::arrow::datatypes::{DataType as ArrowDataType, TimeUnit as ArrowTimeUnit};
64use datatypes::data_type::{ConcreteDataType, DataType as GreptimeDataType};
65use itertools::Itertools;
66use once_cell::sync::Lazy;
67use promql::extension_plan::{
68    EmptyMetric, InstantManipulate, Millisecond, RangeManipulate, SeriesDivide, SeriesNormalize,
69    build_special_time_expr,
70};
71use promql::functions::{
72    AbsentOverTime, AvgOverTime, Changes, CountOverTime, Delta, Deriv, DoubleExponentialSmoothing,
73    IDelta, Increase, LastOverTime, MatchGroupViolation, MaxOverTime, MinOverTime, MixedRange,
74    NativeHistogramAbsentOverTime, NativeHistogramAdd, NativeHistogramAggAvg,
75    NativeHistogramAggSum, NativeHistogramAvg, NativeHistogramAvgOverTime, NativeHistogramChanges,
76    NativeHistogramCount, NativeHistogramCountOverTime, NativeHistogramDelta,
77    NativeHistogramDivScalar, NativeHistogramDrop, NativeHistogramEq, NativeHistogramIDelta,
78    NativeHistogramIRate, NativeHistogramIncrease, NativeHistogramLastOverTime,
79    NativeHistogramMulScalar, NativeHistogramNeg, NativeHistogramNotEq,
80    NativeHistogramPresentOverTime, NativeHistogramRate, NativeHistogramResets,
81    NativeHistogramScalarMul, NativeHistogramStddev, NativeHistogramStdvar, NativeHistogramSub,
82    NativeHistogramSum, NativeHistogramSumOverTime, NativeHistogramToString, PredictLinear,
83    PresentOverTime, PromqlFloatToString, QuantileOverTime, Rate, Resets, Round, StddevOverTime,
84    StdvarOverTime, SumOverTime, UniqueMatchGroup, quantile_udaf,
85};
86use promql_parser::label::{METRIC_NAME, MatchOp, Matcher, Matchers};
87use promql_parser::parser::token::TokenType;
88use promql_parser::parser::value::ValueType;
89use promql_parser::parser::{
90    AggregateExpr, BinModifier, BinaryExpr as PromBinaryExpr, Call, EvalStmt, Expr as PromExpr,
91    Function, LabelModifier, MatrixSelector, NumberLiteral, Offset, ParenExpr, StringLiteral,
92    SubqueryExpr, UnaryExpr, VectorMatchCardinality, VectorSelector, token,
93};
94use regex::{self, Regex};
95use snafu::{OptionExt, ResultExt, ensure};
96use store_api::metric_engine_consts::{
97    DATA_SCHEMA_TABLE_ID_COLUMN_NAME, DATA_SCHEMA_TSID_COLUMN_NAME, LOGICAL_TABLE_METADATA_KEY,
98    METRIC_ENGINE_NAME, is_metric_engine_internal_column,
99};
100use table::table::adapter::DfTableProviderAdapter;
101
102use crate::parser::{
103    ALIAS_NODE_NAME, ANALYZE_NODE_NAME, ANALYZE_VERBOSE_NODE_NAME, AliasExpr, EXPLAIN_NODE_NAME,
104    EXPLAIN_VERBOSE_NODE_NAME,
105};
106use crate::promql::error::{
107    CatalogSnafu, ColumnNotFoundSnafu, DataFusionPlanningSnafu, ExpectRangeSelectorSnafu,
108    FunctionInvalidArgumentSnafu, InvalidDestinationLabelNameSnafu, InvalidRegularExpressionSnafu,
109    InvalidTimeRangeSnafu, MultiFieldsNotSupportedSnafu, MultipleMetricMatchersSnafu,
110    MultipleVectorSnafu, NoMetricMatcherSnafu, Result, TableNameNotFoundSnafu,
111    TimeIndexNotFoundSnafu, UnexpectedPlanExprSnafu, UnexpectedTokenSnafu, UnknownTableSnafu,
112    UnsupportedExprSnafu, UnsupportedMatcherOpSnafu, ValueNotFoundSnafu, ZeroRangeSelectorSnafu,
113};
114use crate::query_engine::QueryEngineState;
115
116/// `time()` function in PromQL.
117const SPECIAL_TIME_FUNCTION: &str = "time";
118/// `scalar()` function in PromQL.
119const SCALAR_FUNCTION: &str = "scalar";
120/// `absent()` function in PromQL
121const SPECIAL_ABSENT_FUNCTION: &str = "absent";
122/// `histogram_quantile` function in PromQL
123const SPECIAL_HISTOGRAM_QUANTILE: &str = "histogram_quantile";
124/// `histogram_fraction` function in PromQL
125const SPECIAL_HISTOGRAM_FRACTION: &str = "histogram_fraction";
126/// `vector` function in PromQL
127const SPECIAL_VECTOR_FUNCTION: &str = "vector";
128/// `le` column for conventional histogram.
129const LE_COLUMN_NAME: &str = "le";
130
131/// Static regex for validating label names according to Prometheus specification.
132/// Label names must match the regex: [a-zA-Z_][a-zA-Z0-9_]*
133static LABEL_NAME_REGEX: Lazy<Regex> =
134    Lazy::new(|| Regex::new(r"^[a-zA-Z_][a-zA-Z0-9_]*$").unwrap());
135
136const DEFAULT_TIME_INDEX_COLUMN: &str = "time";
137
138/// default value column name for empty metric
139const DEFAULT_FIELD_COLUMN: &str = "value";
140
141/// Special modifier to project field columns under multi-field mode
142const FIELD_COLUMN_MATCHER: &str = "__field__";
143
144/// Special modifier for cross schema query
145const SCHEMA_COLUMN_MATCHER: &str = "__schema__";
146const DB_COLUMN_MATCHER: &str = "__database__";
147
148const OR_FLOAT_FIELD_PREFIX: &str = "__promql_or_float_";
149const OR_HISTOGRAM_FIELD_PREFIX: &str = "__promql_or_histogram_";
150const TIMESTAMP_VALUE_PREFIX: &str = "__promql_timestamp_value_";
151/// Per-match-group row count the vector matching cardinality check is built on.
152const MATCH_GROUP_COUNT_COLUMN: &str = "__promql_match_group_count";
153
154/// Threshold for scatter scan mode
155const MAX_SCATTER_POINTS: i64 = 400;
156
157/// Interval 1 hour in millisecond
158const INTERVAL_1H: i64 = 60 * 60 * 1000;
159
160#[derive(Default, Debug, Clone)]
161struct PromPlannerContext {
162    // query parameters
163    start: Millisecond,
164    end: Millisecond,
165    interval: Millisecond,
166    lookback_delta: Millisecond,
167    /// Evaluation range of the whole statement, which `@ start()` and `@ end()` refer to.
168    ///
169    /// Unlike [`Self::start`] and [`Self::end`], these are never rewritten while planning, so a
170    /// selector inside a subquery still resolves `@ start()` / `@ end()` against the statement.
171    stmt_start: Millisecond,
172    stmt_end: Millisecond,
173
174    // planner states
175    table_name: Option<String>,
176    time_index_column: Option<String>,
177    field_columns: Vec<String>,
178    tag_columns: Vec<String>,
179    /// `by(...)` labels of the aggregation that produced this operand that are not series tags of
180    /// its input, i.e. value fields (or a label an inner aggregation already reported as one).
181    ///
182    /// An aggregation reports every `by(...)` label it finds in its input schema among its tag
183    /// columns, and a value field named there is a group key of the aggregate rather than a
184    /// property of a series: its value varies between the samples of one series. Lowering a
185    /// matcher on it into the scan would change which samples are selected (#9242), so
186    /// [`matching_filters`] refuses to propagate such a matcher.
187    aggregation_field_labels: Vec<String>,
188    /// Use metric engine internal series identifier column (`__tsid`) as series key.
189    ///
190    /// This is enabled only when the underlying scan can provide `__tsid` (`UInt64`). The planner
191    /// uses it internally (e.g. as the series key for [`SeriesDivide`]) and strips it from the
192    /// final output.
193    use_tsid: bool,
194    /// The matcher for field columns `__field__`.
195    field_column_matcher: Option<Vec<Matcher>>,
196    /// The matcher for selectors (normal matchers).
197    selector_matcher: Vec<Matcher>,
198    schema_name: Option<String>,
199    /// The range in millisecond of range selector. None if there is no range selector.
200    range: Option<Millisecond>,
201    /// The offset in milliseconds the window of the last planned range selector is folded with,
202    /// or `None` when no range selector has been planned since the last read.
203    ///
204    /// [`Self::start`] and the sample timestamps are compared on the shifted timeline: a range
205    /// payload carries `sample_timestamp + offset`, while the time index column of a folded row
206    /// stays the evaluation timestamp of its step. A function that reads it right after its input
207    /// plan is built, like `predict_linear`, consumes it instead of reading the state, so the
208    /// offset cannot leak from one input to another; see
209    /// [`PromPlanner::create_function_expr`].
210    range_fold_offset: Option<Millisecond>,
211}
212
213/// Result labels a vector-vector binary operation derives from its matching modifier, projected
214/// from the operand each label belongs to.
215#[derive(Debug)]
216struct BinaryResultLabels {
217    exprs: Vec<DfExpr>,
218    names: Vec<String>,
219    aggregation_field_labels: Vec<String>,
220    /// `__tsid` column of the operand the labels come from, when that operand contributes its
221    /// whole tag set and the column still identifies the result series.
222    tsid: Option<DfExpr>,
223}
224
225impl BinaryResultLabels {
226    fn apply(&self, ctx: &mut PromPlannerContext) {
227        ctx.tag_columns = self.names.clone();
228        ctx.aggregation_field_labels = self.aggregation_field_labels.clone();
229        ctx.use_tsid = self.tsid.is_some();
230    }
231
232    /// `__tsid` is projected from whichever operand the labels come from, so it carries that
233    /// operand's qualifier. Re-qualify it as the result's own, which is what the context names
234    /// and what the enclosing expression looks the column up by.
235    fn tsid_projection(&self, table_ref: Option<TableReference>) -> Option<DfExpr> {
236        self.tsid
237            .clone()
238            .map(|tsid| tsid.alias_qualified(table_ref, DATA_SCHEMA_TSID_COLUMN_NAME))
239    }
240}
241
242impl PromPlannerContext {
243    fn from_eval_stmt(stmt: &EvalStmt) -> Self {
244        let start = stmt.start.duration_since(UNIX_EPOCH).unwrap().as_millis() as Millisecond;
245        let end = stmt.end.duration_since(UNIX_EPOCH).unwrap().as_millis() as Millisecond;
246        Self {
247            start,
248            end,
249            interval: stmt.interval.as_millis() as _,
250            lookback_delta: stmt.lookback_delta.as_millis() as _,
251            stmt_start: start,
252            stmt_end: end,
253            ..Default::default()
254        }
255    }
256
257    /// Reset all planner states
258    fn reset(&mut self) {
259        self.table_name = None;
260        self.time_index_column = None;
261        self.field_columns = vec![];
262        self.tag_columns = vec![];
263        self.use_tsid = false;
264        self.field_column_matcher = None;
265        self.selector_matcher.clear();
266        self.schema_name = None;
267        self.range = None;
268        self.range_fold_offset = None;
269    }
270
271    /// Reset table name and schema to empty
272    fn reset_table_name_and_schema(&mut self) {
273        self.table_name = Some(String::new());
274        self.schema_name = None;
275        self.use_tsid = false;
276    }
277
278    /// Check if `le` is present in tag columns
279    fn has_le_tag(&self) -> bool {
280        self.tag_columns.iter().any(|c| c.eq(&LE_COLUMN_NAME))
281    }
282}
283
284pub struct PromPlanner {
285    table_provider: DfTableSourceProvider,
286    ctx: PromPlannerContext,
287    /// Optional collector passed to native histogram UDFs.
288    promql_annotations: Option<PromqlAnnotationCollector>,
289}
290
291type BinaryFieldPair<'a> = (&'a String, &'a String);
292
293impl PromPlanner {
294    pub async fn stmt_to_plan(
295        table_provider: DfTableSourceProvider,
296        stmt: &EvalStmt,
297        query_engine_state: &QueryEngineState,
298    ) -> Result<LogicalPlan> {
299        Self::stmt_to_plan_with_annotations(table_provider, stmt, query_engine_state, None).await
300    }
301
302    /// Plans a PromQL statement and passes the optional collector to histogram UDFs.
303    pub async fn stmt_to_plan_with_annotations(
304        table_provider: DfTableSourceProvider,
305        stmt: &EvalStmt,
306        query_engine_state: &QueryEngineState,
307        promql_annotations: Option<PromqlAnnotationCollector>,
308    ) -> Result<LogicalPlan> {
309        let mut planner = Self {
310            table_provider,
311            ctx: PromPlannerContext::from_eval_stmt(stmt),
312            promql_annotations,
313        };
314
315        let plan = planner
316            .prom_expr_to_plan(&stmt.expr, query_engine_state)
317            .await?;
318
319        // Never leak internal series identifier to output.
320        planner.strip_tsid_column(plan)
321    }
322
323    pub async fn prom_expr_to_plan(
324        &mut self,
325        prom_expr: &PromExpr,
326        query_engine_state: &QueryEngineState,
327    ) -> Result<LogicalPlan> {
328        self.prom_expr_to_plan_inner(prom_expr, false, query_engine_state)
329            .await
330    }
331
332    /**
333    Converts a PromQL expression to a logical plan.
334
335    NOTE:
336        The `timestamp_fn` indicates whether the PromQL `timestamp()` function is being evaluated in the current context.
337        If `true`, the planner generates a logical plan that projects the timestamp (time index) column
338        as the value column for each input row, implementing the PromQL `timestamp()` function semantics.
339        If `false`, the planner generates the standard logical plan for the given PromQL expression.
340    */
341    #[async_recursion]
342    async fn prom_expr_to_plan_inner(
343        &mut self,
344        prom_expr: &PromExpr,
345        timestamp_fn: bool,
346        query_engine_state: &QueryEngineState,
347    ) -> Result<LogicalPlan> {
348        // An anchored range call is step-invariant: evaluate it once, at the start of the
349        // evaluation, and report its result at every step; see
350        // [`Self::promote_anchored_range_call`].
351        if let Some(plan) = self
352            .promote_anchored_range_call(prom_expr, timestamp_fn, query_engine_state)
353            .await?
354        {
355            return Ok(plan);
356        }
357
358        let res = match prom_expr {
359            PromExpr::Aggregate(expr) => {
360                self.prom_aggr_expr_to_plan(query_engine_state, expr)
361                    .await?
362            }
363            PromExpr::Unary(expr) => {
364                self.prom_unary_expr_to_plan(query_engine_state, expr)
365                    .await?
366            }
367            PromExpr::Binary(expr) => {
368                self.prom_binary_expr_to_plan(query_engine_state, expr)
369                    .await?
370            }
371            PromExpr::Paren(ParenExpr { expr }) => {
372                self.prom_expr_to_plan_inner(expr, timestamp_fn, query_engine_state)
373                    .await?
374            }
375            PromExpr::Subquery(expr) => {
376                self.prom_subquery_expr_to_plan(query_engine_state, expr)
377                    .await?
378            }
379            PromExpr::NumberLiteral(lit) => self.prom_number_lit_to_plan(lit)?,
380            PromExpr::StringLiteral(lit) => self.prom_string_lit_to_plan(lit)?,
381            PromExpr::VectorSelector(selector) => {
382                self.prom_vector_selector_to_plan(selector, timestamp_fn)
383                    .await?
384            }
385            PromExpr::MatrixSelector(selector) => {
386                self.prom_matrix_selector_to_plan(selector).await?
387            }
388            PromExpr::Call(expr) => {
389                self.prom_call_expr_to_plan(query_engine_state, expr)
390                    .await?
391            }
392            PromExpr::Extension(expr) => {
393                self.prom_ext_expr_to_plan(query_engine_state, expr).await?
394            }
395        };
396
397        Ok(res)
398    }
399
400    async fn prom_subquery_expr_to_plan(
401        &mut self,
402        query_engine_state: &QueryEngineState,
403        subquery_expr: &SubqueryExpr,
404    ) -> Result<LogicalPlan> {
405        let SubqueryExpr {
406            expr,
407            range,
408            step,
409            offset,
410            ..
411        } = subquery_expr;
412
413        // Prometheus evaluates the inner expression over `(start - offset - range, end - offset]`
414        // (`subqueryTimeRange`). Shift the inner window back here; `RangeManipulate` maps the
415        // samples forward again by the same offset.
416        let offset_ms = match offset {
417            Some(Offset::Pos(duration)) => duration.as_millis() as Millisecond,
418            Some(Offset::Neg(duration)) => -(duration.as_millis() as Millisecond),
419            None => 0,
420        };
421
422        let current_interval = self.ctx.interval;
423        if let Some(step) = step {
424            self.ctx.interval = step.as_millis() as _;
425        }
426        let current_start = self.ctx.start;
427        let current_end = self.ctx.end;
428        self.ctx.start -= offset_ms + range.as_millis() as i64 - self.ctx.interval;
429        self.ctx.end -= offset_ms;
430        let input = self.prom_expr_to_plan(expr, query_engine_state).await?;
431        self.ctx.interval = current_interval;
432        self.ctx.start = current_start;
433        self.ctx.end = current_end;
434
435        ensure!(!range.is_zero(), ZeroRangeSelectorSnafu);
436        let range_ms = range.as_millis() as _;
437        self.ctx.range = Some(range_ms);
438
439        let time_index_column =
440            self.ctx
441                .time_index_column
442                .clone()
443                .with_context(|| TimeIndexNotFoundSnafu {
444                    table: self.ctx.table_name.clone().unwrap_or_default(),
445                })?;
446
447        // `RangeManipulate` assumes each input batch holds exactly one series
448        // (it takes tag column values from row 0 and applies them to every
449        // output row). The inner expression may emit batches that mix series,
450        // so sort by series key + time index and split into per-series batches
451        // with a `SeriesDivide` first.
452        let input_schema = input.schema();
453        let input_has_tsid = input_schema.fields().iter().any(|field| {
454            field.name() == DATA_SCHEMA_TSID_COLUMN_NAME
455                && field.data_type() == &ArrowDataType::UInt64
456        });
457        let (series_key_columns, mut sort_exprs) = if input_has_tsid {
458            (
459                vec![DATA_SCHEMA_TSID_COLUMN_NAME.to_string()],
460                vec![
461                    DfExpr::Column(Column::from_name(DATA_SCHEMA_TSID_COLUMN_NAME))
462                        .sort(true, true),
463                ],
464            )
465        } else {
466            // Only use tag columns that survive in the inner plan's schema —
467            // `ctx.tag_columns` can drift from the actual output.
468            let key_columns: Vec<String> = self
469                .ctx
470                .tag_columns
471                .iter()
472                .filter(|name| input_schema.has_column_with_unqualified_name(name))
473                .cloned()
474                .collect();
475            let sort = key_columns
476                .iter()
477                .map(|name| DfExpr::Column(Column::from_name(name)).sort(true, true))
478                .collect::<Vec<_>>();
479            (key_columns, sort)
480        };
481        sort_exprs.push(DfExpr::Column(Column::from_name(&time_index_column)).sort(true, true));
482
483        let sort_plan = LogicalPlanBuilder::from(input)
484            .sort(sort_exprs)
485            .context(DataFusionPlanningSnafu)?
486            .build()
487            .context(DataFusionPlanningSnafu)?;
488        let divide_plan = LogicalPlan::Extension(Extension {
489            node: Arc::new(SeriesDivide::new(
490                series_key_columns.clone(),
491                time_index_column.clone(),
492                sort_plan,
493            )),
494        });
495
496        // `RangeManipulate` has no offset in its protobuf message; decoding recovers it from the
497        // `SeriesNormalize` directly below. Stale markers are not filtered: the input is computed.
498        let divide_plan = if offset_ms != 0 {
499            LogicalPlan::Extension(Extension {
500                node: Arc::new(SeriesNormalize::new(
501                    offset_ms,
502                    time_index_column.clone(),
503                    false,
504                    series_key_columns,
505                    divide_plan,
506                )),
507            })
508        } else {
509            divide_plan
510        };
511
512        let manipulate = RangeManipulate::new(
513            self.ctx.start,
514            self.ctx.end,
515            self.ctx.interval,
516            offset_ms,
517            range_ms,
518            time_index_column,
519            self.ctx.field_columns.clone(),
520            divide_plan,
521        )
522        .context(DataFusionPlanningSnafu)?;
523        // The payload timestamps are shifted by the subquery offset; see
524        // [`Self::create_range_eval_ts_expr`].
525        self.ctx.range_fold_offset = Some(offset_ms);
526
527        Ok(LogicalPlan::Extension(Extension {
528            node: Arc::new(manipulate),
529        }))
530    }
531
532    async fn prom_aggr_expr_to_plan(
533        &mut self,
534        query_engine_state: &QueryEngineState,
535        aggr_expr: &AggregateExpr,
536    ) -> Result<LogicalPlan> {
537        let AggregateExpr {
538            op,
539            expr,
540            modifier,
541            param,
542        } = aggr_expr;
543
544        let input = self.prom_expr_to_plan(expr, query_engine_state).await?;
545        let input_has_tsid = input.schema().fields().iter().any(|field| {
546            field.name() == DATA_SCHEMA_TSID_COLUMN_NAME
547                && field.data_type() == &ArrowDataType::UInt64
548        });
549
550        match (*op).id() {
551            token::T_TOPK | token::T_BOTTOMK => {
552                self.prom_topk_bottomk_to_plan(aggr_expr, input).await
553            }
554            _ => {
555                // When `__tsid` is available, tag columns may have been pruned from the input plan.
556                // For `keep_tsid` decision we should compare against the full row-key label set,
557                // otherwise we may incorrectly treat label-reducing aggregates as preserving labels.
558                let input_tag_columns = if input_has_tsid {
559                    self.collect_row_key_tag_columns_from_plan(&input)?
560                        .into_iter()
561                        .collect::<Vec<_>>()
562                } else {
563                    self.ctx.tag_columns.clone()
564                };
565                // calculate columns to group by
566                // Need to append time index column into group by columns
567                let mut group_exprs = self.agg_modifier_to_col(input.schema(), modifier, true)?;
568                let mixed_sample_columns =
569                    Self::alternative_sample_columns(input.schema(), &self.ctx.field_columns)
570                        .map(|(float, histogram)| (float.to_string(), histogram.to_string()));
571                // Aggregates over native histogram inputs may drop every sample in a group
572                // (e.g. `min` over histogram-only samples, or `sum` over histograms with
573                // incompatible schemas) and leave a NULL-valued group row behind. Compute this
574                // before `create_aggregate_exprs` mutates `ctx.field_columns`.
575                let preserve_any_value = mixed_sample_columns.is_some();
576                let has_native_histogram = preserve_any_value
577                    || self.all_field_columns_are_native_histograms(input.schema());
578                // convert op and value columns to aggregate exprs
579                let (mut aggr_exprs, prev_field_exprs) =
580                    self.create_aggregate_exprs(*op, param, &input)?;
581                let prev_field_exprs =
582                    normalize_cols(prev_field_exprs, &input).context(DataFusionPlanningSnafu)?;
583
584                let keep_tsid = op.id() != token::T_COUNT_VALUES
585                    && input_has_tsid
586                    && input_tag_columns.iter().collect::<HashSet<_>>()
587                        == self.ctx.tag_columns.iter().collect::<HashSet<_>>();
588
589                if keep_tsid {
590                    aggr_exprs.push(
591                        first_value(
592                            DfExpr::Column(Column::from_name(DATA_SCHEMA_TSID_COLUMN_NAME)),
593                            vec![],
594                        )
595                        .alias(DATA_SCHEMA_TSID_COLUMN_NAME),
596                    );
597                }
598                self.ctx.use_tsid = keep_tsid;
599
600                // create plan
601                let builder = LogicalPlanBuilder::from(input);
602                let builder = if op.id() == token::T_COUNT_VALUES {
603                    let label = Self::get_param_value_as_str(*op, param)?;
604                    // `count_values` must be grouped by fields,
605                    // and project the fields to the new label.
606                    //
607                    // The generated label is a real label (column) of the output, so it must be
608                    // registered in `ctx.tag_columns` below. Otherwise enclosing expressions
609                    // rebuild their projections from `ctx.tag_columns` and silently drop it.
610                    //
611                    // PromQL sets the generated label *before* the grouping key is built, so an
612                    // input label with the same name is overwritten by the sample value and must
613                    // not remain a grouping key either: samples are grouped by the generated
614                    // label only. Dropping it from the projected tag columns is required as well:
615                    // projecting both would emit two columns with the same name (rejected as an
616                    // ambiguous reference).
617                    self.ctx.tag_columns.retain(|tag| tag != label);
618                    group_exprs.retain(
619                        |expr| !matches!(expr, DfExpr::Column(column) if column.name == label),
620                    );
621                    // The tag columns projected below are unqualified `Column` references, so they
622                    // end up qualified with whatever qualifier they carry in the input plan. Give
623                    // the generated label the same qualifier, otherwise qualified references to it
624                    // (e.g. from an enclosing binary expression or a vector join) fail to resolve.
625                    let label_qualifier = self
626                        .ctx
627                        .time_index_column
628                        .as_deref()
629                        .and_then(|time_index| {
630                            builder
631                                .schema()
632                                .qualified_field_with_unqualified_name(time_index)
633                                .ok()
634                        })
635                        .and_then(|(qualifier, _)| qualifier.cloned());
636                    // The generated label carries the *sample value*, so it must be materialized
637                    // as a string using Prometheus' format
638                    // (`strconv.FormatFloat(value, 'f', -1, 64)`: shortest decimal form without
639                    // an exponent, `1.0` becomes "1"). Labels are inferred from string columns
640                    // when a result is converted into the Prometheus HTTP API JSON format, so a
641                    // numeric label column would be mistaken for the sample value of the series.
642                    let count_value_exprs = prev_field_exprs
643                        .iter()
644                        .map(|expr| {
645                            let value = match expr {
646                                DfExpr::Column(column) => {
647                                    let value = DfExpr::Column(column.clone());
648                                    // `prom_float_to_string` formats exactly like Prometheus,
649                                    // while arrow's `Float64 -> Utf8` cast would render `1.0`.
650                                    let value = if matches!(
651                                        builder.schema().field_with_unqualified_name(&column.name),
652                                        Ok(field) if field.data_type() == &ArrowDataType::Float64
653                                    ) {
654                                        value
655                                    } else {
656                                        DfExpr::Cast(Cast::new(
657                                            Box::new(value),
658                                            ArrowDataType::Float64,
659                                        ))
660                                    };
661                                    DfExpr::ScalarFunction(ScalarFunction {
662                                        func: Arc::new(PromqlFloatToString::scalar_udf()),
663                                        args: vec![value],
664                                    })
665                                }
666                                // The value is already formatted (e.g. a native histogram is
667                                // converted to its string form by the aggregation), and the
668                                // aggregate output names it by its schema name.
669                                _ => DfExpr::Column(Column::from_name(
670                                    expr.schema_name().to_string(),
671                                )),
672                            };
673                            DfExpr::Alias(Alias::new(value, label_qualifier.clone(), label))
674                        })
675                        .collect::<Vec<_>>();
676                    let aggregate_group_exprs = group_exprs
677                        .iter()
678                        .cloned()
679                        .chain(prev_field_exprs.clone())
680                        .collect::<Vec<_>>();
681                    group_exprs.push(ident(label));
682                    let project_fields = self
683                        .create_field_column_exprs()?
684                        .into_iter()
685                        .chain(self.create_tag_column_exprs()?)
686                        .chain(Some(self.create_time_index_column_expr()?))
687                        .chain(count_value_exprs);
688
689                    let builder = builder
690                        .aggregate(aggregate_group_exprs, aggr_exprs)
691                        .context(DataFusionPlanningSnafu)?
692                        .project(project_fields)
693                        .context(DataFusionPlanningSnafu)?;
694                    // The label only exists in the output schema from here on.
695                    self.ctx.tag_columns.push(label.to_string());
696                    builder
697                } else {
698                    builder
699                        .aggregate(group_exprs.clone(), aggr_exprs)
700                        .context(DataFusionPlanningSnafu)?
701                };
702
703                let builder = if let Some((float, histogram)) = mixed_sample_columns {
704                    let builder = match op.id() {
705                        token::T_SUM | token::T_AVG => builder
706                            .filter(self.mixed_aggregate_filter_expr(*op, &float, &histogram)?)
707                            .context(DataFusionPlanningSnafu)?,
708                        token::T_MIN
709                        | token::T_MAX
710                        | token::T_STDDEV
711                        | token::T_STDVAR
712                        | token::T_QUANTILE => builder
713                            .filter(self.mixed_ignored_histogram_filter_expr(*op, &histogram)?)
714                            .context(DataFusionPlanningSnafu)?,
715                        _ => builder,
716                    };
717
718                    match op.id() {
719                        token::T_SUM
720                        | token::T_AVG
721                        | token::T_MIN
722                        | token::T_MAX
723                        | token::T_STDDEV
724                        | token::T_STDVAR
725                        | token::T_QUANTILE => {
726                            let project_fields = self
727                                .create_field_column_exprs()?
728                                .into_iter()
729                                .chain(self.create_tag_column_exprs()?)
730                                .chain(self.ctx.use_tsid.then_some(DfExpr::Column(
731                                    Column::from_name(DATA_SCHEMA_TSID_COLUMN_NAME),
732                                )))
733                                .chain(Some(self.create_time_index_column_expr()?));
734                            builder
735                                .project(project_fields)
736                                .context(DataFusionPlanningSnafu)?
737                        }
738                        _ => builder,
739                    }
740                } else {
741                    builder
742                };
743
744                // Drop group rows whose every aggregated sample was discarded (NULL), so that
745                // e.g. `group(min(native_histogram))` doesn't resurrect groups Prometheus
746                // considers unseen. For alternative float/histogram fields keep the row if any
747                // field survived.
748                let builder = if has_native_histogram {
749                    builder
750                        .filter(self.create_empty_values_filter_expr(preserve_any_value)?)
751                        .context(DataFusionPlanningSnafu)?
752                } else {
753                    builder
754                };
755
756                let sort_expr = group_exprs.into_iter().map(|expr| expr.sort(true, false));
757
758                builder
759                    .sort(sort_expr)
760                    .context(DataFusionPlanningSnafu)?
761                    .build()
762                    .context(DataFusionPlanningSnafu)
763            }
764        }
765    }
766
767    /// Create logical plan for PromQL topk and bottomk expr.
768    async fn prom_topk_bottomk_to_plan(
769        &mut self,
770        aggr_expr: &AggregateExpr,
771        input: LogicalPlan,
772    ) -> Result<LogicalPlan> {
773        let AggregateExpr {
774            op,
775            param,
776            modifier,
777            ..
778        } = aggr_expr;
779
780        let input_has_tsid = input.schema().fields().iter().any(|field| {
781            field.name() == DATA_SCHEMA_TSID_COLUMN_NAME
782                && field.data_type() == &ArrowDataType::UInt64
783        });
784        self.ctx.use_tsid = input_has_tsid;
785
786        let group_exprs = self.agg_modifier_to_col(input.schema(), modifier, false)?;
787
788        let mut input = input;
789        if let Some((float_column, histogram_column)) =
790            Self::alternative_sample_columns(input.schema(), &self.ctx.field_columns)
791                .map(|(float, histogram)| (float.to_string(), histogram.to_string()))
792        {
793            let drop_histogram = DfExpr::ScalarFunction(ScalarFunction {
794                func: Arc::new(NativeHistogramDrop::bool_false_udf(
795                    format!(
796                        "{}: dropped native histogram samples because this aggregation is not supported for native histograms",
797                        op
798                    ),
799                    self.promql_annotations.clone(),
800                )),
801                args: vec![ident(&histogram_column)],
802            });
803            let keep_float = when(ident(&histogram_column).is_not_null(), drop_histogram)
804                .otherwise(ident(&float_column).is_not_null())
805                .context(DataFusionPlanningSnafu)?;
806            input = LogicalPlanBuilder::from(input)
807                .filter(keep_float)
808                .context(DataFusionPlanningSnafu)?
809                .build()
810                .context(DataFusionPlanningSnafu)?;
811            self.ctx.field_columns = vec![float_column];
812        }
813
814        if self.all_field_columns_are_native_histograms(input.schema()) {
815            let promql_annotations = self.promql_annotations.clone();
816            let input = self.projection_for_each_field_column(input, |col| {
817                Ok(DfExpr::ScalarFunction(ScalarFunction {
818                    func: Arc::new(NativeHistogramDrop::float_null_udf(
819                        format!(
820                            "{}: dropped native histogram samples because this aggregation is not supported for native histograms",
821                            op
822                        ),
823                        promql_annotations.clone(),
824                    )),
825                    args: vec![DfExpr::Column(Column::from_name(col))],
826                }))
827            })?;
828            return LogicalPlanBuilder::from(input)
829                .filter(self.create_empty_values_filter_expr(false)?)
830                .context(DataFusionPlanningSnafu)?
831                .build()
832                .context(DataFusionPlanningSnafu);
833        }
834
835        let val = Self::get_param_as_literal_expr(
836            param.as_deref(),
837            Some(*op),
838            Some(ArrowDataType::Float64),
839        )?;
840
841        // convert op and value columns to window exprs.
842        let window_exprs = self.create_window_exprs(*op, group_exprs.clone(), &input)?;
843
844        let rank_columns: Vec<_> = window_exprs
845            .iter()
846            .map(|expr| expr.schema_name().to_string())
847            .collect();
848
849        // Create ranks filter with `Operator::Or`.
850        // Safety: at least one rank column
851        let filter: DfExpr = rank_columns
852            .iter()
853            .fold(None, |expr, rank| {
854                let predicate = DfExpr::BinaryExpr(BinaryExpr {
855                    left: Box::new(ident(rank)),
856                    op: Operator::LtEq,
857                    right: Box::new(val.clone()),
858                });
859
860                match expr {
861                    None => Some(predicate),
862                    Some(expr) => Some(DfExpr::BinaryExpr(BinaryExpr {
863                        left: Box::new(expr),
864                        op: Operator::Or,
865                        right: Box::new(predicate),
866                    })),
867                }
868            })
869            .unwrap();
870
871        let rank_columns: Vec<_> = rank_columns.into_iter().map(ident).collect();
872
873        let mut new_group_exprs = group_exprs.clone();
874        // Order by ranks
875        new_group_exprs.extend(rank_columns);
876
877        let group_sort_expr = new_group_exprs
878            .into_iter()
879            .map(|expr| expr.sort(true, false));
880
881        let project_fields = self
882            .create_field_column_exprs()?
883            .into_iter()
884            .chain(self.create_tag_column_exprs()?)
885            .chain(
886                self.ctx
887                    .use_tsid
888                    .then_some(DfExpr::Column(Column::from_name(
889                        DATA_SCHEMA_TSID_COLUMN_NAME,
890                    ))),
891            )
892            .chain(Some(self.create_time_index_column_expr()?));
893
894        LogicalPlanBuilder::from(input)
895            .window(window_exprs)
896            .context(DataFusionPlanningSnafu)?
897            .filter(filter)
898            .context(DataFusionPlanningSnafu)?
899            .sort(group_sort_expr)
900            .context(DataFusionPlanningSnafu)?
901            .project(project_fields)
902            .context(DataFusionPlanningSnafu)?
903            .build()
904            .context(DataFusionPlanningSnafu)
905    }
906
907    async fn prom_unary_expr_to_plan(
908        &mut self,
909        query_engine_state: &QueryEngineState,
910        unary_expr: &UnaryExpr,
911    ) -> Result<LogicalPlan> {
912        let UnaryExpr { expr } = unary_expr;
913        // Unary Expr in PromQL implys the `-` operator
914        let input = self.prom_expr_to_plan(expr, query_engine_state).await?;
915        self.negate_field_columns(input)
916    }
917
918    fn negate_field_columns(&mut self, input: LogicalPlan) -> Result<LogicalPlan> {
919        let input_schema = input.schema().clone();
920        self.projection_for_each_field_column(input, |col| {
921            if Self::field_column_is_native_histogram(&input_schema, col) {
922                Ok(DfExpr::ScalarFunction(ScalarFunction {
923                    func: Arc::new(NativeHistogramNeg::scalar_udf()),
924                    args: vec![DfExpr::Column(Column::from_name(col))],
925                }))
926            } else {
927                Ok(DfExpr::Negative(Box::new(DfExpr::Column(
928                    Column::from_name(col),
929                ))))
930            }
931        })
932    }
933
934    async fn prom_binary_expr_to_plan(
935        &mut self,
936        query_engine_state: &QueryEngineState,
937        binary_expr: &PromBinaryExpr,
938    ) -> Result<LogicalPlan> {
939        // promql-parser accepts fill modifiers, but Greptime does not implement the
940        // required outer joins and missing-value substitution. Reject them before the
941        // binary-island fast path so they cannot silently behave like normal inner joins.
942        if let Some(modifier) = &binary_expr.modifier {
943            ensure!(
944                modifier.fill_values.lhs.is_none() && modifier.fill_values.rhs.is_none(),
945                UnsupportedExprSnafu {
946                    name: "PromQL fill modifiers"
947                }
948            );
949        }
950
951        if let Some(plan) = self.try_plan_binary_island(binary_expr).await? {
952            return Ok(plan);
953        }
954
955        let PromBinaryExpr {
956            lhs,
957            rhs,
958            op,
959            modifier,
960        } = binary_expr;
961
962        // if set to true, comparison operator will return 0/1 (for true/false) instead of
963        // filter on the result column
964        let should_return_bool = if let Some(m) = modifier {
965            m.return_bool
966        } else {
967            false
968        };
969        let is_comparison_op = Self::is_token_a_comparison_op(*op);
970
971        // we should build a filter plan here if the op is comparison op and need not
972        // to return 0/1. Otherwise, we should build a projection plan
973        match (
974            Self::try_build_literal_expr(lhs),
975            Self::try_build_literal_expr(rhs),
976        ) {
977            (Some(lhs), Some(rhs)) => {
978                self.ctx.time_index_column = Some(DEFAULT_TIME_INDEX_COLUMN.to_string());
979                self.ctx.field_columns = vec![DEFAULT_FIELD_COLUMN.to_string()];
980                self.ctx.reset_table_name_and_schema();
981                let field_expr_builder = Self::prom_token_to_binary_expr_builder(*op)?;
982                let mut field_expr = field_expr_builder(lhs, rhs)?;
983
984                if is_comparison_op && should_return_bool {
985                    field_expr =
986                        DfExpr::Cast(Cast::new(Box::new(field_expr), ArrowDataType::Float64));
987                }
988
989                Ok(LogicalPlan::Extension(Extension {
990                    node: Arc::new(
991                        EmptyMetric::new(
992                            self.ctx.start,
993                            self.ctx.end,
994                            self.ctx.interval,
995                            SPECIAL_TIME_FUNCTION.to_string(),
996                            DEFAULT_FIELD_COLUMN.to_string(),
997                            Some(field_expr),
998                        )
999                        .context(DataFusionPlanningSnafu)?,
1000                    ),
1001                }))
1002            }
1003            // lhs is a literal, rhs is a column
1004            (Some(mut expr), None) => {
1005                let input = self.prom_expr_to_plan(rhs, query_engine_state).await?;
1006                // Arithmetic against a literal preserves the series labels, so
1007                // `aggregation_field_labels` passes through with `tag_columns` unchanged.
1008                // check if the literal is a special time expr
1009                if let Some(time_expr) = self.try_build_special_time_expr_with_context(lhs) {
1010                    expr = time_expr
1011                }
1012                let input_schema = input.schema().clone();
1013                let preserve_any_value = Self::field_columns_are_alternative_samples(
1014                    &input_schema,
1015                    &self.ctx.field_columns,
1016                );
1017                let has_native_histogram = Self::field_columns_contain_native_histogram(
1018                    &input_schema,
1019                    &self.ctx.field_columns,
1020                );
1021                let retain_field_columns = self
1022                    .ctx
1023                    .field_columns
1024                    .iter()
1025                    .map(|col| {
1026                        Self::binary_result_is_histogram(
1027                            *op,
1028                            false,
1029                            Self::field_column_is_native_histogram(&input_schema, col),
1030                        )
1031                        .is_some()
1032                    })
1033                    .collect();
1034                let promql_annotations = self.promql_annotations.clone();
1035                let bin_expr_builder = |col: &String| {
1036                    let binary_expr_builder = Self::prom_token_to_binary_expr_builder(*op)?;
1037                    let rhs_is_histogram =
1038                        Self::field_column_is_native_histogram(&input_schema, col);
1039                    let rhs = DfExpr::Column(Column::from_name(col));
1040                    let mut binary_expr = match Self::native_histogram_binary_expr(
1041                        *op,
1042                        expr.clone(),
1043                        false,
1044                        rhs.clone(),
1045                        rhs_is_histogram,
1046                        is_comparison_op && !should_return_bool,
1047                        promql_annotations.clone(),
1048                    )? {
1049                        Some(expr) => expr,
1050                        None => binary_expr_builder(expr.clone(), rhs)?,
1051                    };
1052
1053                    if is_comparison_op && should_return_bool {
1054                        binary_expr =
1055                            DfExpr::Cast(Cast::new(Box::new(binary_expr), ArrowDataType::Float64));
1056                    }
1057                    Ok(binary_expr)
1058                };
1059                if is_comparison_op && !should_return_bool {
1060                    self.filter_on_field_column(input, bin_expr_builder)
1061                } else {
1062                    let projected =
1063                        self.projection_for_each_field_column(input, bin_expr_builder)?;
1064                    self.filter_binary_projection(
1065                        projected,
1066                        has_native_histogram,
1067                        preserve_any_value,
1068                        retain_field_columns,
1069                    )
1070                }
1071            }
1072            // lhs is a column, rhs is a literal
1073            (None, Some(mut expr)) => {
1074                let input = self.prom_expr_to_plan(lhs, query_engine_state).await?;
1075                // check if the literal is a special time expr
1076                if let Some(time_expr) = self.try_build_special_time_expr_with_context(rhs) {
1077                    expr = time_expr
1078                }
1079                let input_schema = input.schema().clone();
1080                let preserve_any_value = Self::field_columns_are_alternative_samples(
1081                    &input_schema,
1082                    &self.ctx.field_columns,
1083                );
1084                let has_native_histogram = Self::field_columns_contain_native_histogram(
1085                    &input_schema,
1086                    &self.ctx.field_columns,
1087                );
1088                let retain_field_columns = self
1089                    .ctx
1090                    .field_columns
1091                    .iter()
1092                    .map(|col| {
1093                        Self::binary_result_is_histogram(
1094                            *op,
1095                            Self::field_column_is_native_histogram(&input_schema, col),
1096                            false,
1097                        )
1098                        .is_some()
1099                    })
1100                    .collect();
1101                let promql_annotations = self.promql_annotations.clone();
1102                let bin_expr_builder = |col: &String| {
1103                    let binary_expr_builder = Self::prom_token_to_binary_expr_builder(*op)?;
1104                    let lhs_is_histogram =
1105                        Self::field_column_is_native_histogram(&input_schema, col);
1106                    let lhs = DfExpr::Column(Column::from_name(col));
1107                    let mut binary_expr = match Self::native_histogram_binary_expr(
1108                        *op,
1109                        lhs.clone(),
1110                        lhs_is_histogram,
1111                        expr.clone(),
1112                        false,
1113                        is_comparison_op && !should_return_bool,
1114                        promql_annotations.clone(),
1115                    )? {
1116                        Some(expr) => expr,
1117                        None => binary_expr_builder(lhs, expr.clone())?,
1118                    };
1119
1120                    if is_comparison_op && should_return_bool {
1121                        binary_expr =
1122                            DfExpr::Cast(Cast::new(Box::new(binary_expr), ArrowDataType::Float64));
1123                    }
1124                    Ok(binary_expr)
1125                };
1126                if is_comparison_op && !should_return_bool {
1127                    self.filter_on_field_column(input, bin_expr_builder)
1128                } else {
1129                    let projected =
1130                        self.projection_for_each_field_column(input, bin_expr_builder)?;
1131                    self.filter_binary_projection(
1132                        projected,
1133                        has_native_histogram,
1134                        preserve_any_value,
1135                        retain_field_columns,
1136                    )
1137                }
1138            }
1139            // both are columns. join them on time index
1140            (None, None) => {
1141                let mut left_input = self.prom_expr_to_plan(lhs, query_engine_state).await?;
1142                let left_field_columns = self.ctx.field_columns.clone();
1143                let left_time_index_column = self.ctx.time_index_column.clone();
1144                let mut left_table_ref = self
1145                    .table_ref()
1146                    .unwrap_or_else(|_| TableReference::bare(""));
1147                let mut left_context = self.ctx.clone();
1148
1149                let mut right_input = self.prom_expr_to_plan(rhs, query_engine_state).await?;
1150                let right_field_columns = self.ctx.field_columns.clone();
1151                let right_time_index_column = self.ctx.time_index_column.clone();
1152                let mut right_table_ref = self
1153                    .table_ref()
1154                    .unwrap_or_else(|_| TableReference::bare(""));
1155                let mut right_context = self.ctx.clone();
1156
1157                // Both operands are planned first because only the planned contexts tell a tag
1158                // from a value field. The rewrite merely adds matchers to a selector, so the
1159                // table reference, time index and field columns captured above stay valid.
1160                if let Some(rewritten) = matching_filters::propagate(
1161                    binary_expr,
1162                    &left_context.tag_columns,
1163                    &right_context.tag_columns,
1164                    &left_context.aggregation_field_labels,
1165                    &right_context.aggregation_field_labels,
1166                ) {
1167                    // A copied matcher belongs to the scan, not to the operand's identity:
1168                    // `absent()` turns `selector_matcher` into the labels it reports, so the
1169                    // re-planned context keeps the matchers the operand was written with.
1170                    if rewritten.lhs.as_ref() != lhs.as_ref() {
1171                        let selectors = std::mem::take(&mut left_context.selector_matcher);
1172                        left_input = self
1173                            .prom_expr_to_plan(&rewritten.lhs, query_engine_state)
1174                            .await?;
1175                        left_context = self.ctx.clone();
1176                        left_context.selector_matcher = selectors;
1177                    }
1178                    if rewritten.rhs.as_ref() != rhs.as_ref() {
1179                        let selectors = std::mem::take(&mut right_context.selector_matcher);
1180                        right_input = self
1181                            .prom_expr_to_plan(&rewritten.rhs, query_engine_state)
1182                            .await?;
1183                        right_context = self.ctx.clone();
1184                        right_context.selector_matcher = selectors;
1185                    }
1186                    // The code below reads `self.ctx` as the right operand's context.
1187                    self.ctx = right_context.clone();
1188                }
1189
1190                let left_is_empty_metric = Self::is_empty_metric(&left_input);
1191                let right_is_empty_metric = Self::is_empty_metric(&right_input);
1192
1193                // TODO(ruihang): avoid join if left and right are the same table
1194
1195                // set op has "special" join semantics
1196                if Self::is_token_a_set_op(*op) {
1197                    return self.set_op_on_non_field_columns(
1198                        left_input,
1199                        right_input,
1200                        left_context,
1201                        right_context,
1202                        *op,
1203                        modifier,
1204                    );
1205                }
1206
1207                let has_native_histogram = Self::field_columns_contain_native_histogram(
1208                    left_input.schema(),
1209                    &left_field_columns,
1210                ) || Self::field_columns_contain_native_histogram(
1211                    right_input.schema(),
1212                    &right_field_columns,
1213                );
1214
1215                // normal join
1216                if left_table_ref == right_table_ref {
1217                    // rename table references to avoid ambiguity
1218                    left_table_ref = TableReference::bare("lhs");
1219                    right_table_ref = TableReference::bare("rhs");
1220                    // `self.ctx` have ctx in right plan, if right plan have no tag,
1221                    // we use left plan ctx as the ctx for subsequent calculations,
1222                    // to avoid case like `host + scalar(...)`
1223                    // we need preserve tag column on `host` table in subsequent projection,
1224                    // which only show in left plan ctx.
1225                    if self.ctx.tag_columns.is_empty() {
1226                        self.ctx = left_context.clone();
1227                        self.ctx.table_name = Some("lhs".to_string());
1228                    } else {
1229                        self.ctx.table_name = Some("rhs".to_string());
1230                    }
1231                } else if right_is_empty_metric && !left_is_empty_metric {
1232                    self.ctx = left_context.clone();
1233                }
1234                // Computed scalars reach this join path instead of the literal projection paths.
1235                // Broadcast them for arithmetic in the same way as literal scalars.
1236                let broadcast_scalar = !is_comparison_op;
1237                let (field_groups, invalid_field_pairs) = Self::align_binary_field_columns(
1238                    left_input.schema(),
1239                    right_input.schema(),
1240                    &left_field_columns,
1241                    &right_field_columns,
1242                    *op,
1243                    broadcast_scalar && lhs.value_type() == ValueType::Scalar,
1244                    broadcast_scalar && rhs.value_type() == ValueType::Scalar,
1245                );
1246                let left_aligned_field_columns = field_groups
1247                    .iter()
1248                    .flat_map(|(_, pairs)| {
1249                        pairs
1250                            .iter()
1251                            .map(|(left_col_name, _)| (*left_col_name).clone())
1252                    })
1253                    .collect::<Vec<_>>();
1254                let right_aligned_field_columns = field_groups
1255                    .iter()
1256                    .flat_map(|(_, pairs)| {
1257                        pairs
1258                            .iter()
1259                            .map(|(_, right_col_name)| (*right_col_name).clone())
1260                    })
1261                    .collect::<Vec<_>>();
1262                // Regular multi-field vectors combine their shared prefix. Alternative
1263                // float/histogram lanes instead align by valid PromQL sample combinations.
1264                self.ctx.field_columns = field_groups
1265                    .iter()
1266                    .map(|(output, _)| output.clone())
1267                    .collect();
1268                let mut field_groups = field_groups.into_iter();
1269                // `vector()` uses EmptyMetric and keeps GreptimeDB's timestamp broadcast.
1270                let has_empty_metric_operand = left_is_empty_metric || right_is_empty_metric;
1271
1272                let only_join_time_index = lhs.value_type() == ValueType::Scalar
1273                    || rhs.value_type() == ValueType::Scalar
1274                    || has_empty_metric_operand
1275                    || ((left_context.tag_columns.is_empty()
1276                        || right_context.tag_columns.is_empty())
1277                        && !left_context
1278                            .tag_columns
1279                            .iter()
1280                            .chain(&right_context.tag_columns)
1281                            .any(|tag| tag == OTLP_AGGREGATION_TEMPORALITY_LABEL));
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                    only_join_time_index,
1290                    modifier,
1291                    &left_context,
1292                    &right_context,
1293                )?;
1294                let join_plan_schema = join_plan.schema().clone();
1295                // The matching modifier derives the result labels instead of the operands keeping
1296                // their own tag set. An operand that is broadcast rather than matched (a scalar,
1297                // or a vector without tags) keeps the other side's labels as before.
1298                let result_labels = if only_join_time_index {
1299                    None
1300                } else {
1301                    Self::binary_result_labels(&left_context, &right_context, modifier)
1302                        .map(|labels| {
1303                            Self::binary_result_label_projection(
1304                                &join_plan_schema,
1305                                &left_table_ref,
1306                                &right_table_ref,
1307                                &left_context,
1308                                &right_context,
1309                                labels,
1310                            )
1311                        })
1312                        .transpose()?
1313                };
1314                if let Some(labels) = &result_labels {
1315                    labels.apply(&mut self.ctx);
1316                }
1317                let promql_annotations = self.promql_annotations.clone();
1318                // These predicates always pass; they only evaluate otherwise-discarded pairs
1319                // while collecting annotations.
1320                let invalid_pair_predicates = invalid_field_pairs
1321                    .into_iter()
1322                    .filter(|_| promql_annotations.is_some())
1323                    .map(|(left_col_name, right_col_name)| {
1324                        let left_field = join_plan_schema
1325                            .qualified_field_with_name(Some(&left_table_ref), left_col_name)
1326                            .context(DataFusionPlanningSnafu)?;
1327                        let right_field = join_plan_schema
1328                            .qualified_field_with_name(Some(&right_table_ref), right_col_name)
1329                            .context(DataFusionPlanningSnafu)?;
1330                        let left_is_histogram =
1331                            left_field.1.data_type() == &Self::native_histogram_arrow_type();
1332                        let right_is_histogram =
1333                            right_field.1.data_type() == &Self::native_histogram_arrow_type();
1334                        let drop_expr = Self::native_histogram_binary_expr(
1335                            *op,
1336                            DfExpr::Column(left_field.into()),
1337                            left_is_histogram,
1338                            DfExpr::Column(right_field.into()),
1339                            right_is_histogram,
1340                            true,
1341                            promql_annotations.clone(),
1342                        )?
1343                        .with_context(|| UnexpectedPlanExprSnafu {
1344                            desc: "invalid native histogram pair produced no drop expression",
1345                        })?;
1346                        Ok(DfExpr::Not(Box::new(drop_expr)))
1347                    })
1348                    .collect::<Result<Vec<_>>>()?;
1349                let join_plan = if let Some(predicate) = conjunction(invalid_pair_predicates) {
1350                    LogicalPlanBuilder::from(join_plan)
1351                        .filter(predicate)
1352                        .context(DataFusionPlanningSnafu)?
1353                        .build()
1354                        .context(DataFusionPlanningSnafu)?
1355                } else {
1356                    join_plan
1357                };
1358
1359                let bin_expr_builder = |_: &String| {
1360                    let (_, field_pairs) =
1361                        field_groups
1362                            .next()
1363                            .with_context(|| UnexpectedPlanExprSnafu {
1364                                desc: "missing binary field group",
1365                            })?;
1366                    let binary_exprs = field_pairs
1367                        .into_iter()
1368                        .map(|(left_col_name, right_col_name)| {
1369                            let left_field = join_plan_schema
1370                                .qualified_field_with_name(Some(&left_table_ref), left_col_name)
1371                                .context(DataFusionPlanningSnafu)?;
1372                            let right_field = join_plan_schema
1373                                .qualified_field_with_name(Some(&right_table_ref), right_col_name)
1374                                .context(DataFusionPlanningSnafu)?;
1375                            let left_is_histogram =
1376                                left_field.1.data_type() == &Self::native_histogram_arrow_type();
1377                            let right_is_histogram =
1378                                right_field.1.data_type() == &Self::native_histogram_arrow_type();
1379                            let left_col = left_field.into();
1380                            let right_col = right_field.into();
1381
1382                            let binary_expr_builder = Self::prom_token_to_binary_expr_builder(*op)?;
1383                            let lhs = DfExpr::Column(left_col);
1384                            let rhs = DfExpr::Column(right_col);
1385                            let mut binary_expr = match Self::native_histogram_binary_expr(
1386                                *op,
1387                                lhs.clone(),
1388                                left_is_histogram,
1389                                rhs.clone(),
1390                                right_is_histogram,
1391                                is_comparison_op && !should_return_bool,
1392                                promql_annotations.clone(),
1393                            )? {
1394                                Some(expr) => expr,
1395                                None => binary_expr_builder(lhs, rhs)?,
1396                            };
1397                            if is_comparison_op && should_return_bool {
1398                                binary_expr = DfExpr::Cast(Cast::new(
1399                                    Box::new(binary_expr),
1400                                    ArrowDataType::Float64,
1401                                ));
1402                            }
1403                            Ok(binary_expr)
1404                        })
1405                        .collect::<Result<Vec<_>>>()?;
1406                    if let [binary_expr] = binary_exprs.as_slice() {
1407                        Ok(binary_expr.clone())
1408                    } else {
1409                        Ok(DfExpr::ScalarFunction(ScalarFunction {
1410                            func: coalesce(),
1411                            args: binary_exprs,
1412                        }))
1413                    }
1414                };
1415                if is_comparison_op && !should_return_bool {
1416                    // PromQL comparison operators without `bool` are filters:
1417                    //   - keep the instant-vector side sample values
1418                    //   - drop samples where the comparison is false
1419                    //
1420                    // So we filter on the join result and then project only the side that should
1421                    // be preserved according to PromQL semantics.
1422                    let filtered = self.filter_on_field_column(join_plan, bin_expr_builder)?;
1423                    let (project_table_ref, mut project_context, project_field_columns) =
1424                        match (lhs.value_type(), rhs.value_type()) {
1425                            (ValueType::Scalar, ValueType::Vector) => (
1426                                &right_table_ref,
1427                                right_context.clone(),
1428                                right_aligned_field_columns,
1429                            ),
1430                            _ => (
1431                                &left_table_ref,
1432                                left_context.clone(),
1433                                left_aligned_field_columns,
1434                            ),
1435                        };
1436                    project_context.field_columns = project_field_columns;
1437                    self.project_binary_join_side(
1438                        filtered,
1439                        project_table_ref,
1440                        &project_context,
1441                        result_labels.as_ref(),
1442                    )
1443                } else {
1444                    let projected = self.projection_for_each_field_column_with_labels(
1445                        join_plan,
1446                        result_labels.as_ref(),
1447                        bin_expr_builder,
1448                    )?;
1449                    let preserve_any_value = Self::field_columns_are_alternative_samples(
1450                        projected.schema(),
1451                        &self.ctx.field_columns,
1452                    );
1453                    let retain_field_columns = vec![true; self.ctx.field_columns.len()];
1454                    self.filter_binary_projection(
1455                        projected,
1456                        has_native_histogram,
1457                        preserve_any_value,
1458                        retain_field_columns,
1459                    )
1460                }
1461            }
1462        }
1463    }
1464
1465    fn filter_binary_projection(
1466        &mut self,
1467        input: LogicalPlan,
1468        has_native_histogram: bool,
1469        preserve_any_value: bool,
1470        retain_field_columns: Vec<bool>,
1471    ) -> Result<LogicalPlan> {
1472        if !has_native_histogram {
1473            return Ok(input);
1474        }
1475
1476        ensure!(
1477            retain_field_columns.len() == self.ctx.field_columns.len(),
1478            UnexpectedPlanExprSnafu {
1479                desc: "binary output field count changed unexpectedly",
1480            }
1481        );
1482
1483        let filtered = LogicalPlanBuilder::from(input)
1484            .filter(self.create_empty_values_filter_expr(preserve_any_value)?)
1485            .context(DataFusionPlanningSnafu)?
1486            .build()
1487            .context(DataFusionPlanningSnafu)?;
1488        if retain_field_columns.iter().all(|retain| *retain) {
1489            return Ok(filtered);
1490        }
1491
1492        let retained = self
1493            .ctx
1494            .field_columns
1495            .iter()
1496            .zip(retain_field_columns)
1497            .filter(|(_, retain)| *retain)
1498            .map(|(field, _)| field.clone())
1499            .collect::<Vec<_>>();
1500        if retained.is_empty() {
1501            return Ok(filtered);
1502        }
1503        self.ctx.field_columns = retained;
1504
1505        let mut output_columns = self
1506            .ctx
1507            .field_columns
1508            .iter()
1509            .chain(&self.ctx.tag_columns)
1510            .cloned()
1511            .collect::<HashSet<_>>();
1512        output_columns.extend(self.ctx.time_index_column.iter().cloned());
1513        if self.ctx.use_tsid {
1514            output_columns.insert(DATA_SCHEMA_TSID_COLUMN_NAME.to_string());
1515        }
1516        let project_exprs = filtered
1517            .schema()
1518            .iter()
1519            .filter(|(_, field)| output_columns.contains(field.name()))
1520            .map(|(qualifier, field)| {
1521                DfExpr::Column(Column::new(qualifier.cloned(), field.name().clone()))
1522            })
1523            .collect::<Vec<_>>();
1524        LogicalPlanBuilder::from(filtered)
1525            .project(project_exprs)
1526            .context(DataFusionPlanningSnafu)?
1527            .build()
1528            .context(DataFusionPlanningSnafu)
1529    }
1530
1531    fn project_binary_join_side(
1532        &mut self,
1533        input: LogicalPlan,
1534        table_ref: &TableReference,
1535        context: &PromPlannerContext,
1536        result_labels: Option<&BinaryResultLabels>,
1537    ) -> Result<LogicalPlan> {
1538        let schema = input.schema();
1539
1540        let mut project_exprs =
1541            Vec::with_capacity(context.tag_columns.len() + context.field_columns.len() + 2);
1542
1543        // Project time index from the chosen side.
1544        if let Some(time_index_column) = &context.time_index_column {
1545            let time_index_col = schema
1546                .qualified_field_with_name(Some(table_ref), time_index_column)
1547                .context(DataFusionPlanningSnafu)?
1548                .into();
1549            project_exprs.push(DfExpr::Column(time_index_col));
1550        }
1551
1552        // Project field columns from the chosen side.
1553        for field_column in &context.field_columns {
1554            let field_col = schema
1555                .qualified_field_with_name(Some(table_ref), field_column)
1556                .context(DataFusionPlanningSnafu)?
1557                .into();
1558            project_exprs.push(DfExpr::Column(field_col));
1559        }
1560
1561        // Project tag columns: the labels the matching modifier derived, or the chosen side's.
1562        match result_labels {
1563            Some(labels) => project_exprs.extend(labels.exprs.iter().cloned()),
1564            None => {
1565                for tag_column in &context.tag_columns {
1566                    let tag_col = schema
1567                        .qualified_field_with_name(Some(table_ref), tag_column)
1568                        .context(DataFusionPlanningSnafu)?
1569                        .into();
1570                    project_exprs.push(DfExpr::Column(tag_col));
1571                }
1572            }
1573        }
1574
1575        // Preserve `__tsid` if present, so it can still be used internally downstream. It's
1576        // stripped from the final output anyway.
1577        let tsid_col = match result_labels {
1578            Some(labels) => labels.tsid_projection(Some(table_ref.clone())),
1579            None => Self::optional_tsid_projection(schema, Some(table_ref), context.use_tsid),
1580        };
1581        if let Some(tsid_col) = tsid_col {
1582            project_exprs.push(tsid_col);
1583        }
1584
1585        let plan = LogicalPlanBuilder::from(input)
1586            .project(project_exprs)
1587            .context(DataFusionPlanningSnafu)?
1588            .build()
1589            .context(DataFusionPlanningSnafu)?;
1590
1591        // Update context to reflect the projected schema. Don't keep a table qualifier since
1592        // the result is a derived expression.
1593        self.ctx = context.clone();
1594        self.ctx.table_name = None;
1595        self.ctx.schema_name = None;
1596        if let Some(labels) = result_labels {
1597            labels.apply(&mut self.ctx);
1598        }
1599
1600        Ok(plan)
1601    }
1602
1603    fn prom_number_lit_to_plan(&mut self, number_literal: &NumberLiteral) -> Result<LogicalPlan> {
1604        let NumberLiteral { val } = number_literal;
1605        self.ctx.time_index_column = Some(DEFAULT_TIME_INDEX_COLUMN.to_string());
1606        self.ctx.field_columns = vec![DEFAULT_FIELD_COLUMN.to_string()];
1607        self.ctx.reset_table_name_and_schema();
1608        let literal_expr = df_prelude::lit(*val);
1609
1610        let plan = LogicalPlan::Extension(Extension {
1611            node: Arc::new(
1612                EmptyMetric::new(
1613                    self.ctx.start,
1614                    self.ctx.end,
1615                    self.ctx.interval,
1616                    SPECIAL_TIME_FUNCTION.to_string(),
1617                    DEFAULT_FIELD_COLUMN.to_string(),
1618                    Some(literal_expr),
1619                )
1620                .context(DataFusionPlanningSnafu)?,
1621            ),
1622        });
1623        Ok(plan)
1624    }
1625
1626    fn prom_string_lit_to_plan(&mut self, string_literal: &StringLiteral) -> Result<LogicalPlan> {
1627        let StringLiteral { val } = string_literal;
1628        self.ctx.time_index_column = Some(DEFAULT_TIME_INDEX_COLUMN.to_string());
1629        self.ctx.field_columns = vec![DEFAULT_FIELD_COLUMN.to_string()];
1630        self.ctx.reset_table_name_and_schema();
1631        let literal_expr = df_prelude::lit(val.clone());
1632
1633        let plan = LogicalPlan::Extension(Extension {
1634            node: Arc::new(
1635                EmptyMetric::new(
1636                    self.ctx.start,
1637                    self.ctx.end,
1638                    self.ctx.interval,
1639                    SPECIAL_TIME_FUNCTION.to_string(),
1640                    DEFAULT_FIELD_COLUMN.to_string(),
1641                    Some(literal_expr),
1642                )
1643                .context(DataFusionPlanningSnafu)?,
1644            ),
1645        });
1646        Ok(plan)
1647    }
1648
1649    /// The offset of a selector in milliseconds. A positive offset selects samples from an earlier
1650    /// time and moves them forward into the evaluation timeline.
1651    fn offset_millis(offset: &Option<Offset>) -> Millisecond {
1652        match offset {
1653            Some(Offset::Pos(duration)) => duration.as_millis() as Millisecond,
1654            Some(Offset::Neg(duration)) => -(duration.as_millis() as Millisecond),
1655            None => 0,
1656        }
1657    }
1658
1659    /// The columns that identify one series, which is the series key expected by the PromQL plan
1660    /// nodes that hold exactly one series per input batch.
1661    fn series_key_columns(&self) -> Vec<String> {
1662        if self.ctx.use_tsid {
1663            vec![DATA_SCHEMA_TSID_COLUMN_NAME.to_string()]
1664        } else {
1665            self.ctx.tag_columns.clone()
1666        }
1667    }
1668
1669    /// Keep replay and series division on the same effective keys when a call rewrites labels.
1670    fn series_key_columns_for_schema(&self, schema: &DFSchemaRef) -> Vec<String> {
1671        let has_tsid = schema.fields().iter().any(|field| {
1672            field.name() == DATA_SCHEMA_TSID_COLUMN_NAME
1673                && field.data_type() == &ArrowDataType::UInt64
1674        });
1675        if has_tsid {
1676            vec![DATA_SCHEMA_TSID_COLUMN_NAME.to_string()]
1677        } else {
1678            self.ctx
1679                .tag_columns
1680                .iter()
1681                .filter(|name| schema.has_column_with_unqualified_name(name))
1682                .cloned()
1683                .collect()
1684        }
1685    }
1686
1687    async fn prom_vector_selector_to_plan(
1688        &mut self,
1689        vector_selector: &VectorSelector,
1690        timestamp_fn: bool,
1691    ) -> Result<LogicalPlan> {
1692        let VectorSelector {
1693            name,
1694            offset,
1695            matchers,
1696            at,
1697        } = vector_selector;
1698        let matchers = self.preprocess_label_matchers(matchers, name)?;
1699        if let Some(empty_plan) = self.setup_context().await? {
1700            return Ok(empty_plan);
1701        }
1702        let offset_ms = Self::offset_millis(offset);
1703        // `@` anchors the sample window at a fixed timestamp: the selector selects its samples
1704        // around the anchor once, instead of following the outer evaluation grid. See
1705        // [`Self::at_modifier_offset`].
1706        let at_offset = self.at_modifier_offset(at, offset)?;
1707        let grid_start = self.ctx.start;
1708        let grid_end = self.ctx.end;
1709        let normalize = match at_offset {
1710            Some(at_offset) => {
1711                // Select the anchored samples at the start of the evaluation, with the offset that
1712                // re-anchors the selector.
1713                // The planner is single-use (one `EvalStmt` produces one plan), so an error
1714                // below aborts the whole planning and `ctx.end` needs no restore-on-error.
1715                self.ctx.end = grid_start;
1716                let plan = self
1717                    .selector_to_series_normalize_plan(at_offset, matchers, false)
1718                    .await?;
1719                self.ctx.end = grid_end;
1720                plan
1721            }
1722            None => {
1723                self.selector_to_series_normalize_plan(offset_ms, matchers, false)
1724                    .await?
1725            }
1726        };
1727        let time_index_column =
1728            self.ctx
1729                .time_index_column
1730                .clone()
1731                .with_context(|| TimeIndexNotFoundSnafu {
1732                    table: self.ctx.table_name.clone().unwrap_or_default(),
1733                })?;
1734
1735        let (normalize, timestamp_value_column) = if timestamp_fn {
1736            // Keep the original sample for stale-marker detection while carrying
1737            // its timestamp through InstantManipulate in a private value column.
1738            let occupied = normalize
1739                .schema()
1740                .fields()
1741                .iter()
1742                .map(|field| field.name().as_str())
1743                .collect::<HashSet<_>>();
1744            let mut timestamp_value_column = TIMESTAMP_VALUE_PREFIX.to_string();
1745            while occupied.contains(timestamp_value_column.as_str()) {
1746                timestamp_value_column.push('_');
1747            }
1748            let mut project_exprs = normalize
1749                .schema()
1750                .iter()
1751                .map(|(qualifier, field)| {
1752                    DfExpr::Column(Column::new(qualifier.cloned(), field.name().clone()))
1753                })
1754                .collect::<Vec<_>>();
1755            // The time index still holds the raw sample timestamp here, which is what
1756            // `timestamp()` reports regardless of `offset` and `@`.
1757            let sample_time = Self::timestamp_seconds_expr(&time_index_column, normalize.schema())?;
1758            project_exprs.push(sample_time.alias(&timestamp_value_column));
1759            let normalize = LogicalPlanBuilder::from(normalize)
1760                .project(project_exprs)
1761                .context(DataFusionPlanningSnafu)?
1762                .build()
1763                .context(DataFusionPlanningSnafu)?;
1764            (normalize, Some(timestamp_value_column))
1765        } else {
1766            (normalize, None)
1767        };
1768
1769        let field_column = self.ctx.field_columns.first().cloned();
1770        let series_key_columns = self.series_key_columns();
1771        let manipulate = match at_offset {
1772            Some(at_offset) => {
1773                // Select the anchored sample once, then report it at every step of the outer
1774                // grid. The samples keep their native anchor-time timestamps, so the manipulate
1775                // must shift them onto the evaluation timeline with the rewritten offset.
1776                let anchored = InstantManipulate::new(
1777                    grid_start,
1778                    grid_start,
1779                    self.ctx.lookback_delta,
1780                    self.ctx.interval,
1781                    at_offset,
1782                    time_index_column.clone(),
1783                    series_key_columns,
1784                    field_column,
1785                    normalize,
1786                );
1787                self.replay_over_grid(
1788                    LogicalPlan::Extension(Extension {
1789                        node: Arc::new(anchored),
1790                    }),
1791                    grid_start,
1792                    grid_end,
1793                    time_index_column,
1794                )
1795            }
1796            None => LogicalPlan::Extension(Extension {
1797                node: Arc::new(InstantManipulate::new(
1798                    grid_start,
1799                    grid_end,
1800                    self.ctx.lookback_delta,
1801                    self.ctx.interval,
1802                    offset_ms,
1803                    time_index_column,
1804                    series_key_columns,
1805                    field_column,
1806                    normalize,
1807                )),
1808            }),
1809        };
1810        if let Some(timestamp_value_column) = timestamp_value_column {
1811            self.create_timestamp_func_plan(manipulate, ident(timestamp_value_column))
1812        } else {
1813            Ok(manipulate)
1814        }
1815    }
1816
1817    /// Converts the timestamp column `column` into PromQL seconds, truncated to milliseconds.
1818    fn timestamp_seconds_expr(column: &str, schema: &DFSchema) -> Result<DfExpr> {
1819        let column = DfExpr::Column(Column::from_name(column));
1820        let ArrowDataType::Timestamp(_, timezone) =
1821            column.get_type(schema).context(DataFusionPlanningSnafu)?
1822        else {
1823            unreachable!("time index is a timestamp")
1824        };
1825        let millis = column
1826            .cast_to(
1827                &ArrowDataType::Timestamp(ArrowTimeUnit::Millisecond, timezone),
1828                schema,
1829            )
1830            .context(DataFusionPlanningSnafu)?
1831            .cast_to(&ArrowDataType::Int64, schema)
1832            .context(DataFusionPlanningSnafu)?
1833            .cast_to(&ArrowDataType::Float64, schema)
1834            .context(DataFusionPlanningSnafu)?;
1835        Ok(DfExpr::BinaryExpr(BinaryExpr {
1836            left: Box::new(millis),
1837            op: Operator::Divide,
1838            right: Box::new(lit(1000.0)),
1839        }))
1840    }
1841
1842    /// Builds a projection plan for the PromQL `timestamp()` function, which reports
1843    /// `timestamp_value` as the value of each row, along with the original tag and time index
1844    /// columns.
1845    ///
1846    /// Updates the planner context's field columns to the timestamp column name.
1847    fn create_timestamp_func_plan(
1848        &mut self,
1849        input: LogicalPlan,
1850        timestamp_value: DfExpr,
1851    ) -> Result<LogicalPlan> {
1852        // A row whose fields are all NULL holds no sample, so it must not get a timestamp. The
1853        // check reads the input fields, which the projection below replaces.
1854        let has_sample = self.create_empty_values_filter_expr(true)?;
1855        let time_expr = timestamp_value.alias(DEFAULT_FIELD_COLUMN);
1856        self.ctx.field_columns = vec![time_expr.schema_name().to_string()];
1857        let mut project_exprs = Vec::with_capacity(self.ctx.tag_columns.len() + 2);
1858        project_exprs.push(self.create_time_index_column_expr()?);
1859        project_exprs.push(time_expr);
1860        project_exprs.extend(self.create_tag_column_exprs()?);
1861
1862        LogicalPlanBuilder::from(input)
1863            .filter(has_sample)
1864            .context(DataFusionPlanningSnafu)?
1865            .project(project_exprs)
1866            .context(DataFusionPlanningSnafu)?
1867            .build()
1868            .context(DataFusionPlanningSnafu)
1869    }
1870
1871    async fn prom_matrix_selector_to_plan(
1872        &mut self,
1873        matrix_selector: &MatrixSelector,
1874    ) -> Result<LogicalPlan> {
1875        let MatrixSelector { vs, range } = matrix_selector;
1876        let VectorSelector {
1877            name,
1878            offset,
1879            matchers,
1880            at,
1881        } = vs;
1882        let matchers = self.preprocess_label_matchers(matchers, name)?;
1883        ensure!(!range.is_zero(), ZeroRangeSelectorSnafu);
1884        let range_ms = range.as_millis() as _;
1885        self.ctx.range = Some(range_ms);
1886        let offset_ms = Self::offset_millis(offset);
1887
1888        // `@` anchors the range selector's window at a fixed timestamp, so the same window is fed
1889        // to the enclosing function at every evaluation step. See [`Self::at_modifier_offset`].
1890        let at_offset = self.at_modifier_offset(at, offset)?;
1891        let grid_start = self.ctx.start;
1892        let grid_end = self.ctx.end;
1893
1894        // Some functions like rate may require special fields in the RangeManipulate plan
1895        // so we can't skip RangeManipulate.
1896        let (normalize, at_offset) = match self.setup_context().await? {
1897            // An empty metric does not contain any sample, so anchoring cannot change the result.
1898            // The manipulate below folds the empty input with `offset_ms`, and the recorded fold
1899            // offset agrees with it instead of with the anchor the window cannot use.
1900            Some(empty_plan) => {
1901                self.ctx.range_fold_offset = Some(offset_ms);
1902                (empty_plan, None)
1903            }
1904            None => {
1905                let normalize = match at_offset {
1906                    Some(at_offset) => {
1907                        // Fold the anchored window once, at the start of the evaluation.
1908                        // Single-use planner: an error below aborts planning, so `ctx.end`
1909                        // needs no restore-on-error.
1910                        self.ctx.end = grid_start;
1911                        let plan = self
1912                            .selector_to_series_normalize_plan(at_offset, matchers, true)
1913                            .await?;
1914                        self.ctx.end = grid_end;
1915                        plan
1916                    }
1917                    None => {
1918                        self.selector_to_series_normalize_plan(offset_ms, matchers, true)
1919                            .await?
1920                    }
1921                };
1922                // Samples are shifted onto the evaluation timeline with the very same offset while
1923                // the window is folded. Record it so that the function above the selector can
1924                // recover the evaluation instant of the folded window; see
1925                // [`Self::create_range_eval_ts_expr`].
1926                self.ctx.range_fold_offset = Some(at_offset.unwrap_or(offset_ms));
1927                (normalize, at_offset)
1928            }
1929        };
1930        let time_index_column = self
1931            .ctx
1932            .time_index_column
1933            .clone()
1934            .expect("time index should be set in `setup_context`");
1935        let manipulate = match at_offset {
1936            Some(at_offset) => {
1937                // Fold the anchored window once, then report it at every step of the outer
1938                // grid. The samples keep their native anchor-time timestamps, so the manipulate
1939                // must shift them onto the evaluation timeline with the rewritten offset.
1940                let anchored = RangeManipulate::new(
1941                    grid_start,
1942                    grid_start,
1943                    self.ctx.interval,
1944                    at_offset,
1945                    // TODO(ruihang): convert via Timestamp datatypes to support different time units
1946                    range_ms,
1947                    time_index_column.clone(),
1948                    self.ctx.field_columns.clone(),
1949                    normalize,
1950                )
1951                .context(DataFusionPlanningSnafu)?;
1952                self.replay_over_grid(
1953                    LogicalPlan::Extension(Extension {
1954                        node: Arc::new(anchored),
1955                    }),
1956                    grid_start,
1957                    grid_end,
1958                    time_index_column,
1959                )
1960            }
1961            None => {
1962                let manipulate = RangeManipulate::new(
1963                    grid_start,
1964                    grid_end,
1965                    self.ctx.interval,
1966                    offset_ms,
1967                    // TODO(ruihang): convert via Timestamp datatypes to support different time units
1968                    range_ms,
1969                    time_index_column,
1970                    self.ctx.field_columns.clone(),
1971                    normalize,
1972                )
1973                .context(DataFusionPlanningSnafu)?;
1974
1975                LogicalPlan::Extension(Extension {
1976                    node: Arc::new(manipulate),
1977                })
1978            }
1979        };
1980
1981        Ok(manipulate)
1982    }
1983
1984    async fn prom_call_expr_to_plan(
1985        &mut self,
1986        query_engine_state: &QueryEngineState,
1987        call_expr: &Call,
1988    ) -> Result<LogicalPlan> {
1989        let Call { func, args } = call_expr;
1990        // some special functions that are not expression but a plan
1991        match func.name {
1992            SPECIAL_HISTOGRAM_QUANTILE | SPECIAL_HISTOGRAM_FRACTION => {
1993                return self
1994                    .create_histogram_plan(func.name, args, query_engine_state)
1995                    .await;
1996            }
1997            SPECIAL_VECTOR_FUNCTION => return self.create_vector_plan(args).await,
1998            SCALAR_FUNCTION => return self.create_scalar_plan(args, query_engine_state).await,
1999            SPECIAL_ABSENT_FUNCTION => {
2000                return self.create_absent_plan(args, query_engine_state).await;
2001            }
2002            _ => {}
2003        }
2004
2005        // transform function arguments
2006        let args = self.create_function_args(&args.args)?;
2007        // Only a vector selector keeps the timestamps of its samples. Any other expression
2008        // produces samples at the evaluation time, which is what `timestamp()` reports for it.
2009        let mut timestamp_arg = args.input.as_ref();
2010        while let Some(PromExpr::Paren(ParenExpr { expr })) = timestamp_arg {
2011            timestamp_arg = Some(expr);
2012        }
2013        let timestamp_of_selector =
2014            func.name == "timestamp" && matches!(timestamp_arg, Some(PromExpr::VectorSelector(_)));
2015        let input = if let Some(prom_expr) = &args.input {
2016            let input = self
2017                .prom_expr_to_plan_inner(prom_expr, timestamp_of_selector, query_engine_state)
2018                .await?;
2019            if func.name == "timestamp" && !timestamp_of_selector {
2020                let time_index_column =
2021                    self.ctx
2022                        .time_index_column
2023                        .clone()
2024                        .with_context(|| TimeIndexNotFoundSnafu {
2025                            table: self.ctx.table_name.clone().unwrap_or_default(),
2026                        })?;
2027                let eval_time = Self::timestamp_seconds_expr(&time_index_column, input.schema())?;
2028                self.create_timestamp_func_plan(input, eval_time)?
2029            } else {
2030                input
2031            }
2032        } else {
2033            self.ctx.time_index_column = Some(SPECIAL_TIME_FUNCTION.to_string());
2034            self.ctx.reset_table_name_and_schema();
2035            self.ctx.tag_columns = vec![];
2036            self.ctx.aggregation_field_labels.clear();
2037            self.ctx.field_columns = vec![DEFAULT_FIELD_COLUMN.to_string()];
2038            LogicalPlan::Extension(Extension {
2039                node: Arc::new(
2040                    EmptyMetric::new(
2041                        self.ctx.start,
2042                        self.ctx.end,
2043                        self.ctx.interval,
2044                        SPECIAL_TIME_FUNCTION.to_string(),
2045                        DEFAULT_FIELD_COLUMN.to_string(),
2046                        None,
2047                    )
2048                    .context(DataFusionPlanningSnafu)?,
2049                ),
2050            })
2051        };
2052        // The input plan records the fold offset of the range selector it is built from. Take it
2053        // here, so that the offset of one input cannot leak into another call, and pass it to
2054        // `create_function_expr`: the function that reads it (`predict_linear`) then depends on an
2055        // argument instead of on planner state written by the selector below it.
2056        let range_fold_offset = self.ctx.range_fold_offset.take();
2057        let input_tag_columns = self.ctx.tag_columns.clone();
2058        let (mut func_exprs, new_tags) = self.create_function_expr(
2059            func,
2060            args.literals.clone(),
2061            input.schema(),
2062            query_engine_state,
2063            range_fold_offset,
2064        )?;
2065        func_exprs.insert(0, self.create_time_index_column_expr()?);
2066        func_exprs.extend_from_slice(&self.create_tag_column_exprs()?);
2067        if let Some(tsid_col) =
2068            Self::optional_tsid_projection(input.schema(), None, self.ctx.use_tsid)
2069        {
2070            func_exprs.push(tsid_col);
2071        }
2072
2073        // A row survives as long as one field column produced a sample, and the fields without
2074        // one stay NULL, which is the shape a selector already emits. Requiring every field to
2075        // be non-NULL would drop one field's samples because another field has none in the same
2076        // window — the reason alternative float/histogram columns already needed this form. A
2077        // single field column reduces to the same predicate either way.
2078        let builder = LogicalPlanBuilder::from(input)
2079            .project(func_exprs)
2080            .context(DataFusionPlanningSnafu)?
2081            .filter(self.create_empty_values_filter_expr(true)?)
2082            .context(DataFusionPlanningSnafu)?;
2083
2084        let builder = match func.name {
2085            "sort" => builder
2086                .sort(self.create_field_columns_sort_exprs(true))
2087                .context(DataFusionPlanningSnafu)?,
2088            "sort_desc" => builder
2089                .sort(self.create_field_columns_sort_exprs(false))
2090                .context(DataFusionPlanningSnafu)?,
2091            "sort_by_label" => builder
2092                .sort(Self::create_sort_exprs_by_tags(
2093                    func.name,
2094                    args.literals,
2095                    true,
2096                )?)
2097                .context(DataFusionPlanningSnafu)?,
2098            "sort_by_label_desc" => builder
2099                .sort(Self::create_sort_exprs_by_tags(
2100                    func.name,
2101                    args.literals,
2102                    false,
2103                )?)
2104                .context(DataFusionPlanningSnafu)?,
2105
2106            _ => builder,
2107        };
2108
2109        // Rewriting a label the input series already have can map several of them onto the same
2110        // label set, which PromQL rejects. A new label keeps the series distinct.
2111        let may_duplicate_label_sets = matches!(func.name, "label_join" | "label_replace")
2112            && new_tags.iter().any(|tag| input_tag_columns.contains(tag));
2113
2114        // Update context tags after building plan
2115        // We can't push them before planning, because they won't exist until projection.
2116        for tag in new_tags {
2117            self.ctx.tag_columns.push(tag);
2118        }
2119
2120        let mut plan = builder.build().context(DataFusionPlanningSnafu)?;
2121        if may_duplicate_label_sets {
2122            let labels = self.ctx.tag_columns.clone();
2123            plan = Self::assert_unique_match_group(
2124                plan,
2125                labels
2126                    .iter()
2127                    .map(|label| DfExpr::Column(Column::from_name(label)))
2128                    .collect(),
2129                labels,
2130                self.create_time_index_column_expr()?,
2131                MatchGroupViolation::DuplicateLabelSet,
2132            )?;
2133        }
2134        common_telemetry::debug!("Created PromQL function plan: {plan:?} for {call_expr:?}");
2135
2136        Ok(plan)
2137    }
2138
2139    async fn prom_ext_expr_to_plan(
2140        &mut self,
2141        query_engine_state: &QueryEngineState,
2142        ext_expr: &promql_parser::parser::ast::Extension,
2143    ) -> Result<LogicalPlan> {
2144        // let promql_parser::parser::ast::Extension { expr } = ext_expr;
2145        let expr = &ext_expr.expr;
2146        let children = expr.children();
2147        let plan = self
2148            .prom_expr_to_plan(&children[0], query_engine_state)
2149            .await?;
2150        // Wrapper for the explanation/analyze of the existing plan
2151        // https://docs.rs/datafusion-expr/latest/datafusion_expr/logical_plan/builder/struct.LogicalPlanBuilder.html#method.explain
2152        // if `analyze` is true, runs the actual plan and produces
2153        // information about metrics during run.
2154        // if `verbose` is true, prints out additional details when VERBOSE keyword is specified
2155        match expr.name() {
2156            ANALYZE_NODE_NAME => LogicalPlanBuilder::from(plan)
2157                .explain(false, true)
2158                .unwrap()
2159                .build()
2160                .context(DataFusionPlanningSnafu),
2161            ANALYZE_VERBOSE_NODE_NAME => LogicalPlanBuilder::from(plan)
2162                .explain(true, true)
2163                .unwrap()
2164                .build()
2165                .context(DataFusionPlanningSnafu),
2166            EXPLAIN_NODE_NAME => LogicalPlanBuilder::from(plan)
2167                .explain(false, false)
2168                .unwrap()
2169                .build()
2170                .context(DataFusionPlanningSnafu),
2171            EXPLAIN_VERBOSE_NODE_NAME => LogicalPlanBuilder::from(plan)
2172                .explain(true, false)
2173                .unwrap()
2174                .build()
2175                .context(DataFusionPlanningSnafu),
2176            ALIAS_NODE_NAME => {
2177                let alias = expr
2178                    .as_any()
2179                    .downcast_ref::<AliasExpr>()
2180                    .context(UnexpectedPlanExprSnafu {
2181                        desc: "Expected AliasExpr",
2182                    })?
2183                    .alias
2184                    .clone();
2185                self.apply_alias(plan, alias)
2186            }
2187            _ => LogicalPlanBuilder::empty(true)
2188                .build()
2189                .context(DataFusionPlanningSnafu),
2190        }
2191    }
2192
2193    /// Extract metric name from `__name__` matcher and set it into [PromPlannerContext].
2194    /// Returns a new [Matchers] that doesn't contain metric name matcher.
2195    ///
2196    /// Each call to this function means new selector is started. Thus, the context will be reset
2197    /// at first.
2198    ///
2199    /// Name rule:
2200    /// - if `name` is some, then the matchers MUST NOT contain `__name__` matcher.
2201    /// - if `name` is none, then the matchers MAY contain NONE OR MULTIPLE `__name__` matchers.
2202    #[allow(clippy::mutable_key_type)]
2203    fn preprocess_label_matchers(
2204        &mut self,
2205        label_matchers: &Matchers,
2206        name: &Option<String>,
2207    ) -> Result<Matchers> {
2208        self.ctx.reset();
2209
2210        let metric_name;
2211        if let Some(name) = name.clone() {
2212            metric_name = Some(name);
2213            ensure!(
2214                label_matchers.find_matchers(METRIC_NAME).is_empty(),
2215                MultipleMetricMatchersSnafu
2216            );
2217        } else {
2218            let mut matches = label_matchers.find_matchers(METRIC_NAME);
2219            ensure!(!matches.is_empty(), NoMetricMatcherSnafu);
2220            ensure!(matches.len() == 1, MultipleMetricMatchersSnafu);
2221            ensure!(
2222                matches[0].op == MatchOp::Equal,
2223                UnsupportedMatcherOpSnafu {
2224                    matcher_op: matches[0].op.to_string(),
2225                    matcher: METRIC_NAME
2226                }
2227            );
2228            metric_name = matches.pop().map(|m| m.value);
2229        }
2230
2231        self.ctx.table_name = metric_name;
2232
2233        // Deduplicate in place instead of through a `HashSet`: the scan filter is built in
2234        // this order, and a hashed order makes the plan of one query vary between runs.
2235        let mut matchers: Vec<Matcher> = Vec::with_capacity(label_matchers.matchers.len());
2236        for matcher in &label_matchers.matchers {
2237            // TODO(ruihang): support other metric match ops
2238            if matcher.name == FIELD_COLUMN_MATCHER {
2239                self.ctx
2240                    .field_column_matcher
2241                    .get_or_insert_default()
2242                    .push(matcher.clone());
2243            } else if matcher.name == SCHEMA_COLUMN_MATCHER || matcher.name == DB_COLUMN_MATCHER {
2244                ensure!(
2245                    matcher.op == MatchOp::Equal,
2246                    UnsupportedMatcherOpSnafu {
2247                        matcher: matcher.name.clone(),
2248                        matcher_op: matcher.op.to_string(),
2249                    }
2250                );
2251                self.ctx.schema_name = Some(matcher.value.clone());
2252            } else if matcher.name != METRIC_NAME {
2253                self.ctx.selector_matcher.push(matcher.clone());
2254                if !matchers.contains(matcher) {
2255                    matchers.push(matcher.clone());
2256                }
2257            }
2258        }
2259
2260        Ok(Matchers::new(matchers))
2261    }
2262
2263    async fn selector_to_series_normalize_plan(
2264        &mut self,
2265        offset_duration: Millisecond,
2266        label_matchers: Matchers,
2267        is_range_selector: bool,
2268    ) -> Result<LogicalPlan> {
2269        // make table scan plan
2270        let table_ref = self.table_ref()?;
2271        let mut table_scan = self.create_table_scan_plan(table_ref.clone()).await?;
2272        let table_schema = table_scan.schema();
2273
2274        // make filter exprs
2275        let mut scan_filters = Self::matchers_to_expr(label_matchers.clone(), table_schema)?;
2276        if let Some(time_index_filter) =
2277            self.build_time_index_filter(offset_duration, table_schema)?
2278        {
2279            scan_filters.push(time_index_filter);
2280        }
2281        if let Some(filter) = conjunction(scan_filters) {
2282            table_scan = LogicalPlanBuilder::from(table_scan)
2283                .filter(filter)
2284                .context(DataFusionPlanningSnafu)?
2285                .build()
2286                .context(DataFusionPlanningSnafu)?;
2287        }
2288
2289        // make a projection plan if there is any `__field__` matcher
2290        if let Some(field_matchers) = &self.ctx.field_column_matcher {
2291            let col_set = self.ctx.field_columns.iter().collect::<HashSet<_>>();
2292            // opt-in set
2293            let mut result_set = HashSet::new();
2294            // opt-out set
2295            let mut reverse_set = HashSet::new();
2296            for matcher in field_matchers {
2297                match &matcher.op {
2298                    MatchOp::Equal => {
2299                        if col_set.contains(&matcher.value) {
2300                            let _ = result_set.insert(matcher.value.clone());
2301                        } else {
2302                            return Err(ColumnNotFoundSnafu {
2303                                col: matcher.value.clone(),
2304                            }
2305                            .build());
2306                        }
2307                    }
2308                    MatchOp::NotEqual => {
2309                        if col_set.contains(&matcher.value) {
2310                            let _ = reverse_set.insert(matcher.value.clone());
2311                        } else {
2312                            return Err(ColumnNotFoundSnafu {
2313                                col: matcher.value.clone(),
2314                            }
2315                            .build());
2316                        }
2317                    }
2318                    MatchOp::Re(regex) => {
2319                        for col in &self.ctx.field_columns {
2320                            if regex.is_match(col) {
2321                                let _ = result_set.insert(col.clone());
2322                            }
2323                        }
2324                    }
2325                    MatchOp::NotRe(regex) => {
2326                        for col in &self.ctx.field_columns {
2327                            if regex.is_match(col) {
2328                                let _ = reverse_set.insert(col.clone());
2329                            }
2330                        }
2331                    }
2332                }
2333            }
2334            // merge two set
2335            if result_set.is_empty() {
2336                result_set = col_set.into_iter().cloned().collect();
2337            }
2338            for col in reverse_set {
2339                let _ = result_set.remove(&col);
2340            }
2341
2342            // mask the field columns in context using computed result set
2343            self.ctx.field_columns = self
2344                .ctx
2345                .field_columns
2346                .drain(..)
2347                .filter(|col| result_set.contains(col))
2348                .collect();
2349
2350            let exprs = result_set
2351                .into_iter()
2352                .map(|col| DfExpr::Column(Column::new_unqualified(col)))
2353                .chain(self.create_tag_column_exprs()?)
2354                .chain(
2355                    self.ctx
2356                        .use_tsid
2357                        .then_some(DfExpr::Column(Column::new_unqualified(
2358                            DATA_SCHEMA_TSID_COLUMN_NAME,
2359                        ))),
2360                )
2361                .chain(Some(self.create_time_index_column_expr()?))
2362                .collect::<Vec<_>>();
2363
2364            // reuse this variable for simplicity
2365            table_scan = LogicalPlanBuilder::from(table_scan)
2366                .project(exprs)
2367                .context(DataFusionPlanningSnafu)?
2368                .build()
2369                .context(DataFusionPlanningSnafu)?;
2370        }
2371
2372        // make sort plan
2373        let series_key_columns = self.series_key_columns();
2374
2375        let sort_exprs = if self.ctx.use_tsid {
2376            vec![
2377                DfExpr::Column(Column::from_name(DATA_SCHEMA_TSID_COLUMN_NAME)).sort(true, true),
2378                self.create_time_index_column_expr()?.sort(true, true),
2379            ]
2380        } else {
2381            self.create_tag_and_time_index_column_sort_exprs()?
2382        };
2383
2384        let sort_plan = LogicalPlanBuilder::from(table_scan)
2385            .sort(sort_exprs)
2386            .context(DataFusionPlanningSnafu)?
2387            .build()
2388            .context(DataFusionPlanningSnafu)?;
2389
2390        // make divide plan
2391        let time_index_column =
2392            self.ctx
2393                .time_index_column
2394                .clone()
2395                .with_context(|| TimeIndexNotFoundSnafu {
2396                    table: table_ref.to_string(),
2397                })?;
2398        let divide_plan = LogicalPlan::Extension(Extension {
2399            node: Arc::new(SeriesDivide::new(
2400                series_key_columns.clone(),
2401                time_index_column,
2402                sort_plan,
2403            )),
2404        });
2405
2406        // make series_normalize plan
2407        if !is_range_selector && offset_duration == 0 {
2408            return Ok(divide_plan);
2409        }
2410        let series_normalize = SeriesNormalize::new(
2411            offset_duration,
2412            self.ctx
2413                .time_index_column
2414                .clone()
2415                .with_context(|| TimeIndexNotFoundSnafu {
2416                    table: table_ref.to_quoted_string(),
2417                })?,
2418            is_range_selector,
2419            series_key_columns,
2420            divide_plan,
2421        );
2422        let logical_plan = LogicalPlan::Extension(Extension {
2423            node: Arc::new(series_normalize),
2424        });
2425
2426        Ok(logical_plan)
2427    }
2428
2429    /// Convert [LabelModifier] to [Column] exprs for aggregation.
2430    /// Timestamp column and tag columns will be included.
2431    ///
2432    /// # Side effect
2433    ///
2434    /// This method will also change the tag columns in ctx if `update_ctx` is true.
2435    fn agg_modifier_to_col(
2436        &mut self,
2437        input_schema: &DFSchemaRef,
2438        modifier: &Option<LabelModifier>,
2439        update_ctx: bool,
2440    ) -> Result<Vec<DfExpr>> {
2441        match modifier {
2442            None => {
2443                if update_ctx {
2444                    self.ctx.tag_columns.clear();
2445                    self.ctx.aggregation_field_labels.clear();
2446                }
2447                Ok(vec![self.create_time_index_column_expr()?])
2448            }
2449            Some(LabelModifier::Include(labels)) => {
2450                if update_ctx {
2451                    // A `by(...)` label can name a value field of the input instead of a tag. The
2452                    // aggregate still reports it among its tag columns below, but unlike a tag it
2453                    // is a group key rather than a property of a series: its value varies between
2454                    // the samples of one series, so a matcher on it must stay above sample
2455                    // selection (#9242). Record it, before the tag columns are overwritten.
2456                    self.ctx.aggregation_field_labels = labels
2457                        .labels
2458                        .iter()
2459                        .filter(|label| {
2460                            self.ctx.field_columns.contains(label)
2461                                || !self.ctx.tag_columns.contains(label)
2462                                || self.ctx.aggregation_field_labels.contains(label)
2463                        })
2464                        .cloned()
2465                        .collect();
2466                    self.ctx.tag_columns.clear();
2467                }
2468                let mut exprs = Vec::with_capacity(labels.labels.len());
2469                for label in &labels.labels {
2470                    if is_metric_engine_internal_column(label) {
2471                        continue;
2472                    }
2473                    // nonexistence label will be ignored
2474                    if let Some(column_name) = Self::find_case_sensitive_column(input_schema, label)
2475                    {
2476                        exprs.push(DfExpr::Column(Column::from_name(column_name.clone())));
2477
2478                        if update_ctx {
2479                            // update the tag columns in context
2480                            self.ctx.tag_columns.push(column_name);
2481                        }
2482                    }
2483                }
2484                // add timestamp column
2485                exprs.push(self.create_time_index_column_expr()?);
2486
2487                Ok(exprs)
2488            }
2489            Some(LabelModifier::Exclude(labels)) => {
2490                let mut all_fields = input_schema
2491                    .fields()
2492                    .iter()
2493                    .map(|f| f.name())
2494                    .collect::<BTreeSet<_>>();
2495
2496                // Exclude metric engine internal columns (not PromQL labels) from the implicit
2497                // "without" label set.
2498                all_fields.retain(|col| !is_metric_engine_internal_column(col.as_str()));
2499
2500                // remove "without"-ed fields
2501                // nonexistence label will be ignored
2502                for label in &labels.labels {
2503                    let _ = all_fields.remove(label);
2504                }
2505
2506                // remove time index and value fields
2507                if let Some(time_index) = &self.ctx.time_index_column {
2508                    let _ = all_fields.remove(time_index);
2509                }
2510                for value in &self.ctx.field_columns {
2511                    let _ = all_fields.remove(value);
2512                }
2513
2514                if update_ctx {
2515                    // change the tag columns in context
2516                    self.ctx.tag_columns = all_fields.iter().map(|col| (*col).clone()).collect();
2517                    // `without(...)` drops the value fields of the input from its grouping labels,
2518                    // so a label stays a grouping label in name only if it survives in the input
2519                    // schema (e.g. an inner aggregation that grouped by it).
2520                    self.ctx
2521                        .aggregation_field_labels
2522                        .retain(|label| all_fields.iter().any(|col| *col == label));
2523                }
2524
2525                // collect remaining fields and convert to col expr
2526                let mut exprs = all_fields
2527                    .into_iter()
2528                    .map(|c| DfExpr::Column(Column::from_name(c)))
2529                    .collect::<Vec<_>>();
2530
2531                // add timestamp column
2532                exprs.push(self.create_time_index_column_expr()?);
2533
2534                Ok(exprs)
2535            }
2536        }
2537    }
2538
2539    // TODO(ruihang): ignore `MetricNameLabel` (`__name__`) matcher
2540    pub fn matchers_to_expr(
2541        label_matchers: Matchers,
2542        table_schema: &DFSchemaRef,
2543    ) -> Result<Vec<DfExpr>> {
2544        let mut exprs = Vec::with_capacity(label_matchers.matchers.len());
2545        for matcher in label_matchers.matchers {
2546            if matcher.name == SCHEMA_COLUMN_MATCHER
2547                || matcher.name == DB_COLUMN_MATCHER
2548                || matcher.name == FIELD_COLUMN_MATCHER
2549            {
2550                continue;
2551            }
2552
2553            let accepts_empty = matcher.is_match("");
2554            let column_name = Self::find_case_sensitive_column(table_schema, matcher.name.as_str());
2555            // Prometheus reads a label a series does not carry as the empty
2556            // string. A row can miss a label two ways: the table has no column
2557            // for it, or the column exists but is NULL on that row — the latter
2558            // is the norm for logical metrics sharing a physical table, which
2559            // holds the union of their label columns.
2560            let col = if let Some(column_name) = column_name {
2561                let column = DfExpr::Column(Column::from_name(&column_name));
2562                let field = table_schema
2563                    .index_of_column_by_name(None, &column_name)
2564                    .map(|index| table_schema.field(index));
2565                if accepts_empty
2566                    && let Some(data_type) = field
2567                        .filter(|field| {
2568                            field.is_nullable()
2569                                && Self::string_value_data_type(field.data_type()).is_some()
2570                        })
2571                        .map(|field| field.data_type())
2572                {
2573                    let empty = Self::string_scalar_value(data_type, Some(String::new()))
2574                        .expect("nullable label has a string type");
2575                    DfExpr::ScalarFunction(ScalarFunction {
2576                        func: coalesce(),
2577                        args: vec![column, DfExpr::Literal(empty, None)],
2578                    })
2579                } else {
2580                    column
2581                }
2582            } else {
2583                DfExpr::Literal(ScalarValue::Utf8(Some(String::new())), None)
2584            };
2585            let lit = DfExpr::Literal(ScalarValue::Utf8(Some(matcher.value)), None);
2586            let expr = match matcher.op {
2587                MatchOp::Equal => col.eq(lit),
2588                MatchOp::NotEqual => col.not_eq(lit),
2589                MatchOp::Re(re) => {
2590                    // TODO(ruihang): a more programmatic way to handle this in datafusion
2591
2592                    // This is a hack to handle `.+` and `.*`, and is not strictly correct
2593                    // `.` doesn't match newline (`\n`). Given this is in PromQL context,
2594                    // most of the time it's fine.
2595                    if re.as_str() == "^(?:.*)$" {
2596                        continue;
2597                    }
2598                    if re.as_str() == "^(?:.+)$" {
2599                        col.not_eq(DfExpr::Literal(
2600                            ScalarValue::Utf8(Some(String::new())),
2601                            None,
2602                        ))
2603                    } else {
2604                        DfExpr::BinaryExpr(BinaryExpr {
2605                            left: Box::new(col),
2606                            op: Operator::RegexMatch,
2607                            right: Box::new(DfExpr::Literal(
2608                                ScalarValue::Utf8(Some(re.as_str().to_string())),
2609                                None,
2610                            )),
2611                        })
2612                    }
2613                }
2614                MatchOp::NotRe(re) => {
2615                    if re.as_str() == "^(?:.*)$" {
2616                        DfExpr::Literal(ScalarValue::Boolean(Some(false)), None)
2617                    } else if re.as_str() == "^(?:.+)$" {
2618                        col.eq(DfExpr::Literal(
2619                            ScalarValue::Utf8(Some(String::new())),
2620                            None,
2621                        ))
2622                    } else {
2623                        DfExpr::BinaryExpr(BinaryExpr {
2624                            left: Box::new(col),
2625                            op: Operator::RegexNotMatch,
2626                            right: Box::new(DfExpr::Literal(
2627                                ScalarValue::Utf8(Some(re.as_str().to_string())),
2628                                None,
2629                            )),
2630                        })
2631                    }
2632                }
2633            };
2634            exprs.push(expr);
2635        }
2636
2637        Ok(exprs)
2638    }
2639
2640    fn find_case_sensitive_column(schema: &DFSchemaRef, column: &str) -> Option<String> {
2641        if is_metric_engine_internal_column(column) {
2642            return None;
2643        }
2644        schema
2645            .fields()
2646            .iter()
2647            .find(|field| field.name() == column)
2648            .map(|field| field.name().clone())
2649    }
2650
2651    fn table_from_source(&self, source: &Arc<dyn TableSource>) -> Result<table::TableRef> {
2652        Ok(source
2653            .downcast_ref::<DefaultTableSource>()
2654            .context(UnknownTableSnafu)?
2655            .table_provider
2656            .downcast_ref::<DfTableProviderAdapter>()
2657            .context(UnknownTableSnafu)?
2658            .table())
2659    }
2660
2661    fn table_ref(&self) -> Result<TableReference> {
2662        let table_name = self
2663            .ctx
2664            .table_name
2665            .clone()
2666            .context(TableNameNotFoundSnafu)?;
2667
2668        // set schema name if `__schema__` is given
2669        let table_ref = if let Some(schema_name) = &self.ctx.schema_name {
2670            TableReference::partial(schema_name.as_str(), table_name.as_str())
2671        } else {
2672            TableReference::bare(table_name.as_str())
2673        };
2674
2675        Ok(table_ref)
2676    }
2677
2678    fn build_time_index_filter(
2679        &self,
2680        offset_duration: i64,
2681        schema: &DFSchemaRef,
2682    ) -> Result<Option<DfExpr>> {
2683        let start = self.ctx.start;
2684        let end = self.ctx.end;
2685        if end < start {
2686            return InvalidTimeRangeSnafu { start, end }.fail();
2687        }
2688        let time_index_expr = self.create_time_index_column_expr()?;
2689        let time_index_name = self.ctx.time_index_column.as_ref().unwrap();
2690        let unit = schema
2691            .index_of_column_by_name(None, time_index_name)
2692            .and_then(|index| match schema.field(index).data_type() {
2693                ArrowDataType::Timestamp(unit, _) => Some(*unit),
2694                _ => None,
2695            })
2696            .unwrap_or(ArrowTimeUnit::Millisecond);
2697        let native_value = |milliseconds: i128| match unit {
2698            ArrowTimeUnit::Second => milliseconds.div_euclid(1_000),
2699            ArrowTimeUnit::Millisecond => milliseconds,
2700            ArrowTimeUnit::Microsecond => milliseconds * 1_000,
2701            ArrowTimeUnit::Nanosecond => milliseconds * 1_000_000,
2702        };
2703        let scalar = |milliseconds: i128| -> Option<ScalarValue> {
2704            let value = i64::try_from(native_value(milliseconds)).ok()?;
2705            Some(match unit {
2706                ArrowTimeUnit::Second => ScalarValue::TimestampSecond(Some(value), None),
2707                ArrowTimeUnit::Millisecond => ScalarValue::TimestampMillisecond(Some(value), None),
2708                ArrowTimeUnit::Microsecond => ScalarValue::TimestampMicrosecond(Some(value), None),
2709                ArrowTimeUnit::Nanosecond => ScalarValue::TimestampNanosecond(Some(value), None),
2710            })
2711        };
2712        let window = self.ctx.range.unwrap_or(self.ctx.lookback_delta);
2713        let filter = |lower_ms: i128, upper_ms: i128| {
2714            let lower_value = native_value(lower_ms);
2715            let upper_value = native_value(upper_ms);
2716            if lower_value > i128::from(i64::MAX) || upper_value < i128::from(i64::MIN) {
2717                return Some(lit(false));
2718            }
2719            let lower_filter = (lower_value >= i128::from(i64::MIN)).then(|| {
2720                let lower = DfExpr::Literal(scalar(lower_ms).unwrap(), None);
2721                if window == 0 {
2722                    time_index_expr.clone().gt_eq(lower)
2723                } else if unit == ArrowTimeUnit::Millisecond
2724                    && let Some(inclusive_lower) = lower_ms
2725                        .checked_add(1)
2726                        .and_then(|lower| i64::try_from(lower).ok())
2727                        .and_then(|lower| scalar(i128::from(lower)))
2728                {
2729                    time_index_expr
2730                        .clone()
2731                        .gt_eq(DfExpr::Literal(inclusive_lower, None))
2732                } else {
2733                    time_index_expr.clone().gt(lower)
2734                }
2735            });
2736            let upper_filter = (upper_value <= i128::from(i64::MAX)).then(|| {
2737                time_index_expr
2738                    .clone()
2739                    .lt_eq(DfExpr::Literal(scalar(upper_ms).unwrap(), None))
2740            });
2741
2742            // An underflowing lower bound must not discard a representable upper
2743            // bound: without it, LastRow could retain a future row and discard the
2744            // older eligible sample before the manipulator can check its time.
2745            match (lower_filter, upper_filter) {
2746                (Some(lower), Some(upper)) => Some(lower.and(upper)),
2747                (Some(filter), None) | (None, Some(filter)) => Some(filter),
2748                (None, None) => None,
2749            }
2750        };
2751        let bounds = |timestamp: i64| {
2752            let upper = i128::from(timestamp) - i128::from(offset_duration);
2753            (upper - i128::from(window), upper)
2754        };
2755        let num_points = (end as i128 - start as i128) / self.ctx.interval as i128;
2756        if num_points > MAX_SCATTER_POINTS as i128 || self.ctx.interval <= INTERVAL_1H {
2757            let (lower, _) = bounds(start);
2758            let (_, upper) = bounds(end);
2759            return Ok(filter(lower, upper));
2760        }
2761        let mut filters = Vec::new();
2762        for timestamp in (start..=end).step_by(self.ctx.interval as usize) {
2763            let (lower, upper) = bounds(timestamp);
2764            let Some(filter) = filter(lower, upper) else {
2765                // A point whose native bounds cannot be represented may cover the whole native
2766                // time domain, so its disjunct cannot be omitted.
2767                return Ok(None);
2768            };
2769            filters.push(filter);
2770        }
2771        Ok(filters.into_iter().reduce(DfExpr::or))
2772    }
2773
2774    /// Create a table scan plan and a filter plan with given filter.
2775    ///
2776    /// # Panic
2777    /// If the filter is empty
2778    async fn create_table_scan_plan(&mut self, table_ref: TableReference) -> Result<LogicalPlan> {
2779        let provider = self
2780            .table_provider
2781            .resolve_table(table_ref.clone())
2782            .await
2783            .context(CatalogSnafu)?;
2784
2785        let logical_table = self.table_from_source(&provider)?;
2786
2787        // Try to rewrite the table scan to physical table scan if possible.
2788        let mut maybe_phy_table_ref = table_ref.clone();
2789        let mut scan_provider = provider;
2790        let mut table_id_filter: Option<u32> = None;
2791
2792        // If it's a metric engine logical table, scan its physical table directly and filter by
2793        // `__table_id = logical_table_id` to get access to internal columns like `__tsid`.
2794        if logical_table.table_info().meta.engine == METRIC_ENGINE_NAME
2795            && let Some(physical_table_name) = logical_table
2796                .table_info()
2797                .meta
2798                .options
2799                .extra_options
2800                .get(LOGICAL_TABLE_METADATA_KEY)
2801        {
2802            let physical_table_ref = if let Some(schema_name) = &self.ctx.schema_name {
2803                TableReference::partial(schema_name.as_str(), physical_table_name.as_str())
2804            } else {
2805                TableReference::bare(physical_table_name.as_str())
2806            };
2807
2808            let physical_provider = match self
2809                .table_provider
2810                .resolve_table(physical_table_ref.clone())
2811                .await
2812            {
2813                Ok(provider) => provider,
2814                Err(e) if e.status_code() == StatusCode::TableNotFound => {
2815                    // Fall back to scanning the logical table. It still works, but without
2816                    // `__tsid` optimization.
2817                    scan_provider.clone()
2818                }
2819                Err(e) => return Err(e).context(CatalogSnafu),
2820            };
2821
2822            if !Arc::ptr_eq(&physical_provider, &scan_provider) {
2823                // Only rewrite when internal columns exist in physical schema.
2824                let physical_table = self.table_from_source(&physical_provider)?;
2825
2826                let has_table_id = physical_table
2827                    .schema()
2828                    .column_schema_by_name(DATA_SCHEMA_TABLE_ID_COLUMN_NAME)
2829                    .is_some();
2830                let has_tsid = physical_table
2831                    .schema()
2832                    .column_schema_by_name(DATA_SCHEMA_TSID_COLUMN_NAME)
2833                    .is_some_and(|col| matches!(col.data_type, ConcreteDataType::UInt64(_)));
2834
2835                if has_table_id && has_tsid {
2836                    scan_provider = physical_provider;
2837                    maybe_phy_table_ref = physical_table_ref;
2838                    table_id_filter = Some(logical_table.table_info().ident.table_id);
2839                }
2840            }
2841        }
2842
2843        let scan_table = self.table_from_source(&scan_provider)?;
2844
2845        let use_tsid = table_id_filter.is_some()
2846            && scan_table
2847                .schema()
2848                .column_schema_by_name(DATA_SCHEMA_TSID_COLUMN_NAME)
2849                .is_some_and(|col| matches!(col.data_type, ConcreteDataType::UInt64(_)));
2850        self.ctx.use_tsid = use_tsid;
2851
2852        let all_table_tags = self.ctx.tag_columns.clone();
2853
2854        let scan_tag_columns = if use_tsid {
2855            let mut scan_tags = self.ctx.tag_columns.clone();
2856            for matcher in &self.ctx.selector_matcher {
2857                if is_metric_engine_internal_column(&matcher.name) {
2858                    continue;
2859                }
2860                if all_table_tags.iter().any(|tag| tag == &matcher.name) {
2861                    scan_tags.push(matcher.name.clone());
2862                }
2863            }
2864            scan_tags.sort_unstable();
2865            scan_tags.dedup();
2866            scan_tags
2867        } else {
2868            self.ctx.tag_columns.clone()
2869        };
2870
2871        let time_index_data_type = scan_table
2872            .schema()
2873            .timestamp_column()
2874            .with_context(|| TimeIndexNotFoundSnafu {
2875                table: maybe_phy_table_ref.to_quoted_string(),
2876            })?
2877            .data_type
2878            .clone();
2879        let is_time_index_second =
2880            time_index_data_type == ConcreteDataType::timestamp_second_datatype();
2881
2882        let scan_projection = if table_id_filter.is_some() {
2883            let mut required_columns = HashSet::new();
2884            required_columns.insert(DATA_SCHEMA_TABLE_ID_COLUMN_NAME.to_string());
2885            required_columns.insert(self.ctx.time_index_column.clone().with_context(|| {
2886                TimeIndexNotFoundSnafu {
2887                    table: maybe_phy_table_ref.to_quoted_string(),
2888                }
2889            })?);
2890            for col in &scan_tag_columns {
2891                required_columns.insert(col.clone());
2892            }
2893            for col in &self.ctx.field_columns {
2894                required_columns.insert(col.clone());
2895            }
2896            if use_tsid {
2897                required_columns.insert(DATA_SCHEMA_TSID_COLUMN_NAME.to_string());
2898            }
2899
2900            let arrow_schema = scan_table.schema().arrow_schema().clone();
2901            Some(
2902                arrow_schema
2903                    .fields()
2904                    .iter()
2905                    .enumerate()
2906                    .filter(|(_, field)| required_columns.contains(field.name().as_str()))
2907                    .map(|(idx, _)| idx)
2908                    .collect::<Vec<_>>(),
2909            )
2910        } else {
2911            None
2912        };
2913
2914        let mut scan_plan =
2915            LogicalPlanBuilder::scan(maybe_phy_table_ref.clone(), scan_provider, scan_projection)
2916                .context(DataFusionPlanningSnafu)?
2917                .build()
2918                .context(DataFusionPlanningSnafu)?;
2919
2920        if let Some(table_id) = table_id_filter {
2921            scan_plan = LogicalPlanBuilder::from(scan_plan)
2922                .filter(
2923                    DfExpr::Column(Column::from_name(DATA_SCHEMA_TABLE_ID_COLUMN_NAME))
2924                        .eq(lit(table_id)),
2925                )
2926                .context(DataFusionPlanningSnafu)?
2927                .alias(table_ref.clone()) // rename the relation back to logical table's name after filtering
2928                .context(DataFusionPlanningSnafu)?
2929                .build()
2930                .context(DataFusionPlanningSnafu)?;
2931        }
2932
2933        if is_time_index_second {
2934            // Promote seconds so millisecond offsets remain exact; retain finer precision.
2935            // Later manipulators compare native sample ticks, while PromQL evaluation and
2936            // emitted timestamps remain millisecond-based, so this projection must not
2937            // silently truncate a finer-grained time index.
2938            let expr: Vec<_> = self
2939                .create_field_column_exprs()?
2940                .into_iter()
2941                .chain(
2942                    scan_tag_columns
2943                        .iter()
2944                        .map(|tag| DfExpr::Column(Column::from_name(tag))),
2945                )
2946                .chain(self.ctx.use_tsid.then_some(DfExpr::Column(Column::new(
2947                    Some(table_ref.clone()),
2948                    DATA_SCHEMA_TSID_COLUMN_NAME.to_string(),
2949                ))))
2950                .chain(Some(DfExpr::Alias(Alias {
2951                    expr: Box::new(DfExpr::Cast(Cast::new(
2952                        Box::new(self.create_time_index_column_expr()?),
2953                        ArrowDataType::Timestamp(ArrowTimeUnit::Millisecond, None),
2954                    ))),
2955                    relation: Some(table_ref.clone()),
2956                    name: self
2957                        .ctx
2958                        .time_index_column
2959                        .as_ref()
2960                        .with_context(|| TimeIndexNotFoundSnafu {
2961                            table: table_ref.to_quoted_string(),
2962                        })?
2963                        .clone(),
2964                    metadata: None,
2965                })))
2966                .collect::<Vec<_>>();
2967            scan_plan = LogicalPlanBuilder::from(scan_plan)
2968                .project(expr)
2969                .context(DataFusionPlanningSnafu)?
2970                .build()
2971                .context(DataFusionPlanningSnafu)?;
2972        } else if table_id_filter.is_some()
2973            || time_index_data_type == ConcreteDataType::timestamp_microsecond_datatype()
2974            || time_index_data_type == ConcreteDataType::timestamp_nanosecond_datatype()
2975        {
2976            // Drop the internal `__table_id` column after filtering and preserve PromQL's
2977            // field/tag/timestamp column order for native microsecond/nanosecond timestamps.
2978            // Keeping the original time column also lets the existing ordering hints
2979            // use PerSeries scans without a cast, repartition, and sort. This benefits
2980            // multi-evaluation selectors too; only a single evaluation can use LastRow.
2981            let project_exprs = self
2982                .create_field_column_exprs()?
2983                .into_iter()
2984                .chain(
2985                    scan_tag_columns
2986                        .iter()
2987                        .map(|tag| DfExpr::Column(Column::from_name(tag))),
2988                )
2989                .chain(
2990                    self.ctx
2991                        .use_tsid
2992                        .then_some(DfExpr::Column(Column::from_name(
2993                            DATA_SCHEMA_TSID_COLUMN_NAME,
2994                        ))),
2995                )
2996                .chain(Some(self.create_time_index_column_expr()?))
2997                .collect::<Vec<_>>();
2998
2999            scan_plan = LogicalPlanBuilder::from(scan_plan)
3000                .project(project_exprs)
3001                .context(DataFusionPlanningSnafu)?
3002                .build()
3003                .context(DataFusionPlanningSnafu)?;
3004        }
3005
3006        let result = LogicalPlanBuilder::from(scan_plan)
3007            .build()
3008            .context(DataFusionPlanningSnafu)?;
3009        Ok(result)
3010    }
3011
3012    fn collect_row_key_tag_columns_from_plan(
3013        &self,
3014        plan: &LogicalPlan,
3015    ) -> Result<BTreeSet<String>> {
3016        fn walk(
3017            planner: &PromPlanner,
3018            plan: &LogicalPlan,
3019            out: &mut BTreeSet<String>,
3020        ) -> Result<()> {
3021            // Derived PromQL plans may contain non-Greptime scans without row-key metadata.
3022            if let LogicalPlan::TableScan(scan) = plan
3023                && let Ok(table) = planner.table_from_source(&scan.source)
3024            {
3025                for col in table.table_info().meta.row_key_column_names() {
3026                    if col != DATA_SCHEMA_TABLE_ID_COLUMN_NAME
3027                        && col != DATA_SCHEMA_TSID_COLUMN_NAME
3028                        && !is_metric_engine_internal_column(col)
3029                    {
3030                        out.insert(col.clone());
3031                    }
3032                }
3033            }
3034
3035            for input in plan.inputs() {
3036                walk(planner, input, out)?;
3037            }
3038            Ok(())
3039        }
3040
3041        let mut out = BTreeSet::new();
3042        walk(self, plan, &mut out)?;
3043        Ok(out)
3044    }
3045
3046    /// Setup [PromPlannerContext]'s state fields.
3047    ///
3048    /// Returns a logical plan for an empty metric.
3049    async fn setup_context(&mut self) -> Result<Option<LogicalPlan>> {
3050        let table_ref = self.table_ref()?;
3051        let source = match self.table_provider.resolve_table(table_ref.clone()).await {
3052            Err(e) if e.status_code() == StatusCode::TableNotFound => {
3053                let plan = self.setup_context_for_empty_metric()?;
3054                return Ok(Some(plan));
3055            }
3056            res => res.context(CatalogSnafu)?,
3057        };
3058        let table = self.table_from_source(&source)?;
3059
3060        // set time index column name
3061        let time_index = table
3062            .schema()
3063            .timestamp_column()
3064            .with_context(|| TimeIndexNotFoundSnafu {
3065                table: table_ref.to_quoted_string(),
3066            })?
3067            .name
3068            .clone();
3069        self.ctx.time_index_column = Some(time_index);
3070
3071        // set values columns
3072        let values = table
3073            .table_info()
3074            .meta
3075            .field_column_names()
3076            .cloned()
3077            .collect();
3078        self.ctx.field_columns = values;
3079
3080        // set primary key (tag) columns
3081        let tags = table
3082            .table_info()
3083            .meta
3084            .row_key_column_names()
3085            .filter(|col| {
3086                // remove metric engine's internal columns
3087                col != &DATA_SCHEMA_TABLE_ID_COLUMN_NAME && col != &DATA_SCHEMA_TSID_COLUMN_NAME
3088            })
3089            .cloned()
3090            .collect();
3091        self.ctx.tag_columns = tags;
3092        // The operand is a plain selector: its tag columns are the table's.
3093        self.ctx.aggregation_field_labels.clear();
3094
3095        self.ctx.use_tsid = false;
3096
3097        Ok(None)
3098    }
3099
3100    /// Setup [PromPlannerContext]'s state fields for a non existent table
3101    /// without any rows.
3102    fn setup_context_for_empty_metric(&mut self) -> Result<LogicalPlan> {
3103        self.ctx.time_index_column = Some(SPECIAL_TIME_FUNCTION.to_string());
3104        self.ctx.reset_table_name_and_schema();
3105        self.ctx.tag_columns = vec![];
3106        self.ctx.aggregation_field_labels.clear();
3107        self.ctx.field_columns = vec![DEFAULT_FIELD_COLUMN.to_string()];
3108        self.ctx.use_tsid = false;
3109
3110        // The table doesn't have any data, so we set start to 0 and end to -1.
3111        let plan = LogicalPlan::Extension(Extension {
3112            node: Arc::new(
3113                EmptyMetric::new(
3114                    0,
3115                    -1,
3116                    self.ctx.interval,
3117                    SPECIAL_TIME_FUNCTION.to_string(),
3118                    DEFAULT_FIELD_COLUMN.to_string(),
3119                    Some(lit(0.0f64)),
3120                )
3121                .context(DataFusionPlanningSnafu)?,
3122            ),
3123        });
3124        Ok(plan)
3125    }
3126
3127    // TODO(ruihang): insert column expr
3128    fn create_function_args(&self, args: &[Box<PromExpr>]) -> Result<FunctionArgs> {
3129        let mut result = FunctionArgs::default();
3130
3131        for arg in args {
3132            // First try to parse as literal expression (including binary expressions like 100.0 + 3.0)
3133            if let Some(expr) = Self::try_build_literal_expr(arg) {
3134                result.literals.push(expr);
3135            } else {
3136                // If not a literal, treat as vector input
3137                match arg.as_ref() {
3138                    PromExpr::Subquery(_)
3139                    | PromExpr::VectorSelector(_)
3140                    | PromExpr::MatrixSelector(_)
3141                    | PromExpr::Extension(_)
3142                    | PromExpr::Aggregate(_)
3143                    | PromExpr::Paren(_)
3144                    | PromExpr::Call(_)
3145                    | PromExpr::Binary(_)
3146                    | PromExpr::Unary(_) => {
3147                        if result.input.replace(*arg.clone()).is_some() {
3148                            MultipleVectorSnafu { expr: *arg.clone() }.fail()?;
3149                        }
3150                    }
3151
3152                    _ => {
3153                        let expr = Self::get_param_as_literal_expr(Some(arg.as_ref()), None, None)?;
3154                        result.literals.push(expr);
3155                    }
3156                }
3157            }
3158        }
3159
3160        Ok(result)
3161    }
3162
3163    fn create_mixed_range_function_exprs(
3164        &mut self,
3165        func: &Function,
3166        mut other_input_exprs: VecDeque<DfExpr>,
3167        float_field: &str,
3168        histogram_field: &str,
3169        input_schema: &DFSchemaRef,
3170        range_fold_offset: Option<Millisecond>,
3171    ) -> Result<Option<Vec<DfExpr>>> {
3172        let returns_histogram = matches!(
3173            func.name,
3174            "rate"
3175                | "increase"
3176                | "delta"
3177                | "idelta"
3178                | "irate"
3179                | "avg_over_time"
3180                | "sum_over_time"
3181                | "last_over_time"
3182        );
3183        if !returns_histogram
3184            && !matches!(
3185                func.name,
3186                "changes"
3187                    | "resets"
3188                    | "deriv"
3189                    | "min_over_time"
3190                    | "max_over_time"
3191                    | "count_over_time"
3192                    | "absent_over_time"
3193                    | "present_over_time"
3194                    | "stddev_over_time"
3195                    | "stdvar_over_time"
3196                    | "quantile_over_time"
3197                    | "predict_linear"
3198                    | "double_exponential_smoothing"
3199                    | "holt_winters"
3200            )
3201        {
3202            return Ok(None);
3203        }
3204
3205        if func.name == "predict_linear" {
3206            other_input_exprs[0] = DfExpr::Cast(Cast::new(
3207                Box::new(other_input_exprs[0].clone()),
3208                ArrowDataType::Int64,
3209            ));
3210            // Same evaluation instant as in the non-mixed path; it follows the other inputs so the
3211            // float UDF below is called as `predict_linear(ts_range, value_range, t, eval_ts)`.
3212            // The regression is only defined for a folded window, so a missing fold offset is an
3213            // error instead of a default.
3214            other_input_exprs.push_back(self.create_range_eval_ts_expr(
3215                range_fold_offset.context(ExpectRangeSelectorSnafu)?,
3216                input_schema,
3217            )?);
3218        }
3219
3220        let timestamp_range = DfExpr::Column(Column::from_name(
3221            RangeManipulate::build_timestamp_range_name(
3222                self.ctx.time_index_column.as_ref().unwrap(),
3223            ),
3224        ));
3225        let float_range = DfExpr::Column(Column::from_name(float_field));
3226        let histogram_range = DfExpr::Column(Column::from_name(histogram_field));
3227        let mut args = Vec::with_capacity(other_input_exprs.len() + 6);
3228        args.push(lit(func.name));
3229        args.push(timestamp_range.clone());
3230        args.push(float_range.clone());
3231        args.push(histogram_range.clone());
3232        args.extend(other_input_exprs);
3233        if matches!(func.name, "rate" | "increase" | "delta") {
3234            args.push(self.create_time_index_column_expr()?);
3235            args.push(lit(self.ctx.range.context(ExpectRangeSelectorSnafu)?));
3236        }
3237
3238        let mut float_expr = DfExpr::ScalarFunction(ScalarFunction {
3239            func: Arc::new(MixedRange::float_udf(self.promql_annotations.clone())),
3240            args: args.clone(),
3241        });
3242        if matches!(func.name, "rate" | "increase") {
3243            let raw_delta_function = if func.name == "rate" {
3244                "raw_delta_rate"
3245            } else {
3246                "raw_delta_increase"
3247            };
3248            let delta_sum = DfExpr::ScalarFunction(ScalarFunction {
3249                func: Arc::new(MixedRange::float_udf(self.promql_annotations.clone())),
3250                args: vec![
3251                    lit(raw_delta_function),
3252                    timestamp_range,
3253                    float_range,
3254                    histogram_range,
3255                ],
3256            });
3257            float_expr = self.select_delta_range_math(
3258                func.name,
3259                input_schema,
3260                self.ctx.range.context(ExpectRangeSelectorSnafu)?,
3261                delta_sum,
3262                float_expr,
3263            )?;
3264        }
3265        let exprs = if returns_histogram {
3266            self.ctx.field_columns = vec![float_field.to_string(), histogram_field.to_string()];
3267            vec![
3268                float_expr.alias(float_field),
3269                DfExpr::ScalarFunction(ScalarFunction {
3270                    func: Arc::new(MixedRange::histogram_udf(self.promql_annotations.clone())),
3271                    args,
3272                })
3273                .alias(histogram_field),
3274            ]
3275        } else {
3276            let display_name = if func.name == "predict_linear" {
3277                // The evaluation instant is the private last argument of the mixed float UDF
3278                // call; keep it out of the output column name like on the non-mixed path.
3279                Self::name_without_last_arg(&float_expr)
3280            } else {
3281                float_expr.schema_name().to_string()
3282            };
3283            self.ctx.field_columns = vec![display_name.clone()];
3284            vec![float_expr.alias(display_name)]
3285        };
3286        Ok(Some(exprs))
3287    }
3288
3289    /// Creates function expressions for projection and returns the expressions and new tags.
3290    ///
3291    /// # Side Effects
3292    ///
3293    /// This method will update [PromPlannerContext]'s fields and tags if needed.
3294    fn create_function_expr(
3295        &mut self,
3296        func: &Function,
3297        other_input_exprs: Vec<DfExpr>,
3298        input_schema: &DFSchemaRef,
3299        query_engine_state: &QueryEngineState,
3300        range_fold_offset: Option<Millisecond>,
3301    ) -> Result<(Vec<DfExpr>, Vec<String>)> {
3302        // TODO(ruihang): check function args list
3303        let mut other_input_exprs: VecDeque<DfExpr> = other_input_exprs.into();
3304        if let Some((float_field, histogram_field)) =
3305            Self::alternative_sample_range_columns(input_schema, &self.ctx.field_columns)
3306                .map(|(float, histogram)| (float.to_string(), histogram.to_string()))
3307            && let Some(exprs) = self.create_mixed_range_function_exprs(
3308                func,
3309                other_input_exprs.clone(),
3310                &float_field,
3311                &histogram_field,
3312                input_schema,
3313                range_fold_offset,
3314            )?
3315        {
3316            return Ok((exprs, vec![]));
3317        }
3318        let alternative_samples =
3319            Self::field_columns_are_alternative_samples(input_schema, &self.ctx.field_columns);
3320        let all_field_columns_are_native_histogram_ranges =
3321            self.all_field_columns_are_native_histogram_ranges(input_schema);
3322
3323        // TODO(ruihang): set this according to in-param list
3324        let field_column_pos = 0;
3325        let mut exprs = Vec::with_capacity(self.ctx.field_columns.len());
3326        // New labels after executing the function, e.g. `label_replace` etc.
3327        let mut new_tags = vec![];
3328        let promql_annotations = self.promql_annotations.clone();
3329        let native_histogram_drop_udf = |name: &str| {
3330            Arc::new(NativeHistogramDrop::float_null_udf(
3331                format!(
3332                    "{name}: dropped native histogram samples because this function is not supported for native histograms"
3333                ),
3334                promql_annotations.clone(),
3335            ))
3336        };
3337        let scalar_func = match func.name {
3338            "increase" => {
3339                if all_field_columns_are_native_histogram_ranges {
3340                    ScalarFunc::ExtrapolateUdf(
3341                        Arc::new(NativeHistogramIncrease::scalar_udf_with_collector(
3342                            self.promql_annotations.clone(),
3343                        )),
3344                        self.ctx.range.context(ExpectRangeSelectorSnafu)?,
3345                    )
3346                } else {
3347                    ScalarFunc::ExtrapolateUdf(
3348                        Arc::new(Increase::scalar_udf()),
3349                        self.ctx.range.context(ExpectRangeSelectorSnafu)?,
3350                    )
3351                }
3352            }
3353            "rate" => {
3354                if all_field_columns_are_native_histogram_ranges {
3355                    ScalarFunc::ExtrapolateUdf(
3356                        Arc::new(NativeHistogramRate::scalar_udf_with_collector(
3357                            self.promql_annotations.clone(),
3358                        )),
3359                        self.ctx.range.context(ExpectRangeSelectorSnafu)?,
3360                    )
3361                } else {
3362                    ScalarFunc::ExtrapolateUdf(
3363                        Arc::new(Rate::scalar_udf()),
3364                        self.ctx.range.context(ExpectRangeSelectorSnafu)?,
3365                    )
3366                }
3367            }
3368            "delta" => {
3369                if all_field_columns_are_native_histogram_ranges {
3370                    ScalarFunc::ExtrapolateUdf(
3371                        Arc::new(NativeHistogramDelta::scalar_udf_with_collector(
3372                            self.promql_annotations.clone(),
3373                        )),
3374                        self.ctx.range.context(ExpectRangeSelectorSnafu)?,
3375                    )
3376                } else {
3377                    ScalarFunc::ExtrapolateUdf(
3378                        Arc::new(Delta::scalar_udf()),
3379                        self.ctx.range.context(ExpectRangeSelectorSnafu)?,
3380                    )
3381                }
3382            }
3383            "idelta" => {
3384                if all_field_columns_are_native_histogram_ranges {
3385                    ScalarFunc::Udf(Arc::new(NativeHistogramIDelta::scalar_udf_with_collector(
3386                        self.promql_annotations.clone(),
3387                    )))
3388                } else {
3389                    ScalarFunc::Udf(Arc::new(IDelta::<false>::scalar_udf()))
3390                }
3391            }
3392            "irate" => {
3393                if all_field_columns_are_native_histogram_ranges {
3394                    ScalarFunc::Udf(Arc::new(NativeHistogramIRate::scalar_udf_with_collector(
3395                        self.promql_annotations.clone(),
3396                    )))
3397                } else {
3398                    ScalarFunc::Udf(Arc::new(IDelta::<true>::scalar_udf()))
3399                }
3400            }
3401            "resets" => {
3402                if all_field_columns_are_native_histogram_ranges {
3403                    ScalarFunc::Udf(Arc::new(NativeHistogramResets::scalar_udf()))
3404                } else {
3405                    ScalarFunc::Udf(Arc::new(Resets::scalar_udf()))
3406                }
3407            }
3408            "changes" => {
3409                if all_field_columns_are_native_histogram_ranges {
3410                    ScalarFunc::Udf(Arc::new(NativeHistogramChanges::scalar_udf()))
3411                } else {
3412                    ScalarFunc::Udf(Arc::new(Changes::scalar_udf()))
3413                }
3414            }
3415            "deriv" => {
3416                if all_field_columns_are_native_histogram_ranges {
3417                    ScalarFunc::Udf(native_histogram_drop_udf(func.name))
3418                } else {
3419                    ScalarFunc::Udf(Arc::new(Deriv::scalar_udf()))
3420                }
3421            }
3422            "avg_over_time" => {
3423                if all_field_columns_are_native_histogram_ranges {
3424                    ScalarFunc::Udf(Arc::new(
3425                        NativeHistogramAvgOverTime::scalar_udf_with_collector(
3426                            self.promql_annotations.clone(),
3427                        ),
3428                    ))
3429                } else {
3430                    ScalarFunc::Udf(Arc::new(AvgOverTime::scalar_udf()))
3431                }
3432            }
3433            "min_over_time" => {
3434                if all_field_columns_are_native_histogram_ranges {
3435                    ScalarFunc::Udf(native_histogram_drop_udf(func.name))
3436                } else {
3437                    ScalarFunc::Udf(Arc::new(MinOverTime::scalar_udf()))
3438                }
3439            }
3440            "max_over_time" => {
3441                if all_field_columns_are_native_histogram_ranges {
3442                    ScalarFunc::Udf(native_histogram_drop_udf(func.name))
3443                } else {
3444                    ScalarFunc::Udf(Arc::new(MaxOverTime::scalar_udf()))
3445                }
3446            }
3447            "sum_over_time" => {
3448                if all_field_columns_are_native_histogram_ranges {
3449                    ScalarFunc::Udf(Arc::new(
3450                        NativeHistogramSumOverTime::scalar_udf_with_collector(
3451                            self.promql_annotations.clone(),
3452                        ),
3453                    ))
3454                } else {
3455                    ScalarFunc::Udf(Arc::new(SumOverTime::scalar_udf()))
3456                }
3457            }
3458            "count_over_time" => {
3459                if all_field_columns_are_native_histogram_ranges {
3460                    ScalarFunc::Udf(Arc::new(NativeHistogramCountOverTime::scalar_udf()))
3461                } else {
3462                    ScalarFunc::Udf(Arc::new(CountOverTime::scalar_udf()))
3463                }
3464            }
3465            "last_over_time" => {
3466                if all_field_columns_are_native_histogram_ranges {
3467                    ScalarFunc::Udf(Arc::new(NativeHistogramLastOverTime::scalar_udf()))
3468                } else {
3469                    ScalarFunc::Udf(Arc::new(LastOverTime::scalar_udf()))
3470                }
3471            }
3472            "absent_over_time" => {
3473                if all_field_columns_are_native_histogram_ranges {
3474                    ScalarFunc::Udf(Arc::new(NativeHistogramAbsentOverTime::scalar_udf()))
3475                } else {
3476                    ScalarFunc::Udf(Arc::new(AbsentOverTime::scalar_udf()))
3477                }
3478            }
3479            "present_over_time" => {
3480                if all_field_columns_are_native_histogram_ranges {
3481                    ScalarFunc::Udf(Arc::new(NativeHistogramPresentOverTime::scalar_udf()))
3482                } else {
3483                    ScalarFunc::Udf(Arc::new(PresentOverTime::scalar_udf()))
3484                }
3485            }
3486            "stddev_over_time" => {
3487                if all_field_columns_are_native_histogram_ranges {
3488                    ScalarFunc::Udf(native_histogram_drop_udf(func.name))
3489                } else {
3490                    ScalarFunc::Udf(Arc::new(StddevOverTime::scalar_udf()))
3491                }
3492            }
3493            "stdvar_over_time" => {
3494                if all_field_columns_are_native_histogram_ranges {
3495                    ScalarFunc::Udf(native_histogram_drop_udf(func.name))
3496                } else {
3497                    ScalarFunc::Udf(Arc::new(StdvarOverTime::scalar_udf()))
3498                }
3499            }
3500            "quantile_over_time" => {
3501                if all_field_columns_are_native_histogram_ranges {
3502                    ScalarFunc::Udf(native_histogram_drop_udf(func.name))
3503                } else {
3504                    ScalarFunc::Udf(Arc::new(QuantileOverTime::scalar_udf()))
3505                }
3506            }
3507            "predict_linear" => {
3508                if all_field_columns_are_native_histogram_ranges {
3509                    ScalarFunc::Udf(native_histogram_drop_udf(func.name))
3510                } else {
3511                    other_input_exprs[0] = DfExpr::Cast(Cast::new(
3512                        Box::new(other_input_exprs[0].clone()),
3513                        ArrowDataType::Int64,
3514                    ));
3515                    // The prediction starts at the evaluation instant of the step, which the
3516                    // window's fold offset recovers from the row's time index; see
3517                    // [`Self::create_range_eval_ts_expr`]. It is appended last, so the UDF is
3518                    // called as `predict_linear(ts_range, value_range, t, eval_ts)`. The
3519                    // regression is only defined for a folded window, so a missing fold offset
3520                    // is an error instead of a default.
3521                    other_input_exprs.push_back(self.create_range_eval_ts_expr(
3522                        range_fold_offset.context(ExpectRangeSelectorSnafu)?,
3523                        input_schema,
3524                    )?);
3525                    ScalarFunc::Udf(Arc::new(PredictLinear::scalar_udf()))
3526                }
3527            }
3528            "double_exponential_smoothing" | "holt_winters" => {
3529                if all_field_columns_are_native_histogram_ranges {
3530                    ScalarFunc::Udf(native_histogram_drop_udf(func.name))
3531                } else {
3532                    ScalarFunc::Udf(Arc::new(DoubleExponentialSmoothing::scalar_udf()))
3533                }
3534            }
3535            "histogram_count" => {
3536                ScalarFunc::NativeHistogramUdf(Arc::new(NativeHistogramCount::scalar_udf()))
3537            }
3538            "histogram_sum" => {
3539                ScalarFunc::NativeHistogramUdf(Arc::new(NativeHistogramSum::scalar_udf()))
3540            }
3541            "histogram_avg" => {
3542                ScalarFunc::NativeHistogramUdf(Arc::new(NativeHistogramAvg::scalar_udf()))
3543            }
3544            "histogram_stddev" => {
3545                ScalarFunc::NativeHistogramUdf(Arc::new(NativeHistogramStddev::scalar_udf()))
3546            }
3547            "histogram_stdvar" => {
3548                ScalarFunc::NativeHistogramUdf(Arc::new(NativeHistogramStdvar::scalar_udf()))
3549            }
3550            "time" => {
3551                exprs.push(build_special_time_expr(
3552                    self.ctx.time_index_column.as_ref().unwrap(),
3553                ));
3554                ScalarFunc::GeneratedExpr
3555            }
3556            "minute" => {
3557                // date_part('minute', time_index)
3558                let expr = self.date_part_on_time_index("minute")?;
3559                exprs.push(expr);
3560                ScalarFunc::GeneratedExpr
3561            }
3562            "hour" => {
3563                // date_part('hour', time_index)
3564                let expr = self.date_part_on_time_index("hour")?;
3565                exprs.push(expr);
3566                ScalarFunc::GeneratedExpr
3567            }
3568            "month" => {
3569                // date_part('month', time_index)
3570                let expr = self.date_part_on_time_index("month")?;
3571                exprs.push(expr);
3572                ScalarFunc::GeneratedExpr
3573            }
3574            "year" => {
3575                // date_part('year', time_index)
3576                let expr = self.date_part_on_time_index("year")?;
3577                exprs.push(expr);
3578                ScalarFunc::GeneratedExpr
3579            }
3580            "day_of_month" => {
3581                // date_part('day', time_index)
3582                let expr = self.date_part_on_time_index("day")?;
3583                exprs.push(expr);
3584                ScalarFunc::GeneratedExpr
3585            }
3586            "day_of_week" => {
3587                // date_part('dow', time_index)
3588                let expr = self.date_part_on_time_index("dow")?;
3589                exprs.push(expr);
3590                ScalarFunc::GeneratedExpr
3591            }
3592            "day_of_year" => {
3593                // date_part('doy', time_index)
3594                let expr = self.date_part_on_time_index("doy")?;
3595                exprs.push(expr);
3596                ScalarFunc::GeneratedExpr
3597            }
3598            "days_in_month" => {
3599                // date_part(
3600                //     'days',
3601                //     (date_trunc('month', <TIME INDEX>::date) + interval '1 month - 1 day')
3602                // );
3603                let day_lit_expr = "day".lit();
3604                let month_lit_expr = "month".lit();
3605                let interval_1month_lit_expr =
3606                    DfExpr::Literal(ScalarValue::IntervalYearMonth(Some(1)), None);
3607                let interval_1day_lit_expr = DfExpr::Literal(
3608                    ScalarValue::IntervalDayTime(Some(IntervalDayTime::new(1, 0))),
3609                    None,
3610                );
3611                let the_1month_minus_1day_expr = DfExpr::BinaryExpr(BinaryExpr {
3612                    left: Box::new(interval_1month_lit_expr),
3613                    op: Operator::Minus,
3614                    right: Box::new(interval_1day_lit_expr),
3615                });
3616                let date_trunc_expr = DfExpr::ScalarFunction(ScalarFunction {
3617                    func: datafusion_functions::datetime::date_trunc(),
3618                    args: vec![month_lit_expr, self.create_time_index_column_expr()?],
3619                });
3620                let date_trunc_plus_interval_expr = DfExpr::BinaryExpr(BinaryExpr {
3621                    left: Box::new(date_trunc_expr),
3622                    op: Operator::Plus,
3623                    right: Box::new(the_1month_minus_1day_expr),
3624                });
3625                let date_part_expr = DfExpr::ScalarFunction(ScalarFunction {
3626                    func: datafusion_functions::datetime::date_part(),
3627                    args: vec![day_lit_expr, date_trunc_plus_interval_expr],
3628                });
3629
3630                exprs.push(date_part_expr);
3631                ScalarFunc::GeneratedExpr
3632            }
3633
3634            "label_join" => {
3635                self.ctx.use_tsid = false;
3636                let (concat_expr, dst_label) = Self::build_concat_labels_expr(
3637                    &mut other_input_exprs,
3638                    &self.ctx,
3639                    input_schema,
3640                    query_engine_state,
3641                )?;
3642
3643                // Reserve the current field columns except the `dst_label`.
3644                for value in &self.ctx.field_columns {
3645                    if *value != dst_label {
3646                        let expr = DfExpr::Column(Column::from_name(value));
3647                        exprs.push(expr);
3648                    }
3649                }
3650
3651                // Remove it from tag columns if exists to avoid duplicated column names
3652                self.ctx.tag_columns.retain(|tag| *tag != dst_label);
3653                new_tags.push(dst_label);
3654                // Add the new label expr to evaluate
3655                exprs.push(concat_expr);
3656
3657                ScalarFunc::GeneratedExpr
3658            }
3659            "label_replace" => {
3660                self.ctx.use_tsid = false;
3661                if let Some((replace_expr, dst_label)) =
3662                    self.build_regexp_replace_label_expr(&mut other_input_exprs, input_schema)?
3663                {
3664                    // Reserve the current field columns except the `dst_label`.
3665                    for value in &self.ctx.field_columns {
3666                        if *value != dst_label {
3667                            let expr = DfExpr::Column(Column::from_name(value));
3668                            exprs.push(expr);
3669                        }
3670                    }
3671
3672                    // Remove it from tag columns if exists to avoid duplicated column names
3673                    self.ctx.tag_columns.retain(|tag| *tag != dst_label);
3674                    new_tags.push(dst_label);
3675                    // Add the new label expr to evaluate
3676                    exprs.push(replace_expr);
3677                } else {
3678                    // Keep the current field columns
3679                    for value in &self.ctx.field_columns {
3680                        let expr = DfExpr::Column(Column::from_name(value));
3681                        exprs.push(expr);
3682                    }
3683                }
3684
3685                ScalarFunc::GeneratedExpr
3686            }
3687            "sort" | "sort_desc" => {
3688                // Value sorting silently ignores native histogram samples.
3689                for value in &self.ctx.field_columns {
3690                    if !Self::field_column_is_native_histogram(input_schema, value) {
3691                        exprs.push(DfExpr::Column(Column::from_name(value)));
3692                    }
3693                }
3694                // Keep a nullable float field so the normal empty-value filter produces an
3695                // empty vector when the input contains only histograms.
3696                if exprs.is_empty() {
3697                    exprs.push(DfExpr::Literal(ScalarValue::Float64(None), None));
3698                }
3699
3700                ScalarFunc::GeneratedExpr
3701            }
3702            "sort_by_label" | "sort_by_label_desc" | "timestamp" => {
3703                // These functions are not expression but a part of plan,
3704                // they are processed by `prom_call_expr_to_plan`.
3705                for value in &self.ctx.field_columns {
3706                    let expr = DfExpr::Column(Column::from_name(value));
3707                    exprs.push(expr);
3708                }
3709
3710                ScalarFunc::GeneratedExpr
3711            }
3712            "round" if self.all_field_columns_are_native_histograms(input_schema) => {
3713                if other_input_exprs.is_empty() {
3714                    other_input_exprs.push_front(0.0f64.lit());
3715                }
3716                ScalarFunc::DataFusionUdf(native_histogram_drop_udf(func.name))
3717            }
3718            "round" => {
3719                if other_input_exprs.is_empty() {
3720                    other_input_exprs.push_front(0.0f64.lit());
3721                }
3722                ScalarFunc::DataFusionUdf(Arc::new(Round::scalar_udf()))
3723            }
3724            "rad" | "deg" | "sgn" if self.all_field_columns_are_native_histograms(input_schema) => {
3725                ScalarFunc::DataFusionUdf(native_histogram_drop_udf(func.name))
3726            }
3727            "rad" => ScalarFunc::DataFusionBuiltin(datafusion::functions::math::radians()),
3728            "deg" => ScalarFunc::DataFusionBuiltin(datafusion::functions::math::degrees()),
3729            "sgn" => ScalarFunc::DataFusionBuiltin(datafusion::functions::math::signum()),
3730            "pi" => {
3731                // pi functions doesn't accepts any arguments, needs special processing
3732                let fn_expr = DfExpr::ScalarFunction(ScalarFunction {
3733                    func: datafusion::functions::math::pi(),
3734                    args: vec![],
3735                });
3736                exprs.push(fn_expr);
3737
3738                ScalarFunc::GeneratedExpr
3739            }
3740            _ => {
3741                if let Some(f) = query_engine_state
3742                    .session_state()
3743                    .scalar_functions()
3744                    .get(func.name)
3745                {
3746                    if self.all_field_columns_are_native_histograms(input_schema) {
3747                        ScalarFunc::DataFusionUdf(native_histogram_drop_udf(func.name))
3748                    } else {
3749                        ScalarFunc::DataFusionBuiltin(f.clone())
3750                    }
3751                } else if let Some(factory) = query_engine_state.scalar_function(func.name) {
3752                    if self.all_field_columns_are_native_histograms(input_schema) {
3753                        ScalarFunc::DataFusionUdf(native_histogram_drop_udf(func.name))
3754                    } else {
3755                        let func_state = query_engine_state.function_state();
3756                        let query_ctx = self.table_provider.query_ctx();
3757
3758                        ScalarFunc::DataFusionUdf(Arc::new(factory.provide(FunctionContext {
3759                            state: func_state,
3760                            query_ctx: query_ctx.clone(),
3761                        })))
3762                    }
3763                } else if let Some(f) = datafusion_functions::math::functions()
3764                    .iter()
3765                    .find(|f| f.name() == func.name)
3766                {
3767                    if self.all_field_columns_are_native_histograms(input_schema) {
3768                        ScalarFunc::DataFusionUdf(native_histogram_drop_udf(func.name))
3769                    } else {
3770                        ScalarFunc::DataFusionUdf(f.clone())
3771                    }
3772                } else {
3773                    return UnsupportedExprSnafu {
3774                        name: func.name.to_string(),
3775                    }
3776                    .fail();
3777                }
3778            }
3779        };
3780
3781        for value in &self.ctx.field_columns {
3782            let col_expr = DfExpr::Column(Column::from_name(value));
3783            let value_is_histogram = Self::field_column_is_native_histogram(input_schema, value);
3784
3785            match scalar_func.clone() {
3786                ScalarFunc::DataFusionBuiltin(func) => {
3787                    if alternative_samples && value_is_histogram {
3788                        continue;
3789                    }
3790                    other_input_exprs.insert(field_column_pos, col_expr);
3791                    let fn_expr = DfExpr::ScalarFunction(ScalarFunction {
3792                        func,
3793                        args: other_input_exprs.clone().into(),
3794                    });
3795                    exprs.push(fn_expr);
3796                    let _ = other_input_exprs.remove(field_column_pos);
3797                }
3798                ScalarFunc::DataFusionUdf(func) => {
3799                    if alternative_samples && value_is_histogram {
3800                        continue;
3801                    }
3802                    let args = itertools::chain!(
3803                        other_input_exprs.iter().take(field_column_pos).cloned(),
3804                        std::iter::once(col_expr),
3805                        other_input_exprs.iter().skip(field_column_pos).cloned()
3806                    )
3807                    .collect_vec();
3808                    exprs.push(DfExpr::ScalarFunction(ScalarFunction { func, args }))
3809                }
3810                ScalarFunc::NativeHistogramUdf(func) => {
3811                    if value_is_histogram {
3812                        let args = itertools::chain!(
3813                            other_input_exprs.iter().take(field_column_pos).cloned(),
3814                            std::iter::once(col_expr),
3815                            other_input_exprs.iter().skip(field_column_pos).cloned()
3816                        )
3817                        .collect_vec();
3818                        exprs.push(DfExpr::ScalarFunction(ScalarFunction { func, args }));
3819                    } else if !alternative_samples {
3820                        exprs.push(
3821                            DfExpr::Literal(ScalarValue::Float64(None), None).alias(format!(
3822                                "{}_{}",
3823                                func.name(),
3824                                value
3825                            )),
3826                        );
3827                    }
3828                }
3829                ScalarFunc::Udf(func) => {
3830                    let ts_range_expr = DfExpr::Column(Column::from_name(
3831                        RangeManipulate::build_timestamp_range_name(
3832                            self.ctx.time_index_column.as_ref().unwrap(),
3833                        ),
3834                    ));
3835                    other_input_exprs.insert(field_column_pos, ts_range_expr);
3836                    other_input_exprs.insert(field_column_pos + 1, col_expr);
3837                    let fn_expr = DfExpr::ScalarFunction(ScalarFunction {
3838                        func,
3839                        args: other_input_exprs.clone().into(),
3840                    });
3841                    exprs.push(fn_expr);
3842                    let _ = other_input_exprs.remove(field_column_pos + 1);
3843                    let _ = other_input_exprs.remove(field_column_pos);
3844                }
3845                ScalarFunc::ExtrapolateUdf(udf, range_length) => {
3846                    let ts_range_expr = DfExpr::Column(Column::from_name(
3847                        RangeManipulate::build_timestamp_range_name(
3848                            self.ctx.time_index_column.as_ref().unwrap(),
3849                        ),
3850                    ));
3851                    other_input_exprs.insert(field_column_pos, ts_range_expr.clone());
3852                    other_input_exprs.insert(field_column_pos + 1, col_expr.clone());
3853                    other_input_exprs
3854                        .insert(field_column_pos + 2, self.create_time_index_column_expr()?);
3855                    other_input_exprs.push_back(lit(range_length));
3856                    let fn_expr = DfExpr::ScalarFunction(ScalarFunction {
3857                        func: udf,
3858                        args: other_input_exprs.clone().into(),
3859                    });
3860                    let fn_expr = if matches!(func.name, "rate" | "increase")
3861                        && !all_field_columns_are_native_histogram_ranges
3862                    {
3863                        let delta_sum = DfExpr::ScalarFunction(ScalarFunction {
3864                            func: Arc::new(SumOverTime::scalar_udf()),
3865                            args: vec![ts_range_expr, col_expr],
3866                        });
3867                        self.select_delta_range_math(
3868                            func.name,
3869                            input_schema,
3870                            range_length,
3871                            delta_sum,
3872                            fn_expr,
3873                        )?
3874                    } else {
3875                        fn_expr
3876                    };
3877                    exprs.push(fn_expr);
3878                    let _ = other_input_exprs.pop_back();
3879                    let _ = other_input_exprs.remove(field_column_pos + 2);
3880                    let _ = other_input_exprs.remove(field_column_pos + 1);
3881                    let _ = other_input_exprs.remove(field_column_pos);
3882                }
3883                ScalarFunc::GeneratedExpr => {}
3884            }
3885        }
3886
3887        // Update value columns' name, and alias them to remove qualifiers
3888        // For label functions such as `label_join`, `label_replace`, etc.,
3889        // we keep the fields unchanged.
3890        if !matches!(func.name, "label_join" | "label_replace") {
3891            let mut new_field_columns = Vec::with_capacity(exprs.len());
3892
3893            exprs = exprs
3894                .into_iter()
3895                .map(|expr| {
3896                    // `predict_linear` appends its private evaluation instant as the last argument
3897                    // of the UDF call; the output column is named after the call without it, so
3898                    // the injected expression stays out of the user-visible schema. The
3899                    // native-histogram drop UDF takes no such argument.
3900                    let display_name = if func.name == "predict_linear"
3901                        && !all_field_columns_are_native_histogram_ranges
3902                    {
3903                        Self::name_without_last_arg(&expr)
3904                    } else {
3905                        expr.schema_name().to_string()
3906                    };
3907                    new_field_columns.push(display_name.clone());
3908                    Ok(expr.alias(display_name))
3909                })
3910                .collect::<std::result::Result<Vec<_>, _>>()
3911                .context(DataFusionPlanningSnafu)?;
3912
3913            self.ctx.field_columns = new_field_columns;
3914        }
3915
3916        Ok((exprs, new_tags))
3917    }
3918
3919    fn select_delta_range_math(
3920        &self,
3921        function: &str,
3922        input_schema: &DFSchemaRef,
3923        range_length: Millisecond,
3924        delta_sum: DfExpr,
3925        cumulative: DfExpr,
3926    ) -> Result<DfExpr> {
3927        let marker_is_delta = if self
3928            .ctx
3929            .tag_columns
3930            .iter()
3931            .any(|tag| tag == OTLP_AGGREGATION_TEMPORALITY_LABEL)
3932        {
3933            Self::field_column_type(input_schema, OTLP_AGGREGATION_TEMPORALITY_LABEL)
3934                .filter(|data_type| Self::string_value_data_type(data_type).is_some())
3935                .map(|_| {
3936                    DfExpr::Column(Column::from_name(OTLP_AGGREGATION_TEMPORALITY_LABEL))
3937                        .eq(lit(GREPTIME_TEMPORALITY_DELTA))
3938                })
3939        } else {
3940            None
3941        };
3942        let Some(marker_is_delta) = marker_is_delta else {
3943            return Ok(cumulative);
3944        };
3945
3946        let delta = if function == "rate" {
3947            DfExpr::BinaryExpr(BinaryExpr {
3948                left: Box::new(delta_sum),
3949                op: Operator::Divide,
3950                right: Box::new(lit(range_length as f64 / 1000.0)),
3951            })
3952        } else {
3953            delta_sum
3954        };
3955        let display_name = cumulative.schema_name().to_string();
3956        when(marker_is_delta, delta)
3957            .otherwise(cumulative)
3958            .context(DataFusionPlanningSnafu)
3959            .map(|expr| expr.alias(display_name))
3960    }
3961
3962    /// Validate label name according to Prometheus specification.
3963    /// Label names must match the regex: [a-zA-Z_][a-zA-Z0-9_]*
3964    /// Additionally, label names starting with double underscores are reserved for internal use.
3965    fn validate_label_name(label_name: &str) -> Result<()> {
3966        // Check if label name starts with double underscores (reserved)
3967        if label_name.starts_with("__") {
3968            return InvalidDestinationLabelNameSnafu { label_name }.fail();
3969        }
3970        // Check if label name matches the required pattern
3971        if !LABEL_NAME_REGEX.is_match(label_name) {
3972            return InvalidDestinationLabelNameSnafu { label_name }.fail();
3973        }
3974
3975        Ok(())
3976    }
3977
3978    /// Build expr for `label_replace` function
3979    fn build_regexp_replace_label_expr(
3980        &self,
3981        other_input_exprs: &mut VecDeque<DfExpr>,
3982        input_schema: &DFSchemaRef,
3983    ) -> Result<Option<(DfExpr, String)>> {
3984        // label_replace(vector, dst_label, replacement, src_label, regex)
3985        let dst_label = match other_input_exprs.pop_front() {
3986            Some(DfExpr::Literal(ScalarValue::Utf8(Some(d)), _)) => d,
3987            other => UnexpectedPlanExprSnafu {
3988                desc: format!("expected dst_label string literal, but found {:?}", other),
3989            }
3990            .fail()?,
3991        };
3992
3993        // Validate the destination label name
3994        Self::validate_label_name(&dst_label)?;
3995        let replacement = match other_input_exprs.pop_front() {
3996            Some(DfExpr::Literal(ScalarValue::Utf8(Some(r)), _)) => r,
3997            other => UnexpectedPlanExprSnafu {
3998                desc: format!("expected replacement string literal, but found {:?}", other),
3999            }
4000            .fail()?,
4001        };
4002        let src_label = match other_input_exprs.pop_front() {
4003            Some(DfExpr::Literal(ScalarValue::Utf8(Some(s)), None)) => s,
4004            other => UnexpectedPlanExprSnafu {
4005                desc: format!("expected src_label string literal, but found {:?}", other),
4006            }
4007            .fail()?,
4008        };
4009
4010        let regex = match other_input_exprs.pop_front() {
4011            Some(DfExpr::Literal(ScalarValue::Utf8(Some(r)), None)) => r,
4012            other => UnexpectedPlanExprSnafu {
4013                desc: format!("expected regex string literal, but found {:?}", other),
4014            }
4015            .fail()?,
4016        };
4017
4018        // Like Prometheus, match the whole source value. A series whose source value matches gets
4019        // `dst_label` set to the expanded replacement; any other series is left unchanged.
4020        // doc: https://prometheus.io/docs/prometheus/latest/querying/functions/#label_replace
4021        let anchored = format!("^(?s:{regex})$");
4022        let compiled = regex::Regex::new(&anchored)
4023            .map_err(|_| InvalidRegularExpressionSnafu { regex }.build())?;
4024        let dst_exists = self.ctx.tag_columns.contains(&dst_label);
4025
4026        // A missing source label reads as the empty string for every series, so the result is
4027        // the same for all of them and is decided here.
4028        if !self.ctx.tag_columns.contains(&src_label) {
4029            let Some(captures) = compiled.captures("") else {
4030                return Ok(None);
4031            };
4032            let mut value = String::new();
4033            captures.expand(&replacement, &mut value);
4034            if value.is_empty() {
4035                // Setting a label to the empty string removes it, which is a no-op for a new
4036                // label.
4037                if !dst_exists {
4038                    return Ok(None);
4039                }
4040                return Ok(Some((
4041                    lit(ScalarValue::Utf8(None)).alias(&dst_label),
4042                    dst_label,
4043                )));
4044            }
4045            return Ok(Some((lit(value).alias(&dst_label), dst_label)));
4046        }
4047
4048        let src = Self::label_value_expr(&src_label, input_schema)?;
4049        let matched = DfExpr::ScalarFunction(ScalarFunction {
4050            func: datafusion_functions::regex::regexp_like(),
4051            args: vec![src.clone(), lit(anchored.clone())],
4052        });
4053        let replaced = Self::empty_label_to_null(DfExpr::ScalarFunction(ScalarFunction {
4054            func: datafusion_functions::regex::regexp_replace(),
4055            args: vec![src, lit(anchored), lit(replacement)],
4056        }));
4057        let unchanged = if dst_exists {
4058            DfExpr::Column(Column::from_name(&dst_label))
4059                .cast_to(&ArrowDataType::Utf8, input_schema)
4060                .context(DataFusionPlanningSnafu)?
4061        } else {
4062            lit(ScalarValue::Utf8(None))
4063        };
4064        let replace_expr = when(matched, replaced)
4065            .otherwise(unchanged)
4066            .context(DataFusionPlanningSnafu)?;
4067
4068        Ok(Some((replace_expr.alias(&dst_label), dst_label)))
4069    }
4070
4071    /// Build expr for `label_join` function
4072    fn build_concat_labels_expr(
4073        other_input_exprs: &mut VecDeque<DfExpr>,
4074        ctx: &PromPlannerContext,
4075        input_schema: &DFSchemaRef,
4076        query_engine_state: &QueryEngineState,
4077    ) -> Result<(DfExpr, String)> {
4078        // label_join(vector, dst_label, separator, src_label_1, src_label_2, ...)
4079
4080        let dst_label = match other_input_exprs.pop_front() {
4081            Some(DfExpr::Literal(ScalarValue::Utf8(Some(d)), _)) => d,
4082            other => UnexpectedPlanExprSnafu {
4083                desc: format!("expected dst_label string literal, but found {:?}", other),
4084            }
4085            .fail()?,
4086        };
4087        let separator = match other_input_exprs.pop_front() {
4088            Some(DfExpr::Literal(ScalarValue::Utf8(Some(d)), _)) => d,
4089            other => UnexpectedPlanExprSnafu {
4090                desc: format!("expected separator string literal, but found {:?}", other),
4091            }
4092            .fail()?,
4093        };
4094
4095        // Create a set of available columns (tag columns + field columns + time index column)
4096        let available_columns: HashSet<&str> = ctx
4097            .tag_columns
4098            .iter()
4099            .chain(ctx.field_columns.iter())
4100            .chain(ctx.time_index_column.as_ref())
4101            .map(|s| s.as_str())
4102            .collect();
4103
4104        let src_labels = other_input_exprs
4105            .iter()
4106            .map(|expr| {
4107                // `concat_ws` skips NULL arguments together with their separator, while an
4108                // absent label joins as the empty string.
4109                match expr {
4110                    DfExpr::Literal(ScalarValue::Utf8(Some(label)), None) => {
4111                        if label.is_empty() {
4112                            FunctionInvalidArgumentSnafu {
4113                                fn_name: "label_join",
4114                            }
4115                            .fail()
4116                        } else if available_columns.contains(label.as_str()) {
4117                            Self::label_value_expr(label, input_schema)
4118                        } else {
4119                            Ok(lit(""))
4120                        }
4121                    }
4122                    other => UnexpectedPlanExprSnafu {
4123                        desc: format!(
4124                            "expected source label string literal, but found {:?}",
4125                            other
4126                        ),
4127                    }
4128                    .fail(),
4129                }
4130            })
4131            .collect::<Result<Vec<_>>>()?;
4132        // Joining no labels yields the empty string, i.e. removes `dst_label`.
4133        if src_labels.is_empty() {
4134            return Ok((lit(ScalarValue::Utf8(None)).alias(&dst_label), dst_label));
4135        }
4136
4137        let session_state = query_engine_state.session_state();
4138        let func = session_state
4139            .scalar_functions()
4140            .get("concat_ws")
4141            .context(UnsupportedExprSnafu { name: "concat_ws" })?;
4142
4143        // concat_ws(separator, src_label_1, src_label_2, ...) as dst_label
4144        let mut args = Vec::with_capacity(1 + src_labels.len());
4145        args.push(DfExpr::Literal(ScalarValue::Utf8(Some(separator)), None));
4146        args.extend(src_labels);
4147
4148        Ok((
4149            Self::empty_label_to_null(DfExpr::ScalarFunction(ScalarFunction {
4150                func: func.clone(),
4151                args,
4152            }))
4153            .alias(&dst_label),
4154            dst_label,
4155        ))
4156    }
4157
4158    /// The value of `label` as a string, where NULL (the series has no such label) reads as the
4159    /// empty string, as in PromQL.
4160    fn label_value_expr(label: &str, input_schema: &DFSchemaRef) -> Result<DfExpr> {
4161        let value = DfExpr::Column(Column::from_name(label))
4162            .cast_to(&ArrowDataType::Utf8, input_schema)
4163            .context(DataFusionPlanningSnafu)?;
4164        Ok(DfExpr::ScalarFunction(ScalarFunction {
4165            func: coalesce(),
4166            args: vec![value, lit("")],
4167        }))
4168    }
4169
4170    /// An empty label value means the label is absent in PromQL. Label functions represent it
4171    /// as NULL, like a series that never had the label, so that both compare equal when label
4172    /// sets are matched and neither is reported as a label.
4173    fn empty_label_to_null(value: DfExpr) -> DfExpr {
4174        DfExpr::ScalarFunction(ScalarFunction {
4175            func: datafusion_functions::core::nullif(),
4176            args: vec![value, lit("")],
4177        })
4178    }
4179
4180    fn create_time_index_column_expr(&self) -> Result<DfExpr> {
4181        Ok(DfExpr::Column(Column::from_name(
4182            self.ctx
4183                .time_index_column
4184                .clone()
4185                .with_context(|| TimeIndexNotFoundSnafu { table: "unknown" })?,
4186        )))
4187    }
4188
4189    /// Builds the evaluation instant the window of the last planned range selector is folded for,
4190    /// as a `Timestamp(Millisecond)` expression.
4191    ///
4192    /// The timestamp payload of a folded window is shifted onto the evaluation timeline by the
4193    /// offset the window is folded with (`fold_offset`), while the time index column of a folded
4194    /// row keeps the evaluation timestamp of its step. Adding the offset back yields the
4195    /// evaluation instant on the payload timeline, which is where the regression of
4196    /// `predict_linear` is centered: neither a plain window (which may end before the step, and
4197    /// is additionally shifted by `offset` on the payload timeline) nor an `@`-anchored one
4198    /// (whose end is the anchor, while the payload is shifted by `at_offset`) ends at the step it
4199    /// is evaluated at.
4200    ///
4201    /// The sum is computed on the millisecond representation and cast back, so that the result
4202    /// keeps the `Timestamp(Millisecond)` type the range functions declare for it.
4203    fn create_range_eval_ts_expr(
4204        &self,
4205        fold_offset: Millisecond,
4206        input_schema: &DFSchemaRef,
4207    ) -> Result<DfExpr> {
4208        let eval_ts = self
4209            .create_time_index_column_expr()?
4210            .cast_to(
4211                &ArrowDataType::Timestamp(ArrowTimeUnit::Millisecond, None),
4212                input_schema,
4213            )
4214            .context(DataFusionPlanningSnafu)?
4215            .cast_to(&ArrowDataType::Int64, input_schema)
4216            .context(DataFusionPlanningSnafu)?;
4217        DfExpr::BinaryExpr(BinaryExpr {
4218            left: Box::new(eval_ts),
4219            op: Operator::Plus,
4220            right: Box::new(lit(fold_offset)),
4221        })
4222        .cast_to(
4223            &ArrowDataType::Timestamp(ArrowTimeUnit::Millisecond, None),
4224            input_schema,
4225        )
4226        .context(DataFusionPlanningSnafu)
4227    }
4228
4229    /// The name of `expr` without its last argument.
4230    ///
4231    /// A `predict_linear` call appends the evaluation instant of its window as a private last
4232    /// argument ([`Self::create_range_eval_ts_expr`]). Naming the output column after the call
4233    /// the user wrote, without that argument, keeps the injected expression out of the
4234    /// user-visible schema; the expression itself keeps every argument it needs.
4235    fn name_without_last_arg(expr: &DfExpr) -> String {
4236        if let DfExpr::ScalarFunction(ScalarFunction { func, args }) = expr
4237            && let Some((_, visible_args)) = args.split_last()
4238        {
4239            let visible = ScalarFunction {
4240                func: func.clone(),
4241                args: visible_args.to_vec(),
4242            };
4243            return DfExpr::ScalarFunction(visible).schema_name().to_string();
4244        }
4245
4246        expr.schema_name().to_string()
4247    }
4248
4249    fn create_tag_column_exprs(&self) -> Result<Vec<DfExpr>> {
4250        let mut result = Vec::with_capacity(self.ctx.tag_columns.len());
4251        for tag in &self.ctx.tag_columns {
4252            let expr = DfExpr::Column(Column::from_name(tag));
4253            result.push(expr);
4254        }
4255        Ok(result)
4256    }
4257
4258    fn create_field_column_exprs(&self) -> Result<Vec<DfExpr>> {
4259        let mut result = Vec::with_capacity(self.ctx.field_columns.len());
4260        for field in &self.ctx.field_columns {
4261            let expr = DfExpr::Column(Column::from_name(field));
4262            result.push(expr);
4263        }
4264        Ok(result)
4265    }
4266
4267    fn create_tag_and_time_index_column_sort_exprs(&self) -> Result<Vec<SortExpr>> {
4268        let mut result = self
4269            .ctx
4270            .tag_columns
4271            .iter()
4272            .map(|col| DfExpr::Column(Column::from_name(col)).sort(true, true))
4273            .collect::<Vec<_>>();
4274        result.push(self.create_time_index_column_expr()?.sort(true, true));
4275        Ok(result)
4276    }
4277
4278    fn create_field_columns_sort_exprs(&self, asc: bool) -> Vec<SortExpr> {
4279        self.ctx
4280            .field_columns
4281            .iter()
4282            .map(|col| DfExpr::Column(Column::from_name(col)).sort(asc, true))
4283            .collect::<Vec<_>>()
4284    }
4285
4286    fn create_sort_exprs_by_tags(
4287        func: &str,
4288        tags: Vec<DfExpr>,
4289        asc: bool,
4290    ) -> Result<Vec<SortExpr>> {
4291        ensure!(
4292            !tags.is_empty(),
4293            FunctionInvalidArgumentSnafu { fn_name: func }
4294        );
4295
4296        tags.iter()
4297            .map(|col| match col {
4298                DfExpr::Literal(ScalarValue::Utf8(Some(label)), _) => {
4299                    Ok(DfExpr::Column(Column::from_name(label)).sort(asc, false))
4300                }
4301                other => UnexpectedPlanExprSnafu {
4302                    desc: format!("expected label string literal, but found {:?}", other),
4303                }
4304                .fail(),
4305            })
4306            .collect::<Result<Vec<_>>>()
4307    }
4308
4309    fn create_empty_values_filter_expr(&self, preserve_any_value: bool) -> Result<DfExpr> {
4310        let mut exprs = Vec::with_capacity(self.ctx.field_columns.len());
4311        for value in &self.ctx.field_columns {
4312            let expr = DfExpr::Column(Column::from_name(value)).is_not_null();
4313            exprs.push(expr);
4314        }
4315
4316        // This error context should be computed lazily: the planner may set `ctx.table_name` to
4317        // `None` for derived expressions (e.g. after projecting the LHS of a vector-vector
4318        // comparison filter). Eagerly calling `table_ref()?` here can turn a valid plan into
4319        // a `TableNameNotFound` error even when predicate construction succeeds.
4320        let predicate = if preserve_any_value {
4321            disjunction(exprs)
4322        } else {
4323            conjunction(exprs)
4324        };
4325        predicate.with_context(|| ValueNotFoundSnafu {
4326            table: self
4327                .table_ref()
4328                .map(|t| t.to_quoted_string())
4329                .unwrap_or_else(|_| "unknown".to_string()),
4330        })
4331    }
4332
4333    /// Creates a set of DataFusion `DfExpr::AggregateFunction` expressions for each value column using the specified aggregate function.
4334    ///
4335    /// # Side Effects
4336    ///
4337    /// This method modifies the value columns in the context by replacing them with the new columns
4338    /// created by the aggregate function application.
4339    ///
4340    /// # Returns
4341    ///
4342    /// Returns a tuple of `(aggregate_expressions, previous_field_expressions)` where:
4343    /// - `aggregate_expressions`: Expressions that apply the aggregate function to the original fields
4344    /// - `previous_field_expressions`: Field expressions naming the pre-aggregation values. This is
4345    ///   non-empty only when the operation is `count_values`, which groups by the sample value and
4346    ///   projects it as the generated label, so these expressions are passed through the same
4347    ///   formatting as that label (`prom_float_to_string`).
4348    ///
4349    fn create_aggregate_exprs(
4350        &mut self,
4351        op: TokenType,
4352        param: &Option<Box<PromExpr>>,
4353        input_plan: &LogicalPlan,
4354    ) -> Result<(Vec<DfExpr>, Vec<DfExpr>)> {
4355        let mixed_sample_columns =
4356            Self::alternative_sample_columns(input_plan.schema(), &self.ctx.field_columns)
4357                .map(|(float, histogram)| (float.to_string(), histogram.to_string()));
4358        let is_group_agg = op.id() == token::T_GROUP;
4359        if is_group_agg && mixed_sample_columns.is_none() {
4360            ensure!(
4361                self.ctx.field_columns.len() == 1,
4362                MultiFieldsNotSupportedSnafu {
4363                    operator: "group()"
4364                }
4365            );
4366        }
4367
4368        if let Some((float, histogram)) = mixed_sample_columns {
4369            return self.create_mixed_aggregate_exprs(op, param, &float, &histogram);
4370        }
4371
4372        if self.all_field_columns_are_native_histograms(input_plan.schema()) {
4373            return self.create_native_histogram_aggregate_exprs(op, input_plan);
4374        }
4375
4376        // perform aggregate operation to each value column
4377        let exprs = self
4378            .ctx
4379            .field_columns
4380            .iter()
4381            .map(|col| {
4382                Self::create_numeric_aggregate_expr(
4383                    op,
4384                    param,
4385                    DfExpr::Column(Column::from_name(col)),
4386                )
4387            })
4388            .collect::<Result<Vec<_>>>()?;
4389
4390        // if the aggregator is `count_values`, it must be grouped by current fields.
4391        //
4392        // The grouping key is the *formatted* sample value, i.e. the same expression that
4393        // produces the generated label below: PromQL groups by the value, and the label is
4394        // that value in Prometheus' textual form (`strconv.FormatFloat(value, 'f', -1, 64)`),
4395        // so grouping by the raw value would split samples that render to one label into
4396        // several groups, each emitting the same label set for one timestamp.
4397        let prev_field_exprs = if op.id() == token::T_COUNT_VALUES {
4398            ensure!(
4399                self.ctx.field_columns.len() == 1,
4400                UnsupportedExprSnafu {
4401                    name: "count_values on multi-value input"
4402                }
4403            );
4404
4405            self.ctx
4406                .field_columns
4407                .iter()
4408                .map(|col| {
4409                    let value = DfExpr::Column(Column::from_name(col));
4410                    // Normalize non `Float64` inputs the same way the label projection does,
4411                    // so both sides agree on the formatted value: `prom_float_to_string`
4412                    // formats exactly like Prometheus, while arrow's `Float64 -> Utf8` cast
4413                    // would render `1.0`.
4414                    let value = if Self::field_column_type(input_plan.schema(), col)
4415                        == Some(&ArrowDataType::Float64)
4416                    {
4417                        value
4418                    } else {
4419                        DfExpr::Cast(Cast::new(Box::new(value), ArrowDataType::Float64))
4420                    };
4421                    DfExpr::ScalarFunction(ScalarFunction {
4422                        func: Arc::new(PromqlFloatToString::scalar_udf()),
4423                        args: vec![value],
4424                    })
4425                })
4426                .collect()
4427        } else {
4428            vec![]
4429        };
4430
4431        // update value column name according to the aggregators,
4432        let mut new_field_columns = Vec::with_capacity(self.ctx.field_columns.len());
4433
4434        let normalized_exprs =
4435            normalize_cols(exprs.iter().cloned(), input_plan).context(DataFusionPlanningSnafu)?;
4436        for expr in normalized_exprs {
4437            new_field_columns.push(expr.schema_name().to_string());
4438        }
4439        self.ctx.field_columns = new_field_columns;
4440
4441        Ok((exprs, prev_field_exprs))
4442    }
4443
4444    fn create_numeric_aggregate_expr(
4445        op: TokenType,
4446        param: &Option<Box<PromExpr>>,
4447        input: DfExpr,
4448    ) -> Result<DfExpr> {
4449        let expr = match op.id() {
4450            token::T_SUM => sum_udaf().call(vec![input]),
4451            token::T_QUANTILE => {
4452                let q = Self::get_param_as_literal_expr(
4453                    param.as_deref(),
4454                    Some(op),
4455                    Some(ArrowDataType::Float64),
4456                )?;
4457                quantile_udaf().call(vec![q, input])
4458            }
4459            token::T_AVG => avg_udaf().call(vec![input]),
4460            token::T_COUNT_VALUES | token::T_COUNT => count_udaf().call(vec![input]),
4461            token::T_MIN => min_udaf().call(vec![input]),
4462            token::T_MAX => max_udaf().call(vec![input]),
4463            // PromQL's `group()` aggregator produces 1 for each group.
4464            // Use `max(1.0)` (per-group) to match semantics and output type (Float64).
4465            token::T_GROUP => max_udaf().call(vec![lit(1_f64)]),
4466            token::T_STDDEV => stddev_pop_udaf().call(vec![input]),
4467            token::T_STDVAR => var_pop_udaf().call(vec![input]),
4468            token::T_TOPK | token::T_BOTTOMK => {
4469                return UnsupportedExprSnafu {
4470                    name: format!("{op:?}"),
4471                }
4472                .fail();
4473            }
4474            _ => return UnexpectedTokenSnafu { token: op }.fail(),
4475        };
4476        Ok(expr)
4477    }
4478
4479    fn create_mixed_aggregate_exprs(
4480        &mut self,
4481        op: TokenType,
4482        param: &Option<Box<PromExpr>>,
4483        float_column: &str,
4484        histogram_column: &str,
4485    ) -> Result<(Vec<DfExpr>, Vec<DfExpr>)> {
4486        let float_input = DfExpr::Column(Column::from_name(float_column));
4487        let histogram_input = DfExpr::Column(Column::from_name(histogram_column));
4488        let float_count = count_udaf().call(vec![float_input.clone()]);
4489        let histogram_count = count_udaf().call(vec![histogram_input.clone()]);
4490        let mixed_sample_value = || {
4491            DfExpr::ScalarFunction(ScalarFunction {
4492                func: coalesce(),
4493                args: vec![
4494                    DfExpr::ScalarFunction(ScalarFunction {
4495                        func: Arc::new(PromqlFloatToString::scalar_udf()),
4496                        args: vec![float_input.clone()],
4497                    }),
4498                    DfExpr::ScalarFunction(ScalarFunction {
4499                        func: Arc::new(NativeHistogramToString::scalar_udf()),
4500                        args: vec![histogram_input.clone()],
4501                    }),
4502                ],
4503            })
4504        };
4505
4506        let (exprs, prev_field_exprs, field_columns) = match op.id() {
4507            token::T_SUM | token::T_AVG => (
4508                vec![
4509                    Self::create_numeric_aggregate_expr(op, param, float_input)?
4510                        .alias(float_column),
4511                    self.create_native_histogram_aggregate_expr(op, histogram_column)?,
4512                    float_count.alias(Self::mixed_sample_count_name(float_column)),
4513                    histogram_count.alias(Self::mixed_sample_count_name(histogram_column)),
4514                ],
4515                vec![],
4516                vec![float_column.to_string(), histogram_column.to_string()],
4517            ),
4518            token::T_COUNT => {
4519                let present = when(
4520                    float_input
4521                        .clone()
4522                        .is_not_null()
4523                        .or(histogram_input.clone().is_not_null()),
4524                    lit(1_i64),
4525                )
4526                .otherwise(lit(ScalarValue::Int64(None)))
4527                .context(DataFusionPlanningSnafu)?;
4528                (
4529                    vec![count_udaf().call(vec![present]).alias(float_column)],
4530                    vec![],
4531                    vec![float_column.to_string()],
4532                )
4533            }
4534            token::T_GROUP => (
4535                vec![max_udaf().call(vec![lit(1_f64)]).alias(float_column)],
4536                vec![],
4537                vec![float_column.to_string()],
4538            ),
4539            token::T_COUNT_VALUES => {
4540                let value = mixed_sample_value();
4541                (
4542                    vec![count_udaf().call(vec![value.clone()]).alias(float_column)],
4543                    vec![value],
4544                    vec![float_column.to_string()],
4545                )
4546            }
4547            token::T_MIN | token::T_MAX | token::T_STDDEV | token::T_STDVAR | token::T_QUANTILE => {
4548                (
4549                    vec![
4550                        Self::create_numeric_aggregate_expr(op, param, float_input)?
4551                            .alias(float_column),
4552                        histogram_count.alias(Self::mixed_sample_count_name(histogram_column)),
4553                    ],
4554                    vec![],
4555                    vec![float_column.to_string()],
4556                )
4557            }
4558            token::T_TOPK | token::T_BOTTOMK => {
4559                return UnsupportedExprSnafu {
4560                    name: format!("{op:?}"),
4561                }
4562                .fail();
4563            }
4564            _ => return UnexpectedTokenSnafu { token: op }.fail(),
4565        };
4566
4567        self.ctx.field_columns = field_columns;
4568        Ok((exprs, prev_field_exprs))
4569    }
4570
4571    fn mixed_sample_count_column(column: &str) -> DfExpr {
4572        DfExpr::Column(Column::from_name(Self::mixed_sample_count_name(column)))
4573    }
4574
4575    fn mixed_sample_count_name(column: &str) -> String {
4576        format!("__promql_sample_count({column})")
4577    }
4578
4579    fn mixed_aggregate_filter_expr(
4580        &self,
4581        op: TokenType,
4582        float_column: &str,
4583        histogram_column: &str,
4584    ) -> Result<DfExpr> {
4585        let float_count = Self::mixed_sample_count_column(float_column);
4586        let histogram_count = Self::mixed_sample_count_column(histogram_column);
4587        let mixed = float_count
4588            .clone()
4589            .gt(lit(0_i64))
4590            .and(histogram_count.clone().gt(lit(0_i64)));
4591        let drop_mixed = DfExpr::ScalarFunction(ScalarFunction {
4592            func: Arc::new(NativeHistogramDrop::warning_bool_false_udf(
4593                format!(
4594                    "{op}: dropped aggregation result containing both float and native histogram samples"
4595                ),
4596                self.promql_annotations.clone(),
4597            )),
4598            args: vec![float_count, histogram_count],
4599        });
4600
4601        when(mixed, drop_mixed)
4602            .otherwise(lit(true))
4603            .context(DataFusionPlanningSnafu)
4604    }
4605
4606    fn mixed_ignored_histogram_filter_expr(
4607        &self,
4608        op: TokenType,
4609        histogram_column: &str,
4610    ) -> Result<DfExpr> {
4611        let histogram_count = Self::mixed_sample_count_column(histogram_column);
4612        let has_histograms = histogram_count.clone().gt(lit(0_i64));
4613        let record_info = DfExpr::ScalarFunction(ScalarFunction {
4614            func: Arc::new(NativeHistogramDrop::bool_true_udf(
4615                format!(
4616                    "{op}: dropped native histogram samples because this aggregation is not supported for native histograms"
4617                ),
4618                self.promql_annotations.clone(),
4619            )),
4620            args: vec![histogram_count],
4621        });
4622
4623        when(has_histograms, record_info)
4624            .otherwise(lit(true))
4625            .context(DataFusionPlanningSnafu)
4626    }
4627
4628    fn create_native_histogram_aggregate_expr(
4629        &self,
4630        op: TokenType,
4631        column: &str,
4632    ) -> Result<DfExpr> {
4633        let input = DfExpr::Column(Column::from_name(column));
4634        let expr = match op.id() {
4635            token::T_SUM => Arc::new(NativeHistogramAggSum::aggregate_udf_with_collector(
4636                self.promql_annotations.clone(),
4637            ))
4638            .call(vec![input])
4639            .alias(column),
4640            token::T_AVG => Arc::new(NativeHistogramAggAvg::aggregate_udf_with_collector(
4641                self.promql_annotations.clone(),
4642            ))
4643            .call(vec![input])
4644            .alias(column),
4645            token::T_COUNT_VALUES | token::T_COUNT => {
4646                count_udaf().call(vec![input]).alias(column)
4647            }
4648            token::T_GROUP => max_udaf().call(vec![lit(1_f64)]).alias(column),
4649            token::T_MIN
4650            | token::T_MAX
4651            | token::T_STDDEV
4652            | token::T_STDVAR
4653            | token::T_QUANTILE
4654            | token::T_TOPK
4655            | token::T_BOTTOMK => sum_udaf()
4656                .call(vec![DfExpr::ScalarFunction(ScalarFunction {
4657                    func: Arc::new(NativeHistogramDrop::float_null_udf(
4658                        format!(
4659                            "{op}: dropped native histogram samples because this aggregation is not supported for native histograms"
4660                        ),
4661                        self.promql_annotations.clone(),
4662                    )),
4663                    args: vec![input],
4664                })])
4665                .alias(column),
4666            _ => return UnexpectedTokenSnafu { token: op }.fail(),
4667        };
4668        Ok(expr)
4669    }
4670
4671    fn create_native_histogram_aggregate_exprs(
4672        &mut self,
4673        op: TokenType,
4674        input_plan: &LogicalPlan,
4675    ) -> Result<(Vec<DfExpr>, Vec<DfExpr>)> {
4676        let prev_field_exprs = if op.id() == token::T_COUNT_VALUES {
4677            ensure!(
4678                self.ctx.field_columns.len() == 1,
4679                UnsupportedExprSnafu {
4680                    name: "count_values on multi-value input"
4681                }
4682            );
4683            self.ctx
4684                .field_columns
4685                .iter()
4686                .map(|col| {
4687                    DfExpr::ScalarFunction(ScalarFunction {
4688                        func: Arc::new(NativeHistogramToString::scalar_udf()),
4689                        args: vec![DfExpr::Column(Column::from_name(col))],
4690                    })
4691                })
4692                .collect::<Vec<_>>()
4693        } else {
4694            vec![]
4695        };
4696
4697        let exprs = self
4698            .ctx
4699            .field_columns
4700            .iter()
4701            .map(|col| self.create_native_histogram_aggregate_expr(op, col))
4702            .collect::<Result<Vec<_>>>()?;
4703
4704        let normalized_exprs =
4705            normalize_cols(exprs.iter().cloned(), input_plan).context(DataFusionPlanningSnafu)?;
4706        self.ctx.field_columns = normalized_exprs
4707            .into_iter()
4708            .map(|expr| expr.schema_name().to_string())
4709            .collect();
4710
4711        Ok((exprs, prev_field_exprs))
4712    }
4713
4714    fn get_param_value_as_str(op: TokenType, param: &Option<Box<PromExpr>>) -> Result<&str> {
4715        let param = param
4716            .as_deref()
4717            .with_context(|| FunctionInvalidArgumentSnafu {
4718                fn_name: op.to_string(),
4719            })?;
4720        let PromExpr::StringLiteral(StringLiteral { val }) = param else {
4721            return FunctionInvalidArgumentSnafu {
4722                fn_name: op.to_string(),
4723            }
4724            .fail();
4725        };
4726
4727        Ok(val)
4728    }
4729
4730    fn get_param_as_literal_expr(
4731        param: Option<&PromExpr>,
4732        op: Option<TokenType>,
4733        expected_type: Option<ArrowDataType>,
4734    ) -> Result<DfExpr> {
4735        let prom_param = param.with_context(|| {
4736            if let Some(op) = op {
4737                FunctionInvalidArgumentSnafu {
4738                    fn_name: op.to_string(),
4739                }
4740            } else {
4741                FunctionInvalidArgumentSnafu {
4742                    fn_name: "unknown".to_string(),
4743                }
4744            }
4745        })?;
4746
4747        let expr = Self::try_build_literal_expr(prom_param).with_context(|| {
4748            if let Some(op) = op {
4749                FunctionInvalidArgumentSnafu {
4750                    fn_name: op.to_string(),
4751                }
4752            } else {
4753                FunctionInvalidArgumentSnafu {
4754                    fn_name: "unknown".to_string(),
4755                }
4756            }
4757        })?;
4758
4759        // check if the type is expected
4760        if let Some(expected_type) = expected_type {
4761            // literal should not have reference to column
4762            let expr_type = expr
4763                .get_type(&DFSchema::empty())
4764                .context(DataFusionPlanningSnafu)?;
4765            if expected_type != expr_type {
4766                return FunctionInvalidArgumentSnafu {
4767                    fn_name: format!("expected {expected_type:?}, but found {expr_type:?}"),
4768                }
4769                .fail();
4770            }
4771        }
4772
4773        Ok(expr)
4774    }
4775
4776    /// Create [DfExpr::WindowFunction] expr for each value column with given window function.
4777    ///
4778    fn create_window_exprs(
4779        &mut self,
4780        op: TokenType,
4781        group_exprs: Vec<DfExpr>,
4782        input_plan: &LogicalPlan,
4783    ) -> Result<Vec<DfExpr>> {
4784        ensure!(
4785            self.ctx.field_columns.len() == 1,
4786            UnsupportedExprSnafu {
4787                name: "topk or bottomk on multi-value input"
4788            }
4789        );
4790
4791        assert!(matches!(op.id(), token::T_TOPK | token::T_BOTTOMK));
4792
4793        let asc = matches!(op.id(), token::T_BOTTOMK);
4794
4795        let tag_sort_exprs = self
4796            .create_tag_column_exprs()?
4797            .into_iter()
4798            .map(|expr| expr.sort(asc, true));
4799
4800        // perform window operation to each value column
4801        let exprs: Vec<DfExpr> = self
4802            .ctx
4803            .field_columns
4804            .iter()
4805            .map(|col| {
4806                let mut sort_exprs = Vec::with_capacity(self.ctx.tag_columns.len() + 1);
4807                // Order by value in the specific order
4808                sort_exprs.push(DfExpr::Column(Column::from_name(col)).sort(asc, true));
4809                // Then tags if the values are equal,
4810                // Try to ensure the relative stability of the output results.
4811                sort_exprs.extend(tag_sort_exprs.clone());
4812
4813                DfExpr::WindowFunction(Box::new(WindowFunction {
4814                    fun: WindowFunctionDefinition::WindowUDF(Arc::new(RowNumber::new().into())),
4815                    params: WindowFunctionParams {
4816                        args: vec![],
4817                        partition_by: group_exprs.clone(),
4818                        order_by: sort_exprs,
4819                        window_frame: WindowFrame::new(Some(true)),
4820                        null_treatment: None,
4821                        distinct: false,
4822                        filter: None,
4823                    },
4824                }))
4825            })
4826            .collect();
4827
4828        let normalized_exprs =
4829            normalize_cols(exprs.iter().cloned(), input_plan).context(DataFusionPlanningSnafu)?;
4830        Ok(normalized_exprs)
4831    }
4832
4833    /// Try to build a DataFusion Literal Expression from PromQL Expr, return
4834    /// `None` if the input is not a literal expression.
4835    fn try_build_literal_expr(expr: &PromExpr) -> Option<DfExpr> {
4836        match expr {
4837            PromExpr::NumberLiteral(NumberLiteral { val }) => Some(val.lit()),
4838            PromExpr::StringLiteral(StringLiteral { val }) => Some(val.lit()),
4839            PromExpr::VectorSelector(_)
4840            | PromExpr::MatrixSelector(_)
4841            | PromExpr::Extension(_)
4842            | PromExpr::Aggregate(_)
4843            | PromExpr::Subquery(_) => None,
4844            PromExpr::Call(Call { func, .. }) => {
4845                if func.name == SPECIAL_TIME_FUNCTION {
4846                    // For time() function, don't treat it as a literal
4847                    // Let it be handled as a regular function call
4848                    None
4849                } else {
4850                    None
4851                }
4852            }
4853            PromExpr::Paren(ParenExpr { expr }) => Self::try_build_literal_expr(expr),
4854            PromExpr::Unary(UnaryExpr { expr, .. }) => Some(DfExpr::Negative(Box::new(
4855                Self::try_build_literal_expr(expr)?,
4856            ))),
4857            PromExpr::Binary(PromBinaryExpr {
4858                lhs,
4859                rhs,
4860                op,
4861                modifier,
4862            }) => {
4863                let lhs = Self::try_build_literal_expr(lhs)?;
4864                let rhs = Self::try_build_literal_expr(rhs)?;
4865                let is_comparison_op = Self::is_token_a_comparison_op(*op);
4866                let expr_builder = Self::prom_token_to_binary_expr_builder(*op).ok()?;
4867                let expr = expr_builder(lhs, rhs).ok()?;
4868
4869                let should_return_bool = if let Some(m) = modifier {
4870                    m.return_bool
4871                } else {
4872                    false
4873                };
4874                if is_comparison_op && should_return_bool {
4875                    Some(DfExpr::Cast(Cast::new(
4876                        Box::new(expr),
4877                        ArrowDataType::Float64,
4878                    )))
4879                } else {
4880                    Some(expr)
4881                }
4882            }
4883        }
4884    }
4885
4886    fn try_build_special_time_expr_with_context(&self, expr: &PromExpr) -> Option<DfExpr> {
4887        match expr {
4888            PromExpr::Call(Call { func, .. }) => {
4889                if func.name == SPECIAL_TIME_FUNCTION
4890                    && let Some(time_index_col) = self.ctx.time_index_column.as_ref()
4891                {
4892                    Some(build_special_time_expr(time_index_col))
4893                } else {
4894                    None
4895                }
4896            }
4897            _ => None,
4898        }
4899    }
4900
4901    fn native_histogram_binary_expr(
4902        token: TokenType,
4903        lhs: DfExpr,
4904        lhs_is_histogram: bool,
4905        rhs: DfExpr,
4906        rhs_is_histogram: bool,
4907        filter_context: bool,
4908        promql_annotations: Option<PromqlAnnotationCollector>,
4909    ) -> Result<Option<DfExpr>> {
4910        if !lhs_is_histogram && !rhs_is_histogram {
4911            return Ok(None);
4912        }
4913
4914        let scalar_fn = |func: ScalarUdfDef, args| {
4915            DfExpr::ScalarFunction(ScalarFunction {
4916                func: Arc::new(func),
4917                args,
4918            })
4919        };
4920        let invalid_expr = || {
4921            let message = format!(
4922                "{}: dropped native histogram samples because this binary operation is not supported for native histograms",
4923                token
4924            );
4925            let func = if filter_context {
4926                NativeHistogramDrop::bool_false_udf(message, promql_annotations.clone())
4927            } else {
4928                NativeHistogramDrop::float_null_udf(message, promql_annotations.clone())
4929            };
4930            let args = vec![lhs.clone(), rhs.clone()];
4931            scalar_fn(func, args)
4932        };
4933
4934        let expr = match (token.id(), lhs_is_histogram, rhs_is_histogram) {
4935            (token::T_ADD, true, true) => scalar_fn(
4936                NativeHistogramAdd::scalar_udf_with_collector(promql_annotations.clone()),
4937                vec![lhs, rhs],
4938            ),
4939            (token::T_SUB, true, true) => scalar_fn(
4940                NativeHistogramSub::scalar_udf_with_collector(promql_annotations.clone()),
4941                vec![lhs, rhs],
4942            ),
4943            (token::T_MUL, true, false) => {
4944                scalar_fn(NativeHistogramMulScalar::scalar_udf(), vec![lhs, rhs])
4945            }
4946            (token::T_MUL, false, true) => {
4947                scalar_fn(NativeHistogramScalarMul::scalar_udf(), vec![lhs, rhs])
4948            }
4949            (token::T_DIV, true, false) => {
4950                scalar_fn(NativeHistogramDivScalar::scalar_udf(), vec![lhs, rhs])
4951            }
4952            (token::T_EQLC, true, true) => {
4953                scalar_fn(NativeHistogramEq::scalar_udf(), vec![lhs, rhs])
4954            }
4955            (token::T_NEQ, true, true) => {
4956                scalar_fn(NativeHistogramNotEq::scalar_udf(), vec![lhs, rhs])
4957            }
4958            _ => invalid_expr(),
4959        };
4960
4961        Ok(Some(expr))
4962    }
4963
4964    /// Return a lambda to build binary expression from token.
4965    /// Because some binary operator are function in DataFusion like `atan2` or `^`.
4966    #[allow(clippy::type_complexity)]
4967    fn prom_token_to_binary_expr_builder(
4968        token: TokenType,
4969    ) -> Result<Box<dyn Fn(DfExpr, DfExpr) -> Result<DfExpr>>> {
4970        let cast_float = |expr| {
4971            if matches!(
4972                &expr,
4973                DfExpr::Cast(Cast { field, .. }) if field.data_type() == &ArrowDataType::Float64
4974            ) || matches!(&expr, DfExpr::Literal(ScalarValue::Float64(_), _))
4975            {
4976                expr
4977            } else {
4978                DfExpr::Cast(Cast::new(Box::new(expr), ArrowDataType::Float64))
4979            }
4980        };
4981        match token.id() {
4982            token::T_ADD => Ok(Box::new(move |lhs, rhs| {
4983                Ok(cast_float(lhs) + cast_float(rhs))
4984            })),
4985            token::T_SUB => Ok(Box::new(move |lhs, rhs| {
4986                Ok(cast_float(lhs) - cast_float(rhs))
4987            })),
4988            token::T_MUL => Ok(Box::new(move |lhs, rhs| {
4989                Ok(cast_float(lhs) * cast_float(rhs))
4990            })),
4991            token::T_DIV => Ok(Box::new(move |lhs, rhs| {
4992                Ok(cast_float(lhs) / cast_float(rhs))
4993            })),
4994            token::T_MOD => Ok(Box::new(move |lhs: DfExpr, rhs| {
4995                Ok(cast_float(lhs) % cast_float(rhs))
4996            })),
4997            token::T_EQLC => Ok(Box::new(|lhs, rhs| Ok(lhs.eq(rhs)))),
4998            token::T_NEQ => Ok(Box::new(|lhs, rhs| Ok(lhs.not_eq(rhs)))),
4999            token::T_GTR => Ok(Box::new(|lhs, rhs| Ok(lhs.gt(rhs)))),
5000            token::T_LSS => Ok(Box::new(|lhs, rhs| Ok(lhs.lt(rhs)))),
5001            token::T_GTE => Ok(Box::new(|lhs, rhs| Ok(lhs.gt_eq(rhs)))),
5002            token::T_LTE => Ok(Box::new(|lhs, rhs| Ok(lhs.lt_eq(rhs)))),
5003            token::T_POW => Ok(Box::new(move |lhs, rhs| {
5004                Ok(DfExpr::ScalarFunction(ScalarFunction {
5005                    func: datafusion_functions::math::power(),
5006                    args: vec![cast_float(lhs), cast_float(rhs)],
5007                }))
5008            })),
5009            token::T_ATAN2 => Ok(Box::new(move |lhs, rhs| {
5010                Ok(DfExpr::ScalarFunction(ScalarFunction {
5011                    func: datafusion_functions::math::atan2(),
5012                    args: vec![cast_float(lhs), cast_float(rhs)],
5013                }))
5014            })),
5015            _ => UnexpectedTokenSnafu { token }.fail(),
5016        }
5017    }
5018
5019    /// Check if the given op is a [comparison operator](https://prometheus.io/docs/prometheus/latest/querying/operators/#comparison-binary-operators).
5020    fn is_token_a_comparison_op(token: TokenType) -> bool {
5021        matches!(
5022            token.id(),
5023            token::T_EQLC
5024                | token::T_NEQ
5025                | token::T_GTR
5026                | token::T_LSS
5027                | token::T_GTE
5028                | token::T_LTE
5029        )
5030    }
5031
5032    /// Check if the given op is a set operator (UNION, INTERSECT and EXCEPT in SQL).
5033    fn is_token_a_set_op(token: TokenType) -> bool {
5034        matches!(
5035            token.id(),
5036            token::T_LAND // INTERSECT
5037                | token::T_LOR // UNION
5038                | token::T_LUNLESS // EXCEPT
5039        )
5040    }
5041
5042    fn align_binary_field_columns<'a>(
5043        left_schema: &DFSchemaRef,
5044        right_schema: &DFSchemaRef,
5045        left_field_columns: &'a [String],
5046        right_field_columns: &'a [String],
5047        op: TokenType,
5048        left_is_scalar: bool,
5049        right_is_scalar: bool,
5050    ) -> (
5051        Vec<(String, Vec<BinaryFieldPair<'a>>)>,
5052        Vec<BinaryFieldPair<'a>>,
5053    ) {
5054        // Mixed vectors store mutually exclusive float and histogram samples in two columns.
5055        // Retain each valid sample combination and group expressions by their output lane.
5056        let left_alternative = Self::alternative_sample_columns(left_schema, left_field_columns);
5057        let right_alternative = Self::alternative_sample_columns(right_schema, right_field_columns);
5058        let alternative_alignment = match (left_alternative, right_alternative) {
5059            (Some(output_names), Some(_)) => Some((
5060                output_names,
5061                left_field_columns
5062                    .iter()
5063                    .flat_map(|left| right_field_columns.iter().map(move |right| (left, right)))
5064                    .collect::<Vec<_>>(),
5065            )),
5066            (Some(output_names), None) if right_field_columns.len() == 1 => Some((
5067                output_names,
5068                left_field_columns
5069                    .iter()
5070                    .map(|left| (left, &right_field_columns[0]))
5071                    .collect::<Vec<_>>(),
5072            )),
5073            (None, Some(output_names)) if left_field_columns.len() == 1 => Some((
5074                output_names,
5075                right_field_columns
5076                    .iter()
5077                    .map(|right| (&left_field_columns[0], right))
5078                    .collect::<Vec<_>>(),
5079            )),
5080            _ => None,
5081        };
5082        let mut invalid_pairs = Vec::new();
5083        if let Some(((float_output, histogram_output), field_pairs)) = alternative_alignment {
5084            let mut float_pairs = Vec::new();
5085            let mut histogram_pairs = Vec::new();
5086            for (left, right) in field_pairs {
5087                let left_is_histogram = Self::field_column_is_native_histogram(left_schema, left);
5088                let right_is_histogram =
5089                    Self::field_column_is_native_histogram(right_schema, right);
5090                match Self::binary_result_is_histogram(op, left_is_histogram, right_is_histogram) {
5091                    Some(false) => float_pairs.push((left, right)),
5092                    Some(true) => histogram_pairs.push((left, right)),
5093                    None => invalid_pairs.push((left, right)),
5094                }
5095            }
5096            if !float_pairs.is_empty() || !histogram_pairs.is_empty() {
5097                return (
5098                    [
5099                        (!float_pairs.is_empty()).then(|| (float_output.to_string(), float_pairs)),
5100                        (!histogram_pairs.is_empty())
5101                            .then(|| (histogram_output.to_string(), histogram_pairs)),
5102                    ]
5103                    .into_iter()
5104                    .flatten()
5105                    .collect(),
5106                    invalid_pairs,
5107                );
5108            }
5109        }
5110
5111        if left_is_scalar && !right_is_scalar && left_field_columns.len() == 1 {
5112            return (
5113                right_field_columns
5114                    .iter()
5115                    .map(|right| (right.clone(), vec![(&left_field_columns[0], right)]))
5116                    .collect(),
5117                invalid_pairs,
5118            );
5119        }
5120        if right_is_scalar && !left_is_scalar && right_field_columns.len() == 1 {
5121            return (
5122                left_field_columns
5123                    .iter()
5124                    .map(|left| (left.clone(), vec![(left, &right_field_columns[0])]))
5125                    .collect(),
5126                invalid_pairs,
5127            );
5128        }
5129
5130        (
5131            left_field_columns
5132                .iter()
5133                .zip(right_field_columns.iter())
5134                .map(|(left, right)| (left.clone(), vec![(left, right)]))
5135                .collect(),
5136            invalid_pairs,
5137        )
5138    }
5139
5140    fn binary_result_is_histogram(
5141        token: TokenType,
5142        lhs_is_histogram: bool,
5143        rhs_is_histogram: bool,
5144    ) -> Option<bool> {
5145        match (token.id(), lhs_is_histogram, rhs_is_histogram) {
5146            (_, false, false) => Some(false),
5147            (token::T_ADD | token::T_SUB, true, true)
5148            | (token::T_MUL, true, false)
5149            | (token::T_MUL, false, true)
5150            | (token::T_DIV, true, false) => Some(true),
5151            (token::T_EQLC | token::T_NEQ, true, true) => Some(false),
5152            _ => None,
5153        }
5154    }
5155
5156    fn plan_has_tsid_column(plan: &LogicalPlan) -> bool {
5157        plan.schema()
5158            .fields()
5159            .iter()
5160            .any(|field| field.name() == DATA_SCHEMA_TSID_COLUMN_NAME)
5161    }
5162
5163    fn is_empty_metric(plan: &LogicalPlan) -> bool {
5164        matches!(plan, LogicalPlan::Extension(Extension { node }) if node.as_any().is::<EmptyMetric>())
5165    }
5166
5167    fn native_histogram_arrow_type() -> ArrowDataType {
5168        native_histogram_value_type().as_arrow_type()
5169    }
5170
5171    fn field_column_type<'a>(
5172        schema: &'a DFSchemaRef,
5173        field_column: &str,
5174    ) -> Option<&'a ArrowDataType> {
5175        schema
5176            .index_of_column_by_name(None, field_column)
5177            .map(|idx| schema.field(idx).data_type())
5178    }
5179
5180    fn field_column_is_native_histogram(schema: &DFSchemaRef, field_column: &str) -> bool {
5181        Self::field_column_type(schema, field_column)
5182            .is_some_and(|data_type| data_type == &Self::native_histogram_arrow_type())
5183    }
5184
5185    fn field_columns_contain_native_histogram(
5186        schema: &DFSchemaRef,
5187        field_columns: &[String],
5188    ) -> bool {
5189        field_columns
5190            .iter()
5191            .any(|field| Self::field_column_is_native_histogram(schema, field))
5192    }
5193
5194    fn field_column_is_float_range(schema: &DFSchemaRef, field_column: &str) -> bool {
5195        Self::field_column_type(schema, field_column).is_some_and(|data_type| {
5196            matches!(
5197                data_type,
5198                ArrowDataType::Dictionary(key_type, value_type)
5199                    if key_type.as_ref() == &ArrowDataType::Int64
5200                        && value_type.as_ref() == &ArrowDataType::Float64
5201            )
5202        })
5203    }
5204
5205    fn field_columns_are_alternative_samples(
5206        schema: &DFSchemaRef,
5207        field_columns: &[String],
5208    ) -> bool {
5209        Self::alternative_sample_columns(schema, field_columns).is_some()
5210    }
5211
5212    fn alternative_sample_columns<'a>(
5213        schema: &DFSchemaRef,
5214        field_columns: &'a [String],
5215    ) -> Option<(&'a str, &'a str)> {
5216        if field_columns.len() != 2 {
5217            return None;
5218        }
5219
5220        let canonical_float = field_columns.iter().find(|field| {
5221            field.as_str() == greptime_value()
5222                && (Self::field_column_type(schema, field) == Some(&ArrowDataType::Float64)
5223                    || Self::field_column_is_float_range(schema, field))
5224        });
5225        let canonical_histogram = field_columns.iter().find(|field| {
5226            field.as_str() == greptime_native_histogram()
5227                && (Self::field_column_is_native_histogram(schema, field)
5228                    || Self::field_column_is_native_histogram_range(schema, field))
5229        });
5230        if let (Some(float), Some(histogram)) = (canonical_float, canonical_histogram) {
5231            return Some((float, histogram));
5232        }
5233
5234        let float = field_columns.iter().find(|field| {
5235            field.starts_with(OR_FLOAT_FIELD_PREFIX)
5236                && (Self::field_column_type(schema, field) == Some(&ArrowDataType::Float64)
5237                    || Self::field_column_is_float_range(schema, field))
5238        })?;
5239        let histogram = field_columns.iter().find(|field| {
5240            field.starts_with(OR_HISTOGRAM_FIELD_PREFIX)
5241                && (Self::field_column_is_native_histogram(schema, field)
5242                    || Self::field_column_is_native_histogram_range(schema, field))
5243        })?;
5244        Some((float, histogram))
5245    }
5246
5247    fn alternative_sample_range_columns<'a>(
5248        schema: &DFSchemaRef,
5249        field_columns: &'a [String],
5250    ) -> Option<(&'a str, &'a str)> {
5251        Self::alternative_sample_columns(schema, field_columns).filter(|(float, histogram)| {
5252            Self::field_column_is_float_range(schema, float)
5253                && Self::field_column_is_native_histogram_range(schema, histogram)
5254        })
5255    }
5256
5257    fn field_column_is_native_histogram_range(schema: &DFSchemaRef, field_column: &str) -> bool {
5258        Self::field_column_type(schema, field_column).is_some_and(|data_type| {
5259            matches!(
5260                data_type,
5261                ArrowDataType::Dictionary(key_type, value_type)
5262                    if key_type.as_ref() == &ArrowDataType::Int64
5263                        && value_type.as_ref() == &Self::native_histogram_arrow_type()
5264            )
5265        })
5266    }
5267
5268    fn all_field_columns_are_native_histograms(&self, schema: &DFSchemaRef) -> bool {
5269        !self.ctx.field_columns.is_empty()
5270            && self
5271                .ctx
5272                .field_columns
5273                .iter()
5274                .all(|field| Self::field_column_is_native_histogram(schema, field))
5275    }
5276
5277    fn all_field_columns_are_native_histogram_ranges(&self, schema: &DFSchemaRef) -> bool {
5278        !self.ctx.field_columns.is_empty()
5279            && self
5280                .ctx
5281                .field_columns
5282                .iter()
5283                .all(|field| Self::field_column_is_native_histogram_range(schema, field))
5284    }
5285
5286    fn optional_tsid_projection(
5287        schema: &DFSchemaRef,
5288        table_ref: Option<&TableReference>,
5289        keep_tsid: bool,
5290    ) -> Option<DfExpr> {
5291        keep_tsid.then_some(()).and_then(|_| {
5292            schema
5293                .qualified_field_with_name(table_ref, DATA_SCHEMA_TSID_COLUMN_NAME)
5294                .ok()
5295                .map(|field| DfExpr::Column(field.into()))
5296        })
5297    }
5298
5299    fn binary_join_key_columns(
5300        &self,
5301        left_schema: &DFSchemaRef,
5302        right_schema: &DFSchemaRef,
5303        left_context: &PromPlannerContext,
5304        right_context: &PromPlannerContext,
5305        only_join_time_index: bool,
5306        modifier: &Option<BinModifier>,
5307    ) -> Result<(BTreeSet<String>, BTreeSet<String>, bool)> {
5308        let has_tsid = |schema: &DFSchemaRef| {
5309            schema
5310                .fields()
5311                .iter()
5312                .any(|field| field.name() == DATA_SCHEMA_TSID_COLUMN_NAME)
5313        };
5314        let use_tsid_join = !only_join_time_index
5315            && self.binary_modifier_preserves_tsid_join_key(left_context, right_context, modifier)
5316            && left_context.use_tsid
5317            && right_context.use_tsid
5318            && has_tsid(left_schema)
5319            && has_tsid(right_schema);
5320
5321        let (mut left_tag_columns, mut right_tag_columns) = if use_tsid_join {
5322            (
5323                BTreeSet::from([DATA_SCHEMA_TSID_COLUMN_NAME.to_string()]),
5324                BTreeSet::from([DATA_SCHEMA_TSID_COLUMN_NAME.to_string()]),
5325            )
5326        } else {
5327            if only_join_time_index {
5328                (BTreeSet::new(), BTreeSet::new())
5329            } else {
5330                (
5331                    left_context
5332                        .tag_columns
5333                        .iter()
5334                        .cloned()
5335                        .collect::<BTreeSet<_>>(),
5336                    right_context
5337                        .tag_columns
5338                        .iter()
5339                        .cloned()
5340                        .collect::<BTreeSet<_>>(),
5341                )
5342            }
5343        };
5344
5345        if !use_tsid_join
5346            && let Some(modifier) = modifier
5347            && let Some(matching) = &modifier.matching
5348        {
5349            match matching {
5350                LabelModifier::Include(on) => {
5351                    let mask = on.labels.iter().cloned().collect::<BTreeSet<_>>();
5352                    left_tag_columns = left_tag_columns.intersection(&mask).cloned().collect();
5353                    right_tag_columns = right_tag_columns.intersection(&mask).cloned().collect();
5354                }
5355                LabelModifier::Exclude(ignoring) => {
5356                    for label in &ignoring.labels {
5357                        let _ = left_tag_columns.remove(label);
5358                        let _ = right_tag_columns.remove(label);
5359                    }
5360                }
5361            }
5362        }
5363
5364        let force_empty_join =
5365            !use_tsid_join && !only_join_time_index && left_tag_columns != right_tag_columns;
5366        if force_empty_join {
5367            let common_tag_columns = left_tag_columns
5368                .intersection(&right_tag_columns)
5369                .cloned()
5370                .collect::<BTreeSet<_>>();
5371            left_tag_columns = common_tag_columns.clone();
5372            right_tag_columns = common_tag_columns;
5373        }
5374
5375        Ok((left_tag_columns, right_tag_columns, force_empty_join))
5376    }
5377
5378    /// Result labels of a vector-vector binary operation, following Prometheus `resultMetric`:
5379    /// `on(...)` keeps only the matching labels, `ignoring(...)` drops them, and a group modifier
5380    /// keeps the "many" side's labels plus the `group_x(...)` labels taken from the "one" side.
5381    ///
5382    /// The flag of each entry tells which operand the label is projected from. `None` means the
5383    /// operation keeps a whole operand tag set, which the default projection already does.
5384    fn binary_result_labels(
5385        left_context: &PromPlannerContext,
5386        right_context: &PromPlannerContext,
5387        modifier: &Option<BinModifier>,
5388    ) -> Option<Vec<(bool, String)>> {
5389        let modifier = modifier.as_ref()?;
5390        let (many_is_left, include) = match &modifier.card {
5391            VectorMatchCardinality::OneToOne => (true, None),
5392            VectorMatchCardinality::ManyToOne(labels) => (true, Some(labels)),
5393            VectorMatchCardinality::OneToMany(labels) => (false, Some(labels)),
5394            // Set operators keep their operands' labels and don't reach this path.
5395            VectorMatchCardinality::ManyToMany => return None,
5396        };
5397
5398        let Some(include) = include else {
5399            let matching = modifier.matching.as_ref()?;
5400            let reduced = |keep: bool, labels: &BTreeSet<&String>| {
5401                left_context
5402                    .tag_columns
5403                    .iter()
5404                    .filter(|tag| labels.contains(tag) == keep)
5405                    .map(|tag| (true, tag.clone()))
5406                    .collect()
5407            };
5408            return Some(match matching {
5409                LabelModifier::Include(on) => reduced(true, &on.labels.iter().collect()),
5410                LabelModifier::Exclude(ignoring) => {
5411                    reduced(false, &ignoring.labels.iter().collect())
5412                }
5413            });
5414        };
5415
5416        let (many_context, one_context) = if many_is_left {
5417            (left_context, right_context)
5418        } else {
5419            (right_context, left_context)
5420        };
5421        let include = include.labels.iter().collect::<BTreeSet<_>>();
5422        // An included label the "one" side doesn't carry is deleted from the result, so it is
5423        // dropped from the "many" side as well.
5424        let mut labels = many_context
5425            .tag_columns
5426            .iter()
5427            .filter(|tag| !include.contains(tag))
5428            .map(|tag| (many_is_left, tag.clone()))
5429            .collect::<Vec<_>>();
5430        labels.extend(
5431            include
5432                .into_iter()
5433                .filter(|label| one_context.tag_columns.contains(label))
5434                .map(|label| (!many_is_left, label.clone())),
5435        );
5436        Some(labels)
5437    }
5438
5439    /// Resolve [`Self::binary_result_labels`] against the join output.
5440    fn binary_result_label_projection(
5441        schema: &DFSchemaRef,
5442        left_table_ref: &TableReference,
5443        right_table_ref: &TableReference,
5444        left_context: &PromPlannerContext,
5445        right_context: &PromPlannerContext,
5446        labels: Vec<(bool, String)>,
5447    ) -> Result<BinaryResultLabels> {
5448        let mut exprs = Vec::with_capacity(labels.len());
5449        let mut names = Vec::with_capacity(labels.len());
5450        let mut aggregation_field_labels = Vec::new();
5451        let mut sources = HashSet::new();
5452        for (from_left, label) in labels {
5453            let (table_ref, context) = if from_left {
5454                (left_table_ref, left_context)
5455            } else {
5456                (right_table_ref, right_context)
5457            };
5458            let field = schema
5459                .qualified_field_with_name(Some(table_ref), &label)
5460                .context(DataFusionPlanningSnafu)?;
5461            exprs.push(DfExpr::Column(field.into()));
5462            if context.aggregation_field_labels.contains(&label) {
5463                aggregation_field_labels.push(label.clone());
5464            }
5465            sources.insert(from_left);
5466            names.push(label);
5467        }
5468
5469        // One operand contributing every one of its tags as the whole result label set keeps the
5470        // one-to-one correspondence between its `__tsid` and a result series: no other operand
5471        // value reaches the labels, and the matching gives each of its rows a single partner.
5472        let source_context = match sources.into_iter().collect::<Vec<_>>().as_slice() {
5473            [true] => Some((left_table_ref, left_context)),
5474            [false] => Some((right_table_ref, right_context)),
5475            _ => None,
5476        };
5477        let tsid = source_context
5478            .filter(|(_, context)| {
5479                context.use_tsid
5480                    && context.tag_columns.len() == names.len()
5481                    && context.tag_columns.iter().all(|tag| names.contains(tag))
5482            })
5483            .and_then(|(table_ref, _)| {
5484                Self::optional_tsid_projection(schema, Some(table_ref), true)
5485            });
5486
5487        Ok(BinaryResultLabels {
5488            exprs,
5489            names,
5490            aggregation_field_labels,
5491            tsid,
5492        })
5493    }
5494
5495    /// Whether the result of a binary operation can hold two series with the same labels, which
5496    /// only [`Self::binary_result_labels`] can introduce: one-to-one matching on a subset of the
5497    /// tags, or a group modifier that overwrites a label of the "many" side.
5498    fn binary_result_labels_may_repeat(
5499        left_context: &PromPlannerContext,
5500        right_context: &PromPlannerContext,
5501        modifier: &Option<BinModifier>,
5502    ) -> bool {
5503        let Some(modifier) = modifier else {
5504            return false;
5505        };
5506        match &modifier.card {
5507            VectorMatchCardinality::OneToOne => match &modifier.matching {
5508                None => false,
5509                Some(LabelModifier::Include(on)) => {
5510                    let on = on.labels.iter().collect::<BTreeSet<_>>();
5511                    !left_context.tag_columns.iter().all(|tag| on.contains(tag))
5512                }
5513                Some(LabelModifier::Exclude(ignoring)) => ignoring
5514                    .labels
5515                    .iter()
5516                    .any(|label| left_context.tag_columns.contains(label)),
5517            },
5518            VectorMatchCardinality::ManyToOne(include) => include
5519                .labels
5520                .iter()
5521                .any(|label| left_context.tag_columns.contains(label)),
5522            VectorMatchCardinality::OneToMany(include) => include
5523                .labels
5524                .iter()
5525                .any(|label| right_context.tag_columns.contains(label)),
5526            VectorMatchCardinality::ManyToMany => false,
5527        }
5528    }
5529
5530    /// Wrap `plan` in a check that fails the query when a match group holds more than one row at
5531    /// a timestamp. `group_exprs` are the label columns of the group, resolved against `plan`.
5532    fn assert_unique_match_group(
5533        plan: LogicalPlan,
5534        group_exprs: Vec<DfExpr>,
5535        group_labels: Vec<String>,
5536        time_index_expr: DfExpr,
5537        violation: MatchGroupViolation,
5538    ) -> Result<LogicalPlan> {
5539        let mut partition_by = group_exprs.clone();
5540        partition_by.push(time_index_expr);
5541        // A label may carry the generated name, and the count column has to stay unambiguous
5542        // against every column the operand already has.
5543        let occupied_column_names = plan
5544            .schema()
5545            .fields()
5546            .iter()
5547            .map(|field| field.name().as_str())
5548            .collect::<HashSet<_>>();
5549        let mut next_suffix = 0;
5550        let count_column = loop {
5551            let name = match next_suffix {
5552                0 => MATCH_GROUP_COUNT_COLUMN.to_string(),
5553                suffix => format!("{MATCH_GROUP_COUNT_COLUMN}_{suffix}"),
5554            };
5555            next_suffix += 1;
5556            if !occupied_column_names.contains(name.as_str()) {
5557                break name;
5558            }
5559        };
5560        let count = DfExpr::WindowFunction(Box::new(WindowFunction {
5561            fun: WindowFunctionDefinition::AggregateUDF(count_udaf()),
5562            params: WindowFunctionParams {
5563                args: vec![lit(1i64)],
5564                partition_by,
5565                order_by: vec![],
5566                window_frame: WindowFrame::new(None),
5567                null_treatment: None,
5568                distinct: false,
5569                filter: None,
5570            },
5571        }))
5572        .alias(count_column.as_str());
5573
5574        let output_exprs = plan
5575            .schema()
5576            .iter()
5577            .map(|(qualifier, field)| DfExpr::Column(Column::new(qualifier.cloned(), field.name())))
5578            .collect::<Vec<_>>();
5579        let assert_expr = DfExpr::ScalarFunction(ScalarFunction {
5580            func: Arc::new(UniqueMatchGroup::scalar_udf(group_labels, violation)),
5581            args: std::iter::once(ident(count_column.as_str()))
5582                .chain(group_exprs)
5583                .collect(),
5584        });
5585
5586        LogicalPlanBuilder::from(plan)
5587            .window(vec![count])
5588            .context(DataFusionPlanningSnafu)?
5589            .filter(assert_expr)
5590            .context(DataFusionPlanningSnafu)?
5591            .project(output_exprs)
5592            .context(DataFusionPlanningSnafu)?
5593            .build()
5594            .context(DataFusionPlanningSnafu)
5595    }
5596
5597    fn binary_modifier_preserves_tsid_join_key(
5598        &self,
5599        left_context: &PromPlannerContext,
5600        right_context: &PromPlannerContext,
5601        modifier: &Option<BinModifier>,
5602    ) -> bool {
5603        let Some(modifier) = modifier else {
5604            return true;
5605        };
5606
5607        if !matches!(modifier.card, VectorMatchCardinality::OneToOne) {
5608            return false;
5609        }
5610
5611        match &modifier.matching {
5612            None => true,
5613            Some(LabelModifier::Exclude(ignoring)) => ignoring.labels.iter().all(|label| {
5614                !left_context.tag_columns.contains(label)
5615                    && !right_context.tag_columns.contains(label)
5616            }),
5617            Some(LabelModifier::Include(on)) => {
5618                let on_labels = on.labels.iter().cloned().collect::<BTreeSet<_>>();
5619                let left_labels = left_context
5620                    .tag_columns
5621                    .iter()
5622                    .cloned()
5623                    .collect::<BTreeSet<_>>();
5624                let right_labels = right_context
5625                    .tag_columns
5626                    .iter()
5627                    .cloned()
5628                    .collect::<BTreeSet<_>>();
5629
5630                on_labels == left_labels && on_labels == right_labels
5631            }
5632        }
5633    }
5634
5635    /// Build a inner join on time index column and tag columns to concat two logical plans.
5636    /// When `only_join_time_index == true` we only join on the time index, because these two plan may not have the same tag columns
5637    #[allow(clippy::too_many_arguments)]
5638    fn join_on_non_field_columns(
5639        &self,
5640        left: LogicalPlan,
5641        right: LogicalPlan,
5642        left_table_ref: TableReference,
5643        right_table_ref: TableReference,
5644        left_time_index_column: Option<String>,
5645        right_time_index_column: Option<String>,
5646        only_join_time_index: bool,
5647        modifier: &Option<BinModifier>,
5648        left_context: &PromPlannerContext,
5649        right_context: &PromPlannerContext,
5650    ) -> Result<LogicalPlan> {
5651        let (mut left_tag_columns, mut right_tag_columns, mut force_empty_join) = self
5652            .binary_join_key_columns(
5653                left.schema(),
5654                right.schema(),
5655                left_context,
5656                right_context,
5657                only_join_time_index,
5658                modifier,
5659            )?;
5660        let use_tsid_join = !only_join_time_index
5661            && !force_empty_join
5662            && left_tag_columns == BTreeSet::from([DATA_SCHEMA_TSID_COLUMN_NAME.to_string()])
5663            && right_tag_columns == BTreeSet::from([DATA_SCHEMA_TSID_COLUMN_NAME.to_string()]);
5664        let (left, right, left_matched_tags, right_matched_tags) = if !only_join_time_index
5665            && !use_tsid_join
5666            && Self::only_temporality_match_label_mismatches(left_context, right_context, modifier)
5667        {
5668            let mut aligned_left_context = left_context.clone();
5669            let mut aligned_right_context = right_context.clone();
5670            let (left, right, _) = Self::align_temporality_match_column(
5671                left,
5672                right,
5673                &mut aligned_left_context,
5674                &mut aligned_right_context,
5675            )?;
5676            (left_tag_columns, right_tag_columns, force_empty_join) = self
5677                .binary_join_key_columns(
5678                    left.schema(),
5679                    right.schema(),
5680                    &aligned_left_context,
5681                    &aligned_right_context,
5682                    false,
5683                    modifier,
5684                )?;
5685            (
5686                left,
5687                right,
5688                aligned_left_context.tag_columns,
5689                aligned_right_context.tag_columns,
5690            )
5691        } else {
5692            (
5693                left,
5694                right,
5695                left_context.tag_columns.clone(),
5696                right_context.tag_columns.clone(),
5697            )
5698        };
5699
5700        // A join key that covers the whole tag set of a side already makes that side's match
5701        // groups unique, and so does a join on `__tsid`. Only the reduced keys need the check.
5702        let checked_matching = !only_join_time_index
5703            && !force_empty_join
5704            && !use_tsid_join
5705            && !matches!(
5706                modifier.as_ref().map(|modifier| &modifier.card),
5707                Some(VectorMatchCardinality::ManyToMany)
5708            );
5709        // The right operand is the "one" side of the matching, unless `group_right` swaps them.
5710        let one_side_is_left = matches!(
5711            modifier.as_ref().map(|modifier| &modifier.card),
5712            Some(VectorMatchCardinality::OneToMany(_))
5713        );
5714        let (left, right) = if !checked_matching {
5715            (left, right)
5716        } else if one_side_is_left {
5717            (
5718                Self::assert_unique_one_side(
5719                    left,
5720                    &left_tag_columns,
5721                    &left_matched_tags,
5722                    left_time_index_column.as_deref(),
5723                    true,
5724                )?,
5725                right,
5726            )
5727        } else {
5728            (
5729                left,
5730                Self::assert_unique_one_side(
5731                    right,
5732                    &right_tag_columns,
5733                    &right_matched_tags,
5734                    right_time_index_column.as_deref(),
5735                    false,
5736                )?,
5737            )
5738        };
5739
5740        // push time index column if it exists
5741        if let (Some(left_time_index_column), Some(right_time_index_column)) = (
5742            left_time_index_column.clone(),
5743            right_time_index_column.clone(),
5744        ) {
5745            left_tag_columns.insert(left_time_index_column);
5746            right_tag_columns.insert(right_time_index_column);
5747        }
5748
5749        let right = LogicalPlanBuilder::from(right)
5750            .alias(right_table_ref.clone())
5751            .context(DataFusionPlanningSnafu)?
5752            .build()
5753            .context(DataFusionPlanningSnafu)?;
5754
5755        // Inner Join on time index column to concat two operator
5756        let join_plan = LogicalPlanBuilder::from(left)
5757            .alias(left_table_ref.clone())
5758            .context(DataFusionPlanningSnafu)?
5759            .join_detailed(
5760                right,
5761                JoinType::Inner,
5762                (
5763                    left_tag_columns
5764                        .into_iter()
5765                        .map(Column::from_name)
5766                        .collect::<Vec<_>>(),
5767                    right_tag_columns
5768                        .into_iter()
5769                        .map(Column::from_name)
5770                        .collect::<Vec<_>>(),
5771                ),
5772                force_empty_join.then_some(lit(false)),
5773                NullEquality::NullEqualsNull,
5774            )
5775            .context(DataFusionPlanningSnafu)?
5776            .build()
5777            .context(DataFusionPlanningSnafu)?;
5778
5779        // The result labels no longer identify a series on their own: the "many" side can hold
5780        // several series per result label set, which PromQL cannot represent.
5781        let labels = checked_matching
5782            .then(|| Self::binary_result_labels(left_context, right_context, modifier))
5783            .flatten()
5784            .filter(|_| {
5785                Self::binary_result_labels_may_repeat(left_context, right_context, modifier)
5786            });
5787        let Some(labels) = labels else {
5788            return Ok(join_plan);
5789        };
5790        let schema = join_plan.schema().clone();
5791        let group_exprs = labels
5792            .iter()
5793            .map(|(from_left, label)| {
5794                let table_ref = if *from_left {
5795                    &left_table_ref
5796                } else {
5797                    &right_table_ref
5798                };
5799                schema
5800                    .qualified_field_with_name(Some(table_ref), label)
5801                    .map(|field| DfExpr::Column(field.into()))
5802                    .context(DataFusionPlanningSnafu)
5803            })
5804            .collect::<Result<Vec<_>>>()?;
5805        let time_index_expr = left_time_index_column
5806            .as_deref()
5807            .map(|column| {
5808                schema
5809                    .qualified_field_with_name(Some(&left_table_ref), column)
5810                    .map(|field| DfExpr::Column(field.into()))
5811                    .context(DataFusionPlanningSnafu)
5812            })
5813            .transpose()?
5814            .with_context(|| UnexpectedPlanExprSnafu {
5815                desc: "vector matching on a plan without a time index",
5816            })?;
5817        let violation = if matches!(
5818            modifier.as_ref().map(|modifier| &modifier.card),
5819            Some(VectorMatchCardinality::OneToOne)
5820        ) {
5821            MatchGroupViolation::ImplicitManyToOne
5822        } else {
5823            MatchGroupViolation::AmbiguousGroupLabels
5824        };
5825        Self::assert_unique_match_group(
5826            join_plan,
5827            group_exprs,
5828            labels.into_iter().map(|(_, label)| label).collect(),
5829            time_index_expr,
5830            violation,
5831        )
5832    }
5833
5834    /// Guard the side of a vector matching that must hold one series per match group.
5835    fn assert_unique_one_side(
5836        plan: LogicalPlan,
5837        join_keys: &BTreeSet<String>,
5838        tag_columns: &[String],
5839        time_index_column: Option<&str>,
5840        one_side_is_left: bool,
5841    ) -> Result<LogicalPlan> {
5842        let Some(time_index_column) = time_index_column else {
5843            return Ok(plan);
5844        };
5845        if tag_columns.iter().all(|tag| join_keys.contains(tag)) {
5846            return Ok(plan);
5847        }
5848
5849        let group_labels = join_keys.iter().cloned().collect::<Vec<_>>();
5850        let group_exprs = group_labels
5851            .iter()
5852            .map(|label| DfExpr::Column(Column::from_name(label)))
5853            .collect();
5854        Self::assert_unique_match_group(
5855            plan,
5856            group_exprs,
5857            group_labels,
5858            DfExpr::Column(Column::from_name(time_index_column)),
5859            MatchGroupViolation::DuplicateOnOneSide { one_side_is_left },
5860        )
5861    }
5862
5863    fn selected_binary_match_labels(
5864        left_context: &PromPlannerContext,
5865        right_context: &PromPlannerContext,
5866        modifier: &Option<BinModifier>,
5867    ) -> BTreeSet<String> {
5868        let mut labels = left_context
5869            .tag_columns
5870            .iter()
5871            .chain(&right_context.tag_columns)
5872            .cloned()
5873            .collect::<BTreeSet<_>>();
5874        if let Some(matching) = modifier
5875            .as_ref()
5876            .and_then(|modifier| modifier.matching.as_ref())
5877        {
5878            match matching {
5879                LabelModifier::Include(on) => {
5880                    labels = on
5881                        .labels
5882                        .iter()
5883                        .filter(|label| {
5884                            left_context.tag_columns.contains(label)
5885                                || right_context.tag_columns.contains(label)
5886                        })
5887                        .cloned()
5888                        .collect();
5889                }
5890                LabelModifier::Exclude(ignoring) => {
5891                    for label in &ignoring.labels {
5892                        labels.remove(label);
5893                    }
5894                }
5895            }
5896        }
5897        labels
5898    }
5899
5900    fn only_temporality_match_label_mismatches(
5901        left_context: &PromPlannerContext,
5902        right_context: &PromPlannerContext,
5903        modifier: &Option<BinModifier>,
5904    ) -> bool {
5905        let mut mismatches =
5906            Self::selected_binary_match_labels(left_context, right_context, modifier)
5907                .into_iter()
5908                .filter(|label| {
5909                    left_context.tag_columns.contains(label)
5910                        != right_context.tag_columns.contains(label)
5911                });
5912        matches!(
5913            (mismatches.next(), mismatches.next()),
5914            (Some(label), None) if label == OTLP_AGGREGATION_TEMPORALITY_LABEL
5915        )
5916    }
5917
5918    fn align_temporality_match_column(
5919        mut left: LogicalPlan,
5920        mut right: LogicalPlan,
5921        left_context: &mut PromPlannerContext,
5922        right_context: &mut PromPlannerContext,
5923    ) -> Result<(LogicalPlan, LogicalPlan, bool)> {
5924        let marker = OTLP_AGGREGATION_TEMPORALITY_LABEL;
5925        let left_has_marker = left_context.tag_columns.iter().any(|tag| tag == marker);
5926        let (data_type, value_type, add_to_left) = {
5927            let (present, add_to_left) = if left_has_marker {
5928                (&left, false)
5929            } else {
5930                (&right, true)
5931            };
5932            let data_type = present
5933                .schema()
5934                .fields()
5935                .iter()
5936                .find(|field| field.name() == marker)
5937                .map(|field| field.data_type().clone())
5938                .with_context(|| ColumnNotFoundSnafu {
5939                    col: marker.to_string(),
5940                })?;
5941            let value_type = Self::string_value_data_type(&data_type)
5942                .cloned()
5943                .with_context(|| UnexpectedPlanExprSnafu {
5944                    desc: format!("temporality match label {marker} must be a string"),
5945                })?;
5946            (data_type, value_type, add_to_left)
5947        };
5948        let null = Self::string_scalar_value(&value_type, None).with_context(|| {
5949            UnexpectedPlanExprSnafu {
5950                desc: format!("temporality match label {marker} must be a string"),
5951            }
5952        })?;
5953        let add_marker = |plan: LogicalPlan| {
5954            let visible = plan
5955                .schema()
5956                .iter()
5957                .map(|(qualifier, field)| {
5958                    DfExpr::Column(Column::new(qualifier.cloned(), field.name().clone()))
5959                })
5960                .collect::<Vec<_>>();
5961            LogicalPlanBuilder::from(plan)
5962                .project(
5963                    visible
5964                        .into_iter()
5965                        .chain([DfExpr::Literal(null, None).alias(marker)]),
5966                )
5967                .context(DataFusionPlanningSnafu)?
5968                .build()
5969                .context(DataFusionPlanningSnafu)
5970        };
5971        if data_type != value_type {
5972            let present = if add_to_left { &mut right } else { &mut left };
5973            let visible = present
5974                .schema()
5975                .iter()
5976                .map(|(qualifier, field)| {
5977                    let column =
5978                        DfExpr::Column(Column::new(qualifier.cloned(), field.name().clone()));
5979                    if field.name() == marker {
5980                        DfExpr::Cast(Cast::new(Box::new(column), value_type.clone()))
5981                            .alias_qualified(qualifier.cloned(), field.name().clone())
5982                    } else {
5983                        column
5984                    }
5985                })
5986                .collect::<Vec<_>>();
5987            *present = LogicalPlanBuilder::from(present.clone())
5988                .project(visible)
5989                .context(DataFusionPlanningSnafu)?
5990                .build()
5991                .context(DataFusionPlanningSnafu)?;
5992        }
5993        if add_to_left {
5994            left = add_marker(left)?;
5995            left_context.tag_columns.push(marker.to_string());
5996        } else {
5997            right = add_marker(right)?;
5998            right_context.tag_columns.push(marker.to_string());
5999        }
6000        Ok((left, right, add_to_left))
6001    }
6002
6003    fn normalized_match_key_expr(
6004        label: &str,
6005        field: Option<(Option<TableReference>, ArrowDataType)>,
6006        value_type: &ArrowDataType,
6007        internal_name: &str,
6008    ) -> DfExpr {
6009        let empty = Self::string_scalar_value(value_type, Some(String::new()))
6010            .expect("match label value type is a string");
6011        let expr = if let Some((qualifier, data_type)) = field {
6012            let column = DfExpr::Column(Column::new(qualifier, label));
6013            let column = if &data_type == value_type {
6014                column
6015            } else {
6016                DfExpr::Cast(Cast::new(Box::new(column), value_type.clone()))
6017            };
6018            DfExpr::ScalarFunction(ScalarFunction {
6019                func: coalesce(),
6020                args: vec![column, DfExpr::Literal(empty, None)],
6021            })
6022        } else {
6023            DfExpr::Literal(empty, None)
6024        };
6025        expr.alias(internal_name)
6026    }
6027
6028    fn is_zero_row_empty_relation(plan: &LogicalPlan) -> bool {
6029        // `produce_one_row` is used for input-free plans that still emit one row;
6030        // only the false case is a statically proven empty vector.
6031        matches!(plan, LogicalPlan::EmptyRelation(relation) if !relation.produce_one_row)
6032    }
6033
6034    fn string_value_data_type(data_type: &ArrowDataType) -> Option<&ArrowDataType> {
6035        match data_type {
6036            data_type if data_type.is_string() => Some(data_type),
6037            ArrowDataType::Dictionary(_, value_type) if value_type.is_string() => Some(value_type),
6038            _ => None,
6039        }
6040    }
6041
6042    fn string_scalar_value(
6043        data_type: &ArrowDataType,
6044        value: Option<String>,
6045    ) -> Option<ScalarValue> {
6046        match data_type {
6047            ArrowDataType::Utf8 => Some(ScalarValue::Utf8(value)),
6048            ArrowDataType::LargeUtf8 => Some(ScalarValue::LargeUtf8(value)),
6049            ArrowDataType::Utf8View => Some(ScalarValue::Utf8View(value)),
6050            ArrowDataType::Dictionary(key_type, value_type) => Some(ScalarValue::Dictionary(
6051                key_type.clone(),
6052                Box::new(Self::string_scalar_value(value_type, value)?),
6053            )),
6054            _ => None,
6055        }
6056    }
6057
6058    fn common_label_data_type(
6059        left: Option<&ArrowDataType>,
6060        right: Option<&ArrowDataType>,
6061    ) -> Option<ArrowDataType> {
6062        match (left, right) {
6063            (Some(left), Some(right)) if left == right => {
6064                Self::string_value_data_type(left).map(|_| left.clone())
6065            }
6066            (Some(left), Some(right)) => {
6067                let left_value_type = Self::string_value_data_type(left)?;
6068                let right_value_type = Self::string_value_data_type(right)?;
6069                // DataFusion projections can decode dictionaries, but do not encode plain strings
6070                // as dictionaries. Preserve the encoding only when both inputs already share it.
6071                match (left_value_type, right_value_type) {
6072                    (left, right) if left == right => Some(left.clone()),
6073                    (ArrowDataType::LargeUtf8, _) | (_, ArrowDataType::LargeUtf8) => {
6074                        Some(ArrowDataType::LargeUtf8)
6075                    }
6076                    (ArrowDataType::Utf8View, ArrowDataType::Utf8View) => {
6077                        Some(ArrowDataType::Utf8View)
6078                    }
6079                    _ => Some(ArrowDataType::Utf8),
6080                }
6081            }
6082            (Some(data_type), None) | (None, Some(data_type)) => {
6083                Self::string_value_data_type(data_type).cloned()
6084            }
6085            (None, None) => Some(ArrowDataType::Utf8),
6086        }
6087    }
6088
6089    /// Build a projection that project and perform operation expr for every value columns.
6090    /// Non-value columns (tag and timestamp) will be preserved in the projection.
6091    ///
6092    /// # Side effect
6093    ///
6094    /// This function will update the value columns in the context. Those new column names
6095    /// don't contains qualifier.
6096    fn projection_for_each_field_column<F>(
6097        &mut self,
6098        input: LogicalPlan,
6099        name_to_expr: F,
6100    ) -> Result<LogicalPlan>
6101    where
6102        F: FnMut(&String) -> Result<DfExpr>,
6103    {
6104        self.projection_for_each_field_column_with_labels(input, None, name_to_expr)
6105    }
6106
6107    /// Like [`Self::projection_for_each_field_column`], but projects `result_labels` instead of
6108    /// the context tag columns when a binary operation derived its own result label set.
6109    fn projection_for_each_field_column_with_labels<F>(
6110        &mut self,
6111        input: LogicalPlan,
6112        result_labels: Option<&BinaryResultLabels>,
6113        name_to_expr: F,
6114    ) -> Result<LogicalPlan>
6115    where
6116        F: FnMut(&String) -> Result<DfExpr>,
6117    {
6118        // Keep the generated float/histogram lane names while an element-wise operation
6119        // preserves both sample types, so downstream operators still recognize the pair.
6120        let preserve_field_names =
6121            Self::field_columns_are_alternative_samples(input.schema(), &self.ctx.field_columns);
6122        let table_ref = self.ctx.table_name.clone().map(TableReference::bare);
6123        // Derived labels can be unqualified even when the context still names the source table.
6124        let input_schema = input.schema().clone();
6125        let lookup = |col: &String| {
6126            input_schema
6127                .qualified_field_with_name(table_ref.as_ref(), col)
6128                .or_else(|_| input_schema.qualified_field_with_unqualified_name(col))
6129                .map(|field| DfExpr::Column(field.into()))
6130                .context(DataFusionPlanningSnafu)
6131        };
6132        let tag_columns_iter = match result_labels {
6133            Some(labels) => labels.exprs.iter().cloned().map(Ok).collect::<Vec<_>>(),
6134            None => self.ctx.tag_columns.iter().map(lookup).collect::<Vec<_>>(),
6135        };
6136        let non_field_columns_iter = tag_columns_iter
6137            .into_iter()
6138            .chain(self.ctx.time_index_column.iter().map(lookup));
6139        let tsid_iter = match result_labels {
6140            Some(labels) => labels.tsid_projection(table_ref.clone()),
6141            None => Self::optional_tsid_projection(
6142                input.schema(),
6143                table_ref.as_ref(),
6144                self.ctx.use_tsid,
6145            ),
6146        }
6147        .into_iter()
6148        .map(Ok);
6149
6150        // build computation exprs
6151        let result_field_columns = self
6152            .ctx
6153            .field_columns
6154            .iter()
6155            .map(name_to_expr)
6156            .collect::<Result<Vec<_>>>()?;
6157
6158        // alias the computation exprs to remove qualifier
6159        if !preserve_field_names {
6160            self.ctx.field_columns = result_field_columns
6161                .iter()
6162                .map(|expr| expr.schema_name().to_string())
6163                .collect();
6164        }
6165        let field_columns_iter = result_field_columns
6166            .into_iter()
6167            .zip(self.ctx.field_columns.iter())
6168            .map(|(expr, name)| Ok(DfExpr::Alias(Alias::new(expr, None::<String>, name))));
6169
6170        // chain non-field columns (unchanged) and field columns (applied computation then alias)
6171        let project_fields = non_field_columns_iter
6172            .chain(tsid_iter)
6173            .chain(field_columns_iter)
6174            .collect::<Result<Vec<_>>>()?;
6175
6176        LogicalPlanBuilder::from(input)
6177            .project(project_fields)
6178            .context(DataFusionPlanningSnafu)?
6179            .build()
6180            .context(DataFusionPlanningSnafu)
6181    }
6182
6183    /// Build a filter plan on one value column or a float/histogram alternative pair.
6184    fn filter_on_field_column<F>(&self, input: LogicalPlan, name_to_expr: F) -> Result<LogicalPlan>
6185    where
6186        F: FnMut(&String) -> Result<DfExpr>,
6187    {
6188        ensure!(
6189            self.ctx.field_columns.len() == 1
6190                || Self::field_columns_are_alternative_samples(
6191                    input.schema(),
6192                    &self.ctx.field_columns,
6193                ),
6194            UnsupportedExprSnafu {
6195                name: "filter on multi-value input"
6196            }
6197        );
6198
6199        let field_column_filters = self
6200            .ctx
6201            .field_columns
6202            .iter()
6203            .map(name_to_expr)
6204            .collect::<Result<Vec<_>>>()?;
6205        let field_column_filter =
6206            disjunction(field_column_filters).context(UnsupportedExprSnafu {
6207                name: "filter on empty input",
6208            })?;
6209
6210        LogicalPlanBuilder::from(input)
6211            .filter(field_column_filter)
6212            .context(DataFusionPlanningSnafu)?
6213            .build()
6214            .context(DataFusionPlanningSnafu)
6215    }
6216
6217    /// Generate an expr like `date_part("hour", <TIME_INDEX>)`. Caller should ensure the
6218    /// time index column in context is set
6219    fn date_part_on_time_index(&self, date_part: &str) -> Result<DfExpr> {
6220        let input_expr = datafusion::logical_expr::ident(
6221            self.ctx
6222                .time_index_column
6223                .as_ref()
6224                // table name doesn't matters here
6225                .with_context(|| TimeIndexNotFoundSnafu {
6226                    table: "<doesn't matter>",
6227                })?,
6228        );
6229        let fn_expr = DfExpr::ScalarFunction(ScalarFunction {
6230            func: datafusion_functions::datetime::date_part(),
6231            args: vec![date_part.lit(), input_expr],
6232        });
6233        Ok(fn_expr)
6234    }
6235
6236    fn strip_tsid_column(&self, plan: LogicalPlan) -> Result<LogicalPlan> {
6237        let schema = plan.schema();
6238        if !schema
6239            .fields()
6240            .iter()
6241            .any(|field| field.name() == DATA_SCHEMA_TSID_COLUMN_NAME)
6242        {
6243            return Ok(plan);
6244        }
6245
6246        // Preserve column qualifiers so downstream plan nodes can keep referencing
6247        // the columns by their original qualified names.
6248        let project_exprs = schema
6249            .iter()
6250            .filter(|(_, field)| field.name() != DATA_SCHEMA_TSID_COLUMN_NAME)
6251            .map(|(qualifier, field)| {
6252                DfExpr::Column(Column::new(qualifier.cloned(), field.name().clone()))
6253            })
6254            .collect::<Vec<_>>();
6255
6256        LogicalPlanBuilder::from(plan)
6257            .project(project_exprs)
6258            .context(DataFusionPlanningSnafu)?
6259            .build()
6260            .context(DataFusionPlanningSnafu)
6261    }
6262
6263    /// Apply an alias to the query result by adding a projection with the alias name
6264    fn apply_alias(&mut self, plan: LogicalPlan, alias_name: String) -> Result<LogicalPlan> {
6265        let fields_expr = self.create_field_column_exprs()?;
6266
6267        // TODO(dennis): how to support multi-value aliasing?
6268        ensure!(
6269            fields_expr.len() == 1,
6270            UnsupportedExprSnafu {
6271                name: "alias on multi-value result"
6272            }
6273        );
6274
6275        let project_fields = fields_expr
6276            .into_iter()
6277            .map(|expr| expr.alias(&alias_name))
6278            .chain(self.create_tag_column_exprs()?)
6279            .chain(Some(self.create_time_index_column_expr()?));
6280
6281        LogicalPlanBuilder::from(plan)
6282            .project(project_fields)
6283            .context(DataFusionPlanningSnafu)?
6284            .build()
6285            .context(DataFusionPlanningSnafu)
6286    }
6287}
6288
6289#[derive(Default, Debug)]
6290struct FunctionArgs {
6291    input: Option<PromExpr>,
6292    literals: Vec<DfExpr>,
6293}
6294
6295/// Represents different types of scalar functions supported in PromQL expressions.
6296/// Each variant defines how the function should be processed and what arguments it expects.
6297#[derive(Debug, Clone)]
6298enum ScalarFunc {
6299    /// DataFusion's registered(including built-in) scalar functions (e.g., abs, sqrt, round, clamp).
6300    /// These are passed through directly to DataFusion's execution engine.
6301    /// Processing: Simple argument insertion at the specified position.
6302    DataFusionBuiltin(Arc<ScalarUdfDef>),
6303    /// User-defined functions registered in DataFusion's function registry.
6304    /// Similar to DataFusionBuiltin but for custom functions not built into DataFusion.
6305    /// Processing: Direct pass-through with argument positioning.
6306    DataFusionUdf(Arc<ScalarUdfDef>),
6307    /// Native histogram helper UDFs. Non-histogram inputs are projected as NULL
6308    /// so the normal PromQL empty-value filter drops them.
6309    NativeHistogramUdf(Arc<ScalarUdfDef>),
6310    /// PromQL-specific functions that operate on time series data with temporal context.
6311    /// These functions require both timestamp ranges and values to perform calculations.
6312    /// Processing: Automatically injects timestamp_range and value columns as first arguments.
6313    /// Examples: idelta, irate, resets, changes, deriv, *_over_time function
6314    Udf(Arc<ScalarUdfDef>),
6315    /// PromQL functions requiring extrapolation calculations with explicit range information.
6316    /// These functions need to know the time range length to perform rate calculations.
6317    /// The second field contains the range length in milliseconds.
6318    /// Processing: Injects timestamp_range, value, time_index columns and appends range_length.
6319    /// Examples: increase, rate, delta
6320    // TODO(ruihang): maybe merge with Udf later
6321    ExtrapolateUdf(Arc<ScalarUdfDef>, i64),
6322    /// Functions that generate expressions directly without external UDF calls.
6323    /// The expression is constructed during function matching and requires no additional processing.
6324    /// Examples: time(), minute(), hour(), month(), year() and other date/time extractors
6325    GeneratedExpr,
6326}
6327
6328#[cfg(test)]
6329mod test;