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);
+ }
+
+ /**
+ * 构建 TTable(schema + col/table stats)供 horn StaticMataProvider,并注册到
+ * {@link HornOptimizationContext#addTable}。
+ * num_rows 规则:所有列都有真实 stats 才传 num_rows>0(stats-full 路径),任一列缺
+ * stats 则 num_rows=-1(no-stats 降级,避免 StatisticsDerive 崩溃);row count 取 colStat.count。
+ * colStat unknown 的列不 set TColumn.col_stats(optional,unset 合法)。
+ */
+ private void buildCatalog(OlapTable table, String dbName, String tblName) {
+ TTable ttable = new TTable();
+ ttable.setDb_name(dbName);
+ ttable.setTbl_name(tblName);
+
+ // clustering_columns / virtual_columns / schema_id 三个字段 horn 端目前无消费者,
+ // FE 不传无影响(schema_id 由 BE 用 -1 通配符匹配);接入 partition pruning / schema
+ // versioning 时再补。
+
+ 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 baseSchema = table.getBaseSchema();
+ List 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++ 从不读的死字段。colocate 身份
+ // (含 table_id)在 scan distribution_spec.special_hash_spec(TDorisHashSpec)里。
+ // 详见 horn/docs/extended4/36 §9。
+ hornCtx.addTable(ttable);
+ }
+
+ @Override
+ public Void visitLogicalFilter(LogicalFilter extends Plan> filter, List 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 texprs) {
+ buildProject(project, project.getProjects(), project.getOutput(), texprs);
+ return null;
+ }
+
+ /**
+ * LogicalResultSink → horn identity Project:sink.outputExprs 即 SELECT 列序(project_list),
+ * sink.getOutput() 对应 rewrite_list;emit 后递归 sink.child(0)。
+ */
+ @Override
+ public Void visitLogicalResultSink(LogicalResultSink extends Plan> sink, List texprs) {
+ buildProject(sink, sink.getOutputExprs(), sink.getOutput(), texprs);
+ return null;
+ }
+
+ /**
+ * 把 plan 当 LogicalProject emit:projects 写 project_list、output 写 rewrite_list(限
+ * SlotReference)。visitLogicalProject 与 visitLogicalResultSink 共用。
+ */
+ private void buildProject(Plan plan, List projects,
+ List output, List texprs) {
+ TLogicalProject projectOp = new TLogicalProject();
+ projectOp.setProject_list(scalarTranslator.translateList(projects));
+ List 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 的 XformSplitAgg
+ * 自己拆 Local/Global)。HAVING 已被 RBO 上提为独立 LogicalFilter,不在此处理。
+ */
+ @Override
+ public Void visitLogicalAggregate(LogicalAggregate extends Plan> agg, List texprs) {
+ // distinct 处理:用 MultiDistinctFunctionStrategy.rewrite 把 count/sum(DISTINCT x) 转成
+ // multi_distinct_count/sum(x)(其 BUFFER 天然支持两阶段,horn 当普通 agg fn 走 SplitAgg);
+ // 转换后仍残留 distinct 的(avg(DISTINCT) 等)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 里)emit 成 TScalarAggFn;
+ // agg_computed_cols 平行存对应输出列(Alias slot,带 name + ExprId),BE 按位对齐注册 slot_map。
+ List aggFuncs = new ArrayList<>();
+ List 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);
+ // FE 进来的 agg 恒未拆分;BE 据此把 agg_type_ 定成 kLocal,消 UB。
+ tagg.setIs_split(false);
+ TOperatorUnion opUnion = new TOperatorUnion();
+ opUnion.setLogical_agg(tagg);
+ emit(agg, TOperatorType.kLogicalAgg, opUnion, texprs);
+ return null;
+ }
+
+ /**
+ * Doris LogicalUnion → horn TLogicalSetOp(kLogicalNarrayUnionAll)。前提:UNION DISTINCT 已被
+ * BuildAggForUnion 降级为 Agg + UnionAll,故 qualifier 恒 ALL,且经 MergeSetOperations 扁平成 N 元。
+ * 字段:output_list 由 emit 设进 TExpression.output_list;children_output_list 每孩子按列对齐;
+ * children_const_expr_list 为常量 SELECT 行(可空)。
+ */
+ @Override
+ public Void visitLogicalUnion(LogicalUnion union, List texprs) {
+ // 只支持 UNION ALL;distinct 必已被 BuildAggForUnion 降级。
+ if (union.getQualifier() != Qualifier.ALL) {
+ throw new UnsupportedOperationException(
+ "Horn: non-ALL union should have been lowered by BuildAggForUnion");
+ }
+ // 纯常量 union(0 孩子、仅 constantExprsList)暂不支持:cc 端 StatisticsDerive 对 0 孩子
+ // SIGSEGV。fallback 回 Nereids。
+ 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;
+ }
+
+ /**
+ * Doris LogicalIntersect → horn TLogicalSetOp(kLogicalIntersect)。MergeSetOperations 可合并成
+ * N 元;intersect 恒 DISTINCT(Doris 绑定期拒 ALL),无 constantExprsList。
+ */
+ @Override
+ public Void visitLogicalIntersect(LogicalIntersect intersect, List 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(kLogicalExcept)。不被 MergeSetOperations 合并,恒嵌套
+ * 二元;恒 DISTINCT,无 constantExprsList。
+ */
+ @Override
+ public Void visitLogicalExcept(LogicalExcept except, List 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;
+ }
+
+ /**
+ * 所有 set op(UNION ALL / INTERSECT / EXCEPT)共用:建 TLogicalSetOp(children_output_list) → emit。
+ * qualifier / empty-children guard 由各 visitX 自负;const-expr 仅 LogicalUnion 有。
+ */
+ private void emitSetOp(LogicalSetOperation setOp, TOperatorType opType,
+ List texprs) {
+ TLogicalSetOp tsetop = new TLogicalSetOp();
+ List> childrenOut = new ArrayList<>();
+ for (List oneChild : setOp.getRegularChildrenOutputs()) {
+ childrenOut.add(scalarTranslator.translateList(oneChild));
+ }
+ tsetop.setChildren_output_list(childrenOut);
+ if (setOp instanceof LogicalUnion) {
+ List> constExprsList = ((LogicalUnion) setOp).getConstantExprsList();
+ if (!constExprsList.isEmpty()) {
+ List> constRows = new ArrayList<>();
+ for (List 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);
+ }
+
+ /**
+ * Doris LogicalRepeat → horn TLogicalGrouping。NormalizeRepeat 已 desugar:groupingSets expr
+ * 重写为 nullable、GROUPING_ID 已填入 repeat.getGroupingId()、Grouping/GroupingId 函数上提外层 Agg。
+ * 共用列重映射(col_to_grouping_col_map)由 cc 端 uid 反查源表达式自行派生,FE 不去重。
+ */
+ @Override
+ public Void visitLogicalRepeat(LogicalRepeat extends Plan> repeat, List texprs) {
+ // 1. grouping_set_list
+ List> tGroupingSets = new ArrayList<>(repeat.getGroupingSets().size());
+ for (List set : repeat.getGroupingSets()) {
+ tGroupingSets.add(scalarTranslator.translateList(set));
+ }
+
+ // 2. grouping_expr_list = flatten distinct
+ Set groupingExprSet = new LinkedHashSet<>();
+ for (List set : repeat.getGroupingSets()) {
+ groupingExprSet.addAll(set);
+ }
+ List tGroupingExprs = new ArrayList<>(groupingExprSet.size());
+ for (Expression e : groupingExprSet) {
+ tGroupingExprs.add(scalarTranslator.translate(e));
+ }
+
+ // 3. grouping_id_func:NormalizeRepeat 已无条件注入 GROUPING_ID 虚拟列,故 groupingId 恒 present。
+ SlotReference groupingIdSlot = repeat.getGroupingId().get();
+ TOperator tGroupingIdFunc = scalarTranslator.visitSlotReference(groupingIdSlot, null);
+
+ // agg_fun_input_cols:repeat.outputExpressions 里「裸 SlotReference 且非 grouping key」的列,
+ // 即上层 Agg 用到的 child 列(instanceof SlotReference && !groupingExprSet.contains)。
+ // 顺带标记有无 GROUPING_PREFIX 生产者(GroupingScalarFunction),下面据此决定是否套 Project。
+ boolean hasGroupingPrefix = false;
+ List 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);
+
+ // GROUPING() 函数支持:grouping 上套普通 Project 透传原始 Grouping 函数(project_list 装
+ // outputExpressions + GROUPING_ID,rewrite_list 是输出 slot),forward 注册 PREFIX slot 消 fallback,
+ // backward 再还原进 Repeat。无 GROUPING() → 直接 emit grouping。
+ if (!hasGroupingPrefix) {
+ emit(repeat, TOperatorType.kLogicalGrouping, opUnion, texprs);
+ return null;
+ }
+ List projects = new ArrayList<>(repeat.getOutputExpressions());
+ projects.add(groupingIdSlot);
+ List 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(自带递归 child),
+ // 形成 Project → grouping → 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;
+ }
+
+ /**
+ * Doris LogicalWindow → horn TLogicalWindow(模板对齐 visitLogicalAggregate)。
+ * 每个 WindowExpression 的 getFunction() → analytic_functions,对应输出 slot → analytic_computed_cols
+ * (BE 按位对齐注册 slot_map)。partition_exprs / order_by_exprs / window_frame 取 leader spec。
+ * 多 spec 由下面按等价类拆成单 spec window 链逐个翻译。
+ */
+ @Override
+ public Void visitLogicalWindow(LogicalWindow extends Plan> logicalWindow, List texprs) {
+ List windowExpressions = logicalWindow.getWindowExpressions();
+ if (windowExpressions.isEmpty()) {
+ throw new UnsupportedOperationException(
+ "Horn: LogicalWindow with empty windowExpressions");
+ }
+
+ // 多 spec 拆分:按 (partition, order, frame) 等价类分组(复用 createWindowFrameGroups),
+ // 多组则重写成单 spec LogicalWindow 链、逐个走单 spec 翻译。
+ List 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,partition/order/frame 取第一项套用整个 TLogicalWindow,
+ // 逐项收集函数体 + 输出列。窗口列恒为 Alias(WindowExpression)。
+ WindowExpression leaderWindow =
+ (WindowExpression) Doris2HornUtils.unwrapExpr(windowExpressions.get(0));
+ List partitionKeys = leaderWindow.getPartitionKeys();
+ List orderKeys = leaderWindow.getOrderKeys();
+ Optional windowFrame = leaderWindow.getWindowFrame();
+
+ List analyticFunctions = new ArrayList<>(windowExpressions.size());
+ List 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 windowOrderKeys = new ArrayList<>(orderKeys.size());
+ for (OrderExpression orderExpr : orderKeys) {
+ windowOrderKeys.add(orderExpr.getOrderKey());
+ }
+ logicalWindowThrift.setOrder_by_exprs(buildOrderSpec(windowOrderKeys));
+ }
+ if (windowFrame.isPresent()) {
+ // 直接调 visitWindowFrame 得到 TOperator(kWindowFrame),不走 translate()
+ // (它会把 frame 包成 fscalar,BE 期望直接的 TScalar)。
+ 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;
+ }
+
+ // sort / limit / topn 全 emit 成 TLogicalLimit(order_spec / limit / offset):
+ // 纯 sort → limit=-1,offset=0,order_spec=keys;纯 limit → limit/offset,order_spec=null;
+ // topn → limit/offset + order_spec=keys。
+
+ @Override
+ public Void visitLogicalSort(LogicalSort extends Plan> sort, List 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 texprs) {
+ // phase != ORIGIN 表明已被 Nereids 拆出 Local/Global → is_split=true,BE 不再二次 split;
+ // phase=LOCAL 时 is_global_limit=false,否则 true。
+ 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 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 orderKeys, boolean isGlobal, boolean isSplit,
+ List 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;供
+ * base_table_order_spec 与 distribution_spec 把 key/分桶列对应到 scan 输出 slot。
+ * 按名字匹配对齐 mainline(列名比 length/type 稳定),匹配 getOriginalColumn().getName()。 */
+ private SlotReference findOutputSlotByColumn(List outputSlots, String columnName) {
+ for (Slot slot : outputSlots) {
+ if (slot instanceof SlotReference) {
+ Optional 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 orderKeys) {
+ TOrderSpec orderSpec = new TOrderSpec();
+ List exprList = new ArrayList<>(orderKeys.size());
+ List ascList = new ArrayList<>(orderKeys.size());
+ List 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 texprs) {
+ // 全部 10 种 Doris JoinType 映射到对应 horn logical join op_type。
+ // Mark join(IN / EXISTS-OR 改写成 LEFT_SEMI mark join):horn 不重排 semi,round-trip 即可;
+ // 唯独 NULL_AWARE_LEFT_ANTI(NOT IN)的 null-aware mark 语义 horn 不实现 → fallback。
+ 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 携带 external_slot_id=ExprId)。
+ // markJoinConjuncts 非空时一并 emit;cc 端注册 uid 供上层 Filter 引用 $c$N 时 Resolve。
+ 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;
+ }
+}
diff --git a/fe/fe-core/src/main/java/org/apache/doris/horn/doris2horn/HornScalarThriftBuilder.java b/fe/fe-core/src/main/java/org/apache/doris/horn/doris2horn/HornScalarThriftBuilder.java
new file mode 100644
index 00000000000000..ce054e4266589d
--- /dev/null
+++ b/fe/fe-core/src/main/java/org/apache/doris/horn/doris2horn/HornScalarThriftBuilder.java
@@ -0,0 +1,573 @@
+// 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 {
+
+ /** 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 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 translateList(List extends Expression> exprs) {
+ List 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 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"
+ * (对齐 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);
+ }
+
+}
diff --git a/fe/fe-core/src/main/java/org/apache/doris/horn/horn2doris/DorisExpressionBuilder.java b/fe/fe-core/src/main/java/org/apache/doris/horn/horn2doris/DorisExpressionBuilder.java
new file mode 100644
index 00000000000000..9bf1adcbbbe713
--- /dev/null
+++ b/fe/fe-core/src/main/java/org/apache/doris/horn/horn2doris/DorisExpressionBuilder.java
@@ -0,0 +1,485 @@
+// 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.horn2doris;
+
+
+import org.apache.doris.horn.HornOptimizationContext;
+import org.apache.doris.horn.doris2horn.HornScalarThriftBuilder;
+import org.apache.doris.nereids.trees.expressions.Add;
+import org.apache.doris.nereids.trees.expressions.And;
+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.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.Slot;
+import org.apache.doris.nereids.trees.expressions.SlotReference;
+import org.apache.doris.nereids.trees.expressions.Subtract;
+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.scalar.Grouping;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.GroupingId;
+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.DateV2Literal;
+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.Literal;
+import org.apache.doris.nereids.trees.expressions.literal.NullLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.StringLiteral;
+import org.apache.doris.nereids.types.DataType;
+import org.apache.doris.nereids.types.DecimalV3Type;
+import org.apache.doris.nereids.types.FloatType;
+
+import com.google.common.collect.ImmutableList;
+import org.apache.horn4j.thrift.TArithmeticOperatorType;
+import org.apache.horn4j.thrift.TLiteralValue;
+import org.apache.horn4j.thrift.TOperator;
+import org.apache.horn4j.thrift.TOperatorType;
+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.TScalarWindowFrame;
+import org.apache.horn4j.thrift.TWindowType;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Translate Horn TOperator (TScalar expressions) back to Doris Nereids Expressions.
+ * 不支持的 TOperator / 字面值类型一律抛 {@link UnsupportedOperationException},
+ * 由 caller fallback 到 Nereids 主路径。
+ */
+public class DorisExpressionBuilder {
+
+ private static final Logger LOG = LogManager.getLogger(DorisExpressionBuilder.class);
+
+ /**
+ * 反译共享状态:ColumnRef 反译时按 unique_id 查
+ * {@link HornOptimizationContext#getScalarUidToSlot()} 复用同一 Slot 实例,
+ * 保持 ExprId 跨 plan 节点一致。
+ */
+ private final HornOptimizationContext hornCtx;
+
+ public DorisExpressionBuilder(HornOptimizationContext hornCtx) {
+ this.hornCtx = hornCtx;
+ }
+
+ public Expression translate(TOperator op) {
+ return translateHelper(op);
+ }
+
+ public List translateList(List ops) {
+ List result = new ArrayList<>();
+ if (ops == null) {
+ return result;
+ }
+ for (TOperator op : ops) {
+ result.add(translate(op));
+ }
+ return result;
+ }
+
+
+ private Expression translateHelper(TOperator op) {
+ // BE output can be either fscalar (pre-order flattened) or scalar (single node);
+ // fscalar must be rebuilt into a tree before translating.
+ if (op.getOp_union().isSetFscalar()) {
+ List scalars = op.getOp_union().getFscalar().getScalars();
+ if (scalars != null && !scalars.isEmpty()) {
+ return translateFlattenedScalar(scalars, new AtomicInteger(0));
+ }
+ }
+
+ TOperatorType opType = op.getOp_type();
+ if (!op.getOp_union().isSetScalar()) {
+ throw new IllegalArgumentException(
+ "Horn expr translator: TOperator has no scalar data, type=" + opType);
+ }
+
+ TScalar scalar = op.getOp_union().getScalar();
+
+ List children = new ArrayList<>();
+ if (scalar.isSetChildren()) {
+ for (TOperator childOp : scalar.getChildren()) {
+ children.add(translateHelper(childOp));
+ }
+ }
+
+ return translateScalarNode(opType, scalar, children);
+ }
+
+ /**
+ * Rebuild an expression tree from a pre-order flattened TScalar list.
+ * Inverse of BE Scalar::FromFlattenedThrift.
+ */
+ private Expression translateFlattenedScalar(List scalars, AtomicInteger idx) {
+ TOperator op = scalars.get(idx.getAndIncrement());
+ TScalar scalar = op.getOp_union().getScalar();
+ int arity = (int) scalar.getArity();
+
+ List children = new ArrayList<>();
+ for (int i = 0; i < arity; i++) {
+ children.add(translateFlattenedScalar(scalars, idx));
+ }
+ return translateScalarNode(op.getOp_type(), scalar, children);
+ }
+
+ private Expression translateScalarNode(TOperatorType opType, TScalar scalar,
+ List children) {
+ // resultType lazy:只有 kLiteral / kScalarFnCall 真正需要 evaluated 类型。
+ // kWindowFrame 的 data_type 是 INVALID_TYPE,eager convert 会抛异常。
+ switch (opType) {
+ case kColumnRef:
+ return translateColumnRef(scalar);
+ case kLiteral:
+ return translateLiteral(scalar,
+ HornTypeToDorisConverter.convert(scalar.getData_type()));
+ case kBinaryPredicate:
+ return translateBinaryPredicate(scalar, children);
+ case kAndPredicate:
+ return new And(children);
+ case kOrPredicate:
+ return new Or(children);
+ case kArithmetic:
+ return translateArithmetic(scalar, children);
+ case kScalarFnCall:
+ return translateScalarFnCall(scalar, children,
+ HornTypeToDorisConverter.convert(scalar.getData_type()));
+ case kAggFn:
+ return translateAggFn(scalar, children);
+ case kWindowFrame:
+ // WindowFrame is-a Expression,统一从 translateScalarNode 分发。
+ // boundary 在 union 字段(window_start/window_end),由 buildWindowFrame 读取。
+ return buildWindowFrame(scalar);
+ default:
+ throw new UnsupportedOperationException(
+ "Horn expr translator: unsupported operator type " + opType);
+ }
+ }
+
+ private Expression translateArithmetic(TScalar scalar, List children) {
+ TScalarArithmetic arith = scalar.getScalar_union().getScalar_arithmetic();
+ Expression left = children.get(0);
+ Expression right = children.get(1);
+ TArithmeticOperatorType opType = arith.getArithmetic_op_type();
+ switch (opType) {
+ case kAdd:
+ return new Add(left, right);
+ case kSubtract:
+ return new Subtract(left, right);
+ case kMultiply:
+ return new Multiply(left, right);
+ case kDivide:
+ return new Divide(left, right);
+ case kMod:
+ return new Mod(left, right);
+ case kIntDivide:
+ return new IntegralDivide(left, right);
+ case kBitAnd:
+ return new BitAnd(left, right);
+ case kBitOr:
+ return new BitOr(left, right);
+ case kBitXor:
+ return new BitXor(left, right);
+ default:
+ throw new UnsupportedOperationException(
+ "Horn expr translator: unsupported arithmetic op " + opType);
+ }
+ }
+
+ /**
+ * Reverse of {@link HornScalarThriftBuilder#visitCast}.
+ * fn_name "castto<type>" → Cast; target type from {@code scalar.data_type};
+ * is_implicit ⇒ !isExplicitType. TryCast/Cast distinction not preserved.
+ */
+ private Expression translateScalarFnCall(TScalar scalar, List children,
+ DataType resultType) {
+ TScalarFnCall fn = scalar.getScalar_union().getScalar_fn_call();
+ String fnName = fn.getFn_name();
+ if (fnName == null) {
+ throw new UnsupportedOperationException(
+ "Horn expr translator: ScalarFnCall without fn_name");
+ }
+ if (fnName.startsWith("castto") && children.size() == 1) {
+ return new Cast(children.get(0), resultType, !fn.isIs_implicit());
+ }
+ // alias:BE 把重命名副本的 producer 包成黑盒标记函数 alias()(独立 uid);
+ // backward 剥回唯一 child,在 buildProject 里还原成真 Doris Alias。
+ if ("alias".equals(fnName)) {
+ return children.get(0);
+ }
+ // 特例:not / in / is_null 不是 BoundFunction 子类,FunctionRegistry 不能 lookup,
+ // 单独处理;其他普通 scalar fn 走通用工厂 DorisFunctionBuilder.build()。
+ switch (fnName) {
+ case "not":
+ requireArity(fnName, children, 1);
+ return new Not(children.get(0));
+ case "in":
+ // children = [compareExpr, opt1, ..., optN],与 Doris InPredicate 同序。
+ // NOT IN 是 Not(In(...)),由 "not" case 处理。
+ if (children.size() < 2) {
+ throw new UnsupportedOperationException(
+ "Horn expr translator: in needs ≥2 children, got "
+ + children.size());
+ }
+ return new InPredicate(children.get(0),
+ children.subList(1, children.size()));
+ case "is_null":
+ // x IS NULL,与正向 visitIsNull 的 fn_name="is_null" 同步。
+ requireArity(fnName, children, 1);
+ return new IsNull(children.get(0));
+ case "grouping":
+ // 反向 visitGroupingScalarFunction;黑盒通道用于 mv rewrite 等保留原
+ // 函数形态的兜底(一般 NormalizeRepeat 已替成 GROUPING_PREFIX 虚拟 slot)。
+ requireArity(fnName, children, 1);
+ return new Grouping(children.get(0));
+ case "grouping_id":
+ if (children.isEmpty()) {
+ throw new UnsupportedOperationException(
+ "Horn expr translator: grouping_id needs ≥1 args");
+ }
+ // GroupingId(Expression firstArg, Expression... varArgs) varargs ctor
+ Expression[] rest = children.subList(1, children.size())
+ .toArray(new Expression[0]);
+ return new GroupingId(children.get(0), rest);
+ default:
+ // 普通 scalar / window fn:白名单内的走 Doris FunctionRegistry 通用工厂
+ // DorisFunctionBuilder.build。新增 fn 只动 KNOWN_SCALAR_FNS / KNOWN_WINDOW_FNS。
+ if (DorisFunctionBuilder.KNOWN_SCALAR_FNS.contains(fnName)
+ || DorisFunctionBuilder.KNOWN_WINDOW_FNS.contains(fnName)) {
+ return DorisFunctionBuilder.build(fnName, children);
+ }
+ throw new UnsupportedOperationException(
+ "Horn expr translator: unsupported ScalarFnCall fn_name=" + fnName
+ + " arity=" + children.size());
+ }
+ }
+
+ /**
+ * horn ScalarAggFn → Doris AggregateFunction。白名单内的 fn_name
+ * ({@link DorisFunctionBuilder#KNOWN_AGG_FNS})走 FunctionRegistry 通用工厂。
+ * phase/mode 不在此表达(由 buildAgg 设 AggregateParam);distinct 已消解成 multi_distinct_xxx。
+ */
+ private Expression translateAggFn(TScalar scalar, List children) {
+ TScalarAggFn fn = scalar.getScalar_union().getScalar_agg_fn();
+ String fnName = fn.getFn_name();
+ if (fnName == null) {
+ throw new UnsupportedOperationException(
+ "Horn expr translator: ScalarAggFn missing fn_name");
+ }
+ if (DorisFunctionBuilder.KNOWN_AGG_FNS.contains(fnName)) {
+ return DorisFunctionBuilder.build(fnName, children);
+ }
+ throw new UnsupportedOperationException(
+ "Horn expr translator: unsupported ScalarAggFn fn_name=" + fnName);
+ }
+
+ private static void requireArity(String fnName, List children, int expected) {
+ if (children.size() != expected) {
+ throw new UnsupportedOperationException(
+ "Horn expr translator: " + fnName + " expected arity=" + expected
+ + " got=" + children.size());
+ }
+ }
+
+ /**
+ * Reverse of HornScalarThriftBuilder.visitWindowFrame.
+ * 经 translateScalarNode 的 {@code case kWindowFrame} 分发(WindowFrame is-a Expression)。
+ * boundary 在 union 字段 window_start/window_end,由 buildFrameBoundary 反序列化;
+ * 缺失 → EMPTY_BOUNDARY。
+ */
+ private WindowFrame buildWindowFrame(TScalar scalar) {
+ TScalarWindowFrame twf = scalar.getScalar_union().getScalar_window_frame();
+ FrameUnitsType units = twf.getWindow_type() == TWindowType.kRows
+ ? FrameUnitsType.ROWS : FrameUnitsType.RANGE;
+ FrameBoundary left = twf.isSetWindow_start() && !twf.getWindow_start().isEmpty()
+ ? buildFrameBoundary(twf.getWindow_start().get(0), true)
+ : new FrameBoundary(FrameBoundType.EMPTY_BOUNDARY);
+ FrameBoundary right = twf.isSetWindow_end() && !twf.getWindow_end().isEmpty()
+ ? buildFrameBoundary(twf.getWindow_end().get(0), false)
+ : new FrameBoundary(FrameBoundType.EMPTY_BOUNDARY);
+ return new WindowFrame(units, left, right);
+ }
+
+ /**
+ * Reverse of HornScalarThriftBuilder.buildFrameBoundary.
+ * kUnbounded 端点根据 isLeft 决定 UNBOUNDED_PRECEDING / UNBOUNDED_FOLLOWING。
+ */
+ private FrameBoundary buildFrameBoundary(TOperator op, boolean isLeft) {
+ // boundary 子树取 root —— 复用 Horn2DorisUtils.getRootScalar
+ // (兼容 scalar 单节点 / fscalar 扁平两形态)。
+ TScalar scalar = Horn2DorisUtils.getRootScalar(op);
+ TScalarAnalyticWindowBoundary tb = scalar
+ .getScalar_union().getScalar_analytic_window_boundary();
+ switch (tb.getBoundary_type()) {
+ case kCurrentRow:
+ return new FrameBoundary(FrameBoundType.CURRENT_ROW);
+ case kUnbounded:
+ return new FrameBoundary(isLeft
+ ? FrameBoundType.UNBOUNDED_PRECEDING
+ : FrameBoundType.UNBOUNDED_FOLLOWING);
+ case kPreceding:
+ return new FrameBoundary(
+ Optional.of(buildBoundaryOffset(tb)),
+ FrameBoundType.PRECEDING);
+ case kFollowing:
+ return new FrameBoundary(
+ Optional.of(buildBoundaryOffset(tb)),
+ FrameBoundType.FOLLOWING);
+ default:
+ throw new UnsupportedOperationException(
+ "Horn expr translator: unsupported boundary type "
+ + tb.getBoundary_type());
+ }
+ }
+
+ /**
+ * Extract offset for PRECEDING / FOLLOWING boundary.
+ * - range_offset_predicate (TOperator) → translate() 递归反译(RANGE frame)
+ * - rows_offset_value (long) → BigIntLiteral(ROWS frame)
+ *
+ * 顺序敏感:range_offset_predicate **先**判断。cc 端无条件 set rows_offset_value,
+ * RANGE boundary wire 上同时 isset 两边;先判 rows 会拿 BigIntLiteral(0) 覆盖 RANGE 谓词。
+ */
+ private Expression buildBoundaryOffset(TScalarAnalyticWindowBoundary tb) {
+ if (tb.isSetRange_offset_predicate() && !tb.getRange_offset_predicate().isEmpty()) {
+ return translate(tb.getRange_offset_predicate().get(0));
+ }
+ if (tb.isSetRows_offset_value()) {
+ return new BigIntLiteral(tb.getRows_offset_value());
+ }
+ throw new UnsupportedOperationException(
+ "Horn expr translator: FrameBoundary PRECEDING/FOLLOWING missing offset");
+ }
+
+ private Expression translateColumnRef(TScalar scalar) {
+ long hornScalarUid = scalar.getScalar_unique_id().getUnique_id();
+ Slot existing = hornCtx.getScalarUidToSlot().get(hornScalarUid);
+ if (existing != null) {
+ return existing;
+ }
+ TScalarColumnRef colRef = scalar.getScalar_union().getScalar_column_ref();
+ DataType type = HornTypeToDorisConverter.convert(scalar.getData_type());
+ List qualifier = ImmutableList.of(colRef.getDatabase_name(), colRef.getTable_name());
+ SlotReference newSlot = new SlotReference(colRef.getColumn_name(), type, true, qualifier);
+ hornCtx.getScalarUidToSlot().put(hornScalarUid, newSlot);
+ return newSlot;
+ }
+
+ private Expression translateLiteral(TScalar scalar, DataType resultType) {
+ TScalarLiteral lit = scalar.getScalar_union().getScalar_literal();
+
+ if (lit.isIs_null_type()) {
+ return new NullLiteral(resultType);
+ }
+
+ if (lit.isSetLiteral_value()) {
+ TLiteralValue literalValue = lit.getLiteral_value();
+ if (literalValue.isSetBool_literal()) {
+ return BooleanLiteral.of(literalValue.getBool_literal().isValue());
+ }
+ if (literalValue.isSetInt_literal()) {
+ Literal typed = Literal.convertToTypedLiteral(
+ literalValue.getInt_literal().getValue(), resultType);
+ if (typed != null) {
+ return typed;
+ }
+ }
+ if (literalValue.isSetString_literal()) {
+ return new StringLiteral(literalValue.getString_literal().getValue());
+ }
+ if (literalValue.isSetFloat_literal()) {
+ double val = literalValue.getFloat_literal().getValue();
+ if (resultType instanceof FloatType) {
+ return new FloatLiteral((float) val);
+ }
+ return new DoubleLiteral(val);
+ }
+ if (literalValue.isSetDecimal_literal()) {
+ // 统一转化使用DecimalV3Literal
+ String decimalString = new String(
+ literalValue.getDecimal_literal().getValue(), StandardCharsets.UTF_8);
+ return new DecimalV3Literal((DecimalV3Type) resultType, new BigDecimal(decimalString));
+ }
+ if (literalValue.isSetDate_literal()) {
+ // 统一转化使用DateV2Literal
+ String dateString = literalValue.getDate_literal().getDate_string();
+ return new DateV2Literal(dateString);
+ }
+ }
+
+ throw new UnsupportedOperationException(
+ "Horn expr translator: unrecognized literal type for result " + resultType);
+ }
+
+ private Expression translateBinaryPredicate(TScalar scalar, List children) {
+ TScalarBinaryPredicate pred = scalar.getScalar_union()
+ .getScalar_predicate().getScalar_binary_predicate();
+ Expression left = children.get(0);
+ Expression right = children.get(1);
+
+ switch (pred.getBinary_type()) {
+ case kEqual:
+ return new EqualTo(left, right);
+ case kNotEqual:
+ // Nereids 没有 NotEqual 类,用 Not(EqualTo) 表达
+ return new Not(new EqualTo(left, right));
+ case kLowerThan:
+ return new LessThan(left, right);
+ case kLowerEqual:
+ return new LessThanEqual(left, right);
+ case kGatherThan:
+ return new GreaterThan(left, right);
+ case kGatherEqual:
+ return new GreaterThanEqual(left, right);
+ default:
+ throw new UnsupportedOperationException(
+ "Horn expr translator: unsupported binary predicate type "
+ + pred.getBinary_type());
+ }
+ }
+
+}
diff --git a/fe/fe-core/src/main/java/org/apache/doris/horn/horn2doris/DorisFunctionBuilder.java b/fe/fe-core/src/main/java/org/apache/doris/horn/horn2doris/DorisFunctionBuilder.java
new file mode 100644
index 00000000000000..cd07ad9dec84a2
--- /dev/null
+++ b/fe/fe-core/src/main/java/org/apache/doris/horn/horn2doris/DorisFunctionBuilder.java
@@ -0,0 +1,102 @@
+// 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.horn2doris;
+
+
+import org.apache.doris.catalog.Env;
+import org.apache.doris.catalog.FunctionRegistry;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.functions.BoundFunction;
+import org.apache.doris.nereids.trees.expressions.functions.FunctionBuilder;
+import org.apache.doris.nereids.util.TypeCoercionUtils;
+
+import com.google.common.collect.ImmutableSet;
+
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * horn wire 上的 fn_name 跟 Doris BoundFunction 的双向桥接:forward 用 {@link #KNOWN_SCALAR_FNS}
+ * 白名单 gate,backward 调 {@link #build(String, List)} 复用 Doris {@link FunctionRegistry}
+ * (lookup → canApply → build → TypeCoercion,同 BindExpression 路径)。
+ */
+public final class DorisFunctionBuilder {
+
+ /**
+ * forward + backward 共享的 scalar fn 白名单。小写、须跟 BuiltinScalarFunctions primary name 一致。
+ */
+ public static final ImmutableSet KNOWN_SCALAR_FNS = ImmutableSet.of(
+ "abs", "ceil", "coalesce", "concat",
+ "datediff", "date_add", "date_format", "date_sub",
+ "day", "floor", "from_unixtime", "greatest",
+ "if", "ifnull", "least", "length",
+ "like", "lower", "ltrim", "mod",
+ "month", "nullif", "power", "quarter",
+ "round", "rtrim", "sqrt", "substring",
+ "to_date", "trim", "unix_timestamp", "upper",
+ "year",
+ // 日期算术(TimestampArithmetic + DaysAdd/DaysSub 两种形态)
+ "days_add", "days_sub", "weeks_add", "weeks_sub",
+ "months_add", "months_sub", "quarters_add", "quarters_sub",
+ "years_add", "years_sub");
+
+ /**
+ * forward + backward 共享的 window fn 白名单,走 {@link #build} 同款 FunctionRegistry lookup。
+ */
+ public static final ImmutableSet KNOWN_WINDOW_FNS = ImmutableSet.of(
+ "cume_dist", "dense_rank", "first_value", "lag", "last_value",
+ "lead", "nth_value", "ntile", "percent_rank", "rank", "row_number");
+
+ /**
+ * forward + backward 共享的 agg fn 白名单,走 {@link #build} 同款 lookup(canApply 按 arity 选
+ * ctor)。multi_distinct_count / multi_distinct_sum 是 forward 端 distinct 重写后的 builtin 名。
+ */
+ public static final ImmutableSet KNOWN_AGG_FNS = ImmutableSet.of(
+ "any_value", "avg", "count", "max", "min", "multi_distinct_count",
+ "multi_distinct_sum", "stddev_samp", "sum");
+
+ /**
+ * 反译:fn_name + children → BoundFunction。走 {@link FunctionRegistry#tryGetBuiltinBuilders},
+ * canApply 按 arity + 参数类型选 ctor,最后 {@link TypeCoercionUtils#processBoundFunction} 补 implicit cast。
+ */
+ public static Expression build(String fnName, List children) {
+ FunctionRegistry registry = Env.getCurrentEnv().getFunctionRegistry();
+ Optional> builders = registry.tryGetBuiltinBuilders(fnName);
+ if (!builders.isPresent() || builders.get().isEmpty()) {
+ throw new UnsupportedOperationException(
+ "DorisFunctionBuilder: no builtin builder for fn_name=" + fnName);
+ }
+ FunctionBuilder picked = null;
+ for (FunctionBuilder fb : builders.get()) {
+ if (fb.canApply(children)) {
+ picked = fb;
+ break;
+ }
+ }
+ if (picked == null) {
+ throw new UnsupportedOperationException(
+ "DorisFunctionBuilder: no ctor for fn=" + fnName
+ + " arity=" + children.size());
+ }
+ BoundFunction fn = (BoundFunction) picked.build(fnName, children).second;
+ return (Expression) TypeCoercionUtils.processBoundFunction(fn);
+ }
+
+ private DorisFunctionBuilder() {
+ }
+}
diff --git a/fe/fe-core/src/main/java/org/apache/doris/horn/horn2doris/DorisPhysicalPlanBuilder.java b/fe/fe-core/src/main/java/org/apache/doris/horn/horn2doris/DorisPhysicalPlanBuilder.java
new file mode 100644
index 00000000000000..d1be7776be2ce4
--- /dev/null
+++ b/fe/fe-core/src/main/java/org/apache/doris/horn/horn2doris/DorisPhysicalPlanBuilder.java
@@ -0,0 +1,989 @@
+// 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.horn2doris;
+
+
+import org.apache.doris.catalog.Column;
+import org.apache.doris.catalog.DistributionInfo;
+import org.apache.doris.catalog.Env;
+import org.apache.doris.catalog.HashDistributionInfo;
+import org.apache.doris.catalog.OlapTable;
+import org.apache.doris.common.UserException;
+import org.apache.doris.horn.HornOptimizationContext;
+import org.apache.doris.nereids.StatementContext;
+import org.apache.doris.nereids.hint.DistributeHint;
+import org.apache.doris.nereids.properties.DataTrait;
+import org.apache.doris.nereids.properties.DistributionSpec;
+import org.apache.doris.nereids.properties.DistributionSpecExecutionAny;
+import org.apache.doris.nereids.properties.DistributionSpecGather;
+import org.apache.doris.nereids.properties.DistributionSpecHash;
+import org.apache.doris.nereids.properties.DistributionSpecHash.ShuffleType;
+import org.apache.doris.nereids.properties.DistributionSpecReplicated;
+import org.apache.doris.nereids.properties.LogicalProperties;
+import org.apache.doris.nereids.properties.OrderKey;
+import org.apache.doris.nereids.properties.RequireProperties;
+import org.apache.doris.nereids.rules.implementation.LogicalWindowToPhysicalWindow.WindowFrameGroup;
+import org.apache.doris.nereids.trees.expressions.AggregateExpression;
+import org.apache.doris.nereids.trees.expressions.Alias;
+import org.apache.doris.nereids.trees.expressions.ExprId;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.MarkJoinSlotReference;
+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.trees.expressions.functions.agg.AggregateFunction;
+import org.apache.doris.nereids.trees.expressions.functions.agg.AggregateParam;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.GroupingScalarFunction;
+import org.apache.doris.nereids.trees.plans.AggMode;
+import org.apache.doris.nereids.trees.plans.AggPhase;
+import org.apache.doris.nereids.trees.plans.DistributeType;
+import org.apache.doris.nereids.trees.plans.JoinType;
+import org.apache.doris.nereids.trees.plans.LimitPhase;
+import org.apache.doris.nereids.trees.plans.Plan;
+import org.apache.doris.nereids.trees.plans.PreAggStatus;
+import org.apache.doris.nereids.trees.plans.SortPhase;
+import org.apache.doris.nereids.trees.plans.algebra.SetOperation.Qualifier;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalDistribute;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalEmptyRelation;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalExcept;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalFilter;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalHashAggregate;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalHashJoin;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalIntersect;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalLimit;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalNestedLoopJoin;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalOlapScan;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalOneRowRelation;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalPlan;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalProject;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalQuickSort;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalRepeat;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalResultSink;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalTopN;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalUnion;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalWindow;
+import org.apache.doris.nereids.util.ExpressionUtils;
+import org.apache.doris.nereids.util.JoinUtils;
+
+import com.google.common.collect.ImmutableList;
+import org.apache.horn4j.thrift.TDistributionSpec;
+import org.apache.horn4j.thrift.TDorisHashSpec;
+import org.apache.horn4j.thrift.TExpression;
+import org.apache.horn4j.thrift.TFlattenedExpression;
+import org.apache.horn4j.thrift.TOperator;
+import org.apache.horn4j.thrift.TOperatorType;
+import org.apache.horn4j.thrift.TOrderSpec;
+import org.apache.horn4j.thrift.TPhysicalAgg;
+import org.apache.horn4j.thrift.TPhysicalFilter;
+import org.apache.horn4j.thrift.TPhysicalGrouping;
+import org.apache.horn4j.thrift.TPhysicalJoin;
+import org.apache.horn4j.thrift.TPhysicalLimit;
+import org.apache.horn4j.thrift.TPhysicalMotion;
+import org.apache.horn4j.thrift.TPhysicalProject;
+import org.apache.horn4j.thrift.TPhysicalScan;
+import org.apache.horn4j.thrift.TPhysicalSetOp;
+import org.apache.horn4j.thrift.TPhysicalSort;
+import org.apache.horn4j.thrift.TPhysicalTableScan;
+import org.apache.horn4j.thrift.TPhysicalValueScan;
+import org.apache.horn4j.thrift.TPhysicalWindow;
+import org.apache.horn4j.thrift.TScalar;
+import org.apache.horn4j.thrift.TSpecialHashSpecKind;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+
+/**
+ * Rebuild a Doris PhysicalPlan from a Horn TFlattenedExpression.
+ *
+ * 支持算子集:PhysicalTableScan / PhysicalFilter / PhysicalProject /
+ * PhysicalMotion(Fragment boundary)。其它 physical 算子抛错由 caller fallback Nereids。
+ *
+ *
所有反译产生的 SlotReference 通过 {@link #scalarUidToSlot} 注册(key=horn 端
+ * scalar_unique_id,全 plan tree 唯一),共享给 {@link DorisExpressionBuilder}
+ * 反译 ColumnRef 时直接查表,保持上下游 plan 节点 slot 引用一致。
+ */
+public class DorisPhysicalPlanBuilder {
+
+ private static final Logger LOG = LogManager.getLogger(DorisPhysicalPlanBuilder.class);
+
+ private final HornOptimizationContext hornCtx;
+ private final DorisExpressionBuilder exprTranslator;
+
+ public DorisPhysicalPlanBuilder(HornOptimizationContext hornCtx) {
+ this.hornCtx = hornCtx;
+ this.exprTranslator = new DorisExpressionBuilder(hornCtx);
+ }
+
+ public PhysicalPlan build(TFlattenedExpression flatPlan) throws UserException {
+ if (flatPlan == null || flatPlan.getTexprs() == null || flatPlan.getTexprs().isEmpty()) {
+ throw new UserException("Horn CBO: empty optimized plan");
+ }
+
+ TExpression root = restoreTree(flatPlan, new AtomicInteger(0));
+
+ // 默认 need_singleton=false
+ PhysicalPlan hornPhysicalPlan = buildHelper(root);
+
+ // SELECT 列序由 project_list 承载、被 horn PhysicalProject 保留,反译后
+ // hornPhysicalPlan.getOutput() 即 SELECT 序,PhysicalResultSink.outputExprs 直接复用。
+ List sinkOutputs = hornPhysicalPlan.getOutput().stream()
+ .map(s -> (NamedExpression) s)
+ .collect(Collectors.toList());
+ return new PhysicalResultSink<>(
+ sinkOutputs,
+ Optional.empty(),
+ hornPhysicalPlan.getLogicalProperties(),
+ hornPhysicalPlan);
+ }
+
+ private TExpression restoreTree(TFlattenedExpression flat, AtomicInteger idx) {
+ TExpression expr = flat.getTexprs().get(idx.getAndIncrement());
+ long arity = expr.getArity();
+ List children = new ArrayList<>();
+ for (int i = 0; i < arity; i++) {
+ children.add(restoreTree(flat, idx));
+ }
+ expr.setChildren(children);
+ return expr;
+ }
+
+ private PhysicalPlan buildHelper(TExpression texpr) throws UserException {
+ // Build children first
+ List children = new ArrayList<>();
+ if (texpr.getChildren() != null) {
+ for (TExpression child : texpr.getChildren()) {
+ children.add(buildHelper(child));
+ }
+ }
+
+ TOperatorType opType = texpr.getOp().getOp_type();
+ PhysicalPlan plan;
+
+ switch (opType) {
+ case kPhysicalTableScan:
+ plan = buildScan(texpr);
+ break;
+
+ case kPhysicalValueScan:
+ plan = buildValueScan(texpr);
+ break;
+
+ case kPhysicalFilter:
+ plan = buildFilter(texpr, children.get(0));
+ break;
+
+ case kPhysicalProject:
+ plan = buildProject(texpr, children.get(0));
+ break;
+
+ case kPhysicalSort:
+ plan = buildSort(texpr, children.get(0));
+ break;
+
+ case kPhysicalLimit:
+ plan = buildLimit(texpr, children.get(0));
+ break;
+
+ // group_by 非空 → kPhysicalHashAgg;空 → kPhysicalScalarAgg。同一套 buildAgg。
+ case kPhysicalHashAgg:
+ case kPhysicalScalarAgg:
+ plan = buildAgg(texpr, children.get(0));
+ break;
+
+ // Doris PhysicalWindow 走外部排序模式:child 的 sort 由 PhysicalSort case 反译,
+ // BE AnalyticSinkOperator 假设 input 已 sorted。kPhysicalInternalSortWindow
+ // (window 内部自排)Doris 无对应算子,落 default 走 unsupported fallback。
+ case kPhysicalWindow:
+ plan = buildWindow(texpr, children.get(0));
+ break;
+
+ // grouping → Doris PhysicalRepeat。
+ case kPhysicalGrouping:
+ plan = buildRepeat(texpr, children.get(0));
+ break;
+
+ // Motion → PhysicalDistribute (Fragment boundary)
+ case kPhysicalMotionGather:
+ case kPhysicalMotionBroadcast:
+ case kPhysicalMotionHashDistribute:
+ case kPhysicalMotionRandom:
+ plan = buildMotion(texpr, children.get(0));
+ break;
+
+ case kPhysicalInnerHashJoin:
+ case kPhysicalInnerNestedLoopJoin:
+ case kPhysicalLeftOuterHashJoin:
+ case kPhysicalLeftOuterNestedLoopJoin:
+ case kPhysicalRightOuterHashJoin:
+ case kPhysicalRightOuterNestedLoopJoin:
+ case kPhysicalFullOuterHashJoin:
+ case kPhysicalFullOuterNestedLoopJoin:
+ case kPhysicalLeftSemiHashJoin:
+ case kPhysicalLeftSemiNestedLoopJoin:
+ case kPhysicalRightSemiHashJoin:
+ case kPhysicalRightSemiNestedLoopJoin:
+ case kPhysicalLeftAntiSemiHashJoin:
+ case kPhysicalLeftAntiSemiNestedLoopJoin:
+ case kPhysicalRightAntiSemiHashJoin:
+ case kPhysicalRightAntiSemiNestedLoopJoin:
+ case kPhysicalNullAwareLeftAntiSemiHashJoin:
+ case kPhysicalNullAwareLeftAntiSemiNestedLoopJoin:
+ case kPhysicalCrossJoin:
+ plan = buildJoin(texpr, children.get(0), children.get(1));
+ break;
+
+ // 集合算子 → PhysicalUnion/Intersect/Except。distinct 已被 BuildAggForUnion
+ // 降级成 Agg+UnionAll;传整个 children 列表(N 元,非 children.get(0))。
+ case kPhysicalNarrayUnionAll:
+ case kPhysicalUnionAll:
+ case kPhysicalIntersect:
+ case kPhysicalExcept:
+ plan = buildSetOp(texpr, children);
+ break;
+
+ default:
+ throw new UserException("Horn output: unsupported operator " + opType);
+ }
+
+ // 统一在末尾给 plan 设 PhysicalProperties + Statistics
+ PhysicalPlan finalPlan = Horn2DorisUtils.setPhysicalProperties(texpr, plan);
+ // 统一同步:plan 节点 ctor(outer join / agg 等)会用 withNullable 产生新 Slot 实例
+ // (ExprId 不变、nullable 变),旧 slot 在 map 里过期会导致上游按 uid 直查拿错。
+ // 在此按 ExprId 把 scalarUidToSlot 覆盖成 finalPlan.getOutput() 的真值。
+ syncMapWithPlanOutput(finalPlan);
+ return finalPlan;
+ }
+
+ /** 按 ExprId 把 finalPlan.getOutput() 的 slot 真值同步进 scalarUidToSlot。 */
+ private void syncMapWithPlanOutput(PhysicalPlan finalPlan) {
+ if (hornCtx.getScalarUidToSlot().isEmpty()) {
+ return;
+ }
+ List output = finalPlan.getOutput();
+ if (output.isEmpty()) {
+ return;
+ }
+ Map byId = new HashMap<>(output.size());
+ for (Slot s : output) {
+ byId.put(s.getExprId(), s);
+ }
+ hornCtx.getScalarUidToSlot().replaceAll((uid, slot) -> {
+ Slot fresh = byId.get(slot.getExprId());
+ return fresh != null ? fresh : slot;
+ });
+ }
+
+ /**
+ * Horn TPhysicalSetOp → Doris PhysicalUnion / Intersect / Except。三字段:
+ * - output_list → outputs:union 自身输出列(plain column ref → SlotReference,注册 uid)。
+ * - children_output_list → childrenOutputs:每孩子一组按列对齐 SlotReference。
+ * - children_const_expr_list → constantExprsList:常量行(literal,可空)。
+ * 分布派生由 buildHelper 末尾 setPhysicalProperties 自动接管;logicalProperties 传 null → lazy。
+ */
+ private PhysicalPlan buildSetOp(TExpression texpr, List children) throws UserException {
+ TPhysicalSetOp tsetop = texpr.getOp().getOp_union().getPhysical_set_op();
+
+ // outputs:union 自身输出列(plain column ref → SlotReference,translate 内部注册 uid→slot)。
+ List outputs = new ArrayList<>();
+ for (Expression e : exprTranslator.translateList(tsetop.getOutput_list())) {
+ outputs.add((NamedExpression) e);
+ }
+
+ // childrenOutputs:每孩子一组 SlotReference(按列对齐 output,位置映射契约)。
+ List> childrenOutputs = new ArrayList<>();
+ for (List oneChild : tsetop.getChildren_output_list()) {
+ List cols = new ArrayList<>();
+ for (Expression e : exprTranslator.translateList(oneChild)) {
+ cols.add((SlotReference) e);
+ }
+ childrenOutputs.add(cols);
+ }
+
+ List planChildren = new ArrayList<>(children);
+
+ // intersect/except 恒 DISTINCT、无 const-expr,分布由 cc 端 enforce 全列 hash。
+ TOperatorType opType = texpr.getOp().getOp_type();
+ if (opType == TOperatorType.kPhysicalIntersect || opType == TOperatorType.kPhysicalExcept) {
+ List aligned = Horn2DorisUtils.alignSetOpOutputNullable(outputs, childrenOutputs);
+ if (opType == TOperatorType.kPhysicalIntersect) {
+ return new PhysicalIntersect(Qualifier.DISTINCT, aligned, childrenOutputs,
+ null /* logicalProperties → lazy */, planChildren);
+ }
+ return new PhysicalExcept(Qualifier.DISTINCT, aligned, childrenOutputs,
+ null /* logicalProperties → lazy */, planChildren);
+ }
+
+ // union all:constantExprsList(可空,透传)。常量行经 translate 后是 literal/cast
+ // (Alias 已在 forward flattenPreOrder 剥掉),非 NamedExpression,故统一包 Alias。
+ List> constExprs = new ArrayList<>();
+ if (tsetop.isSetChildren_const_expr_list()) {
+ for (List row : tsetop.getChildren_const_expr_list()) {
+ List r = new ArrayList<>();
+ for (Expression e : exprTranslator.translateList(row)) {
+ r.add(new Alias(e));
+ }
+ constExprs.add(r);
+ }
+ }
+ return new PhysicalUnion(Qualifier.ALL, outputs, childrenOutputs, constExprs,
+ null /* logicalProperties → lazy computeOutput */, planChildren);
+ }
+
+ private PhysicalPlan buildScan(TExpression texpr) throws UserException {
+ TPhysicalScan physicalScan = texpr.getOp().getOp_union().getPhysical_scan();
+ TPhysicalTableScan tableScan = physicalScan.getPhysical_table_scan();
+ String tableName = tableScan.getTable_name();
+ String dbName = tableScan.getDatabase_name();
+
+ // horn 只处理 OlapTable
+ OlapTable olapTable = (OlapTable) hornCtx.getCascadesContext().getConnectContext()
+ .getCurrentCatalog()
+ .getDbOrAnalysisException(dbName)
+ .getTableOrAnalysisException(tableName);
+
+ // 从 Horn output_list 构造 scan slots,并添加到 scalarUidToSlot。
+ StatementContext stmtCtx = hornCtx.getCascadesContext().getStatementContext();
+ ImmutableList qualifier = ImmutableList.of(dbName, tableName);
+ List scanSlots = new ArrayList<>();
+ if (texpr.getOutput_list() != null) {
+ for (TOperator outOp : texpr.getOutput_list()) {
+ TScalar scalar = Horn2DorisUtils.getRootScalar(outOp);
+ String colName = scalar.getScalar_union().getScalar_column_ref().getColumn_name();
+ SlotReference slotRef = SlotReference.fromColumn(
+ stmtCtx.getNextExprId(), olapTable, olapTable.getColumn(colName), qualifier);
+ hornCtx.getScalarUidToSlot().put(scalar.getScalar_unique_id().getUnique_id(), slotRef);
+ scanSlots.add(slotRef);
+ }
+ }
+ // 分布列被裁剪时(如 count(*) / GROUP BY 非分布键)补回 scanSlots —— 对齐
+ // Doris 原生行为(scan output 永远全列),保证 buildScanDistributionSpec 的 hash
+ // 列凑齐、DataPartition 构造不空。
+ DistributionInfo scanDistInfo = olapTable.getDefaultDistributionInfo();
+ if (scanDistInfo instanceof HashDistributionInfo) {
+ for (Column distCol : ((HashDistributionInfo) scanDistInfo).getDistributionColumns()) {
+ if (scanSlots.stream().noneMatch(
+ s -> s.getName().equalsIgnoreCase(distCol.getName()))) {
+ scanSlots.add(SlotReference.fromColumn(
+ stmtCtx.getNextExprId(), olapTable, distCol, qualifier));
+ }
+ }
+ }
+ // RANDOM 分布表(无分布列可补)的 count(*) 场景 scanSlots 仍空,会 NPE:
+ // 跟 Doris 原生 ColumnPruning.pruneOutput 一致,selectMinimumColumn 选最小宽度列兜底。
+ if (scanSlots.isEmpty()) {
+ List candidates = new ArrayList<>();
+ for (Column col : olapTable.getBaseSchema()) {
+ candidates.add(SlotReference.fromColumn(
+ stmtCtx.getNextExprId(), olapTable, col, qualifier));
+ }
+ scanSlots.add(ExpressionUtils.selectMinimumColumn(candidates));
+ }
+ // 分区集走 external_relation_id 查 forward 登记的 prunedPartitionIds,
+ // 不再从 thrift selected_partition_ids 读。查不到/未 set 都是 bug,直接抛
+ // ——不兜底全分区(否则 self-join 裁剪不同时静默用错分区集)。
+ if (!tableScan.isSetExternal_relation_id()) {
+ throw new UserException("horn buildScan: TPhysicalTableScan missing external_relation_id (table "
+ + tableName + ")");
+ }
+ long extRelationId = tableScan.getExternal_relation_id();
+ List partitionIds = hornCtx.getScanPrunedPartitions(extRelationId);
+ if (partitionIds == null) {
+ throw new UserException("horn buildScan: no pruned-partition binding for external_relation_id="
+ + extRelationId + " (table " + tableName + ")");
+ }
+ List tabletIds = new ArrayList<>();
+ for (Long partId : partitionIds) {
+ olapTable.getPartition(partId).getBaseIndex().getTablets()
+ .forEach(t -> tabletIds.add(t.getId()));
+ }
+
+ // PhysicalCatalogRelation.computeOutput() 用 table.getBaseSchema() 重 ExprId,
+ // 跟我们登记 scalarUidToSlot 的 ExprId 不一致,所以必须显式传 props 锁定。
+ ImmutableList immutableScanSlots = ImmutableList.copyOf(scanSlots);
+ LogicalProperties scanProps = new LogicalProperties(
+ () -> immutableScanSlots,
+ () -> DataTrait.EMPTY_TRAIT);
+
+ DistributionSpec scanDistSpec = Horn2DorisUtils.buildScanDistributionSpec(
+ olapTable, scanSlots, partitionIds);
+
+ PhysicalOlapScan scan = new PhysicalOlapScan(
+ hornCtx.getCascadesContext().getStatementContext().getNextRelationId(),
+ olapTable,
+ ImmutableList.of(dbName, tableName),
+ olapTable.getBaseIndexId(),
+ tabletIds,
+ partitionIds,
+ scanDistSpec,
+ PreAggStatus.on(),
+ immutableScanSlots,
+ Optional.empty(),
+ scanProps,
+ Optional.empty(),
+ immutableScanSlots,
+ Collections.emptyList(),
+ Collections.emptyList(),
+ Optional.empty(),
+ Optional.empty(),
+ Collections.emptyList(),
+ Optional.empty()
+ );
+
+ return scan;
+ }
+
+ /**
+ * horn PhysicalValueScan 反译为 Doris 物理叶子:
+ *
+ * - 0 行 → {@link PhysicalEmptyRelation}(schema 列由 output_list 重建)
+ * - 1 行 → {@link PhysicalOneRowRelation}(projects = Alias(lit, schemaName),
+ * Alias.exprId 复用 schemaSlot.exprId 保证上层引用稳定)
+ *
+ * N 行不可达:Doris BindExpression 已把 InlineTable 拆成 OneRowRelation × N + Union。
+ */
+ private PhysicalPlan buildValueScan(TExpression texpr) throws UserException {
+ // horn PhysicalValueScan::ToThrift 走 TPhysicalScan wrapper sub-struct
+ // (跟 TPhysicalTableScan 同模式),不是 TOperatorUnion 直挂的 slot 14。
+ TPhysicalValueScan valueScan = texpr.getOp().getOp_union()
+ .getPhysical_scan().getPhysical_value_scan();
+ StatementContext stmtCtx = hornCtx.getCascadesContext().getStatementContext();
+
+ List schemaColumns = valueScan.isSetOutput_list()
+ ? valueScan.getOutput_list() : Collections.emptyList();
+ if (schemaColumns.isEmpty()) {
+ throw new UserException(
+ "Horn output: PhysicalValueScan missing output_list (schema)");
+ }
+
+ List schemaExprs = exprTranslator.translateList(schemaColumns);
+ List schemaSlots = new ArrayList<>(schemaExprs.size());
+ for (Expression e : schemaExprs) {
+ schemaSlots.add((Slot) e);
+ }
+
+ List> rows = valueScan.isSetValue_lists()
+ ? valueScan.getValue_lists() : Collections.emptyList();
+
+ // PhysicalOneRowRelation 主线没 override computeOutput,传 null 会抛 "Not support
+ // compute output",故显式构造 LogicalProperties + supplier 锁定 schemaSlots
+ // (PhysicalEmptyRelation 为一致性也走同款)。
+ ImmutableList immutableSchema = ImmutableList.copyOf(schemaSlots);
+ LogicalProperties relationProps = new LogicalProperties(
+ () -> immutableSchema,
+ () -> DataTrait.EMPTY_TRAIT);
+
+ if (rows.isEmpty()) {
+ return new PhysicalEmptyRelation(stmtCtx.getNextRelationId(), schemaSlots, relationProps);
+ }
+ if (rows.size() > 1) {
+ throw new UserException("Horn output: PhysicalValueScan with "
+ + rows.size() + " rows not supported");
+ }
+ // 1 行:PhysicalOneRowRelation.output 直接从 projects.toSlot 派生,
+ // Alias 必须复用 schemaSlot.exprId,否则 OneRow 的 output ExprId 跟
+ // scalarUidToSlot 登记的不一致,上层 Project/Filter 引用就断。
+ List singleRow = rows.get(0);
+ if (singleRow.size() != schemaSlots.size()) {
+ throw new UserException("Horn output: PhysicalValueScan row arity "
+ + singleRow.size() + " != schema arity " + schemaSlots.size());
+ }
+ List projects = new ArrayList<>(schemaSlots.size());
+ for (int i = 0; i < schemaSlots.size(); i++) {
+ Slot slot = schemaSlots.get(i);
+ Expression cellExpr = exprTranslator.translate(singleRow.get(i));
+ projects.add(new Alias(slot.getExprId(), cellExpr, slot.getName()));
+ }
+ return new PhysicalOneRowRelation(stmtCtx.getNextRelationId(), projects, relationProps);
+ }
+
+ private PhysicalPlan buildFilter(TExpression texpr, PhysicalPlan child) throws UserException {
+ TPhysicalFilter tfilter = texpr.getOp().getOp_union().getPhysical_filter();
+
+ Set conjuncts = new HashSet<>();
+ for (TOperator predOp : tfilter.getFilter_list()) {
+ Expression expr = exprTranslator.translate(predOp);
+ LOG.debug("HORN_FILTER predicate: {}", expr);
+ conjuncts.add(expr);
+ }
+ return new PhysicalFilter<>(conjuncts, null, child);
+ }
+
+ private PhysicalPlan buildProject(TExpression texpr, PhysicalPlan child) throws UserException {
+ TPhysicalProject tproj = texpr.getOp().getOp_union().getPhysical_project();
+
+ List projectList = tproj.getProject_list();
+ List rewriteList = tproj.getRewrite_list();
+ List projects = new ArrayList<>();
+ for (int i = 0; i < projectList.size(); i++) {
+ TOperator projOp = projectList.get(i);
+ long projUid = Horn2DorisUtils.getRootScalar(projOp).getScalar_unique_id().getUnique_id();
+ Slot existingSlot = hornCtx.getScalarUidToSlot().get(projUid);
+ if (existingSlot != null) {
+ // mark slot 特例:AS 别名投影时 mark slot 与别名共享 ExprId,直接透传会显示内部名 $c$1;
+ // 对 MarkJoinSlotReference 包 Alias 恢复用户列名(仅 mark slot,普通列包 Alias 会多余 Project)。
+ if (existingSlot instanceof MarkJoinSlotReference) {
+ String aliasName = Horn2DorisUtils.getRootScalar(rewriteList.get(i))
+ .getScalar_union().getScalar_column_ref().getColumn_name();
+ if (aliasName != null && !aliasName.equalsIgnoreCase(existingSlot.getName())) {
+ NamedExpression aliased = new Alias(existingSlot, aliasName);
+ projects.add(aliased);
+ hornCtx.getScalarUidToSlot().put(projUid, aliased.toSlot());
+ continue;
+ }
+ }
+ projects.add(existingSlot);
+ continue;
+ }
+ Expression projExpr = exprTranslator.translate(projOp);
+ String aliasName = Horn2DorisUtils.getRootScalar(rewriteList.get(i))
+ .getScalar_union().getScalar_column_ref().getColumn_name();
+ NamedExpression namedProj = new Alias(projExpr, aliasName);
+ projects.add(namedProj);
+ hornCtx.getScalarUidToSlot().put(projUid, namedProj.toSlot());
+ }
+
+ // GROUPING() 函数还原:Doris 不能在 Project 里算 Grouping(),故把含 GroupingScalarFunction
+ // 的 Alias 下推进 child Repeat.outputExpressions(由 RepeatNode 原生物化),本 Project 对应
+ // 列退化成透传 slot;这层 Project 保留作 GROUPING_ID 的桥(Repeat.getOutput() 不含 groupingId)。
+ if (child instanceof PhysicalRepeat
+ && projects.stream().anyMatch(p -> p.containsType(GroupingScalarFunction.class))) {
+ PhysicalRepeat> repeat = (PhysicalRepeat>) child;
+ List repeatOutput = new ArrayList<>(repeat.getOutputExpressions());
+ List newProjects = new ArrayList<>(projects.size());
+ for (NamedExpression p : projects) {
+ if (p.containsType(GroupingScalarFunction.class)) {
+ repeatOutput.add(p); // Grouping(..) AS GROUPING_PREFIX_* 还原进 Repeat
+ newProjects.add(p.toSlot()); // Project 退化成透传该 slot
+ } else {
+ newProjects.add(p);
+ }
+ }
+ // Doris 计划不可变:withAggOutput 重建带新 output 的 Repeat,resetLogicalProperties 让 output 随新增列重算
+ child = repeat.withAggOutput(repeatOutput).resetLogicalProperties();
+ projects = newProjects;
+ }
+
+ // logicalProperties 传 null → AbstractPhysicalPlan ctor 转 Optional.empty()
+ // → AbstractPlan 自动 LazyCompute(this::computeLogicalProperties)
+ // → PhysicalProject.computeOutput() = projects.map(toSlot)
+ return new PhysicalProject<>(projects, null, child);
+ }
+
+ /** Translate a Horn Motion node to a Doris PhysicalDistribute (Fragment boundary). */
+ private PhysicalPlan buildMotion(TExpression texpr, PhysicalPlan child) throws UserException {
+ TPhysicalMotion tmotion = texpr.getOp().getOp_union().getPhysical_motion();
+ TDistributionSpec tspec = tmotion.getDistribution_spec();
+ // Merge gather: horn PhysicalMotionGather 自带 sort_info 表示跨 fragment 多路归并,
+ // 反译成 MERGE_SORT + PhysicalDistribute[Gather] 两层(不需包 LOCAL_SORT 占位)。
+ if (tmotion.isSetSort_info()
+ && tmotion.getSort_info().getOrder_by_exprs() != null
+ && !tmotion.getSort_info().getOrder_by_exprs().isEmpty()) {
+ List orderKeys = buildOrderKeys(tmotion.getSort_info());
+ PhysicalPlan gather = new PhysicalDistribute<>(toDistributionSpec(tspec), child);
+ return new PhysicalQuickSort<>(orderKeys, SortPhase.MERGE_SORT, null, gather);
+ }
+ // 普通 Motion (无 merge gather):单纯 PhysicalDistribute。
+ return new PhysicalDistribute<>(toDistributionSpec(tspec), child);
+ }
+
+ private PhysicalPlan buildJoin(TExpression texpr, PhysicalPlan left, PhysicalPlan right)
+ throws UserException {
+ TOperatorType opType = texpr.getOp().getOp_type();
+ JoinType joinType = Horn2DorisUtils.getJoinType(opType);
+ boolean useHash = Horn2DorisUtils.isHashJoin(opType);
+
+ TPhysicalJoin tjoin = texpr.getOp().getOp_union().getPhysical_join();
+ List hashConjuncts = exprTranslator.translateList(tjoin.getEqual_join_predicate_list());
+ List otherConjuncts = exprTranslator.translateList(tjoin.getOther_join_conjuncts());
+
+ // mark join(IN / EXISTS-OR 子查询):重建 MarkJoinSlotReference + mark 谓词。
+ // 必须是 MarkJoinSlotReference 子类,否则 isMarkJoin()=false、不 append mark 列。
+ // slot 身份经 scalarUidToSlot[uid] 复用:首次见到 uid 新建并注册,上层 Filter 同 uid 复用。
+ Optional markSlotRef = Optional.empty();
+ List markConjuncts = ExpressionUtils.EMPTY_CONDITION;
+ if (tjoin.isSetMark_slot() && !tjoin.getMark_slot().isEmpty()) {
+ // mark_slot 是单元素 list(同 grouping_id_func)。
+ TScalar markScalar = Horn2DorisUtils.getRootScalar(tjoin.getMark_slot().get(0));
+ long markUid = markScalar.getScalar_unique_id().getUnique_id();
+ Slot existing = hornCtx.getScalarUidToSlot().get(markUid);
+ MarkJoinSlotReference mjsr;
+ if (existing instanceof MarkJoinSlotReference) {
+ mjsr = (MarkJoinSlotReference) existing;
+ } else {
+ String markName = markScalar.getScalar_union().getScalar_column_ref().getColumn_name();
+ mjsr = new MarkJoinSlotReference(
+ hornCtx.getCascadesContext().getStatementContext().getNextExprId(),
+ markName, false);
+ hornCtx.getScalarUidToSlot().put(markUid, mjsr);
+ }
+ markSlotRef = Optional.of(mjsr);
+ if (tjoin.isSetMark_join_predicate_list()
+ && tjoin.getMark_join_predicate_list() != null
+ && !tjoin.getMark_join_predicate_list().isEmpty()) {
+ markConjuncts = exprTranslator.translateList(tjoin.getMark_join_predicate_list());
+ }
+ }
+
+ // mark join:传 null logicalProperties → 惰性 computeOutput() 会 append mark boolean;
+ // 预建 props 不含 mark 会漏列。非 mark join 仍用预建 props(保持既有行为)。
+ LogicalProperties props = markSlotRef.isPresent() ? null : new LogicalProperties(
+ () -> JoinUtils.getJoinOutput(joinType, left, right),
+ () -> DataTrait.EMPTY_TRAIT);
+ // PhysicalProperties 由 buildHelper 末尾 attachProperties 统一处理
+ // (join driving-side inherit 规则;见 attachProperties 注释)
+ PhysicalPlan join;
+ if (useHash) {
+ join = new PhysicalHashJoin<>(joinType, hashConjuncts, otherConjuncts,
+ markConjuncts, new DistributeHint(DistributeType.NONE), markSlotRef,
+ props, left, right);
+ } else {
+ join = new PhysicalNestedLoopJoin<>(joinType, hashConjuncts, otherConjuncts,
+ markConjuncts, markSlotRef, props, left, right);
+ }
+ // outer join 输出侧 nullable 升格回写由 buildHelper 末尾 syncMapWithPlanOutput 接管。
+ return join;
+ }
+
+ /**
+ * Convert a Horn TDistributionSpec to a Doris DistributionSpec.
+ * 专给 buildMotion 用,返回 motion 的 target 分布(scan 节点分布由 buildScan 单独构造)。
+ * 仅处理 motion derive 可能 emit 的 5 种类型;require-only 标签(kAny/kNonSingleton)出现即 bug,直接 throw。
+ */
+ private DistributionSpec toDistributionSpec(TDistributionSpec tspec) {
+ switch (tspec.getDistribution_type()) {
+ case kSingleton:
+ // motion gather 之后单点 → DistributionSpecGather。
+ return DistributionSpecGather.INSTANCE;
+ case kReplicated:
+ case kUniversal:
+ // kReplicated: Broadcast target,每 instance 拿全份。
+ // kUniversal: horn "everywhere"(系统表/常量),Doris 无此概念,Replicated 功能等价。
+ return DistributionSpecReplicated.INSTANCE;
+ case kHashed: {
+ // kHashed 一定带 hash_columns;每个 hash 列的 unique_id 一定已被
+ // child plan 登记到 scalarUidToSlot 且是 SlotReference(直接 cast)。
+ List hashExprIds = new ArrayList<>();
+ for (TOperator hashCol : tspec.getHash_columns()) {
+ long hornScalarUid = Horn2DorisUtils.getRootScalar(hashCol).getScalar_unique_id().getUnique_id();
+ hashExprIds.add(((SlotReference) hornCtx.getScalarUidToSlot().get(hornScalarUid)).getExprId());
+ }
+ // 带 DorisHashSpec → 绑定目标表 storage 桶布局。shuffleType 按 is_natural 分:
+ // true(scan 原生分布)→ NATURAL;false(motion shuffle 产物)→ STORAGE_BUCKETED。
+ // 三要素(tableId/baseIndexId/partitionIds)与 scan 侧 buildScanDistributionSpec 逐字段对齐。
+ if (tspec.isSetSpecial_hash_spec()
+ && tspec.getSpecial_hash_spec().getKind() == TSpecialHashSpecKind.kDoris) {
+ TDorisHashSpec storageBucketSpec = tspec.getSpecial_hash_spec().getDoris_hash_spec();
+ long storageBucketTableId = storageBucketSpec.getTable_id();
+ OlapTable storageBucketTable = (OlapTable) Env.getCurrentInternalCatalog()
+ .getTableByTableId(storageBucketTableId);
+ ShuffleType shuffleType = storageBucketSpec.isIs_natural()
+ ? ShuffleType.NATURAL : ShuffleType.STORAGE_BUCKETED;
+ return new DistributionSpecHash(
+ hashExprIds, shuffleType,
+ storageBucketTableId,
+ storageBucketTable.getBaseIndexId(),
+ new LinkedHashSet<>(storageBucketSpec.getSelected_partition_ids()));
+ }
+ // 无 special(Empty)→ 普通运行时 hash 重分布 → EXECUTION_BUCKETED
+ // (BE HASH_PARTITIONED)。NATURAL 仅描述 scan 自带桶分布、不能作 shuffle target。
+ return new DistributionSpecHash(hashExprIds, ShuffleType.EXECUTION_BUCKETED);
+ }
+ case kRandom:
+ // PhysicalMotionRandom 主动 scramble 到任意节点。DistributionSpecExecutionAny
+ // 是 Doris "必 shuffle 但 target 任意"的专用 spec,跟 random target 语义吻合。
+ return DistributionSpecExecutionAny.INSTANCE;
+ default:
+ // kAny / kNonSingleton: horn 端 require-only 标签,motion derive
+ // emit 这俩 = horn bug。kSentinel: thrift 哨兵值,合法 plan
+ // 不该带。其它未知值同样异常。
+ throw new IllegalStateException(
+ "Unexpected horn motion distribution type on derive path: "
+ + tspec.getDistribution_type());
+ }
+ }
+
+ // PhysicalSort 反译成 Doris LOCAL_SORT phase(单 fragment 内完整排序)。
+ // 上方若有 PhysicalMotionGather 配套,buildMotion 会自动包 MERGE_SORT + Distribute[Gather]。
+ private PhysicalPlan buildSort(TExpression texpr, PhysicalPlan child) throws UserException {
+ TPhysicalSort tsort = texpr.getOp().getOp_union().getPhysical_sort();
+ TOrderSpec sortInfo = tsort.getSort_info();
+ if (sortInfo == null) {
+ throw new UserException("Horn output: PhysicalSort missing sort_info");
+ }
+ List orderKeys = buildOrderKeys(sortInfo);
+ return new PhysicalQuickSort<>(orderKeys, SortPhase.LOCAL_SORT, null, child);
+ }
+
+ // PhysicalLimit 反译:
+ // 带 order_spec → PhysicalTopN (Global→MERGE_SORT, Local→LOCAL_SORT);若 child 是
+ // PhysicalQuickSort 则跳过它(Limit 自带 order_spec 已表达 sort+truncate)。
+ // 无 order_spec → 1:1 PhysicalLimit
+ private PhysicalPlan buildLimit(TExpression texpr, PhysicalPlan child) {
+ TPhysicalLimit tlimit = texpr.getOp().getOp_union().getPhysical_limit();
+ long limit = tlimit.isSetLimit() ? tlimit.getLimit() : -1;
+ long offset = tlimit.isSetOffset() ? tlimit.getOffset() : 0;
+ boolean isGlobal = tlimit.isSetIs_global_limit() ? tlimit.isIs_global_limit() : true;
+ if (tlimit.isSetOrder_spec()
+ && tlimit.getOrder_spec().getOrder_by_exprs() != null
+ && !tlimit.getOrder_spec().getOrder_by_exprs().isEmpty()) {
+ List orderKeys = buildOrderKeys(tlimit.getOrder_spec());
+ SortPhase sortPhase = isGlobal ? SortPhase.MERGE_SORT : SortPhase.LOCAL_SORT;
+ PhysicalPlan topnChild = child instanceof PhysicalQuickSort
+ ? (PhysicalPlan) ((PhysicalQuickSort>) child).child(0)
+ : child;
+ return new PhysicalTopN<>(orderKeys, limit, offset, sortPhase, null, topnChild);
+ }
+ LimitPhase phase = isGlobal ? LimitPhase.GLOBAL : LimitPhase.LOCAL;
+ return new PhysicalLimit<>(limit, offset, phase, null, child);
+ }
+
+ /**
+ * 反译 horn PhysicalHashAgg / PhysicalScalarAgg → Doris PhysicalHashAggregate。三态 phase 映射:
+ *
+ * is_split | is_local | AggregateParam | agg_funcs 形态 | 动作
+ * ---------+----------+-----------------------------+--------------------+---------
+ * true | true | (LOCAL, INPUT_TO_BUFFER) | 原函数 ScalarAggFn | 注册 map
+ * true | false | (GLOBAL, BUFFER_TO_RESULT) | ColumnRef | lookup map
+ * false | * | (GLOBAL, INPUT_TO_RESULT) | 原函数 | 单层直出
+ *
+ * 第三态覆盖 CombineTwoAdjacentAggs(撤销 split)的产物;scalar agg 走同一路径。
+ */
+ private PhysicalPlan buildAgg(TExpression texpr, PhysicalPlan child) throws UserException {
+ TPhysicalAgg tagg = texpr.getOp().getOp_union().getPhysical_agg();
+ boolean isSplit = tagg.isIs_split();
+ boolean isLocal = tagg.isIs_local_agg();
+
+ // group_by:列引用(极少表达式),反译后透传进 output(两阶段 group key slot 不变)
+ List groupByExprs = new ArrayList<>();
+ List output = new ArrayList<>();
+ if (tagg.getGroup_by_exprs() != null) {
+ for (TOperator gbOp : tagg.getGroup_by_exprs()) {
+ Expression gb = exprTranslator.translate(gbOp);
+ groupByExprs.add(gb);
+ if (gb instanceof NamedExpression) {
+ output.add((NamedExpression) gb);
+ } else {
+ output.add(new Alias(gb));
+ }
+ }
+ }
+
+ // 循环里只拼 output 并首次绑定 uid → slot(供上游 buildFilter/buildProject 按 uid 命中)。
+ // ctor 的 adjustNullableForOutputs 会替换输出对象、改 nullable,交 buildHelper 末尾
+ // syncMapWithPlanOutput 接管。scalarUidToAggFunc 的 fn 不在 plan.getOutput() 里,构造后单独刷新。
+ AggregateParam param;
+ if (isSplit && isLocal) {
+ // ---- local: 原函数 → AggregateExpression(INPUT_TO_BUFFER) 包 Alias 出 buffer slot ----
+ param = new AggregateParam(AggPhase.LOCAL, AggMode.INPUT_TO_BUFFER);
+ if (tagg.getAggregate_functions() != null) {
+ for (TOperator fnOp : tagg.getAggregate_functions()) {
+ long uid = Horn2DorisUtils.getRootScalar(fnOp).getScalar_unique_id().getUnique_id();
+ Expression fnExpr = exprTranslator.translate(fnOp);
+ if (!(fnExpr instanceof AggregateFunction)) {
+ throw new UserException("Horn: local agg expected AggregateFunction, got "
+ + fnExpr.getClass().getSimpleName());
+ }
+ AggregateFunction fn = (AggregateFunction) fnExpr;
+ Alias buffer = new Alias(new AggregateExpression(fn, param));
+ output.add(buffer);
+ hornCtx.getScalarUidToSlot().put(uid, buffer.toSlot());
+ hornCtx.getScalarUidToAggFunc().put(uid, fn);
+ }
+ }
+ } else if (isSplit) {
+ // ---- global: thrift 上是 ColumnRef,按 uid lookup 原函数 + buffer slot
+ // (local 节点 buildAgg 尾部已注册构造后真值,bottom-up 先于 global)----
+ param = new AggregateParam(AggPhase.GLOBAL, AggMode.BUFFER_TO_RESULT);
+ if (tagg.getAggregate_functions() != null) {
+ for (TOperator fnOp : tagg.getAggregate_functions()) {
+ long uid = Horn2DorisUtils.getRootScalar(fnOp).getScalar_unique_id().getUnique_id();
+ AggregateFunction fn = hornCtx.getScalarUidToAggFunc().get(uid);
+ Slot buffer = hornCtx.getScalarUidToSlot().get(uid);
+ if (fn == null || buffer == null) {
+ throw new UserException(
+ "Horn: global agg cannot find local fn/buffer for uid=" + uid);
+ }
+ Alias result = new Alias(new AggregateExpression(fn, param, buffer));
+ output.add(result);
+ hornCtx.getScalarUidToSlot().put(uid, result.toSlot());
+ }
+ }
+ } else {
+ // ---- 单层 (CombineTwoAdjacentAggs 撤销 split 或未拆): INPUT_TO_RESULT ----
+ param = new AggregateParam(AggPhase.GLOBAL, AggMode.INPUT_TO_RESULT);
+ if (tagg.getAggregate_functions() != null) {
+ for (TOperator fnOp : tagg.getAggregate_functions()) {
+ long uid = Horn2DorisUtils.getRootScalar(fnOp).getScalar_unique_id().getUnique_id();
+ Expression fnExpr = exprTranslator.translate(fnOp);
+ if (!(fnExpr instanceof AggregateFunction)) {
+ throw new UserException("Horn: single agg expected AggregateFunction, got "
+ + fnExpr.getClass().getSimpleName());
+ }
+ AggregateFunction fn = (AggregateFunction) fnExpr;
+ Alias result = new Alias(new AggregateExpression(fn, param));
+ output.add(result);
+ hornCtx.getScalarUidToSlot().put(uid, result.toSlot());
+ }
+ }
+ }
+
+ PhysicalHashAggregate aggPlan = new PhysicalHashAggregate<>(
+ groupByExprs, output, Optional.empty(),
+ param, false /* maybeUsingStream */, null /* logicalProperties */, child);
+
+ // nullable 升格回写由 buildHelper 末尾 syncMapWithPlanOutput 接管。
+ // scalarUidToAggFunc 无需在此再注册:local 已 put OLD fn,global ctor 的
+ // adjustNullableForOutputs 幂等补救(groupBy 空性一致 → fn 重建为等价新版)。
+ return aggPlan;
+ }
+
+ /**
+ * 反译 horn PhysicalWindow → Doris PhysicalWindow。
+ * forward 已保证:analytic_functions 非空、共享同一 (partition, order, frame) spec、
+ * window_frame 一定 emit,故本函数不再兜底空检查 / frame 默认值 / spec 拆组。
+ * alias name 用 {@code fnExpr.toSql()}(TPhysicalWindow 不携带原始 SQL alias);
+ * isSkew 暂固定 false。
+ */
+ private PhysicalPlan buildWindow(TExpression texpr, PhysicalPlan child) throws UserException {
+ TPhysicalWindow twindow = texpr.getOp().getOp_union().getPhysical_window();
+
+ List partitionKeys = twindow.isSetPartition_exprs()
+ ? exprTranslator.translateList(twindow.getPartition_exprs())
+ : ImmutableList.of();
+
+ List orderKeys = new ArrayList<>();
+ if (twindow.isSetOrder_by_exprs()) {
+ for (OrderKey orderKey : buildOrderKeys(twindow.getOrder_by_exprs())) {
+ orderKeys.add(new OrderExpression(orderKey));
+ }
+ }
+
+ // frame 经 translateScalarNode 的 kWindowFrame case 分发(唯一返回 WindowFrame),直接强转。
+ WindowFrame frame = (WindowFrame) exprTranslator.translate(twindow.getWindow_frame().get(0));
+
+ List windowExpressions = new ArrayList<>();
+ for (TOperator fnOp : twindow.getAnalytic_functions()) {
+ TScalar rootScalar = Horn2DorisUtils.getRootScalar(fnOp);
+ long uid = rootScalar.getScalar_unique_id().getUnique_id();
+ Expression fnExpr = exprTranslator.translate(fnOp);
+ WindowExpression wExpr = new WindowExpression(
+ fnExpr, partitionKeys, orderKeys, frame, false /* isSkew */);
+ Alias windowAlias = new Alias(wExpr, fnExpr.toSql());
+ windowExpressions.add(windowAlias);
+ hornCtx.getScalarUidToSlot().put(uid, windowAlias.toSlot());
+ }
+
+ WindowFrameGroup wfg = new WindowFrameGroup(windowExpressions.get(0));
+ for (int i = 1; i < windowExpressions.size(); i++) {
+ wfg.addGroup(windowExpressions.get(i));
+ }
+
+ // 跟 Doris 主线一致:requireProperties 只是占位符,真正的 child 需求由
+ // RequestPropertyDeriver.visitPhysicalWindow 从 partition/order 动态推导。
+ RequireProperties requireProperties = RequireProperties.followParent();
+
+ return new PhysicalWindow<>(
+ wfg, requireProperties, windowExpressions, false /* isSkew */,
+ null /* logicalProperties */, child);
+ }
+
+ /**
+ * thrift kPhysicalGrouping → Doris PhysicalRepeat.
+ * GROUPING_ID slot nullable=false to match Doris NormalizeRepeat / BE repeat_operator DCHECK;
+ * other grouping_expr_list slots keep nullable as-is. col_to_grouping_col_map is built on BE,
+ * not read here.
+ */
+ private PhysicalPlan buildRepeat(TExpression texpr, PhysicalPlan child) throws UserException {
+ TPhysicalGrouping tg = texpr.getOp().getOp_union().getPhysical_grouping();
+
+ // 1. grouping_set_list → List>
+ List> groupingSets = new ArrayList<>(tg.getGrouping_set_list().size());
+ for (List tset : tg.getGrouping_set_list()) {
+ groupingSets.add(exprTranslator.translateList(tset));
+ }
+
+ // 2. grouping_id_func:单元素 list → SlotReference(fresh ExprId 由 forward 端 emit 保留)
+ // 强制 nullable=false 对齐 BE repeat_operator 的 GROUPING_ID 非空契约。
+ TOperator tGroupingIdFn = tg.getGrouping_id_func().get(0);
+ SlotReference groupingId = (SlotReference) exprTranslator.translate(tGroupingIdFn);
+ groupingId = groupingId.withNullable(false);
+ long groupingIdUid = Horn2DorisUtils.getRootScalar(tGroupingIdFn)
+ .getScalar_unique_id().getUnique_id();
+ hornCtx.getScalarUidToSlot().put(groupingIdUid, groupingId);
+
+ // 3. outputExpressions = grouping_expr_list ∪ aggregate_function_inputs
+ // 每个 slot 注册到 scalarUidToSlot 让上层 group_by 引用命中;nullable 保留已有结果,
+ // backward 不再二次强制。
+ List outputExpressions = new ArrayList<>();
+ for (TOperator colOp : tg.getGrouping_expr_list()) {
+ SlotReference slot = (SlotReference) exprTranslator.translate(colOp);
+ outputExpressions.add(slot);
+ long uid = Horn2DorisUtils.getRootScalar(colOp).getScalar_unique_id().getUnique_id();
+ hornCtx.getScalarUidToSlot().put(uid, slot);
+ }
+ if (tg.isSetAggregate_function_inputs()) {
+ for (TOperator colOp : tg.getAggregate_function_inputs()) {
+ SlotReference slot = (SlotReference) exprTranslator.translate(colOp);
+ outputExpressions.add(slot);
+ long uid = Horn2DorisUtils.getRootScalar(colOp).getScalar_unique_id().getUnique_id();
+ hornCtx.getScalarUidToSlot().put(uid, slot);
+ }
+ }
+
+ // 4. PhysicalRepeat ctor(主线签名不含 col_to_grouping_col_map);logicalProperties 传 null:
+ // buildHelper 末尾 setPhysicalProperties + syncMapWithPlanOutput 统一处理。
+ return new PhysicalRepeat<>(
+ groupingSets, outputExpressions, groupingId,
+ null /* logicalProperties */, child);
+ }
+
+ /** 把 horn TOrderSpec.order_by_exprs 翻译成 Doris OrderKey 列表。 */
+ private List buildOrderKeys(TOrderSpec sortInfo) {
+ List orderKeys = new ArrayList<>();
+ List exprList = sortInfo.getOrder_by_exprs();
+ List ascList = sortInfo.getIs_asc_order();
+ List nullsFirstList = sortInfo.getNulls_first();
+ if (exprList == null) {
+ return orderKeys;
+ }
+ for (int i = 0; i < exprList.size(); ++i) {
+ Expression expr = exprTranslator.translate(exprList.get(i));
+ boolean asc = i < ascList.size() ? ascList.get(i) : true;
+ boolean nullsFirst = i < nullsFirstList.size() ? nullsFirstList.get(i) : !asc;
+ orderKeys.add(new OrderKey(expr, asc, nullsFirst));
+ }
+ return orderKeys;
+ }
+
+ // getRootScalar 已移到 Horn2DorisUtils(跨 builder 复用:DorisExpressionBuilder.buildFrameBoundary 也需要)
+
+}
diff --git a/fe/fe-core/src/main/java/org/apache/doris/horn/horn2doris/Horn2DorisUtils.java b/fe/fe-core/src/main/java/org/apache/doris/horn/horn2doris/Horn2DorisUtils.java
new file mode 100644
index 00000000000000..bca48337c2250e
--- /dev/null
+++ b/fe/fe-core/src/main/java/org/apache/doris/horn/horn2doris/Horn2DorisUtils.java
@@ -0,0 +1,330 @@
+// 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.horn2doris;
+
+
+import org.apache.doris.catalog.ColocateTableIndex;
+import org.apache.doris.catalog.Column;
+import org.apache.doris.catalog.DistributionInfo;
+import org.apache.doris.catalog.Env;
+import org.apache.doris.catalog.HashDistributionInfo;
+import org.apache.doris.catalog.OlapTable;
+import org.apache.doris.catalog.PartitionType;
+import org.apache.doris.nereids.properties.ChildOutputPropertyDeriver;
+import org.apache.doris.nereids.properties.DistributionSpec;
+import org.apache.doris.nereids.properties.DistributionSpecHash;
+import org.apache.doris.nereids.properties.DistributionSpecHash.ShuffleType;
+import org.apache.doris.nereids.properties.DistributionSpecStorageAny;
+import org.apache.doris.nereids.properties.PhysicalProperties;
+import org.apache.doris.nereids.trees.expressions.ExprId;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.NamedExpression;
+import org.apache.doris.nereids.trees.expressions.Slot;
+import org.apache.doris.nereids.trees.expressions.SlotReference;
+import org.apache.doris.nereids.trees.plans.JoinType;
+import org.apache.doris.nereids.trees.plans.Plan;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalDistribute;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalOlapScan;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalPlan;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalUnion;
+import org.apache.doris.qe.ConnectContext;
+import org.apache.doris.statistics.ColumnStatistic;
+import org.apache.doris.statistics.Statistics;
+import org.apache.doris.statistics.StatisticsCache;
+
+import org.apache.horn4j.thrift.TExpression;
+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.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * horn2doris 反译路径下的纯静态辅助方法集中点 —— 跟 {@link DorisPhysicalPlanBuilder}
+ * 实例状态无关的逻辑都放这里。
+ */
+public final class Horn2DorisUtils {
+
+ private static final Logger LOG = LogManager.getLogger(Horn2DorisUtils.class);
+
+ private Horn2DorisUtils() {
+ }
+
+ /**
+ * 从 TOperator 取 root TScalar,兼容 scalar 单节点 / fscalar 扁平树(root = scalars[0])两种形态。
+ */
+ public static TScalar getRootScalar(TOperator op) {
+ TOperatorUnion union = op.getOp_union();
+ if (union.isSetFscalar()) {
+ List scalars = union.getFscalar().getScalars();
+ if (scalars == null || scalars.isEmpty()) {
+ throw new IllegalArgumentException(
+ "Horn: fscalar with empty scalars list, op_type=" + op.getOp_type());
+ }
+ return scalars.get(0).getOp_union().getScalar();
+ }
+ if (!union.isSetScalar()) {
+ throw new IllegalArgumentException(
+ "Horn: TOperator has neither scalar nor fscalar payload, op_type="
+ + op.getOp_type());
+ }
+ return union.getScalar();
+ }
+
+ public static boolean isHashJoin(TOperatorType opType) {
+ switch (opType) {
+ case kPhysicalInnerHashJoin:
+ case kPhysicalLeftOuterHashJoin:
+ case kPhysicalRightOuterHashJoin:
+ case kPhysicalFullOuterHashJoin:
+ case kPhysicalLeftSemiHashJoin:
+ case kPhysicalRightSemiHashJoin:
+ case kPhysicalLeftAntiSemiHashJoin:
+ case kPhysicalRightAntiSemiHashJoin:
+ case kPhysicalNullAwareLeftAntiSemiHashJoin:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+
+ public static JoinType getJoinType(TOperatorType opType) {
+ switch (opType) {
+ case kPhysicalInnerHashJoin:
+ case kPhysicalInnerNestedLoopJoin:
+ return JoinType.INNER_JOIN;
+ case kPhysicalLeftOuterHashJoin:
+ case kPhysicalLeftOuterNestedLoopJoin:
+ return JoinType.LEFT_OUTER_JOIN;
+ case kPhysicalRightOuterHashJoin:
+ case kPhysicalRightOuterNestedLoopJoin:
+ return JoinType.RIGHT_OUTER_JOIN;
+ case kPhysicalFullOuterHashJoin:
+ case kPhysicalFullOuterNestedLoopJoin:
+ return JoinType.FULL_OUTER_JOIN;
+ case kPhysicalLeftSemiHashJoin:
+ case kPhysicalLeftSemiNestedLoopJoin:
+ return JoinType.LEFT_SEMI_JOIN;
+ case kPhysicalRightSemiHashJoin:
+ case kPhysicalRightSemiNestedLoopJoin:
+ return JoinType.RIGHT_SEMI_JOIN;
+ case kPhysicalLeftAntiSemiHashJoin:
+ case kPhysicalLeftAntiSemiNestedLoopJoin:
+ return JoinType.LEFT_ANTI_JOIN;
+ case kPhysicalRightAntiSemiHashJoin:
+ case kPhysicalRightAntiSemiNestedLoopJoin:
+ return JoinType.RIGHT_ANTI_JOIN;
+ case kPhysicalNullAwareLeftAntiSemiHashJoin:
+ case kPhysicalNullAwareLeftAntiSemiNestedLoopJoin:
+ return JoinType.NULL_AWARE_LEFT_ANTI_JOIN;
+ case kPhysicalCrossJoin:
+ return JoinType.CROSS_JOIN;
+ default:
+ throw new IllegalStateException("Horn: unsupported join op_type: " + opType);
+ }
+ }
+
+ /**
+ * 用 Horn TExpression.cost_info.row_numbers 作行数,列 stats 从 Doris StatisticsCache 查。
+ * 仅 base-table 列(SlotReference 带 OlapTable + originalColumn)查真实 stats,其余回退 UNKNOWN。
+ */
+ public static Statistics buildStatistics(TExpression texpr, PhysicalPlan plan) {
+ double rowCount = 1.0;
+ if (texpr.isSetCost_info() && texpr.getCost_info().isSetRow_numbers()) {
+ rowCount = Math.max(texpr.getCost_info().getRow_numbers(), 1.0);
+ }
+ Map colStats = new HashMap<>();
+ ConnectContext ctx = ConnectContext.get();
+ StatisticsCache statsCache = ctx != null ? Env.getCurrentEnv().getStatisticsCache() : null;
+ for (Slot slot : plan.getOutput()) {
+ colStats.put(slot, resolveColumnStatistic(slot, statsCache, ctx));
+ }
+ return new Statistics(rowCount, colStats);
+ }
+
+ private static ColumnStatistic resolveColumnStatistic(
+ Slot slot, StatisticsCache statsCache, ConnectContext ctx) {
+ if (statsCache == null || !(slot instanceof SlotReference)) {
+ return ColumnStatistic.UNKNOWN;
+ }
+ SlotReference sr = (SlotReference) slot;
+ if (!sr.getOriginalTable().isPresent() || !sr.getOriginalColumn().isPresent()) {
+ return ColumnStatistic.UNKNOWN;
+ }
+ if (!(sr.getOriginalTable().get() instanceof OlapTable)) {
+ return ColumnStatistic.UNKNOWN;
+ }
+ OlapTable table = (OlapTable) sr.getOriginalTable().get();
+ ColumnStatistic stat = statsCache.getColumnStatistics(
+ table.getDatabase().getCatalog().getId(),
+ table.getDatabase().getId(),
+ table.getId(),
+ table.getBaseIndexId(),
+ sr.getOriginalColumn().get().getName(),
+ ctx);
+ return stat != null ? stat : ColumnStatistic.UNKNOWN;
+ }
+
+ /**
+ * 给 plan 设 PhysicalProperties + Statistics。分布派生复用主线 {@code ChildOutputPropertyDeriver}。
+ * 仅 {@code PhysicalOlapScan} / {@code PhysicalDistribute} 因 deriver 需解引用反译路径没有的
+ * context,改用节点自带的 DistributionSpec;其余算子委托 deriver(传 {@code null} context 安全)。
+ */
+ public static PhysicalPlan setPhysicalProperties(TExpression texpr, PhysicalPlan plan) {
+ PhysicalProperties props;
+ if (plan instanceof PhysicalOlapScan) {
+ props = new PhysicalProperties(((PhysicalOlapScan) plan).getDistributionSpec());
+ } else if (plan instanceof PhysicalDistribute) {
+ props = new PhysicalProperties(((PhysicalDistribute>) plan).getDistributionSpec());
+ } else {
+ // union 孩子混合 NATURAL 与 STORAGE_BUCKETED 时无损重贴标签,避免 deriver 降级、保住 bucketShuffle
+ if (plan instanceof PhysicalUnion) {
+ plan = relabelUnionNaturalChildrenToStorageBucketed((PhysicalUnion) plan);
+ }
+ List childProps = new ArrayList<>();
+ for (Plan child : plan.children()) {
+ childProps.add(((PhysicalPlan) child).getPhysicalProperties());
+ }
+ props = plan.accept(new ChildOutputPropertyDeriver(childProps), null);
+ }
+ return (PhysicalPlan) plan.withPhysicalPropertiesAndStats(
+ props, buildStatistics(texpr, plan));
+ }
+
+ /**
+ * intersect/except 输出列 nullable 对齐——复刻 Doris {@code LogicalSetOperation.buildNewOutputs}
+ * (output nullable = 各孩子对应列 nullable 的 OR)。BE {@code set_source_operator} 严格校验此关系,
+ * 不一致即报错;仅 intersect/except 需要,union 的 BE 无此校验。
+ */
+ public static List alignSetOpOutputNullable(
+ List outputs, List> childrenOutputs) {
+ List aligned = new ArrayList<>(outputs.size());
+ for (int i = 0; i < outputs.size(); i++) {
+ boolean childOrNullable = false;
+ for (List childCols : childrenOutputs) {
+ if (i < childCols.size() && childCols.get(i).nullable()) {
+ childOrNullable = true;
+ break;
+ }
+ }
+ NamedExpression out = outputs.get(i);
+ if (out instanceof SlotReference && ((SlotReference) out).nullable() != childOrNullable) {
+ aligned.add(((SlotReference) out).withNullable(childOrNullable));
+ } else {
+ aligned.add(out);
+ }
+ }
+ return aligned;
+ }
+
+ /**
+ * union 孩子若同时有 NATURAL 与 STORAGE_BUCKETED 的 DistributionSpecHash 且对齐同一张表存储布局
+ * (tableId + indexId + partitionIds 全相同),把 NATURAL 无损重贴为 STORAGE_BUCKETED,使各孩子
+ * shuffleType 字面一致,让 union 产出干净 Hash、上层 join 得以 bucketShuffle。copy-on-write,不改原节点。
+ */
+ private static PhysicalUnion relabelUnionNaturalChildrenToStorageBucketed(PhysicalUnion union) {
+ long tableId = -1;
+ long indexId = -1;
+ java.util.Set partitionIds = null;
+ boolean hasNatural = false;
+ boolean hasStorageBucketed = false;
+ boolean aligned = true;
+ for (Plan child : union.children()) {
+ DistributionSpec spec = ((PhysicalPlan) child).getPhysicalProperties().getDistributionSpec();
+ if (!(spec instanceof DistributionSpecHash)) {
+ return union; // 有非 Hash 孩子 → 不适用
+ }
+ DistributionSpecHash hash = (DistributionSpecHash) spec;
+ ShuffleType st = hash.getShuffleType();
+ if (st == ShuffleType.NATURAL) {
+ hasNatural = true;
+ } else if (st == ShuffleType.STORAGE_BUCKETED) {
+ hasStorageBucketed = true;
+ } else {
+ return union; // 其它 shuffleType(EXECUTION_BUCKETED 等)→ 不动
+ }
+ if (tableId == -1) {
+ tableId = hash.getTableId();
+ indexId = hash.getSelectedIndexId();
+ partitionIds = hash.getPartitionIds();
+ } else if (tableId != hash.getTableId() || indexId != hash.getSelectedIndexId()
+ || !partitionIds.equals(hash.getPartitionIds())) {
+ aligned = false;
+ }
+ }
+ if (!hasNatural || !hasStorageBucketed || !aligned) {
+ return union; // 无混合,或跨表/桶布局不同 → 不重贴
+ }
+ List newChildren = new ArrayList<>();
+ for (Plan child : union.children()) {
+ PhysicalPlan pc = (PhysicalPlan) child;
+ DistributionSpecHash hash = (DistributionSpecHash) pc.getPhysicalProperties().getDistributionSpec();
+ if (hash.getShuffleType() == ShuffleType.NATURAL) {
+ PhysicalProperties relabeled = new PhysicalProperties(
+ hash.withShuffleType(ShuffleType.STORAGE_BUCKETED));
+ newChildren.add((Plan) pc.withPhysicalPropertiesAndStats(relabeled, pc.getStats()));
+ } else {
+ newChildren.add(child);
+ }
+ }
+ return union.withChildren(newChildren);
+ }
+
+ /**
+ * 给 scan 算子构造 colocate-capable DistributionSpec —— 仿主线
+ * {@code LogicalOlapScanToPhysicalOlapScan.convertDistribution}。HashDistributionInfo +
+ * (stable colocate group || 单分区) → 带 tableId/indexId/partitionIds 的 NATURAL hash spec,
+ * 否则 fallback {@code DistributionSpecStorageAny.INSTANCE}。
+ */
+ public static DistributionSpec buildScanDistributionSpec(
+ OlapTable olapTable, List scanSlots, List partitionIds) {
+ DistributionInfo distInfo = olapTable.getDefaultDistributionInfo();
+ ColocateTableIndex colocateIndex = Env.getCurrentColocateIndex();
+ boolean isBelongStableCG = colocateIndex.isColocateTable(olapTable.getId())
+ && !colocateIndex.isGroupUnstable(colocateIndex.getGroup(olapTable.getId()))
+ && olapTable.getCatalogId() == Env.getCurrentInternalCatalog().getId();
+ boolean isSelectUnpartition = olapTable.getPartitionInfo().getType() == PartitionType.UNPARTITIONED
+ || partitionIds.size() == 1;
+ if (!(distInfo instanceof HashDistributionInfo) || (!isBelongStableCG && !isSelectUnpartition)) {
+ return DistributionSpecStorageAny.INSTANCE;
+ }
+ HashDistributionInfo hashDist = (HashDistributionInfo) distInfo;
+ List hashExprIds = new ArrayList<>();
+ for (Column distCol : hashDist.getDistributionColumns()) {
+ for (Slot slot : scanSlots) {
+ if (slot instanceof SlotReference
+ && slot.getName().equalsIgnoreCase(distCol.getName())) {
+ hashExprIds.add(slot.getExprId());
+ break;
+ }
+ }
+ }
+ return new DistributionSpecHash(hashExprIds, ShuffleType.NATURAL,
+ olapTable.getId(), olapTable.getBaseIndexId(),
+ new LinkedHashSet<>(partitionIds));
+ }
+
+}
diff --git a/fe/fe-core/src/main/java/org/apache/doris/horn/horn2doris/HornTypeToDorisConverter.java b/fe/fe-core/src/main/java/org/apache/doris/horn/horn2doris/HornTypeToDorisConverter.java
new file mode 100644
index 00000000000000..c357cbbcd251e1
--- /dev/null
+++ b/fe/fe-core/src/main/java/org/apache/doris/horn/horn2doris/HornTypeToDorisConverter.java
@@ -0,0 +1,100 @@
+// 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.horn2doris;
+
+
+import org.apache.doris.nereids.types.BigIntType;
+import org.apache.doris.nereids.types.BooleanType;
+import org.apache.doris.nereids.types.CharType;
+import org.apache.doris.nereids.types.DataType;
+import org.apache.doris.nereids.types.DateV2Type;
+import org.apache.doris.nereids.types.DecimalV3Type;
+import org.apache.doris.nereids.types.DoubleType;
+import org.apache.doris.nereids.types.FloatType;
+import org.apache.doris.nereids.types.IntegerType;
+import org.apache.doris.nereids.types.NullType;
+import org.apache.doris.nereids.types.SmallIntType;
+import org.apache.doris.nereids.types.StringType;
+import org.apache.doris.nereids.types.TinyIntType;
+import org.apache.doris.nereids.types.VarcharType;
+
+import org.apache.horn4j.thrift.TColumnType;
+import org.apache.horn4j.thrift.TPrimitiveType;
+import org.apache.horn4j.thrift.TScalarType;
+import org.apache.horn4j.thrift.TTypeNode;
+
+/**
+ * Converts Horn TColumnType (Thrift) back to Doris Nereids DataType (reverse of DorisTypeToHornConverter).
+ */
+public class HornTypeToDorisConverter {
+
+ /**
+ * Convert Horn TColumnType to Doris DataType. Throws {@link UnsupportedOperationException}
+ * on unrecognised input so the caller can fall back to the Nereids native optimizer.
+ */
+ public static DataType convert(TColumnType hornType) {
+ if (hornType == null || hornType.getTypes() == null || hornType.getTypes().isEmpty()) {
+ throw new UnsupportedOperationException("Horn: empty TColumnType");
+ }
+
+ TTypeNode typeNode = hornType.getTypes().get(0);
+ if (!typeNode.isSetScalar_type()) {
+ throw new UnsupportedOperationException(
+ "Horn: non-scalar TColumnType not supported: " + hornType);
+ }
+
+ TScalarType scalarType = typeNode.getScalar_type();
+ TPrimitiveType primType = scalarType.getType();
+
+ switch (primType) {
+ case BOOLEAN:
+ return BooleanType.INSTANCE;
+ case TINYINT:
+ return TinyIntType.INSTANCE;
+ case SMALLINT:
+ return SmallIntType.INSTANCE;
+ case INT:
+ return IntegerType.INSTANCE;
+ case BIGINT:
+ return BigIntType.INSTANCE;
+ case FLOAT:
+ return FloatType.INSTANCE;
+ case DOUBLE:
+ return DoubleType.INSTANCE;
+ case DECIMAL:
+ // 反译统一回 DecimalV3
+ return DecimalV3Type.createDecimalV3Type(
+ scalarType.getPrecision(), scalarType.getScale());
+ case CHAR:
+ return CharType.createCharType(scalarType.getLen());
+ case VARCHAR:
+ return VarcharType.createVarcharType(scalarType.getLen());
+ case STRING:
+ return StringType.INSTANCE;
+ case DATE:
+ // 反译统一回 DateV2
+ return DateV2Type.INSTANCE;
+ case NULL_TYPE:
+ return NullType.INSTANCE;
+ default:
+ // 暂不支持 TIMESTAMP
+ throw new UnsupportedOperationException(
+ "Horn: unsupported TPrimitiveType: " + primType);
+ }
+ }
+}
diff --git a/fe/fe-core/src/main/java/org/apache/doris/horn/package-info.java b/fe/fe-core/src/main/java/org/apache/doris/horn/package-info.java
new file mode 100644
index 00000000000000..52458b2fd80e51
--- /dev/null
+++ b/fe/fe-core/src/main/java/org/apache/doris/horn/package-info.java
@@ -0,0 +1,32 @@
+// 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.
+
+/**
+ * Horn CBO Optimizer integration for Apache Doris.
+ *
+ *
+ * Doris LogicalPlan (Nereids)
+ * -> Doris -> Horn TFlattenedExpression
+ * -> JNI (HornNative.optimize via libhorn4j.so)
+ * -> Horn -> Doris PhysicalPlan
+ *
+ *
+ * Enable: {@code SET enable_horn_optimizer = true;}
+ *
+ * @see org.apache.doris.horn.HornOptimizer
+ */
+package org.apache.doris.horn;
diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/NereidsPlanner.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/NereidsPlanner.java
index 6747f0a6bea032..49937fa0897ca3 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/NereidsPlanner.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/NereidsPlanner.java
@@ -33,6 +33,7 @@
import org.apache.doris.common.util.DebugUtil;
import org.apache.doris.common.util.TimeUtils;
import org.apache.doris.common.util.Util;
+import org.apache.doris.horn.HornOptimizer;
import org.apache.doris.mysql.FieldInfo;
import org.apache.doris.nereids.exceptions.AnalysisException;
import org.apache.doris.nereids.glue.LogicalPlanAdapter;
@@ -118,6 +119,11 @@ public class NereidsPlanner extends Planner {
protected Plan rewrittenPlan;
protected Plan optimizedPlan;
protected PhysicalPlan physicalPlan;
+ // Horn CBO result: set by optimize(), consumed by planWithoutLock().
+ // null means Horn was not used or failed (fallback to Nereids).
+ private PhysicalPlan hornPhysicalPlan;
+ private String hornExplainString;
+ private String hornFallbackReason;
private CascadesContext cascadesContext;
private final StatementContext statementContext;
@@ -294,7 +300,19 @@ private Plan planWithoutLock(
}
// rule-based optimize
- rewrite(showRewriteProcess(explainLevel, showPlanProcess));
+ // Horn's tree IR cannot model a materialized CTE (a DAG). Force CTE inline when Horn is enabled so
+ // the rewrite phase desugars every CTE into plain operators; restored right after rewrite to keep
+ // the session variable clean.
+ SessionVariable cteSv = cascadesContext.getConnectContext().getSessionVariable();
+ boolean savedCteMaterialize = cteSv.enableCTEMaterialize;
+ if (cteSv.enableHornOptimizer) {
+ cteSv.enableCTEMaterialize = false;
+ }
+ try {
+ rewrite(showRewriteProcess(explainLevel, showPlanProcess));
+ } finally {
+ cteSv.enableCTEMaterialize = savedCteMaterialize;
+ }
// try to pre mv rewrite
preMaterializedViewRewrite();
if (explainLevel == ExplainLevel.REWRITTEN_PLAN || explainLevel == ExplainLevel.ALL_PLAN) {
@@ -305,27 +323,45 @@ private Plan planWithoutLock(
}
optimize(showPlanProcess);
- // print memo before choose plan.
- // if chooseNthPlan failed, we could get memo to debug
- if (cascadesContext.getConnectContext().getSessionVariable().dumpNereidsMemo) {
- Memo memo = cascadesContext.getMemo();
- if (memo != null) {
- LOG.info("{}\n{}", ConnectContext.get().getQueryIdentifier(), memo.toString());
- } else {
- LOG.info("{}\nMemo is null", ConnectContext.get().getQueryIdentifier());
+
+ PhysicalPlan physicalPlan;
+ if (hornPhysicalPlan != null) {
+ // postProcess(RuntimeFilter / Fragment assignment 等)。
+ // Horn does not support lazy materialization yet; disable LazyMaterializeTopN.
+ SessionVariable sv = cascadesContext.getConnectContext().getSessionVariable();
+ int savedThreshold = sv.topNLazyMaterializationThreshold;
+ sv.topNLazyMaterializationThreshold = -1;
+ // 关闭 BE 的 LEFT_SEMI_DIRECT_RETURN 优化:该优化在 multi-instance 下决策不一致
+ // (consumer 拿全局 merged filter,部分 instance DISABLED 却仍走 direct_return、probe 未过滤)。
+ // sticky 修改(不恢复):仅关一个 hash join 性能优化,不动 plan 形态、不关 RF。
+ sv.enableLeftSemiDirectReturnOpt = false;
+ try {
+ physicalPlan = postProcess(hornPhysicalPlan);
+ } finally {
+ sv.topNLazyMaterializationThreshold = savedThreshold;
}
+ } else {
+ // Standard Nereids path: extract best plan from Memo.
+ if (cascadesContext.getConnectContext().getSessionVariable().dumpNereidsMemo) {
+ Memo memo = cascadesContext.getMemo();
+ if (memo != null) {
+ LOG.info("{}\n{}", ConnectContext.get().getQueryIdentifier(), memo.toString());
+ } else {
+ LOG.info("{}\nMemo is null", ConnectContext.get().getQueryIdentifier());
+ }
+ }
+ int nth = cascadesContext.getConnectContext().getSessionVariable().getNthOptimizedPlan();
+ physicalPlan = chooseNthPlan(getRoot(), requireProperties, nth);
+ physicalPlan = postProcess(physicalPlan);
}
- int nth = cascadesContext.getConnectContext().getSessionVariable().getNthOptimizedPlan();
- PhysicalPlan physicalPlan = chooseNthPlan(getRoot(), requireProperties, nth);
-
- physicalPlan = postProcess(physicalPlan);
if (cascadesContext.getConnectContext().getSessionVariable().dumpNereidsMemo) {
String tree = physicalPlan.treeString();
LOG.info("{}\n{}", ConnectContext.get().getQueryIdentifier(), tree);
}
if (explainLevel == ExplainLevel.OPTIMIZED_PLAN
|| explainLevel == ExplainLevel.ALL_PLAN
- || explainLevel == ExplainLevel.SHAPE_PLAN) {
+ || explainLevel == ExplainLevel.SHAPE_PLAN
+ || explainLevel == ExplainLevel.HORN_PLAN) {
optimizedPlan = physicalPlan;
}
// serialize optimized plan to dumpfile, dumpfile do not have this part means optimize failed
@@ -531,9 +567,44 @@ protected void optimize(boolean showPlanProcess) {
if (LOG.isDebugEnabled()) {
LOG.debug("Start optimize plan");
}
- keepOrShowPlanProcess(showPlanProcess, () -> {
- new Optimizer(cascadesContext).execute();
- });
+
+ // Horn CBO optimizer: try Horn first, fall back to Nereids on any failure.
+ boolean useHorn = cascadesContext.getConnectContext().getSessionVariable().enableHornOptimizer;
+ if (useHorn) {
+ long hornStart = System.currentTimeMillis();
+ try {
+ HornOptimizer hornOptimizer = new HornOptimizer(cascadesContext);
+ Plan rewrittenPlan = cascadesContext.getRewritePlan();
+ if (rewrittenPlan instanceof LogicalPlan) {
+ hornPhysicalPlan = hornOptimizer.optimize((LogicalPlan) rewrittenPlan);
+ }
+ long hornElapsed = System.currentTimeMillis() - hornStart;
+ if (hornPhysicalPlan != null) {
+ hornExplainString = hornOptimizer.getHornExplainString();
+ LOG.info("Horn CBO: succeeded in {} ms", hornElapsed);
+ } else {
+ String reason = hornOptimizer.getFallbackReason();
+ hornFallbackReason = (reason != null ? reason : "returned null")
+ + " (" + hornElapsed + " ms)";
+ // 反译失败时 horn C++ 侧仍已产出计划文本;捕获它,让 EXPLAIN HORN PLAN
+ // 能打印 horn 生成的计划,而非只显示 fallback 原因。
+ hornExplainString = hornOptimizer.getHornExplainString();
+ LOG.info("Horn CBO: fallback to Nereids: {}", hornFallbackReason);
+ }
+ } catch (Exception e) {
+ long hornElapsed = System.currentTimeMillis() - hornStart;
+ hornFallbackReason = e.getMessage() + " (" + hornElapsed + " ms)";
+ LOG.warn("Horn CBO: fallback to Nereids: {}", hornFallbackReason);
+ hornPhysicalPlan = null;
+ }
+ }
+
+ // If Horn did not produce a usable plan, run the standard Nereids CBO.
+ if (hornPhysicalPlan == null) {
+ keepOrShowPlanProcess(showPlanProcess, () -> {
+ new Optimizer(cascadesContext).execute();
+ });
+ }
NereidsTracer.logImportantTime("EndOptimizePlan");
if (LOG.isDebugEnabled()) {
LOG.debug("End optimize plan");
@@ -870,6 +941,26 @@ public String getExplainString(ExplainOptions explainOptions) {
case OPTIMIZED_PLAN:
plan = "cost = " + cost + "\n" + optimizedPlan.treeString() + mvSummary;
break;
+ case HORN_PLAN:
+ if (hornExplainString != null && !hornExplainString.isEmpty()) {
+ plan = hornExplainString;
+ // horn 出了计划但 Doris 反译失败:打印 horn 计划并附上失败原因;执行仍走 Nereids fallback。
+ if (hornFallbackReason != null) {
+ plan += "\n\n"
+ + "==================== HORN → DORIS BACKWARD FAILED ====================\n"
+ + "Horn produced the plan above, but Doris backward translation failed.\n"
+ + "Execution fell back to Nereids.\n"
+ + "Reason:\n"
+ + " " + hornFallbackReason.replace(". ", ".\n ") + "\n"
+ + "=====================================================================";
+ }
+ } else if (hornFallbackReason != null) {
+ plan = "Horn CBO fallback to Nereids: " + hornFallbackReason;
+ } else {
+ plan = "Horn CBO was not used for this query. "
+ + "Check enable_horn_optimizer=true.";
+ }
+ break;
case SHAPE_PLAN:
plan = optimizedPlan.shape("");
break;
@@ -927,7 +1018,10 @@ public String getExplainString(ExplainOptions explainOptions) {
default:
plan = super.getExplainString(explainOptions);
plan += mvSummary;
- plan += "\n\n\n========== STATISTICS ==========\n";
+ if (hornFallbackReason != null) {
+ plan += "\n\nHorn CBO fallback to Nereids: " + hornFallbackReason + "\n";
+ }
+ plan += "\n\n========== STATISTICS ==========\n";
if (statementContext != null) {
if (statementContext.isHasUnknownColStats()) {
plan += "planned with unknown column statistics\n";
diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/glue/translator/PhysicalPlanTranslator.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/glue/translator/PhysicalPlanTranslator.java
index f16703518af0f8..0ff932b1f871ab 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/glue/translator/PhysicalPlanTranslator.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/glue/translator/PhysicalPlanTranslator.java
@@ -2449,13 +2449,35 @@ public PlanFragment visitPhysicalQuickSort(PhysicalQuickSort extends Plan> sor
// It means the local has satisfied the Gather property. We can just ignore mergeSort
return inputFragment;
}
- SortNode sortNode = (SortNode) inputFragment.getPlanRoot().getChild(0);
- ((ExchangeNode) inputFragment.getPlanRoot()).setMergeInfo(sortNode.getSortInfo());
+ ExchangeNode exchangeNode = (ExchangeNode) inputFragment.getPlanRoot();
+ // 主路径 (Nereids 标准 ORDER BY):从 child LOCAL_SORT 的 SortNode 取 mergeInfo。
+ // 兜底路径 (horn sort_elimination):child 已天然有序、无 LOCAL_SORT 占位,
+ // 直接从 PhysicalQuickSort orderKeys 构造 SortInfo,sortTuple 必须复用 child input tuple。
+ if (exchangeNode.getChild(0) instanceof SortNode) {
+ SortNode childSort = (SortNode) exchangeNode.getChild(0);
+ exchangeNode.setMergeInfo(childSort.getSortInfo());
+ childSort.setMergeByExchange();
+ // distributeExprLists 必须设到下游 SortNode:BE mergeInfo 路径从 SortNode 取
+ childSort.setChildrenDistributeExprLists(distributeExprLists);
+ } else {
+ TupleDescriptor childTuple = context.getDescTable()
+ .getTupleDesc(exchangeNode.getTupleIds().get(0));
+ List orderingExprs = Lists.newArrayList();
+ List ascOrders = Lists.newArrayList();
+ List nullsFirstParams = Lists.newArrayList();
+ sort.getOrderKeys().forEach(k -> {
+ orderingExprs.add(ExpressionTranslator.translate(k.getExpr(), context));
+ ascOrders.add(k.isAsc());
+ nullsFirstParams.add(k.isNullFirst());
+ });
+ exchangeNode.setMergeInfo(
+ new SortInfo(orderingExprs, ascOrders, nullsFirstParams, childTuple));
+ // 兜底路径无 child SortNode,distributeExprLists 由 exchangeNode 自持作 fallback。
+ exchangeNode.setChildrenDistributeExprLists(distributeExprLists);
+ }
if (inputFragment.hasChild(0) && inputFragment.getChild(0).getSink() != null) {
inputFragment.getChild(0).getSink().setMerge(true);
}
- sortNode.setMergeByExchange();
- sortNode.setChildrenDistributeExprLists(distributeExprLists);
}
return inputFragment;
}
diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
index e3e666e5e0e733..160c8d198ea54e 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
@@ -4937,6 +4937,9 @@ private ExplainLevel parseExplainPlanType(PlanTypeContext planTypeContext) {
if (planTypeContext.DISTRIBUTED() != null) {
return ExplainLevel.DISTRIBUTED_PLAN;
}
+ if (planTypeContext.HORN() != null) {
+ return ExplainLevel.HORN_PLAN;
+ }
return ExplainLevel.ALL_PLAN;
}
diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/NereidsParser.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/NereidsParser.java
index 37c7f9351908f7..1ab6730c74d982 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/NereidsParser.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/NereidsParser.java
@@ -90,6 +90,7 @@ public class NereidsParser {
EXPLAIN_TOKENS.set(DorisLexer.OPTIMIZED);
EXPLAIN_TOKENS.set(DorisLexer.PLAN);
EXPLAIN_TOKENS.set(DorisLexer.PROCESS);
+ EXPLAIN_TOKENS.set(DorisLexer.HORN);
ImmutableSet.Builder nonReserveds = ImmutableSet.builder();
for (Method declaredMethod : NonReservedContext.class.getDeclaredMethods()) {
@@ -178,6 +179,9 @@ public static Optional> tryParseExplainPlan(String
} else if (tokenType == DorisLexer.PHYSICAL || tokenType == DorisLexer.OPTIMIZED) {
explainLevel = ExplainLevel.OPTIMIZED_PLAN;
token = readUntilNonComment(tokenSource);
+ } else if (tokenType == DorisLexer.HORN) {
+ explainLevel = ExplainLevel.HORN_PLAN;
+ token = readUntilNonComment(tokenSource);
}
if (token == null) {
diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/implementation/LogicalWindowToPhysicalWindow.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/implementation/LogicalWindowToPhysicalWindow.java
index 2581a87cdfc421..3e1a3cbfbc0539 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/implementation/LogicalWindowToPhysicalWindow.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/implementation/LogicalWindowToPhysicalWindow.java
@@ -142,8 +142,10 @@ private PhysicalWindow createPhysicalWindow(Plan root, WindowFrameGroup wi
* WindowFunctionRelatedGroups
* ******************************************************************************************** */
- // todo: can we simplify the following three algorithms?
- private List createWindowFrameGroups(List windowList) {
+ /**
+ * Create window frame groups by partition, order, and frame.
+ */
+ public static List createWindowFrameGroups(List windowList) {
List windowFrameGroupList = Lists.newArrayList();
for (int i = 0; i < windowList.size(); i++) {
NamedExpression windowAlias = windowList.get(i);
diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ExplainCommand.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ExplainCommand.java
index badd660734c033..fb5e3aba0f18ea 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ExplainCommand.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ExplainCommand.java
@@ -54,7 +54,8 @@ public enum ExplainLevel {
SHAPE_PLAN(true),
MEMO_PLAN(true),
DISTRIBUTED_PLAN(true),
- ALL_PLAN(true)
+ ALL_PLAN(true),
+ HORN_PLAN(true)
;
public final boolean isPlanLevel;
diff --git a/fe/fe-core/src/main/java/org/apache/doris/qe/SessionVariable.java b/fe/fe-core/src/main/java/org/apache/doris/qe/SessionVariable.java
index 99fdf1dd3ecc6c..f3ee6994fce015 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/qe/SessionVariable.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/qe/SessionVariable.java
@@ -368,6 +368,9 @@ public class SessionVariable implements Serializable, Writable {
public static final String ENABLE_DPHYP_OPTIMIZER = "enable_dphyp_optimizer";
public static final String DPHYPER_LIMIT = "dphyper_limit";
+ public static final String ENABLE_HORN_OPTIMIZER = "enable_horn_optimizer";
+ public static final String ENABLE_HORN_DETAIL_INFO = "enable_horn_detail_info";
+ public static final String ENABLE_HORN_PROFILE = "enable_horn_profile";
public static final String ENABLE_LEFT_ZIG_ZAG = "enable_left_zig_zag";
public static final String ENABLE_HBO_OPTIMIZATION = "enable_hbo_optimization";
public static final String ENABLE_HBO_INFO_COLLECTION = "enable_hbo_info_collection";
@@ -1781,6 +1784,21 @@ public void setMaxJoinNumberOfReorder(int maxJoinNumberOfReorder) {
@VariableMgr.VarAttr(name = ENABLE_DPHYP_OPTIMIZER)
public boolean enableDPHypOptimizer = false;
+ @VariableMgr.VarAttr(name = ENABLE_HORN_OPTIMIZER, description = {
+ "启用 Horn CBO 优化器替代 Nereids 内置优化器进行查询优化",
+ "Enable Horn CBO optimizer to replace the built-in Nereids optimizer for query optimization"})
+ public boolean enableHornOptimizer = true;
+
+ @VariableMgr.VarAttr(name = ENABLE_HORN_DETAIL_INFO, description = {
+ "启用 Horn 优化器详细日志(cascade log、detail log、stats log)",
+ "Enable Horn optimizer detailed logging (cascade log, detail log, stats log)"})
+ public boolean enableHornDetailInfo = true;
+
+ @VariableMgr.VarAttr(name = ENABLE_HORN_PROFILE, description = {
+ "EXPLAIN HORN PLAN 输出附带 cascades 各阶段耗时 timing 列表",
+ "Append cascades stage timing (Total Time + per-stage) to EXPLAIN HORN PLAN output"})
+ public boolean enableHornProfile = true;
+
@VariableMgr.VarAttr(name = SHORT_CIRCUIT_EVALUATION, fuzzy = true, description = { "是否启用短路求值",
"Whether to enable short-circuit evaluation" })
public boolean shortCircuitEvaluation = false;
diff --git a/fe/horn4j/pom.xml b/fe/horn4j/pom.xml
new file mode 100644
index 00000000000000..978aaac918db7b
--- /dev/null
+++ b/fe/horn4j/pom.xml
@@ -0,0 +1,82 @@
+
+
+
+ 4.0.0
+
+ org.apache.doris
+ fe
+ ${revision}
+ ../pom.xml
+
+ org.apache.horn4j
+ horn4j
+ 1.0.0-SNAPSHOT
+ jar
+
+
+ ${project.basedir}/../custom_libs/horn4j-1.0.0-SNAPSHOT.jar.parts
+ ${project.build.directory}/horn4j-1.0.0-SNAPSHOT.jar
+ 0ce074e632d188dbc671c8e50414374b8db7699ce7269deb3a3ac1c271414212
+
+
+
+
+ org.apache.thrift
+ libthrift
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-antrun-plugin
+ 3.1.0
+
+
+ unpack-vendored-horn4j
+ generate-resources
+
+ run
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fe/pom.xml b/fe/pom.xml
index 5f083768bd6e8d..2a4d6425361b84 100644
--- a/fe/pom.xml
+++ b/fe/pom.xml
@@ -218,6 +218,7 @@ under the License.
fe-common
fe-extension-spi
fe-extension-loader
+ horn4j
fe-core
hive-udf
be-java-extensions