HornLogicalPlanThriftBuilder.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.horn.HornOptimizationContext;
import org.apache.doris.catalog.ColocateTableIndex;
import org.apache.doris.catalog.Column;
import org.apache.doris.catalog.Env;
import org.apache.doris.catalog.DistributionInfo;
import org.apache.doris.catalog.HashDistributionInfo;
import org.apache.doris.catalog.KeysType;
import org.apache.doris.catalog.OlapTable;
import org.apache.horn4j.thrift.TColumn;
import org.apache.horn4j.thrift.TColumnStats;
import org.apache.horn4j.thrift.TColumnValue;
import org.apache.horn4j.thrift.TDistributionSpec;
import org.apache.horn4j.thrift.TDistributionType;
import org.apache.horn4j.thrift.TDorisHashSpec;
import org.apache.horn4j.thrift.TSpecialHashSpec;
import org.apache.horn4j.thrift.TSpecialHashSpecKind;
import org.apache.horn4j.thrift.TExpression;
import org.apache.horn4j.thrift.TFlattenedExpression;
import org.apache.horn4j.thrift.TLogicalAgg;
import org.apache.horn4j.thrift.TLogicalFilter;
import org.apache.horn4j.thrift.TLogicalGrouping;
import org.apache.horn4j.thrift.TLogicalJoin;
import org.apache.horn4j.thrift.TLogicalLimit;
import org.apache.horn4j.thrift.TLogicalProject;
import org.apache.horn4j.thrift.TLogicalSetOp;
import org.apache.horn4j.thrift.TLogicalWindow;
import org.apache.horn4j.thrift.TOrderSpec;
import org.apache.horn4j.thrift.TLogicalScan;
import org.apache.horn4j.thrift.TLogicalTableScan;
import org.apache.horn4j.thrift.TLogicalValueScan;
import org.apache.horn4j.thrift.TOperator;
import org.apache.horn4j.thrift.TOperatorType;
import org.apache.horn4j.thrift.TOperatorUnion;
import org.apache.horn4j.thrift.TScanTableType;
import org.apache.horn4j.thrift.TTable;
import org.apache.horn4j.thrift.TTableStats;
import org.apache.doris.nereids.trees.expressions.Expression;
import org.apache.doris.nereids.trees.expressions.NamedExpression;
import org.apache.doris.nereids.trees.expressions.OrderExpression;
import org.apache.doris.nereids.trees.expressions.Slot;
import org.apache.doris.nereids.trees.expressions.SlotReference;
import org.apache.doris.nereids.trees.expressions.WindowExpression;
import org.apache.doris.nereids.trees.expressions.WindowFrame;
import org.apache.doris.nereids.rules.implementation.LogicalWindowToPhysicalWindow;
import org.apache.doris.nereids.rules.implementation.LogicalWindowToPhysicalWindow.WindowFrameGroup;
import org.apache.doris.nereids.rules.rewrite.MultiDistinctFunctionStrategy;
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.plans.LimitPhase;
import org.apache.doris.nereids.trees.plans.Plan;
import org.apache.doris.nereids.trees.plans.logical.LogicalAggregate;
import org.apache.doris.nereids.trees.plans.logical.LogicalEmptyRelation;
import org.apache.doris.nereids.trees.plans.logical.LogicalFilter;
import org.apache.doris.nereids.trees.plans.logical.LogicalOneRowRelation;
import org.apache.doris.nereids.trees.plans.logical.LogicalJoin;
import org.apache.doris.nereids.trees.plans.logical.LogicalOlapScan;
import org.apache.doris.nereids.trees.plans.logical.LogicalPlan;
import org.apache.doris.nereids.trees.plans.logical.LogicalLimit;
import org.apache.doris.nereids.trees.plans.logical.LogicalProject;
import org.apache.doris.nereids.trees.plans.logical.LogicalRepeat;
import org.apache.doris.nereids.trees.plans.algebra.Repeat;
import org.apache.doris.nereids.trees.plans.algebra.SetOperation.Qualifier;
import org.apache.doris.nereids.trees.plans.logical.LogicalResultSink;
import org.apache.doris.nereids.trees.plans.logical.LogicalSink;
import org.apache.doris.nereids.trees.plans.logical.LogicalSort;
import org.apache.doris.nereids.trees.plans.logical.LogicalTopN;
import org.apache.doris.nereids.trees.plans.logical.LogicalUnion;
import org.apache.doris.nereids.trees.plans.logical.LogicalExcept;
import org.apache.doris.nereids.trees.plans.logical.LogicalIntersect;
import org.apache.doris.nereids.trees.plans.logical.LogicalSetOperation;
import org.apache.doris.nereids.trees.plans.logical.LogicalWindow;
import org.apache.doris.nereids.properties.OrderKey;
import org.apache.doris.nereids.properties.DistributionSpec;
import org.apache.doris.nereids.properties.DistributionSpecHash;
import org.apache.doris.horn.horn2doris.Horn2DorisUtils;
import org.apache.doris.nereids.trees.plans.visitor.DefaultPlanVisitor;
import org.apache.doris.qe.ConnectContext;
import org.apache.doris.statistics.ColumnStatistic;
import org.apache.doris.statistics.StatisticsCache;

import com.google.common.collect.Lists;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;

/** Visitor-based Doris LogicalPlan → Horn TFlattenedExpression translator */
public class HornLogicalPlanThriftBuilder extends DefaultPlanVisitor<Void, List<TExpression>> {

    private static final org.apache.logging.log4j.Logger LOG =
            org.apache.logging.log4j.LogManager.getLogger(HornLogicalPlanThriftBuilder.class);

    private final HornOptimizationContext hornCtx;
    private final HornScalarThriftBuilder scalarTranslator;

    public HornLogicalPlanThriftBuilder(HornOptimizationContext hornCtx) {
        this.hornCtx = hornCtx;
        this.scalarTranslator = new HornScalarThriftBuilder();
    }

    /** Translate the whole LogicalPlan tree to TFlattenedExpression */
    public TFlattenedExpression translate(LogicalPlan logicalPlan) {
        TFlattenedExpression result = new TFlattenedExpression();
        List<TExpression> texprs = new ArrayList<>();
        // sink 语义 = identity Project,承载 SELECT 列序
        logicalPlan.accept(this, texprs);
        result.setTexprs(texprs);
        return result;
    }

    @Override
    public Void visit(Plan plan, List<TExpression> texprs) {
        throw new UnsupportedOperationException(
                "Horn: unsupported Nereids node: " + plan.getClass().getSimpleName());
    }

    /** Append a TExpression for {@code plan}, then recurse into children pre-order */
    private void emit(Plan plan, TOperatorType opType, TOperatorUnion opUnion,
                      List<TExpression> texprs) {
        TExpression texpr = new TExpression();
        TOperator top = new TOperator();
        top.setOp_type(opType);
        top.setOp_union(opUnion);
        texpr.setOp(top);
        texpr.setArity(plan.children().size());

        // FE-declared output column refs: each entry is a kColumnRef-shaped TOperator
        List<TOperator> outputList = new ArrayList<>();
        for (Slot slot : plan.getOutput()) {
            if (slot instanceof SlotReference) {
                outputList.add(scalarTranslator.visitSlotReference((SlotReference) slot, null));
            }
        }
        if (!outputList.isEmpty()) {
            texpr.setOutput_list(outputList);
        }

        texprs.add(texpr);

        for (Plan child : plan.children()) {
            child.accept(this, texprs);
        }
    }

    @Override
    public Void visitLogicalOlapScan(LogicalOlapScan scan, List<TExpression> texprs) {
        TOperatorUnion opUnion = new TOperatorUnion();
        TLogicalTableScan tscan = new TLogicalTableScan();
        OlapTable table = scan.getTable();

        // ====== DEBUG: LogicalOlapScan 此时是否已 partition pruning ======
        LOG.warn("HORN_FWD_SCAN table={} totalPartitions={} selectedPartitionIds={} selectedTabletIds.size={}",
                table.getName(), table.getPartitionIds().size(),
                scan.getSelectedPartitionIds(), scan.getSelectedTabletIds().size());
        // ====== END DEBUG ======

        tscan.setDatabase_name(scan.getQualifier().get(1));
        tscan.setTable_name(table.getName());
        tscan.setTable_alias(table.getName());
        tscan.setTable_type(TScanTableType.kTciTable);
        // schema_id 不设:BE expression_builder.cc:40 默认读 -1,FindTablePair 用

        // scan_list:scan 自有输出列(含列裁剪)的 ColumnRef(带 external_slot_id),
        List<TOperator> scanList = new ArrayList<>();
        for (Slot slot : scan.getOutput()) {
            if (slot instanceof SlotReference) {
                scanList.add(scalarTranslator.visitSlotReference((SlotReference) slot, null));
            }
        }
        tscan.setScan_list(scanList);

        // external_relation_id: FE 自有 per-scan opaque id(relationId)
        long relationId = scan.getRelationId().asInt();
        tscan.setExternal_relation_id(relationId);
        hornCtx.putScanPrunedPartitions(relationId, new ArrayList<>(scan.getSelectedPartitionIds()));

        // task #227 sort elimination: emit base table 物理排序键 (OLAP tablet 内部
        KeysType keysType = table.getKeysType();
        if (keysType == KeysType.DUP_KEYS || keysType == KeysType.UNIQUE_KEYS
                || keysType == KeysType.AGG_KEYS) {
            // Doris OLAP 的 key 列物理上恒为「升序存储、NULL 作最小值(nulls first)」,据此声明 scan
            List<OrderKey> keyOrderKeys = new ArrayList<>();
            for (Column keyCol : table.getBaseSchemaKeyColumns()) {
                SlotReference keySlot = findOutputSlotByColumn(scan.getOutput(), keyCol.getName());
                if (keySlot == null) {
                    break;
                }
                keyOrderKeys.add(new OrderKey(keySlot, /*isAsc=*/true, /*nullFirst=*/true));
            }
            if (!keyOrderKeys.isEmpty()) {
                tscan.setBase_table_order_spec(buildOrderSpec(keyOrderKeys));
            }
        }

        // scan 自有 distribution_spec:HASH 表 → kHashed + 分桶列 ColumnRef(getDistributionColumns
        DistributionInfo distInfo = table.getDefaultDistributionInfo();
        TDistributionSpec distSpec = new TDistributionSpec();
        if (distInfo instanceof HashDistributionInfo) {
            distSpec.setDistribution_type(TDistributionType.kHashed);
            distSpec.setBucket_num(distInfo.getBucketNum());
            List<TOperator> hashColumns = new ArrayList<>();
            for (Column distCol : ((HashDistributionInfo) distInfo).getDistributionColumns()) {
                SlotReference distSlot = findOutputSlotByColumn(scan.getOutput(), distCol.getName());
                if (distSlot != null) {
                    hashColumns.add(scalarTranslator.visitSlotReference(distSlot, null));
                }
            }
            distSpec.setHash_columns(hashColumns);
            // colocate 物理身份:属同一 stable colocate group 的所有 scan 拿同一个 grpId
            ColocateTableIndex colocateIndex = Env.getCurrentColocateIndex();
            long colocateGroupId = -1;
            if (colocateIndex.isColocateTable(table.getId())
                    && !colocateIndex.isGroupUnstable(colocateIndex.getGroup(table.getId()))) {
                colocateGroupId = colocateIndex.getGroup(table.getId()).grpId;
            }
            // FE 直接把整个 colocate 物理身份组装成 TDorisHashSpec 灌进 special_hash_spec
            TDorisHashSpec dorisHashSpec = new TDorisHashSpec();
            dorisHashSpec.setColocate_group_id(colocateGroupId);
            dorisHashSpec.setTable_id(table.getId());
            dorisHashSpec.setSelected_partition_ids(new ArrayList<>(scan.getSelectedPartitionIds()));
            TSpecialHashSpec specialHashSpec = new TSpecialHashSpec();
            specialHashSpec.setKind(TSpecialHashSpecKind.kDoris);
            specialHashSpec.setDoris_hash_spec(dorisHashSpec);
            distSpec.setSpecial_hash_spec(specialHashSpec);
        } else {
            distSpec.setDistribution_type(TDistributionType.kRandom);
        }
        tscan.setDistribution_spec(distSpec);

        buildCatalog(table, tscan.getDatabase_name(), tscan.getTable_name());

        TLogicalScan tLogicalScan = new TLogicalScan();
        tLogicalScan.setLogical_table_scan(tscan);
        opUnion.setLogical_scan(tLogicalScan);
        emit(scan, TOperatorType.kLogicalTableGet, opUnion, texprs);
        return null;
    }

    @Override
    public Void visitLogicalOneRowRelation(LogicalOneRowRelation oneRow, List<TExpression> texprs) {
        // SELECT 1, 'x' → LogicalValueGet, value_lists = [[lit, ...]]
        buildValueGet(oneRow, java.util.Collections.singletonList(oneRow.getProjects()), texprs);
        return null;
    }

    @Override
    public Void visitLogicalEmptyRelation(LogicalEmptyRelation emptyRelation, List<TExpression> texprs) {
        // SELECT * FROM t LIMIT 0 / WHERE false → LogicalValueGet, value_lists = []
        buildValueGet(emptyRelation, java.util.Collections.emptyList(), texprs);
        return null;
    }

    /** 两个 Doris constants-source 节点(OneRow / Empty)→ horn */
    private void buildValueGet(Plan plan, List<List<NamedExpression>> rows, List<TExpression> texprs) {
        TLogicalValueScan valueScan = new TLogicalValueScan();
        if (!rows.isEmpty()) {
            List<List<TOperator>> valueLists = new ArrayList<>(rows.size());
            for (List<NamedExpression> row : rows) {
                List<TOperator> rowCells = new ArrayList<>(row.size());
                for (NamedExpression cell : row) {
                    Expression literalExpr = Doris2HornUtils.unwrapExpr(cell);
                    rowCells.add(scalarTranslator.translate(literalExpr));
                }
                valueLists.add(rowCells);
            }
            valueScan.setValue_lists(valueLists);
        }
        List<TOperator> outputSchema = new ArrayList<>();
        for (Slot slot : plan.getOutput()) {
            if (slot instanceof SlotReference) {
                outputSchema.add(scalarTranslator.visitSlotReference((SlotReference) slot, null));
            }
        }
        valueScan.setOutput_list(outputSchema);

        TLogicalScan scanWrapper = new TLogicalScan();
        scanWrapper.setLogical_value_scan(valueScan);
        TOperatorUnion opUnion = new TOperatorUnion();
        opUnion.setLogical_scan(scanWrapper);
        emit(plan, TOperatorType.kLogicalValueGet, opUnion, texprs);
    }

    /** Build TTable (schema + col stats + table stats) for horn's StaticMataProvider */
    private void buildCatalog(OlapTable table, String dbName, String tblName) {
        TTable ttable = new TTable();
        ttable.setDb_name(dbName);
        ttable.setTbl_name(tblName);

        // 以下 3 个 thrift 字段 horn BE 接到后 ParseTableDescriptor 解析存进

        ConnectContext ctx = hornCtx.getCascadesContext().getConnectContext();
        StatisticsCache statsCache = Env.getCurrentEnv().getStatisticsCache();
        long catalogId = table.getDatabase().getCatalog().getId();
        long dbId = table.getDatabase().getId();
        long tblId = table.getId();
        long idxId = table.getBaseIndexId();

        List<Column> baseSchema = table.getBaseSchema();
        List<TColumn> columns = new ArrayList<>();
        boolean allColumnStatsAvailable = true;
        long tableRowCount = -1;
        for (int i = 0; i < baseSchema.size(); i++) {
            Column col = baseSchema.get(i);
            ColumnStatistic colStat = statsCache.getColumnStatistics(
                    catalogId, dbId, tblId, idxId, col.getName(), ctx);
            boolean columnStatsAvailable = colStat != null && colStat != ColumnStatistic.UNKNOWN;
            if (columnStatsAvailable && tableRowCount < 0) {
                tableRowCount = (long) colStat.count;
            } else if (!columnStatsAvailable) {
                allColumnStatsAvailable = false;
            }

            TColumn tcol = new TColumn();
            tcol.setColumnName(col.getName());
            tcol.setColumnType(DorisTypeToHornConverter.convertCatalogType(col.getType()));
            tcol.setPosition(i);
            if (columnStatsAvailable) {
                TColumnStats tcolStats = new TColumnStats();
                tcolStats.setAvg_size(colStat.avgSizeByte);
                tcolStats.setMax_size(col.getType().getLength());
                tcolStats.setNum_distinct_values((long) colStat.ndv);
                tcolStats.setNum_nulls((long) colStat.numNulls);
                TColumnValue low = new TColumnValue();
                low.setDouble_val(colStat.minValue);
                tcolStats.setLow_value(low);
                TColumnValue high = new TColumnValue();
                high.setDouble_val(colStat.maxValue);
                tcolStats.setHigh_value(high);
                tcol.setCol_stats(tcolStats);
            }
            columns.add(tcol);
        }
        ttable.setColumns(columns);

        TTableStats tableStats = new TTableStats();
        tableStats.setNum_rows(allColumnStatsAvailable ? tableRowCount : -1);
        ttable.setTable_stats(tableStats);

        // table_id / partition_num 不再传:都是 horn C++ 从不读的死字段
        hornCtx.addTable(ttable);
    }

    @Override
    public Void visitLogicalFilter(LogicalFilter<? extends Plan> filter, List<TExpression> texprs) {
        TOperatorUnion opUnion = new TOperatorUnion();
        TLogicalFilter tfilter = new TLogicalFilter();
        tfilter.setPredicate_list(
                scalarTranslator.translateList(new ArrayList<>(filter.getConjuncts())));
        opUnion.setLogical_filter(tfilter);
        emit(filter, TOperatorType.kLogicalSelect, opUnion, texprs);
        return null;
    }

    @Override
    public Void visitLogicalProject(LogicalProject<? extends Plan> project, List<TExpression> texprs) {
        buildProject(project, project.getProjects(), project.getOutput(), texprs);
        return null;
    }

    /** identity Project,sink.outputExprs 即 SELECT 列序、对应 Project 的 */
    @Override
    public Void visitLogicalResultSink(LogicalResultSink<? extends Plan> sink, List<TExpression> texprs) {
        buildProject(sink, sink.getOutputExprs(), sink.getOutput(), texprs);
        return null;
    }

    /** 把 plan 当成 LogicalProject emit:projects 写 project_list、output 写 */
    private void buildProject(Plan plan, List<NamedExpression> projects,
                                       List<Slot> output, List<TExpression> texprs) {
        TLogicalProject projectOp = new TLogicalProject();
        projectOp.setProject_list(scalarTranslator.translateList(projects));
        List<TOperator> rewriteList = new ArrayList<>();
        for (Slot slot : output) {
            if (slot instanceof SlotReference) {
                rewriteList.add(scalarTranslator.visitSlotReference((SlotReference) slot, null));
            }
        }
        projectOp.setRewrite_list(rewriteList);
        TOperatorUnion opUnion = new TOperatorUnion();
        opUnion.setLogical_project(projectOp);
        emit(plan, TOperatorType.kLogicalProject, opUnion, texprs);
    }

    /** Doris LogicalAggregate → horn TLogicalAgg(单层 forward,horn cascades */
    @Override
    public Void visitLogicalAggregate(LogicalAggregate<? extends Plan> agg, List<TExpression> texprs) {
        // 由 visitLogicalRepeat 单独 emit TLogicalGrouping,所以这里不再 fallback
        if (hasDistinctAggFn(agg)) {
            LogicalAggregate<? extends Plan> converted = MultiDistinctFunctionStrategy.rewrite(agg);
            if (hasDistinctAggFn(converted)) {
                throw new UnsupportedOperationException(
                        "Horn: distinct agg fn not convertible to multi_distinct");
            }
            return converted.accept(this, texprs);
        }
        TLogicalAgg tagg = new TLogicalAgg();
        tagg.setGroup_by_exprs(scalarTranslator.translateList(agg.getGroupByExpressions()));
        // 从 outputExpressions 抽出 AggregateFunction(通常包在 Alias 里),逐个
        List<TOperator> aggFuncs = new ArrayList<>();
        List<TOperator> aggComputedCols = new ArrayList<>();
        for (NamedExpression out : agg.getOutputExpressions()) {
            Expression inner = Doris2HornUtils.unwrapExpr(out);
            if (inner instanceof AggregateFunction) {
                aggFuncs.add(scalarTranslator.translate(inner));
                aggComputedCols.add(scalarTranslator.visitSlotReference(
                        (SlotReference) out.toSlot(), null));
            }
        }
        tagg.setAggregate_functions(aggFuncs);
        tagg.setAgg_computed_cols(aggComputedCols);
        tagg.setIs_split(false);
        TOperatorUnion opUnion = new TOperatorUnion();
        opUnion.setLogical_agg(tagg);
        emit(agg, TOperatorType.kLogicalAgg, opUnion, texprs);
        return null;
    }

    /** 降级成 {@code Agg(group by 全列) → UnionAll},故到此 qualifier 恒为 ALL;且经 */
    @Override
    public Void visitLogicalUnion(LogicalUnion union, List<TExpression> texprs) {
        if (union.getQualifier() != Qualifier.ALL) {
            throw new UnsupportedOperationException(
                    "Horn: non-ALL union should have been lowered by BuildAggForUnion");
        }
        // 纯常量 union(0 孩子、仅 constantExprsList,如 SELECT 1 UNION ALL SELECT 2)暂不支持:
        if (union.children().isEmpty()) {
            throw new UnsupportedOperationException(
                    "Horn: constant-only union (no child) not supported (cc StatisticsDerive SIGSEGV)");
        }
        emitSetOp(union, TOperatorType.kLogicalNarrayUnionAll, texprs);
        return null;
    }

    /** MergeSetOperations 可把同型 intersect 合并成 N 元单节点;cc 的 implement xform 已改 PatternMultiLeaf */
    @Override
    public Void visitLogicalIntersect(LogicalIntersect intersect, List<TExpression> texprs) {
        // intersect 恒 DISTINCT(Doris 绑定期拒 ALL);防御性断言,非 DISTINCT → fallback。
        if (intersect.getQualifier() != Qualifier.DISTINCT) {
            throw new UnsupportedOperationException(
                    "Horn: intersect must be DISTINCT (Doris rejects ALL at binding)");
        }
        emitSetOp(intersect, TOperatorType.kLogicalIntersect, texprs);
        return null;
    }

    /** Doris LogicalExcept → horn TLogicalSetOp(op_type=kLogicalExcept) */
    @Override
    public Void visitLogicalExcept(LogicalExcept except, List<TExpression> texprs) {
        // except 恒 DISTINCT(Doris 绑定期拒 ALL);防御性断言,非 DISTINCT → fallback。
        if (except.getQualifier() != Qualifier.DISTINCT) {
            throw new UnsupportedOperationException(
                    "Horn: except must be DISTINCT (Doris rejects ALL at binding)");
        }
        emitSetOp(except, TOperatorType.kLogicalExcept, texprs);
        return null;
    }

    /** 所有 N 元 set op(UNION ALL / INTERSECT / EXCEPT)共用:建 TLogicalSetOp(children_output_list) */
    private void emitSetOp(LogicalSetOperation setOp, TOperatorType opType,
                           List<TExpression> texprs) {
        TLogicalSetOp tsetop = new TLogicalSetOp();
        List<List<TOperator>> childrenOut = new ArrayList<>();
        for (List<SlotReference> oneChild : setOp.getRegularChildrenOutputs()) {
            childrenOut.add(scalarTranslator.translateList(oneChild));
        }
        tsetop.setChildren_output_list(childrenOut);
        if (setOp instanceof LogicalUnion) {
            List<List<NamedExpression>> constExprsList = ((LogicalUnion) setOp).getConstantExprsList();
            if (!constExprsList.isEmpty()) {
                List<List<TOperator>> constRows = new ArrayList<>();
                for (List<NamedExpression> row : constExprsList) {
                    constRows.add(scalarTranslator.translateList(row));
                }
                tsetop.setChildren_const_expr_list(constRows);
            }
        }
        TOperatorUnion opUnion = new TOperatorUnion();
        opUnion.setLogical_set_op(tsetop);
        emit(setOp, opType, opUnion, texprs);
    }

    /** NormalizeRepeat 已 desugar: */
    @Override
    public Void visitLogicalRepeat(LogicalRepeat<? extends Plan> repeat, List<TExpression> texprs) {
        // 1. grouping_set_list
        List<List<TOperator>> tGroupingSets = new ArrayList<>(repeat.getGroupingSets().size());
        for (List<Expression> set : repeat.getGroupingSets()) {
            tGroupingSets.add(scalarTranslator.translateList(set));
        }

        // 2. grouping_expr_list = flatten distinct
        Set<Expression> groupingExprSet = new LinkedHashSet<>();
        for (List<Expression> set : repeat.getGroupingSets()) {
            groupingExprSet.addAll(set);
        }
        List<TOperator> tGroupingExprs = new ArrayList<>(groupingExprSet.size());
        for (Expression e : groupingExprSet) {
            tGroupingExprs.add(scalarTranslator.translate(e));
        }

        // 3
        SlotReference groupingIdSlot = repeat.getGroupingId().get();
        TOperator tGroupingIdFunc = scalarTranslator.visitSlotReference(groupingIdSlot, null);

        // agg_fun_input_cols:repeat.outputExpressions 里「裸 SlotReference 且不是 grouping key」的列,
        boolean hasGroupingPrefix = false;
        List<TOperator> tAggFunInputCols = new ArrayList<>();
        for (NamedExpression out : repeat.getOutputExpressions()) {
            Expression inner = Doris2HornUtils.unwrapExpr(out);
            if (inner instanceof SlotReference && !groupingExprSet.contains(inner)) {
                tAggFunInputCols.add(scalarTranslator.visitSlotReference((SlotReference) inner, null));
            } else if (inner instanceof GroupingScalarFunction) {
                hasGroupingPrefix = true;
            }
        }

        TLogicalGrouping tlg = new TLogicalGrouping();
        tlg.setGrouping_set_list(tGroupingSets);
        tlg.setGrouping_expr_list(tGroupingExprs);
        tlg.setGrouping_id_func(Lists.newArrayList(tGroupingIdFunc));
        if (!tAggFunInputCols.isEmpty()) {
            tlg.setAgg_fun_input_cols(tAggFunInputCols);
        }
        TOperatorUnion opUnion = new TOperatorUnion();
        opUnion.setLogical_grouping(tlg);

        // 装 outputExpressions(含 `Grouping(..) AS PREFIX`,visitAlias 剥到函数)+ GROUPING_ID 透传,rewrite_list 是其
        if (!hasGroupingPrefix) {
            emit(repeat, TOperatorType.kLogicalGrouping, opUnion, texprs);
            return null;
        }
        List<NamedExpression> projects = new ArrayList<>(repeat.getOutputExpressions());
        projects.add(groupingIdSlot);
        List<TOperator> projectOutput = new ArrayList<>();
        for (NamedExpression project : projects) {
            projectOutput.add(scalarTranslator.visitSlotReference((SlotReference) project.toSlot(), null));
        }
        TLogicalProject projectOp = new TLogicalProject();
        projectOp.setProject_list(scalarTranslator.translateList(projects));
        projectOp.setRewrite_list(projectOutput);
        TOperatorUnion projectUnion = new TOperatorUnion();
        projectUnion.setLogical_project(projectOp);

        // pre-order:先手 add Project texpr(arity=1,不递归),再 emit grouping(emit 自带递归到 child),自然形成
        TExpression projectTexpr = new TExpression();
        TOperator projectTop = new TOperator();
        projectTop.setOp_type(TOperatorType.kLogicalProject);
        projectTop.setOp_union(projectUnion);
        projectTexpr.setOp(projectTop);
        projectTexpr.setArity(1);
        projectTexpr.setOutput_list(projectOutput);
        texprs.add(projectTexpr);
        emit(repeat, TOperatorType.kLogicalGrouping, opUnion, texprs);
        return null;
    }

    /** agg 的 outputExpressions 中是否含带 distinct 标志的 AggregateFunction */
    private boolean hasDistinctAggFn(LogicalAggregate<? extends Plan> agg) {
        for (NamedExpression out : agg.getOutputExpressions()) {
            Expression inner = Doris2HornUtils.unwrapExpr(out);
            if (inner instanceof AggregateFunction && ((AggregateFunction) inner).isDistinct()) {
                return true;
            }
        }
        return false;
    }

    /** <p>单层 forward 模板对齐 visitLogicalAggregate: */
    @Override
    public Void visitLogicalWindow(LogicalWindow<? extends Plan> logicalWindow, List<TExpression> texprs) {
        List<NamedExpression> windowExpressions = logicalWindow.getWindowExpressions();
        if (windowExpressions.isEmpty()) {
            throw new UnsupportedOperationException(
                    "Horn: LogicalWindow with empty windowExpressions");
        }

        // LogicalWindowToPhysicalWindow.createWindowFrameGroups(partition 比较忽略 key 顺序)
        List<WindowFrameGroup> sameSpecGroups =
                LogicalWindowToPhysicalWindow.createWindowFrameGroups(windowExpressions);
        if (sameSpecGroups.size() > 1) {
            Plan windowChain = logicalWindow.child();
            for (WindowFrameGroup sameSpecGroup : sameSpecGroups) {
                windowChain = new LogicalWindow<>(sameSpecGroup.getGroups(), windowChain);
            }
            windowChain.accept(this, texprs);
            return null;
        }

        // 走到这里 windowExpressions 必为同一 spec:createWindowFrameGroups 只返回 1 组,或多 spec
        WindowExpression leaderWindow =
                (WindowExpression) Doris2HornUtils.unwrapExpr(windowExpressions.get(0));
        List<Expression> partitionKeys = leaderWindow.getPartitionKeys();
        List<OrderExpression> orderKeys = leaderWindow.getOrderKeys();
        Optional<WindowFrame> windowFrame = leaderWindow.getWindowFrame();

        List<TOperator> analyticFunctions = new ArrayList<>(windowExpressions.size());
        List<TOperator> analyticComputedColumns = new ArrayList<>(windowExpressions.size());
        for (NamedExpression windowExpression : windowExpressions) {
            WindowExpression analyticFunction =
                    (WindowExpression) Doris2HornUtils.unwrapExpr(windowExpression);
            analyticFunctions.add(scalarTranslator.translate(analyticFunction.getFunction()));
            // Alias.toSlot() 携带 ExprId + name + type —— 跟 agg_computed_cols 同样路径。
            analyticComputedColumns.add(scalarTranslator.visitSlotReference(
                    (SlotReference) windowExpression.toSlot(), null));
        }

        TLogicalWindow logicalWindowThrift = new TLogicalWindow();
        logicalWindowThrift.setAnalytic_functions(analyticFunctions);
        logicalWindowThrift.setAnalytic_computed_cols(analyticComputedColumns);
        if (!partitionKeys.isEmpty()) {
            logicalWindowThrift.setPartition_exprs(scalarTranslator.translateList(partitionKeys));
        }
        if (!orderKeys.isEmpty()) {
            List<OrderKey> windowOrderKeys = new ArrayList<>(orderKeys.size());
            for (OrderExpression orderExpr : orderKeys) {
                windowOrderKeys.add(orderExpr.getOrderKey());
            }
            logicalWindowThrift.setOrder_by_exprs(buildOrderSpec(windowOrderKeys));
        }
        if (windowFrame.isPresent()) {
            // 直接调 visitWindowFrame 得到 TOperator(op_type=kWindowFrame, scalar union),
            logicalWindowThrift.setWindow_frame(Collections.singletonList(
                    scalarTranslator.visitWindowFrame(windowFrame.get(), null)));
        }

        TOperatorUnion operatorUnion = new TOperatorUnion();
        operatorUnion.setLogical_window(logicalWindowThrift);
        emit(logicalWindow, TOperatorType.kLogicalWindow, operatorUnion, texprs);
        return null;
    }

    // v7 (=v3) 路径:sort / limit / topn 全 emit 成 TLogicalLimit (带 order_spec / limit / offset)

    @Override
    public Void visitLogicalSort(LogicalSort<? extends Plan> sort, List<TExpression> texprs) {
        // sort wrapper:limit=-1 表示纯 ORDER BY,没有 split 概念
        buildLimit(sort, -1L, 0L, sort.getOrderKeys(), true, false, texprs);
        return null;
    }

    @Override
    public Void visitLogicalLimit(LogicalLimit<? extends Plan> limit, List<TExpression> texprs) {
        // Doris Nereids LogicalLimit 的 phase != ORIGIN 表明已经被 Nereids 端拆出
        boolean isSplit = limit.getPhase() != LimitPhase.ORIGIN;
        boolean isGlobal = limit.getPhase() != LimitPhase.LOCAL;
        buildLimit(limit, limit.getLimit(), limit.getOffset(), null, isGlobal, isSplit, texprs);
        return null;
    }

    @Override
    public Void visitLogicalTopN(LogicalTopN<? extends Plan> topn, List<TExpression> texprs) {
        // Doris LogicalTopN 没有 phase 字段 — 单一形态,相当于 ORIGIN,is_split=false
        buildLimit(topn, topn.getLimit(), topn.getOffset(), topn.getOrderKeys(), true, false, texprs);
        return null;
    }

    /** 公共 emit helper:visitLogicalSort/Limit/TopN 三个变体共用同一份 TLogicalLimit wire */
    private void buildLimit(Plan plan, long limit, long offset,
                            List<OrderKey> orderKeys, boolean isGlobal, boolean isSplit,
                            List<TExpression> texprs) {
        TLogicalLimit tlimit = new TLogicalLimit();
        tlimit.setLimit(limit);
        tlimit.setOffset(offset);
        tlimit.setIs_global_limit(isGlobal);
        tlimit.setIs_split(isSplit);
        if (orderKeys != null && !orderKeys.isEmpty()) {
            tlimit.setOrder_spec(buildOrderSpec(orderKeys));
        }
        TOperatorUnion opUnion = new TOperatorUnion();
        opUnion.setLogical_limit(tlimit);
        emit(plan, TOperatorType.kLogicalLimit, opUnion, texprs);
    }

    /** 在 scan 输出里按列名(忽略大小写)找对应 SlotReference;找不到返回 null */
    private SlotReference findOutputSlotByColumn(List<Slot> outputSlots, String columnName) {
        for (Slot slot : outputSlots) {
            if (slot instanceof SlotReference) {
                Optional<Column> originalColumn = ((SlotReference) slot).getOriginalColumn();
                if (originalColumn.isPresent()
                        && originalColumn.get().getName().equalsIgnoreCase(columnName)) {
                    return (SlotReference) slot;
                }
            }
        }
        return null;
    }

    /** 把 Doris OrderKey 列表翻成 horn TOrderSpec (parallel lists 形态) */
    private TOrderSpec buildOrderSpec(List<OrderKey> orderKeys) {
        TOrderSpec orderSpec = new TOrderSpec();
        List<TOperator> exprList = new ArrayList<>(orderKeys.size());
        List<Boolean> ascList = new ArrayList<>(orderKeys.size());
        List<Boolean> nullsFirstList = new ArrayList<>(orderKeys.size());
        for (OrderKey orderKey : orderKeys) {
            exprList.add(scalarTranslator.translate(orderKey.getExpr()));
            ascList.add(orderKey.isAsc());
            nullsFirstList.add(orderKey.isNullFirst());
        }
        orderSpec.setOrder_by_exprs(exprList);
        orderSpec.setIs_asc_order(ascList);
        orderSpec.setNulls_first(nullsFirstList);
        return orderSpec;
    }

    @Override
    public Void visitLogicalJoin(LogicalJoin<? extends Plan, ? extends Plan> join, List<TExpression> texprs) {
        // 全部 10 种 Doris JoinType 映射到对应 horn logical join op_type
        TOperatorType opType;
        switch (join.getJoinType()) {
            case INNER_JOIN:
                opType = TOperatorType.kLogicalInnerJoin;
                break;
            case CROSS_JOIN:
                opType = TOperatorType.kLogicalCrossJoin;
                break;
            case LEFT_OUTER_JOIN:
                opType = TOperatorType.kLogicalLeftOuterJoin;
                break;
            case RIGHT_OUTER_JOIN:
                opType = TOperatorType.kLogicalRightOuterJoin;
                break;
            case FULL_OUTER_JOIN:
                opType = TOperatorType.kLogicalFullOuterJoin;
                break;
            case LEFT_SEMI_JOIN:
                opType = TOperatorType.kLogicalLeftSemiJoin;
                break;
            case RIGHT_SEMI_JOIN:
                opType = TOperatorType.kLogicalRightSemiJoin;
                break;
            case LEFT_ANTI_JOIN:
                opType = TOperatorType.kLogicalLeftAntiSemiJoin;
                break;
            case RIGHT_ANTI_JOIN:
                opType = TOperatorType.kLogicalRightAntiSemiJoin;
                break;
            case NULL_AWARE_LEFT_ANTI_JOIN:
                // null-aware mark join(NOT IN)的 null 传播语义 horn 不实现 → fallback Nereids。
                if (join.isMarkJoin()) {
                    throw new UnsupportedOperationException(
                            "Horn: null-aware mark join (NOT IN) not supported");
                }
                opType = TOperatorType.kLogicalNullAwareLeftAntiSemiJoin;
                break;
            default:
                throw new UnsupportedOperationException(
                        "Horn: unsupported join type: " + join.getJoinType());
        }
        TOperatorUnion opUnion = new TOperatorUnion();
        TLogicalJoin tjoin = new TLogicalJoin();
        if (!join.getHashJoinConjuncts().isEmpty()) {
            // 等值 join 条件 (hash join 用)
            tjoin.setEqual_join_predicate_list(
                    scalarTranslator.translateList(new ArrayList<>(join.getHashJoinConjuncts())));
        }
        if (!join.getOtherJoinConjuncts().isEmpty()) {
            // 非等值 / 后置 join 条件 (post-match filter)
            tjoin.setOther_join_predicate_list(
                    scalarTranslator.translateList(new ArrayList<>(join.getOtherJoinConjuncts())));
        }
        // mark join:emit mark_slot(MarkJoinSlotReference 是 SlotReference 子类,visitSlotReference
        if (join.isMarkJoin()) {
            // mark_slot 用单元素 list(同 grouping_id_func)规避 thrift TOperator 递归。
            tjoin.setMark_slot(Lists.newArrayList(scalarTranslator.visitSlotReference(
                    join.getMarkJoinSlotReference().get(), null)));
            if (!join.getMarkJoinConjuncts().isEmpty()) {
                tjoin.setMark_join_predicate_list(
                        scalarTranslator.translateList(new ArrayList<>(join.getMarkJoinConjuncts())));
            }
        }
        opUnion.setLogical_join(tjoin);
        // emit 内 plan.children() = [left, right] 顺序 (BinaryPlan)
        emit(join, opType, opUnion, texprs);
        return null;
    }
}