ExprToThriftVisitor.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.analysis;

import org.apache.doris.analysis.ArithmeticExpr.Operator;
import org.apache.doris.catalog.ArrayType;
import org.apache.doris.catalog.ScalarType;
import org.apache.doris.catalog.StructType;
import org.apache.doris.common.AnalysisException;
import org.apache.doris.common.ErrorCode;
import org.apache.doris.common.ErrorReport;
import org.apache.doris.nereids.trees.expressions.functions.scalar.SearchDslParser;
import org.apache.doris.qe.ConnectContext;
import org.apache.doris.thrift.TBoolLiteral;
import org.apache.doris.thrift.TCaseExpr;
import org.apache.doris.thrift.TColumnRef;
import org.apache.doris.thrift.TDateLiteral;
import org.apache.doris.thrift.TDecimalLiteral;
import org.apache.doris.thrift.TExpr;
import org.apache.doris.thrift.TExprNode;
import org.apache.doris.thrift.TExprNodeType;
import org.apache.doris.thrift.TExprOpcode;
import org.apache.doris.thrift.TFloatLiteral;
import org.apache.doris.thrift.TIPv4Literal;
import org.apache.doris.thrift.TIPv6Literal;
import org.apache.doris.thrift.TInPredicate;
import org.apache.doris.thrift.TInfoFunc;
import org.apache.doris.thrift.TIntLiteral;
import org.apache.doris.thrift.TJsonLiteral;
import org.apache.doris.thrift.TLargeIntLiteral;
import org.apache.doris.thrift.TMatchPredicate;
import org.apache.doris.thrift.TSearchClause;
import org.apache.doris.thrift.TSearchFieldBinding;
import org.apache.doris.thrift.TSearchOccur;
import org.apache.doris.thrift.TSearchParam;
import org.apache.doris.thrift.TSlotRef;
import org.apache.doris.thrift.TStringLiteral;
import org.apache.doris.thrift.TTimeV2Literal;
import org.apache.doris.thrift.TTypeDesc;
import org.apache.doris.thrift.TTypeNode;
import org.apache.doris.thrift.TVarBinaryLiteral;

import com.google.common.collect.Lists;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * Visitor that converts any {@link Expr} node into its Thrift {@link TExprNode}
 * representation. Replaces the per-subclass {@code toThrift(TExprNode)} methods.
 *
 * <p>Usage: {@code ExprToThriftVisitor.treeToThrift(expr)} or
 * {@code expr.accept(ExprToThriftVisitor.INSTANCE, texprNode)}.
 */
public class ExprToThriftVisitor extends ExprVisitor<Void, TExprNode> {
    private static final Logger LOG = LogManager.getLogger(ExprToThriftVisitor.class);

    public static final ExprToThriftVisitor INSTANCE = new ExprToThriftVisitor();

    protected ExprToThriftVisitor() {
    }

    // -----------------------------------------------------------------------
    // Static utility methods (moved from Expr.java)
    // -----------------------------------------------------------------------

    public static TExpr treeToThrift(Expr expr) {
        TExpr result = new TExpr();
        treeToThriftHelper(expr, result, INSTANCE);
        return result;
    }

    public static List<TExpr> treesToThrift(List<? extends Expr> exprs) {
        List<TExpr> result = Lists.newArrayList();
        for (Expr expr : exprs) {
            result.add(treeToThrift(expr));
        }
        return result;
    }

    /**
     * Append a flattened version of {@code expr} (including all children)
     * to {@code container}, using the given visitor for per-node conversion.
     */
    public static void treeToThriftHelper(Expr expr, TExpr container,
            ExprVisitor<Void, TExprNode> visitor) {
        // CastExpr no-op: skip the cast and serialize the child directly
        if (expr instanceof CastExpr && ((CastExpr) expr).isNoOp()) {
            treeToThriftHelper(expr.getChild(0), container, visitor);
            return;
        }

        TExprNode msg = new TExprNode();
        msg.type = expr.getType().toThrift();
        msg.num_children = expr.getChildren().size();
        if (expr.getFn() != null) {
            msg.setFn(expr.getFn().toThrift(
                    expr.getType(), expr.collectChildReturnTypes(), expr.collectChildReturnNullables()));
            if (expr.getFn().hasVarArgs()) {
                msg.setVarargStartIdx(expr.getFn().getNumArgs() - 1);
            }
        }
        msg.output_scale = -1;
        msg.setIsNullable(expr.isNullable());

        expr.accept(visitor, msg);
        container.addToNodes(msg);

        for (Expr child : expr.getChildren()) {
            treeToThriftHelper(child, container, visitor);
        }
    }

    @Override
    public Void visit(Expr expr, TExprNode msg) {
        throw new UnsupportedOperationException(
                "ExprToThriftVisitor does not support Expr type: " + expr.getClass().getSimpleName());
    }

    // -----------------------------------------------------------------------
    // Literals
    // -----------------------------------------------------------------------

    @Override
    public Void visitBoolLiteral(BoolLiteral expr, TExprNode msg) {
        msg.node_type = TExprNodeType.BOOL_LITERAL;
        msg.bool_literal = new TBoolLiteral(expr.getValue());
        return null;
    }

    @Override
    public Void visitStringLiteral(StringLiteral expr, TExprNode msg) {
        if (expr.getValue() == null) {
            msg.node_type = TExprNodeType.NULL_LITERAL;
        } else {
            msg.string_literal = new TStringLiteral(expr.getValue());
            msg.node_type = TExprNodeType.STRING_LITERAL;
        }
        return null;
    }

    @Override
    public Void visitIntLiteral(IntLiteral expr, TExprNode msg) {
        msg.node_type = TExprNodeType.INT_LITERAL;
        msg.int_literal = new TIntLiteral(expr.getValue());
        return null;
    }

    @Override
    public Void visitLargeIntLiteral(LargeIntLiteral expr, TExprNode msg) {
        msg.node_type = TExprNodeType.LARGE_INT_LITERAL;
        msg.large_int_literal = new TLargeIntLiteral(expr.getStringValue());
        return null;
    }

    @Override
    public Void visitFloatLiteral(FloatLiteral expr, TExprNode msg) {
        msg.node_type = TExprNodeType.FLOAT_LITERAL;
        msg.float_literal = new TFloatLiteral(expr.getValue());
        return null;
    }

    @Override
    public Void visitDecimalLiteral(DecimalLiteral expr, TExprNode msg) {
        msg.node_type = TExprNodeType.DECIMAL_LITERAL;
        msg.decimal_literal = new TDecimalLiteral(expr.getStringValue());
        return null;
    }

    @Override
    public Void visitDateLiteral(DateLiteral expr, TExprNode msg) {
        if (expr.getType().isDatetimeV2() || expr.getType().isTimeStampTz()) {
            expr.roundFloor(((ScalarType) expr.getType()).getScalarScale());
        }
        msg.node_type = TExprNodeType.DATE_LITERAL;
        msg.date_literal = new TDateLiteral(expr.getStringValue());
        try {
            expr.checkValueValid();
        } catch (AnalysisException e) {
            LOG.warn("meet invalid value when plan to translate " + expr.toString() + " to thrift node");
        }
        return null;
    }

    @Override
    public Void visitTimeV2Literal(TimeV2Literal expr, TExprNode msg) {
        msg.node_type = TExprNodeType.TIMEV2_LITERAL;
        msg.timev2_literal = new TTimeV2Literal(expr.getValue());
        return null;
    }

    @Override
    public Void visitNullLiteral(NullLiteral expr, TExprNode msg) {
        msg.node_type = TExprNodeType.NULL_LITERAL;
        return null;
    }

    @Override
    public Void visitMaxLiteral(MaxLiteral expr, TExprNode msg) {
        // TODO: complete this type
        return null;
    }

    @Override
    public Void visitJsonLiteral(JsonLiteral expr, TExprNode msg) {
        msg.node_type = TExprNodeType.JSON_LITERAL;
        msg.json_literal = new TJsonLiteral(expr.getUnescapedValue());
        return null;
    }

    @Override
    public Void visitIPv4Literal(IPv4Literal expr, TExprNode msg) {
        msg.node_type = TExprNodeType.IPV4_LITERAL;
        msg.ipv4_literal = new TIPv4Literal(expr.getValue());
        return null;
    }

    @Override
    public Void visitIPv6Literal(IPv6Literal expr, TExprNode msg) {
        msg.node_type = TExprNodeType.IPV6_LITERAL;
        msg.ipv6_literal = new TIPv6Literal(expr.getValue());
        return null;
    }

    @Override
    public Void visitVarBinaryLiteral(VarBinaryLiteral expr, TExprNode msg) {
        msg.node_type = TExprNodeType.VARBINARY_LITERAL;
        msg.varbinary_literal = new TVarBinaryLiteral(ByteBuffer.wrap(expr.getValue()));
        return null;
    }

    @Override
    public Void visitArrayLiteral(ArrayLiteral expr, TExprNode msg) {
        msg.node_type = TExprNodeType.ARRAY_LITERAL;
        msg.setChildType(((ArrayType) expr.getType()).getItemType().getPrimitiveType().toThrift());
        return null;
    }

    @Override
    public Void visitMapLiteral(MapLiteral expr, TExprNode msg) {
        msg.node_type = TExprNodeType.MAP_LITERAL;
        TTypeDesc container = new TTypeDesc();
        container.setTypes(new ArrayList<TTypeNode>());
        expr.getType().toThrift(container);
        msg.setType(container);
        return null;
    }

    @Override
    public Void visitStructLiteral(StructLiteral expr, TExprNode msg) {
        msg.node_type = TExprNodeType.STRUCT_LITERAL;
        ((StructType) expr.getType()).getFields()
                .forEach(v -> msg.setChildType(v.getType().getPrimitiveType().toThrift()));
        TTypeDesc container = new TTypeDesc();
        container.setTypes(new ArrayList<TTypeNode>());
        expr.getType().toThrift(container);
        msg.setType(container);
        return null;
    }

    @Override
    public Void visitPlaceHolderExpr(PlaceHolderExpr expr, TExprNode msg) {
        expr.getLiteral().accept(this, msg);
        return null;
    }

    // -----------------------------------------------------------------------
    // Reference / slot expressions
    // -----------------------------------------------------------------------

    @Override
    public Void visitSlotRef(SlotRef expr, TExprNode msg) {
        msg.node_type = TExprNodeType.SLOT_REF;
        msg.slot_ref = new TSlotRef(expr.getDesc().getId().asInt(), expr.getDesc().getParent().getId().asInt());
        msg.slot_ref.setColUniqueId(expr.getDesc().getUniqueId());
        msg.slot_ref.setIsVirtualSlot(expr.getDesc().getVirtualColumn() != null);
        msg.setLabel(expr.getLabel());
        return null;
    }

    @Override
    public Void visitColumnRefExpr(ColumnRefExpr expr, TExprNode msg) {
        msg.node_type = TExprNodeType.COLUMN_REF;
        TColumnRef columnRef = new TColumnRef();
        columnRef.setColumnId(expr.getColumnId());
        columnRef.setColumnName(expr.getName());
        msg.column_ref = columnRef;
        return null;
    }

    @Override
    public Void visitInformationFunction(InformationFunction expr, TExprNode msg) {
        msg.node_type = TExprNodeType.INFO_FUNC;
        msg.info_func = new TInfoFunc(Long.parseLong(expr.getIntValue()), expr.getStrValue());
        return null;
    }

    @Override
    public Void visitEncryptKeyRef(EncryptKeyRef expr, TExprNode msg) {
        // no operation
        return null;
    }

    @Override
    public Void visitVariableExpr(VariableExpr expr, TExprNode msg) {
        switch (expr.getType().getPrimitiveType()) {
            case BOOLEAN:
                msg.node_type = TExprNodeType.BOOL_LITERAL;
                msg.bool_literal = new TBoolLiteral(expr.getBoolValue());
                break;
            case TINYINT:
            case SMALLINT:
            case INT:
            case BIGINT:
                msg.node_type = TExprNodeType.INT_LITERAL;
                msg.int_literal = new TIntLiteral(expr.getIntValue());
                break;
            case FLOAT:
            case DOUBLE:
                msg.node_type = TExprNodeType.FLOAT_LITERAL;
                msg.float_literal = new TFloatLiteral(expr.getFloatValue());
                break;
            default:
                if (expr.getStrValue() == null) {
                    msg.node_type = TExprNodeType.NULL_LITERAL;
                } else {
                    msg.node_type = TExprNodeType.STRING_LITERAL;
                    msg.string_literal = new TStringLiteral(expr.getStrValue());
                }
        }
        return null;
    }

    // -----------------------------------------------------------------------
    // Predicates
    // -----------------------------------------------------------------------

    @Override
    public Void visitBinaryPredicate(BinaryPredicate expr, TExprNode msg) {
        msg.node_type = TExprNodeType.BINARY_PRED;
        msg.setOpcode(toThriftOpcode(expr.getOp()));
        msg.setChildType(expr.getChild(0).getType().getPrimitiveType().toThrift());
        return null;
    }

    @Override
    public Void visitIsNullPredicate(IsNullPredicate expr, TExprNode msg) {
        msg.node_type = TExprNodeType.FUNCTION_CALL;
        return null;
    }

    @Override
    public Void visitCompoundPredicate(CompoundPredicate expr, TExprNode msg) {
        msg.node_type = TExprNodeType.COMPOUND_PRED;
        msg.setOpcode(toThriftOpcode(expr.getOp()));
        return null;
    }

    @Override
    public Void visitInPredicate(InPredicate expr, TExprNode msg) {
        msg.in_predicate = new TInPredicate(expr.isNotIn());
        msg.node_type = TExprNodeType.IN_PRED;
        msg.setOpcode(toThriftOpcode(expr));
        return null;
    }

    @Override
    public Void visitLikePredicate(LikePredicate expr, TExprNode msg) {
        msg.node_type = TExprNodeType.FUNCTION_CALL;
        return null;
    }

    @Override
    public Void visitMatchPredicate(MatchPredicate expr, TExprNode msg) {
        msg.node_type = TExprNodeType.MATCH_PRED;
        msg.setOpcode(toThriftOpcode(expr.getOp()));
        msg.match_predicate = new TMatchPredicate(
                expr.getInvertedIndexParser(), expr.getInvertedIndexParserMode());
        msg.match_predicate.setCharFilterMap(expr.getInvertedIndexCharFilter());
        msg.match_predicate.setParserLowercase(expr.getInvertedIndexParserLowercase());
        msg.match_predicate.setParserStopwords(expr.getInvertedIndexParserStopwords());
        msg.match_predicate.setAnalyzerName(expr.getInvertedIndexAnalyzerName());
        return null;
    }

    @Override
    public Void visitBetweenPredicate(BetweenPredicate expr, TExprNode msg) {
        throw new IllegalStateException(
                "BetweenPredicate needs to be rewritten into a CompoundPredicate.");
    }

    @Override
    public Void visitSearchPredicate(SearchPredicate expr, TExprNode msg) {
        msg.node_type = TExprNodeType.SEARCH_EXPR;
        msg.setSearchParam(buildSearchThriftParam(expr));

        LOG.info("SearchPredicate.toThrift: dsl='{}', num_children_in_base={}, children_size={}",
                expr.getDslString(), msg.num_children, expr.getChildren().size());

        if (expr.getQsPlan() != null) {
            LOG.info("SearchPredicate.toThrift: QsPlan fieldBindings.size={}",
                    expr.getQsPlan().getFieldBindings() != null
                            ? expr.getQsPlan().getFieldBindings().size() : 0);
            if (expr.getQsPlan().getFieldBindings() != null) {
                for (int i = 0; i < expr.getQsPlan().getFieldBindings().size(); i++) {
                    SearchDslParser.QsFieldBinding binding = expr.getQsPlan().getFieldBindings().get(i);
                    LOG.info("SearchPredicate.toThrift: binding[{}] fieldName='{}', slotIndex={}",
                            i, binding.getFieldName(), binding.getSlotIndex());
                }
            }
        }

        for (int i = 0; i < expr.getChildren().size(); i++) {
            Expr child = expr.getChildren().get(i);
            LOG.info("SearchPredicate.toThrift: child[{}] = {} (type={})",
                    i, child.getClass().getSimpleName(), child.getType());
            if (child instanceof SlotRef) {
                SlotRef slotRef = (SlotRef) child;
                LOG.info("SearchPredicate.toThrift: SlotRef details - column={}",
                        slotRef.getColumnName());
                if (slotRef.getDesc() != null) {
                    LOG.info("SearchPredicate.toThrift: SlotRef analyzed - slotId={}",
                            slotRef.getSlotId());
                }
            }
        }
        return null;
    }

    // -----------------------------------------------------------------------
    // Arithmetic / cast
    // -----------------------------------------------------------------------

    @Override
    public Void visitArithmeticExpr(ArithmeticExpr expr, TExprNode msg) {
        msg.node_type = TExprNodeType.ARITHMETIC_EXPR;
        if (!(expr.getType().isDecimalV2() || expr.getType().isDecimalV3())) {
            msg.setOpcode(toThriftOpcode(expr.getOp()));
        }
        return null;
    }

    @Override
    public Void visitCastExpr(CastExpr expr, TExprNode msg) {
        msg.node_type = TExprNodeType.CAST_EXPR;
        msg.setOpcode(TExprOpcode.CAST);
        return null;
    }

    @Override
    public Void visitTryCastExpr(TryCastExpr expr, TExprNode msg) {
        msg.node_type = TExprNodeType.TRY_CAST_EXPR;
        msg.setIsCastNullable(expr.isOriginCastNullable());
        msg.setOpcode(TExprOpcode.TRY_CAST);
        return null;
    }

    @Override
    public Void visitTimestampArithmeticExpr(TimestampArithmeticExpr expr, TExprNode msg) {
        msg.node_type = TExprNodeType.COMPUTE_FUNCTION_CALL;
        msg.setOpcode(toThriftOpcode(expr));
        return null;
    }

    // -----------------------------------------------------------------------
    // Functions / lambda / case
    // -----------------------------------------------------------------------

    @Override
    public Void visitFunctionCallExpr(FunctionCallExpr expr, TExprNode msg) {
        if (expr.isAggregateFunction() || expr.isAnalyticFnCall()) {
            msg.node_type = TExprNodeType.AGG_EXPR;
            FunctionParams aggParams = expr.getAggFnParams();
            if (aggParams == null) {
                aggParams = expr.getFnParams();
            }
            msg.setAggExpr(aggParams.createTAggregateExpr(expr.isMergeAggFn()));
        } else {
            msg.node_type = TExprNodeType.FUNCTION_CALL;
        }

        if (ConnectContext.get() != null) {
            msg.setShortCircuitEvaluation(ConnectContext.get().getSessionVariable().isShortCircuitEvaluation());
        }
        return null;
    }

    @Override
    public Void visitLambdaFunctionCallExpr(LambdaFunctionCallExpr expr, TExprNode msg) {
        msg.node_type = TExprNodeType.LAMBDA_FUNCTION_CALL_EXPR;
        return null;
    }

    @Override
    public Void visitLambdaFunctionExpr(LambdaFunctionExpr expr, TExprNode msg) {
        msg.setNodeType(TExprNodeType.LAMBDA_FUNCTION_EXPR);
        return null;
    }

    @Override
    public Void visitCaseExpr(CaseExpr expr, TExprNode msg) {
        msg.node_type = TExprNodeType.CASE_EXPR;
        msg.case_expr = new TCaseExpr(expr.isHasCaseExpr(), expr.isHasElseExpr());
        if (ConnectContext.get() != null) {
            msg.setShortCircuitEvaluation(ConnectContext.get().getSessionVariable().isShortCircuitEvaluation());
        }
        return null;
    }

    // -----------------------------------------------------------------------
    // TExprOpcode conversion helpers
    // -----------------------------------------------------------------------

    public static TExprOpcode toThriftOpcode(ArithmeticExpr.Operator op) {
        switch (op) {
            case MULTIPLY: return TExprOpcode.MULTIPLY;
            case DIVIDE: return TExprOpcode.DIVIDE;
            case MOD: return TExprOpcode.MOD;
            case INT_DIVIDE: return TExprOpcode.INT_DIVIDE;
            case ADD: return TExprOpcode.ADD;
            case SUBTRACT: return TExprOpcode.SUBTRACT;
            case BITAND: return TExprOpcode.BITAND;
            case BITOR: return TExprOpcode.BITOR;
            case BITXOR: return TExprOpcode.BITXOR;
            case BITNOT: return TExprOpcode.BITNOT;
            default:
                throw new IllegalStateException("Unknown ArithmeticExpr.Operator: " + op);
        }
    }

    public static TExprOpcode toThriftOpcode(BinaryPredicate.Operator op) {
        switch (op) {
            case EQ: return TExprOpcode.EQ;
            case NE: return TExprOpcode.NE;
            case LE: return TExprOpcode.LE;
            case GE: return TExprOpcode.GE;
            case LT: return TExprOpcode.LT;
            case GT: return TExprOpcode.GT;
            case EQ_FOR_NULL: return TExprOpcode.EQ_FOR_NULL;
            default:
                throw new IllegalStateException("Unknown BinaryPredicate.Operator: " + op);
        }
    }

    public static TExprOpcode toThriftOpcode(CompoundPredicate.Operator op) {
        switch (op) {
            case AND: return TExprOpcode.COMPOUND_AND;
            case OR: return TExprOpcode.COMPOUND_OR;
            case NOT: return TExprOpcode.COMPOUND_NOT;
            default:
                throw new IllegalStateException("Unknown CompoundPredicate.Operator: " + op);
        }
    }

    public static TExprOpcode toThriftOpcode(MatchPredicate.Operator op) {
        switch (op) {
            case MATCH_ANY: return TExprOpcode.MATCH_ANY;
            case MATCH_ALL: return TExprOpcode.MATCH_ALL;
            case MATCH_PHRASE: return TExprOpcode.MATCH_PHRASE;
            case MATCH_PHRASE_PREFIX: return TExprOpcode.MATCH_PHRASE_PREFIX;
            case MATCH_REGEXP: return TExprOpcode.MATCH_REGEXP;
            case MATCH_PHRASE_EDGE: return TExprOpcode.MATCH_PHRASE_EDGE;
            default:
                throw new IllegalStateException("Unknown MatchPredicate.Operator: " + op);
        }
    }

    public static TExprOpcode toThriftOpcode(InPredicate expr) {
        boolean allConstant = expr.getAllConstant();
        if (allConstant) {
            return expr.isNotIn() ? TExprOpcode.FILTER_NOT_IN : TExprOpcode.FILTER_IN;
        } else {
            return expr.isNotIn() ? TExprOpcode.FILTER_NEW_NOT_IN : TExprOpcode.FILTER_NEW_IN;
        }
    }

    public static TExprOpcode toThriftOpcode(TimestampArithmeticExpr expr) {
        ArithmeticExpr.Operator op = expr.getOp();
        TimestampArithmeticExpr.TimeUnit timeUnit = expr.getTimeUnit();
        switch (timeUnit) {
            case YEAR:
                return op == Operator.ADD ? TExprOpcode.TIMESTAMP_YEARS_ADD : TExprOpcode.TIMESTAMP_YEARS_SUB;
            case MONTH:
                return op == Operator.ADD ? TExprOpcode.TIMESTAMP_MONTHS_ADD : TExprOpcode.TIMESTAMP_MONTHS_SUB;
            case WEEK:
                return op == Operator.ADD ? TExprOpcode.TIMESTAMP_WEEKS_ADD : TExprOpcode.TIMESTAMP_WEEKS_SUB;
            case DAY:
                return op == Operator.ADD ? TExprOpcode.TIMESTAMP_DAYS_ADD : TExprOpcode.TIMESTAMP_DAYS_SUB;
            case HOUR:
                return op == Operator.ADD ? TExprOpcode.TIMESTAMP_HOURS_ADD : TExprOpcode.TIMESTAMP_HOURS_SUB;
            case MINUTE:
                return op == Operator.ADD ? TExprOpcode.TIMESTAMP_MINUTES_ADD : TExprOpcode.TIMESTAMP_MINUTES_SUB;
            case SECOND:
                return op == Operator.ADD ? TExprOpcode.TIMESTAMP_SECONDS_ADD : TExprOpcode.TIMESTAMP_SECONDS_SUB;
            default:
                try {
                    ErrorReport.reportAnalysisException(ErrorCode.ERR_BAD_TIMEUNIT, timeUnit);
                } catch (AnalysisException e) {
                    throw new IllegalStateException(e);
                }
                return TExprOpcode.INVALID_OPCODE;
        }
    }

    // -----------------------------------------------------------------------
    // SearchPredicate helpers (moved from SearchPredicate.java)
    // -----------------------------------------------------------------------

    static TSearchParam buildSearchThriftParam(SearchPredicate expr) {
        TSearchParam param = new TSearchParam();
        param.setOriginalDsl(expr.getDslString());
        param.setRoot(convertQsNodeToThrift(expr.getQsPlan().getRoot()));

        List<TSearchFieldBinding> bindings = new ArrayList<>();
        SearchDslParser.QsPlan qsPlan = expr.getQsPlan();
        for (int i = 0; i < qsPlan.getFieldBindings().size(); i++) {
            SearchDslParser.QsFieldBinding binding = qsPlan.getFieldBindings().get(i);
            TSearchFieldBinding thriftBinding = new TSearchFieldBinding();

            String fieldPath = binding.getFieldName();
            thriftBinding.setFieldName(fieldPath);

            if (fieldPath.contains(".")) {
                int firstDotPos = fieldPath.indexOf('.');
                String parentField = fieldPath.substring(0, firstDotPos);
                String subcolumnPath = fieldPath.substring(firstDotPos + 1);

                thriftBinding.setIsVariantSubcolumn(true);
                thriftBinding.setParentFieldName(parentField);
                thriftBinding.setSubcolumnPath(subcolumnPath);

                LOG.info("buildThriftParam: variant subcolumn field='{}', parent='{}', subcolumn='{}'",
                        fieldPath, parentField, subcolumnPath);
            } else {
                thriftBinding.setIsVariantSubcolumn(false);
            }

            thriftBinding.setSlotIndex(i);

            if (i < expr.getChildren().size() && expr.getChildren().get(i) instanceof SlotRef) {
                SlotRef slotRef = (SlotRef) expr.getChildren().get(i);
                int actualSlotId = slotRef.getSlotId().asInt();
                thriftBinding.setSlotIndex(actualSlotId);
                LOG.info("buildThriftParam: binding field='{}', actual slotId={}",
                        binding.getFieldName(), actualSlotId);
            } else {
                LOG.warn("buildThriftParam: No corresponding SlotRef for field '{}'", binding.getFieldName());
                thriftBinding.setSlotIndex(i);
            }

            List<org.apache.doris.catalog.Index> fieldIndexes = expr.getFieldIndexes();
            if (i < fieldIndexes.size() && fieldIndexes.get(i) != null) {
                Map<String, String> properties = fieldIndexes.get(i).getProperties();
                if (properties != null && !properties.isEmpty()) {
                    thriftBinding.setIndexProperties(properties);
                    LOG.debug("buildThriftParam: field='{}' index_properties={}",
                            fieldPath, properties);
                }
            }

            bindings.add(thriftBinding);
        }
        param.setFieldBindings(bindings);

        if (qsPlan.getDefaultOperator() != null) {
            param.setDefaultOperator(qsPlan.getDefaultOperator());
        }

        if (qsPlan.getMinimumShouldMatch() != null) {
            param.setMinimumShouldMatch(qsPlan.getMinimumShouldMatch());
        }

        return param;
    }

    static TSearchClause convertQsNodeToThrift(SearchDslParser.QsNode node) {
        TSearchClause clause = new TSearchClause();
        clause.setClauseType(node.getType().name());

        if (node.getField() != null) {
            clause.setFieldName(node.getField());
        }

        if (node.getType() == SearchDslParser.QsClauseType.NESTED && node.getNestedPath() != null) {
            clause.setNestedPath(node.getNestedPath());
        }

        if (node.getValue() != null) {
            clause.setValue(node.getValue());
        }

        if (node.getOccur() != null) {
            clause.setOccur(convertQsOccurToThrift(node.getOccur()));
        }

        if (node.getMinimumShouldMatch() != null) {
            clause.setMinimumShouldMatch(node.getMinimumShouldMatch());
        }

        if (node.getChildren() != null && !node.getChildren().isEmpty()) {
            List<TSearchClause> childClauses = new ArrayList<>();
            for (SearchDslParser.QsNode child : node.getChildren()) {
                childClauses.add(convertQsNodeToThrift(child));
            }
            clause.setChildren(childClauses);
        }

        return clause;
    }

    static TSearchOccur convertQsOccurToThrift(SearchDslParser.QsOccur occur) {
        switch (occur) {
            case MUST:
                return TSearchOccur.MUST;
            case SHOULD:
                return TSearchOccur.SHOULD;
            case MUST_NOT:
                return TSearchOccur.MUST_NOT;
            default:
                return TSearchOccur.MUST;
        }
    }

    /**
     * Build DSL AST explain lines for a SearchPredicate.
     * Moved from SearchPredicate to remove thrift dependency from Expr subclass.
     */
    public static List<String> buildDslAstExplainLines(SearchPredicate expr) {
        List<String> lines = new ArrayList<>();
        if (expr.getQsPlan() == null || expr.getQsPlan().getRoot() == null) {
            return lines;
        }
        TSearchClause rootClause = convertQsNodeToThrift(expr.getQsPlan().getRoot());
        appendClauseExplain(rootClause, lines, 0);
        return lines;
    }

    private static void appendClauseExplain(TSearchClause clause, List<String> lines, int depth) {
        StringBuilder line = new StringBuilder();
        line.append(indent(depth)).append("- clause_type=").append(clause.getClauseType());
        if (clause.isSetNestedPath()) {
            line.append(", nested_path=").append('"').append(escapeText(clause.getNestedPath())).append('"');
        }
        if (clause.isSetFieldName()) {
            line.append(", field=").append('"').append(escapeText(clause.getFieldName())).append('"');
        }
        if (clause.isSetValue()) {
            line.append(", value=").append('"').append(escapeText(clause.getValue())).append('"');
        }
        lines.add(line.toString());

        if (clause.isSetChildren() && clause.getChildren() != null && !clause.getChildren().isEmpty()) {
            for (TSearchClause child : clause.getChildren()) {
                appendClauseExplain(child, lines, depth + 1);
            }
        }
    }

    private static String indent(int level) {
        if (level <= 0) {
            return "";
        }
        StringBuilder sb = new StringBuilder(level * 2);
        for (int i = 0; i < level; i++) {
            sb.append("  ");
        }
        return sb.toString();
    }

    private static String escapeText(String value) {
        if (value == null) {
            return "";
        }
        return value
                .replace("\\", "\\\\")
                .replace("\"", "\\\"")
                .replace("\n", "\\n")
                .replace("\r", "\\r");
    }
}