HornScalarThriftBuilder.java

// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

package org.apache.doris.horn.doris2horn;


import org.apache.doris.catalog.TableIf;
import org.apache.doris.catalog.Type;
import org.apache.doris.horn.horn2doris.DorisFunctionBuilder;
import org.apache.doris.nereids.trees.expressions.Add;
import org.apache.doris.nereids.trees.expressions.Alias;
import org.apache.doris.nereids.trees.expressions.And;
import org.apache.doris.nereids.trees.expressions.BinaryArithmetic;
import org.apache.doris.nereids.trees.expressions.BitAnd;
import org.apache.doris.nereids.trees.expressions.BitOr;
import org.apache.doris.nereids.trees.expressions.BitXor;
import org.apache.doris.nereids.trees.expressions.Cast;
import org.apache.doris.nereids.trees.expressions.ComparisonPredicate;
import org.apache.doris.nereids.trees.expressions.CompoundPredicate;
import org.apache.doris.nereids.trees.expressions.Divide;
import org.apache.doris.nereids.trees.expressions.EqualTo;
import org.apache.doris.nereids.trees.expressions.Expression;
import org.apache.doris.nereids.trees.expressions.GreaterThan;
import org.apache.doris.nereids.trees.expressions.GreaterThanEqual;
import org.apache.doris.nereids.trees.expressions.InPredicate;
import org.apache.doris.nereids.trees.expressions.IntegralDivide;
import org.apache.doris.nereids.trees.expressions.IsNull;
import org.apache.doris.nereids.trees.expressions.LessThan;
import org.apache.doris.nereids.trees.expressions.LessThanEqual;
import org.apache.doris.nereids.trees.expressions.Mod;
import org.apache.doris.nereids.trees.expressions.Multiply;
import org.apache.doris.nereids.trees.expressions.Not;
import org.apache.doris.nereids.trees.expressions.Or;
import org.apache.doris.nereids.trees.expressions.SlotReference;
import org.apache.doris.nereids.trees.expressions.Subtract;
import org.apache.doris.nereids.trees.expressions.TimestampArithmetic;
import org.apache.doris.nereids.trees.expressions.WindowFrame;
import org.apache.doris.nereids.trees.expressions.WindowFrame.FrameBoundType;
import org.apache.doris.nereids.trees.expressions.WindowFrame.FrameBoundary;
import org.apache.doris.nereids.trees.expressions.WindowFrame.FrameUnitsType;
import org.apache.doris.nereids.trees.expressions.functions.agg.AggregateFunction;
import org.apache.doris.nereids.trees.expressions.functions.scalar.GroupingScalarFunction;
import org.apache.doris.nereids.trees.expressions.functions.scalar.ScalarFunction;
import org.apache.doris.nereids.trees.expressions.functions.window.WindowFunction;
import org.apache.doris.nereids.trees.expressions.literal.BigIntLiteral;
import org.apache.doris.nereids.trees.expressions.literal.BooleanLiteral;
import org.apache.doris.nereids.trees.expressions.literal.DateLiteral;
import org.apache.doris.nereids.trees.expressions.literal.DateV2Literal;
import org.apache.doris.nereids.trees.expressions.literal.DecimalLiteral;
import org.apache.doris.nereids.trees.expressions.literal.DecimalV3Literal;
import org.apache.doris.nereids.trees.expressions.literal.DoubleLiteral;
import org.apache.doris.nereids.trees.expressions.literal.FloatLiteral;
import org.apache.doris.nereids.trees.expressions.literal.IntegerLikeLiteral;
import org.apache.doris.nereids.trees.expressions.literal.IntegerLiteral;
import org.apache.doris.nereids.trees.expressions.literal.Literal;
import org.apache.doris.nereids.trees.expressions.literal.NullLiteral;
import org.apache.doris.nereids.trees.expressions.literal.SmallIntLiteral;
import org.apache.doris.nereids.trees.expressions.literal.StringLikeLiteral;
import org.apache.doris.nereids.trees.expressions.literal.TinyIntLiteral;
import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor;

import org.apache.horn4j.thrift.TArithmeticOperatorType;
import org.apache.horn4j.thrift.TBinaryOperatorType;
import org.apache.horn4j.thrift.TBoolLiteral;
import org.apache.horn4j.thrift.TColumnType;
import org.apache.horn4j.thrift.TDateLiteral;
import org.apache.horn4j.thrift.TDecimalLiteral;
import org.apache.horn4j.thrift.TFlattenedScalar;
import org.apache.horn4j.thrift.TFloatLiteral;
import org.apache.horn4j.thrift.TIntLiteral;
import org.apache.horn4j.thrift.TLiteralType;
import org.apache.horn4j.thrift.TLiteralValue;
import org.apache.horn4j.thrift.TOperator;
import org.apache.horn4j.thrift.TOperatorType;
import org.apache.horn4j.thrift.TOperatorUnion;
import org.apache.horn4j.thrift.TScalar;
import org.apache.horn4j.thrift.TScalarAggFn;
import org.apache.horn4j.thrift.TScalarAnalyticWindowBoundary;
import org.apache.horn4j.thrift.TScalarArithmetic;
import org.apache.horn4j.thrift.TScalarBinaryPredicate;
import org.apache.horn4j.thrift.TScalarColumnRef;
import org.apache.horn4j.thrift.TScalarFnCall;
import org.apache.horn4j.thrift.TScalarLiteral;
import org.apache.horn4j.thrift.TScalarPredicate;
import org.apache.horn4j.thrift.TScalarUnion;
import org.apache.horn4j.thrift.TScalarUniqueId;
import org.apache.horn4j.thrift.TScalarWindowFrame;
import org.apache.horn4j.thrift.TStringLiteral;
import org.apache.horn4j.thrift.TWindowBoundaryType;
import org.apache.horn4j.thrift.TWindowType;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * ExpressionVisitor: Doris Nereids Expression → Horn TOperator (TFlattenedScalar)。
 * 不支持的表达式 throw {@link UnsupportedOperationException},caller fallback 回 Nereids。
 */
public class HornScalarThriftBuilder extends ExpressionVisitor<TOperator, Void> {

    /** Serialize an Expression as a TOperator (fscalar slot, pre-order flattened). */
    public TOperator translate(Expression expr) {
        TOperator top = new TOperator();
        TOperatorUnion opUnion = new TOperatorUnion();
        TFlattenedScalar fscalar = new TFlattenedScalar();
        List<TOperator> scalars = new ArrayList<>();
        flattenPreOrder(expr, scalars);
        fscalar.setScalars(scalars);
        opUnion.setFscalar(fscalar);
        top.setOp_union(opUnion);
        top.setOp_type(TOperatorType.kInvalid);
        return top;
    }

    public List<TOperator> translateList(List<? extends Expression> exprs) {
        List<TOperator> result = new ArrayList<>();
        for (Expression expr : exprs) {
            result.add(translate(expr));
        }
        return result;
    }

    /** Pre-order traversal: visit, then recurse children. */
    private void flattenPreOrder(Expression expr, List<TOperator> out) {
        // Alias is transparent: emit only its inner expression's subtree. The Alias's
        // exprId/name lives on the output SlotReference carried via TExpression.output_list.
        if (expr instanceof Alias) {
            flattenPreOrder(expr.child(0), out);
            return;
        }
        out.add(expr.accept(this, null));
        for (Expression child : expr.children()) {
            flattenPreOrder(child, out);
        }
    }

    /**
     * 把已算好的 (data_type, arity, op_type, scalar_union) 打包成 TOperator(纯打包,
     * data_type / arity 由 caller 传入)。无 dataType 的表达式(WindowFrame / FrameBoundary)
     * 传 INVALID_TYPE + arity=0 占位。
     */
    private TOperator buildScalar(TColumnType dataType, int arity,
                                  TOperatorType opType, TScalarUnion scalarUnion) {
        TScalar tscalar = new TScalar();
        tscalar.setData_type(dataType);
        // Set uid to -1 (empty): BE's CreateUniqueId will assign a proper counter-based uid.
        // Column identity across operators is resolved via external_slot_id, not this field.
        tscalar.setScalar_unique_id(new TScalarUniqueId(-1));
        tscalar.setArity(arity);
        tscalar.setScalar_union(scalarUnion);

        TOperatorUnion opUnion = new TOperatorUnion();
        opUnion.setScalar(tscalar);
        TOperator top = new TOperator();
        top.setOp_union(opUnion);
        top.setOp_type(opType);
        return top;
    }

    @Override
    public TOperator visit(Expression expr, Void ctx) {
        throw new UnsupportedOperationException(
                "Horn: unsupported expression type: " + expr.getClass().getSimpleName());
    }

    @Override
    public TOperator visitSlotReference(SlotReference slot, Void ctx) {
        TScalarUnion scalarUnion = new TScalarUnion();
        TScalarColumnRef columnRef = new TScalarColumnRef();
        columnRef.setColumn_name(slot.getName());
        // 用 oneLevelTable(不穿透 view)保持 MySQL 协议一致:query view 时显示 view 名;
        // table_name 空时 BE 按 computed 列处理(例如 subquery)。
        if (slot.getOneLevelTable().isPresent()) {
            TableIf table = slot.getOneLevelTable().get();
            columnRef.setTable_name(table.getName());
            if (table.getDatabase() != null) {
                columnRef.setDatabase_name(table.getDatabase().getFullName());
            }
        }
        columnRef.setExternal_slot_id((int) slot.getExprId().asInt());
        scalarUnion.setScalar_column_ref(columnRef);
        return buildScalar(DorisTypeToHornConverter.convert(slot.getDataType()),
                0, TOperatorType.kColumnRef, scalarUnion);
    }

    @Override
    public TOperator visitLiteral(Literal literal, Void ctx) {
        TScalarLiteral scalarLiteral = new TScalarLiteral();
        TStringLiteral displayLiteral = new TStringLiteral();
        displayLiteral.setValue(literal.toString());
        scalarLiteral.setDisplay_literal(displayLiteral);
        if (literal instanceof NullLiteral) {
            // Null literal: no literal_value set, only is_null_type=true.
            scalarLiteral.setIs_null_type(true);
        } else {
            TLiteralValue literalValue = new TLiteralValue();
            TLiteralType literalType;
            if (literal instanceof BooleanLiteral) {
                literalValue.setBool_literal(new TBoolLiteral(((BooleanLiteral) literal).getValue()));
                literalType = TLiteralType.kBoolLiteral;
            } else if (literal instanceof TinyIntLiteral || literal instanceof SmallIntLiteral
                    || literal instanceof IntegerLiteral || literal instanceof BigIntLiteral) {
                // LargeIntLiteral 暂不支持
                literalValue.setInt_literal(
                        new TIntLiteral(((IntegerLikeLiteral) literal).getLongValue()));
                literalType = TLiteralType.kIntLiteral;
            } else if (literal instanceof DecimalLiteral || literal instanceof DecimalV3Literal) {
                String decimalString = literal.getValue().toString();
                TDecimalLiteral decimalLiteral = new TDecimalLiteral();
                decimalLiteral.setValue(decimalString.getBytes(StandardCharsets.UTF_8));
                literalValue.setDecimal_literal(decimalLiteral);
                literalType = TLiteralType.kDecimalLiteral;
            } else if (literal instanceof FloatLiteral || literal instanceof DoubleLiteral) {
                // Float / Double
                double doubleValue = ((Number) literal.getValue()).doubleValue();
                literalValue.setFloat_literal(new TFloatLiteral(doubleValue));
                literalType = TLiteralType.kFloatLiteral;
            } else if (literal instanceof StringLikeLiteral) {
                // Varchar/Char/String: use getStringValue() to avoid quotes.
                TStringLiteral stringLiteral = new TStringLiteral();
                stringLiteral.setValue(((StringLikeLiteral) literal).getStringValue());
                literalValue.setString_literal(stringLiteral);
                literalType = TLiteralType.kStringLiteral;
            } else if (literal instanceof DateLiteral || literal instanceof DateV2Literal) {
                // 统一先转化为 DateV2Literal
                DateLiteral src = (DateLiteral) literal;
                DateV2Literal dateLiteral = (literal instanceof DateV2Literal)
                        ? (DateV2Literal) literal
                        : new DateV2Literal(src.getYear(), src.getMonth(), src.getDay());
                long daysSinceEpoch = dateLiteral.toJavaDateType().toLocalDate().toEpochDay();
                TDateLiteral tDate = new TDateLiteral();
                tDate.setDays_since_epoch((int) daysSinceEpoch);
                tDate.setDate_string(dateLiteral.toString());
                literalValue.setDate_literal(tDate);
                literalType = TLiteralType.kDateLiteral;
            } else {
                // fallback
                return visit(literal, ctx);
            }
            scalarLiteral.setLiteral_value(literalValue);
            scalarLiteral.setLiteral_type(literalType);
            scalarLiteral.setIs_null_type(false);
        }
        TScalarUnion scalarUnion = new TScalarUnion();
        scalarUnion.setScalar_literal(scalarLiteral);
        return buildScalar(DorisTypeToHornConverter.convert(literal.getDataType()),
                0, TOperatorType.kLiteral, scalarUnion);
    }

    @Override
    public TOperator visitComparisonPredicate(ComparisonPredicate cp, Void ctx) {
        TBinaryOperatorType type;
        if (cp instanceof EqualTo) {
            type = TBinaryOperatorType.kEqual;
        } else if (cp instanceof GreaterThan) {
            type = TBinaryOperatorType.kGatherThan;
        } else if (cp instanceof GreaterThanEqual) {
            type = TBinaryOperatorType.kGatherEqual;
        } else if (cp instanceof LessThan) {
            type = TBinaryOperatorType.kLowerThan;
        } else if (cp instanceof LessThanEqual) {
            type = TBinaryOperatorType.kLowerEqual;
        } else {
            // NullSafeEqual / DistinctFrom 走 horn 的特殊 binary type,独立任务。
            return visit(cp, ctx);
        }
        TScalarUnion scalarUnion = new TScalarUnion();
        TScalarBinaryPredicate binaryPredicate = new TScalarBinaryPredicate();
        binaryPredicate.setBinary_type(type);
        TScalarPredicate scalarPredicate = new TScalarPredicate();
        scalarPredicate.setHas_always_true_hint(false);
        scalarPredicate.setScalar_binary_predicate(binaryPredicate);
        scalarUnion.setScalar_predicate(scalarPredicate);
        return buildScalar(DorisTypeToHornConverter.convert(cp.getDataType()),
                cp.children().size(), TOperatorType.kBinaryPredicate, scalarUnion);
    }

    @Override
    public TOperator visitCompoundPredicate(CompoundPredicate cp, Void ctx) {
        TOperatorType opType;
        if (cp instanceof And) {
            opType = TOperatorType.kAndPredicate;
        } else if (cp instanceof Or) {
            opType = TOperatorType.kOrPredicate;
        } else {
            return visit(cp, ctx);
        }
        TScalarUnion scalarUnion = new TScalarUnion();
        scalarUnion.setScalar_predicate(new TScalarPredicate(false));
        return buildScalar(DorisTypeToHornConverter.convert(cp.getDataType()),
                cp.children().size(), opType, scalarUnion);
    }

    /**
     * Alias 在 horn flattened scalar 模型里透明:exprId/name 挂在输出 Slot(TExpression.output_list),
     * project_list 元素是底层表达式本身,故 visitAlias 剥到 child。
     */
    @Override
    public TOperator visitAlias(Alias alias, Void ctx) {
        return alias.child().accept(this, ctx);
    }

    /**
     * 白名单聚合函数(count/sum/min/max/avg + multi_distinct_count/sum)→ horn ScalarAggFn(fn_name)。
     * count(*) 自然支持(children 空 → arity=0);is_merge 不发(horn 拆 Local/Global 时自己 set);
     * distinct 已由 visitLogicalAggregate 消解成 multi_distinct,残留的 distinct / 白名单外函数 throw fallback。
     */
    @Override
    public TOperator visitAggregateFunction(AggregateFunction agg, Void ctx) {
        if (agg.isDistinct()) {
            throw new UnsupportedOperationException(
                    "Horn: unsupported expression type: distinct " + agg.getName().toLowerCase());
        }
        // 共享白名单 KNOWN_AGG_FNS:backward 反译走 FunctionRegistry 通用工厂,免逐个 case。
        String name = agg.getName().toLowerCase();
        if (!DorisFunctionBuilder.KNOWN_AGG_FNS.contains(name)) {
            throw new UnsupportedOperationException(
                    "Horn: unsupported expression type: " + agg.getClass().getSimpleName());
        }
        TScalarAggFn aggFn = new TScalarAggFn();
        aggFn.setFn_name(name);
        TScalarUnion scalarUnion = new TScalarUnion();
        scalarUnion.setScalar_agg_fn(aggFn);
        return buildScalar(DorisTypeToHornConverter.convert(agg.getDataType()),
                agg.children().size(), TOperatorType.kAggFn, scalarUnion);
    }

    /**
     * Cast → horn ScalarFunctionCall,fn_name="castto<lowercased_target_type>"
     * (对齐 horn 约定,如 "casttobigint")。
     */
    @Override
    public TOperator visitCast(Cast cast, Void ctx) {
        String typeName = cast.getDataType().simpleString().toLowerCase();
        TScalarFnCall fnCall = new TScalarFnCall();
        fnCall.setFn_name("castto" + typeName);
        fnCall.setIs_implicit(!cast.isExplicitType());
        TScalarUnion scalarUnion = new TScalarUnion();
        scalarUnion.setScalar_fn_call(fnCall);
        return buildScalar(DorisTypeToHornConverter.convert(cast.getDataType()),
                cast.children().size(), TOperatorType.kScalarFnCall, scalarUnion);
    }

    /**
     * 通用标量函数通道:白名单内的 ScalarFunction → TScalarFnCall(fn_name),复用 cast wire 通道;
     * horn 端当黑盒(只存 fn_name),backward 按 fn_name + children 重建 Doris ScalarFunction。
     * 对称性约束:新函数必须两端白名单同时加,否则反译期整查询失败;白名单外 throw fallback。
     */
    /**
     * Not(NOT LIKE / Not(EqualTo) 等)走 fn 通道黑盒:horn wire 无 kNotPredicate,
     * 谓词对 cascades 原子化 → TScalarFnCall("not") 1 child。
     */
    @Override
    public TOperator visitNot(Not not, Void ctx) {
        TScalarFnCall fnCall = new TScalarFnCall();
        fnCall.setFn_name("not");
        TScalarUnion scalarUnion = new TScalarUnion();
        scalarUnion.setScalar_fn_call(fnCall);
        return buildScalar(DorisTypeToHornConverter.convert(not.getDataType()),
                not.children().size(), TOperatorType.kScalarFnCall, scalarUnion);
    }

    /**
     * InPredicate 走 fn 通道黑盒:TScalarFnCall(fn_name="in"),children [compareExpr, opt1..optN]
     * 与 Doris InPredicate 顺序天然一致。horn 端当黑盒不展开成 OR;NOT IN 是 Not(InPredicate) 走 visitNot。
     */
    @Override
    public TOperator visitInPredicate(InPredicate in, Void ctx) {
        TScalarFnCall fnCall = new TScalarFnCall();
        fnCall.setFn_name("in");
        TScalarUnion scalarUnion = new TScalarUnion();
        scalarUnion.setScalar_fn_call(fnCall);
        return buildScalar(DorisTypeToHornConverter.convert(in.getDataType()),
                in.children().size(), TOperatorType.kScalarFnCall, scalarUnion);
    }

    /**
     * IsNull(x) 走 fn 通道黑盒,fn_name="is_null",child=[x];IS NOT NULL = Not(IsNull(x))。
     * backward translateScalarFnCall case "is_null" 重建。
     */
    @Override
    public TOperator visitIsNull(IsNull isNull, Void ctx) {
        TScalarFnCall fnCall = new TScalarFnCall();
        fnCall.setFn_name("is_null");
        TScalarUnion scalarUnion = new TScalarUnion();
        scalarUnion.setScalar_fn_call(fnCall);
        return buildScalar(DorisTypeToHornConverter.convert(isNull.getDataType()),
                isNull.children().size(), TOperatorType.kScalarFnCall, scalarUnion);
    }

    /**
     * GROUPING(col) / GROUPING_ID(...) 走 ScalarFunc("grouping"/"grouping_id") 黑盒通道。
     * NormalizeRepeat 一般已 desugar 成 GROUPING_PREFIX 虚拟 slot,mv rewrite 等保留原函数形态时走此兜底;
     * fn_name 从 fn.getName().toLowerCase() 取。
     */
    @Override
    public TOperator visitGroupingScalarFunction(GroupingScalarFunction fn, Void ctx) {
        TScalarFnCall fnCall = new TScalarFnCall();
        fnCall.setFn_name(fn.getName().toLowerCase());
        TScalarUnion scalarUnion = new TScalarUnion();
        scalarUnion.setScalar_fn_call(fnCall);
        return buildScalar(DorisTypeToHornConverter.convert(fn.getDataType()),
                fn.children().size(), TOperatorType.kScalarFnCall, scalarUnion);
    }

    @Override
    public TOperator visitScalarFunction(ScalarFunction fn, Void ctx) {
        // 共享白名单 KNOWN_SCALAR_FNS:backward 用同一份 set 校验后调 FunctionRegistry 构造,
        // 加新 fn 只动一处。
        String name = fn.getName().toLowerCase();
        if (!DorisFunctionBuilder.KNOWN_SCALAR_FNS.contains(name)) {
            return visit(fn, ctx);
        }
        TScalarFnCall fnCall = new TScalarFnCall();
        fnCall.setFn_name(name);
        TScalarUnion scalarUnion = new TScalarUnion();
        scalarUnion.setScalar_fn_call(fnCall);
        return buildScalar(DorisTypeToHornConverter.convert(fn.getDataType()),
                fn.children().size(), TOperatorType.kScalarFnCall, scalarUnion);
    }

    /**
     * TimestampArithmetic(date +/- INTERVAL n UNIT):analysis 已填 getFuncName()(如 "days_add")、
     * children pre-cast 成 [date, int]。复用 scalar fn 通道 emit kScalarFnCall(fn_name),白名单 gate。
     */
    @Override
    public TOperator visitTimestampArithmetic(TimestampArithmetic arith, Void ctx) {
        String name = arith.getFuncName() == null ? null : arith.getFuncName().toLowerCase();
        if (name == null || !DorisFunctionBuilder.KNOWN_SCALAR_FNS.contains(name)) {
            return visit(arith, ctx);
        }
        TScalarFnCall fnCall = new TScalarFnCall();
        fnCall.setFn_name(name);
        TScalarUnion scalarUnion = new TScalarUnion();
        scalarUnion.setScalar_fn_call(fnCall);
        return buildScalar(DorisTypeToHornConverter.convert(arith.getDataType()),
                arith.children().size(), TOperatorType.kScalarFnCall, scalarUnion);
    }

    /**
     * Window 函数漏斗:WindowFunction 子类与普通 ScalarFunction 共用 wire 通道,
     * emit kScalarFnCall + fn_name=lower(getName()),backward 经 FunctionRegistry 反建。
     * aggregate-as-window(sum/avg 等 OVER(...))走 visitAggregateFunction + kAggFn,不在本漏斗。
     */
    @Override
    public TOperator visitWindowFunction(WindowFunction fn, Void ctx) {
        // 共享白名单 KNOWN_WINDOW_FNS:backward 反译走 FunctionRegistry 通用工厂,免逐个 case。
        String name = fn.getName().toLowerCase();
        if (!DorisFunctionBuilder.KNOWN_WINDOW_FNS.contains(name)) {
            return visit(fn, ctx);
        }
        TScalarFnCall fnCall = new TScalarFnCall();
        fnCall.setFn_name(name);
        TScalarUnion scalarUnion = new TScalarUnion();
        scalarUnion.setScalar_fn_call(fnCall);
        return buildScalar(DorisTypeToHornConverter.convert(fn.getDataType()),
                fn.children().size(), TOperatorType.kScalarFnCall, scalarUnion);
    }

    /**
     * WindowFrame → horn TScalarWindowFrame(kWindowFrame):frameUnits → TWindowType
     * (ROWS→kRows / RANGE→kRange);非 EMPTY boundary 经 buildFrameBoundary 挂 window_start/end
     * (单元素 list 模拟多态)。WindowFrame 未 override getDataType(),故传 INVALID_TYPE + arity=0 占位。
     */
    @Override
    public TOperator visitWindowFrame(WindowFrame frame, Void ctx) {
        TScalarWindowFrame twf = new TScalarWindowFrame();
        twf.setWindow_type(frame.getFrameUnits() == FrameUnitsType.ROWS
                ? TWindowType.kRows : TWindowType.kRange);
        if (!frame.getLeftBoundary().isNull()) {
            twf.setWindow_start(Collections.singletonList(
                    buildFrameBoundary(frame.getLeftBoundary(), true)));
        }
        if (!frame.getRightBoundary().isNull()) {
            twf.setWindow_end(Collections.singletonList(
                    buildFrameBoundary(frame.getRightBoundary(), false)));
        }
        TScalarUnion union = new TScalarUnion();
        union.setScalar_window_frame(twf);
        return buildScalar(DorisTypeToHornConverter.convertCatalogType(Type.INVALID),
                0, TOperatorType.kWindowFrame, union);
    }

    /**
     * FrameBoundary(WindowFrame 静态内部类,非 Expression)无 visit hook,用 private 方法走
     * {@link #buildScalar}(INVALID_TYPE + arity=0 占位)。boundary_type:UNBOUNDED_* → kUnbounded,
     * 其余各自对应;偏移量整数走 rows_offset_value,其它表达式走 range_offset_predicate。
     */
    private TOperator buildFrameBoundary(FrameBoundary frameBoundary, boolean isLeft) {
        TScalarAnalyticWindowBoundary tBoundary = new TScalarAnalyticWindowBoundary();
        FrameBoundType frameBoundType = frameBoundary.getFrameBoundType();
        switch (frameBoundType) {
            case UNBOUNDED_PRECEDING:
            case UNBOUNDED_FOLLOWING:
                tBoundary.setBoundary_type(TWindowBoundaryType.kUnbounded);
                break;
            case CURRENT_ROW:
                tBoundary.setBoundary_type(TWindowBoundaryType.kCurrentRow);
                break;
            case PRECEDING:
                tBoundary.setBoundary_type(TWindowBoundaryType.kPreceding);
                break;
            case FOLLOWING:
                tBoundary.setBoundary_type(TWindowBoundaryType.kFollowing);
                break;
            default:
                throw new UnsupportedOperationException(
                        "Horn: unsupported FrameBoundType: " + frameBoundType);
        }
        if (frameBoundary.hasOffset() && frameBoundary.getBoundOffset().isPresent()) {
            Expression offset = frameBoundary.getBoundOffset().get();
            if (offset instanceof IntegerLikeLiteral) {
                tBoundary.setRows_offset_value(((IntegerLikeLiteral) offset).getLongValue());
            } else {
                tBoundary.setRange_offset_predicate(Collections.singletonList(
                        offset.accept(this, null)));
            }
        }
        TScalarUnion union = new TScalarUnion();
        union.setScalar_analytic_window_boundary(tBoundary);
        return buildScalar(DorisTypeToHornConverter.convertCatalogType(Type.INVALID),
                0, TOperatorType.kAnalyticWindowBoundary, union);
    }

    @Override
    public TOperator visitBinaryArithmetic(BinaryArithmetic arith, Void ctx) {
        TArithmeticOperatorType opType;
        if (arith instanceof Add) {
            opType = TArithmeticOperatorType.kAdd;
        } else if (arith instanceof Subtract) {
            opType = TArithmeticOperatorType.kSubtract;
        } else if (arith instanceof Multiply) {
            opType = TArithmeticOperatorType.kMultiply;
        } else if (arith instanceof Divide) {
            opType = TArithmeticOperatorType.kDivide;
        } else if (arith instanceof Mod) {
            opType = TArithmeticOperatorType.kMod;
        } else if (arith instanceof IntegralDivide) {
            opType = TArithmeticOperatorType.kIntDivide;
        } else if (arith instanceof BitAnd) {
            opType = TArithmeticOperatorType.kBitAnd;
        } else if (arith instanceof BitOr) {
            opType = TArithmeticOperatorType.kBitOr;
        } else if (arith instanceof BitXor) {
            opType = TArithmeticOperatorType.kBitXor;
        } else {
            return visit(arith, ctx);
        }
        TScalarUnion scalarUnion = new TScalarUnion();
        TScalarArithmetic arithmetic = new TScalarArithmetic();
        arithmetic.setArithmetic_op_type(opType);
        scalarUnion.setScalar_arithmetic(arithmetic);
        return buildScalar(DorisTypeToHornConverter.convert(arith.getDataType()),
                arith.children().size(), TOperatorType.kArithmetic, scalarUnion);
    }

}