HornLogicalPlanThriftBuilder.java
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.doris.horn.doris2horn;
import org.apache.doris.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.KeysType;
import org.apache.doris.catalog.OlapTable;
import org.apache.doris.horn.HornOptimizationContext;
import org.apache.doris.horn.horn2doris.Horn2DorisUtils;
import org.apache.doris.nereids.properties.DistributionSpec;
import org.apache.doris.nereids.properties.DistributionSpecHash;
import org.apache.doris.nereids.properties.OrderKey;
import org.apache.doris.nereids.rules.implementation.LogicalWindowToPhysicalWindow;
import org.apache.doris.nereids.rules.implementation.LogicalWindowToPhysicalWindow.WindowFrameGroup;
import org.apache.doris.nereids.rules.rewrite.MultiDistinctFunctionStrategy;
import org.apache.doris.nereids.trees.expressions.Expression;
import org.apache.doris.nereids.trees.expressions.NamedExpression;
import org.apache.doris.nereids.trees.expressions.OrderExpression;
import org.apache.doris.nereids.trees.expressions.Slot;
import org.apache.doris.nereids.trees.expressions.SlotReference;
import org.apache.doris.nereids.trees.expressions.WindowExpression;
import org.apache.doris.nereids.trees.expressions.WindowFrame;
import org.apache.doris.nereids.trees.expressions.functions.agg.AggregateFunction;
import org.apache.doris.nereids.trees.expressions.functions.scalar.GroupingScalarFunction;
import org.apache.doris.nereids.trees.plans.LimitPhase;
import org.apache.doris.nereids.trees.plans.Plan;
import org.apache.doris.nereids.trees.plans.algebra.SetOperation.Qualifier;
import org.apache.doris.nereids.trees.plans.logical.LogicalAggregate;
import org.apache.doris.nereids.trees.plans.logical.LogicalEmptyRelation;
import org.apache.doris.nereids.trees.plans.logical.LogicalExcept;
import org.apache.doris.nereids.trees.plans.logical.LogicalFilter;
import org.apache.doris.nereids.trees.plans.logical.LogicalIntersect;
import org.apache.doris.nereids.trees.plans.logical.LogicalJoin;
import org.apache.doris.nereids.trees.plans.logical.LogicalLimit;
import org.apache.doris.nereids.trees.plans.logical.LogicalOlapScan;
import org.apache.doris.nereids.trees.plans.logical.LogicalOneRowRelation;
import org.apache.doris.nereids.trees.plans.logical.LogicalPlan;
import org.apache.doris.nereids.trees.plans.logical.LogicalProject;
import org.apache.doris.nereids.trees.plans.logical.LogicalRepeat;
import org.apache.doris.nereids.trees.plans.logical.LogicalResultSink;
import org.apache.doris.nereids.trees.plans.logical.LogicalSetOperation;
import org.apache.doris.nereids.trees.plans.logical.LogicalSort;
import org.apache.doris.nereids.trees.plans.logical.LogicalTopN;
import org.apache.doris.nereids.trees.plans.logical.LogicalUnion;
import org.apache.doris.nereids.trees.plans.logical.LogicalWindow;
import org.apache.doris.nereids.trees.plans.visitor.DefaultPlanVisitor;
import org.apache.doris.qe.ConnectContext;
import org.apache.doris.statistics.ColumnStatistic;
import org.apache.doris.statistics.StatisticsCache;
import com.google.common.collect.Lists;
import org.apache.horn4j.thrift.TColumn;
import org.apache.horn4j.thrift.TColumnStats;
import org.apache.horn4j.thrift.TColumnValue;
import org.apache.horn4j.thrift.TDistributionSpec;
import org.apache.horn4j.thrift.TDistributionType;
import org.apache.horn4j.thrift.TDorisHashSpec;
import org.apache.horn4j.thrift.TExpression;
import org.apache.horn4j.thrift.TFlattenedExpression;
import org.apache.horn4j.thrift.TLogicalAgg;
import org.apache.horn4j.thrift.TLogicalFilter;
import org.apache.horn4j.thrift.TLogicalGrouping;
import org.apache.horn4j.thrift.TLogicalJoin;
import org.apache.horn4j.thrift.TLogicalLimit;
import org.apache.horn4j.thrift.TLogicalProject;
import org.apache.horn4j.thrift.TLogicalScan;
import org.apache.horn4j.thrift.TLogicalSetOp;
import org.apache.horn4j.thrift.TLogicalTableScan;
import org.apache.horn4j.thrift.TLogicalValueScan;
import org.apache.horn4j.thrift.TLogicalWindow;
import org.apache.horn4j.thrift.TOperator;
import org.apache.horn4j.thrift.TOperatorType;
import org.apache.horn4j.thrift.TOperatorUnion;
import org.apache.horn4j.thrift.TOrderSpec;
import org.apache.horn4j.thrift.TScanTableType;
import org.apache.horn4j.thrift.TSpecialHashSpec;
import org.apache.horn4j.thrift.TSpecialHashSpecKind;
import org.apache.horn4j.thrift.TTable;
import org.apache.horn4j.thrift.TTableStats;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
/**
* Visitor: Doris LogicalPlan → Horn TFlattenedExpression。不支持的 Nereids 节点
* 落 {@link #visit} throw,触发 fallback 回 Nereids 原生优化器。
*/
public class HornLogicalPlanThriftBuilder extends DefaultPlanVisitor<Void, List<TExpression>> {
private static final org.apache.logging.log4j.Logger LOG =
org.apache.logging.log4j.LogManager.getLogger(HornLogicalPlanThriftBuilder.class);
private final HornOptimizationContext hornCtx;
private final HornScalarThriftBuilder scalarTranslator;
public HornLogicalPlanThriftBuilder(HornOptimizationContext hornCtx) {
this.hornCtx = hornCtx;
this.scalarTranslator = new HornScalarThriftBuilder();
}
/** Translate the whole LogicalPlan tree to TFlattenedExpression. */
public TFlattenedExpression translate(LogicalPlan logicalPlan) {
TFlattenedExpression result = new TFlattenedExpression();
List<TExpression> texprs = new ArrayList<>();
// sink 由 visitLogicalResultSink emit 成 identity Project,承载 SELECT 列序;
// horn 不消除 Project,列序经 root LogicalProject 自然保留。
logicalPlan.accept(this, texprs);
result.setTexprs(texprs);
return result;
}
@Override
public Void visit(Plan plan, List<TExpression> texprs) {
throw new UnsupportedOperationException(
"Horn: unsupported Nereids node: " + plan.getClass().getSimpleName());
}
/** Append a TExpression for {@code plan}, then recurse into children pre-order. */
private void emit(Plan plan, TOperatorType opType, TOperatorUnion opUnion,
List<TExpression> texprs) {
TExpression texpr = new TExpression();
TOperator top = new TOperator();
top.setOp_type(opType);
top.setOp_union(opUnion);
texpr.setOp(top);
texpr.setArity(plan.children().size());
// FE-declared output column refs:每项是 kColumnRef TOperator(column_name +
// table_name + external_slot_id + data_type),BE 按 external_slot_id 注册 slot_map。
List<TOperator> outputList = new ArrayList<>();
for (Slot slot : plan.getOutput()) {
if (slot instanceof SlotReference) {
outputList.add(scalarTranslator.visitSlotReference((SlotReference) slot, null));
}
}
if (!outputList.isEmpty()) {
texpr.setOutput_list(outputList);
}
texprs.add(texpr);
for (Plan child : plan.children()) {
child.accept(this, texprs);
}
}
@Override
public Void visitLogicalOlapScan(LogicalOlapScan scan, List<TExpression> texprs) {
TOperatorUnion opUnion = new TOperatorUnion();
TLogicalTableScan tscan = new TLogicalTableScan();
OlapTable table = scan.getTable();
// DEBUG: 检查 LogicalOlapScan 是否已做 partition pruning
LOG.warn("HORN_FWD_SCAN table={} totalPartitions={} selectedPartitionIds={} selectedTabletIds.size={}",
table.getName(), table.getPartitionIds().size(),
scan.getSelectedPartitionIds(), scan.getSelectedTabletIds().size());
tscan.setDatabase_name(scan.getQualifier().get(1));
tscan.setTable_name(table.getName());
tscan.setTable_alias(table.getName());
tscan.setTable_type(TScanTableType.kTciTable);
// schema_id 不设:BE 默认 -1 作通配符匹配任意 catalog entry,FE 不需要传。
// scan_list:scan 自有输出列(含列裁剪)的 ColumnRef(带 external_slot_id),
// BE 直接用它而非 TExpression.output_list。
List<TOperator> scanList = new ArrayList<>();
for (Slot slot : scan.getOutput()) {
if (slot instanceof SlotReference) {
scanList.add(scalarTranslator.visitSlotReference((SlotReference) slot, null));
}
}
tscan.setScan_list(scanList);
// external_relation_id:FE per-scan opaque id(relationId),horn 只透传;
// backward 用它重建分区/索引,self-join 两 scan relationId 不同可区分。
long relationId = scan.getRelationId().asInt();
tscan.setExternal_relation_id(relationId);
hornCtx.putScanPrunedPartitions(relationId, new ArrayList<>(scan.getSelectedPartitionIds()));
// emit base table 物理排序键(OLAP tablet 内按 key 列升序、nulls first)供消冗余 sort;
// 只取 scan_list 中可见的 leading key prefix,遇第一个被裁掉的 key 列即停。
KeysType keysType = table.getKeysType();
if (keysType == KeysType.DUP_KEYS || keysType == KeysType.UNIQUE_KEYS
|| keysType == KeysType.AGG_KEYS) {
// OLAP key 列物理恒为升序 + nulls first;某 key 列被裁剪则顺序在该列断开,停。
List<OrderKey> keyOrderKeys = new ArrayList<>();
for (Column keyCol : table.getBaseSchemaKeyColumns()) {
SlotReference keySlot = findOutputSlotByColumn(scan.getOutput(), keyCol.getName());
if (keySlot == null) {
break;
}
keyOrderKeys.add(new OrderKey(keySlot, /*isAsc=*/true, /*nullFirst=*/true));
}
if (!keyOrderKeys.isEmpty()) {
tscan.setBase_table_order_spec(buildOrderSpec(keyOrderKeys));
}
}
// scan distribution_spec:HASH 表 → kHashed + 分桶列 ColumnRef(保序) + bucket_num;
// 其余 → kRandom。
DistributionInfo distInfo = table.getDefaultDistributionInfo();
TDistributionSpec distSpec = new TDistributionSpec();
if (distInfo instanceof HashDistributionInfo) {
distSpec.setDistribution_type(TDistributionType.kHashed);
distSpec.setBucket_num(distInfo.getBucketNum());
List<TOperator> hashColumns = new ArrayList<>();
for (Column distCol : ((HashDistributionInfo) distInfo).getDistributionColumns()) {
SlotReference distSlot = findOutputSlotByColumn(scan.getOutput(), distCol.getName());
if (distSlot != null) {
hashColumns.add(scalarTranslator.visitSlotReference(distSlot, null));
}
}
distSpec.setHash_columns(hashColumns);
// colocate 物理身份:同一 stable colocate group 的 scan 拿同一 grpId(对齐判据),
// 非 colocate 表 grpId = -1。
ColocateTableIndex colocateIndex = Env.getCurrentColocateIndex();
long colocateGroupId = -1;
if (colocateIndex.isColocateTable(table.getId())
&& !colocateIndex.isGroupUnstable(colocateIndex.getGroup(table.getId()))) {
colocateGroupId = colocateIndex.getGroup(table.getId()).grpId;
}
// 整个 colocate 物理身份组装成 TDorisHashSpec 灌进 special_hash_spec,horn 还原成 DorisHashSpec。
// is_natural:复用 buildScanDistributionSpec 判定(stable colocate group 或单分区为 NATURAL),
// 纯标记,仅供反译标 shuffleType,不参与 horn cascade 匹配。
DistributionSpec scanSpec = Horn2DorisUtils.buildScanDistributionSpec(
table, scan.getOutput(), new ArrayList<>(scan.getSelectedPartitionIds()));
boolean isNatural = (scanSpec instanceof DistributionSpecHash)
&& ((DistributionSpecHash) scanSpec).getShuffleType() == DistributionSpecHash.ShuffleType.NATURAL;
TDorisHashSpec dorisHashSpec = new TDorisHashSpec();
dorisHashSpec.setColocate_group_id(colocateGroupId);
dorisHashSpec.setTable_id(table.getId());
dorisHashSpec.setSelected_partition_ids(new ArrayList<>(scan.getSelectedPartitionIds()));
dorisHashSpec.setIs_natural(isNatural);
TSpecialHashSpec specialHashSpec = new TSpecialHashSpec();
specialHashSpec.setKind(TSpecialHashSpecKind.kDoris);
specialHashSpec.setDoris_hash_spec(dorisHashSpec);
distSpec.setSpecial_hash_spec(specialHashSpec);
} else {
distSpec.setDistribution_type(TDistributionType.kRandom);
}
tscan.setDistribution_spec(distSpec);
buildCatalog(table, tscan.getDatabase_name(), tscan.getTable_name());
TLogicalScan tLogicalScan = new TLogicalScan();
tLogicalScan.setLogical_table_scan(tscan);
opUnion.setLogical_scan(tLogicalScan);
emit(scan, TOperatorType.kLogicalTableGet, opUnion, texprs);
return null;
}
@Override
public Void visitLogicalOneRowRelation(LogicalOneRowRelation oneRow, List<TExpression> texprs) {
// SELECT 1, 'x' → LogicalValueGet, value_lists = [[lit, ...]]
buildValueGet(oneRow, java.util.Collections.singletonList(oneRow.getProjects()), texprs);
return null;
}
@Override
public Void visitLogicalEmptyRelation(LogicalEmptyRelation emptyRelation, List<TExpression> texprs) {
// WHERE false / LIMIT 0 → LogicalValueGet, value_lists = [](0 行,schema 由 plan.getOutput() 抽)。
buildValueGet(emptyRelation, java.util.Collections.emptyList(), texprs);
return null;
}
/**
* OneRow / Empty 两个常量源节点 → horn LogicalValueGet 共用 emit 路径。
* rows:每行 {@code List<NamedExpression>}(cell 为 Alias(Literal) 或裸表达式,剥 Alias 译内层);
* schema 直接从 {@code plan.getOutput()} 抽。
*/
private void buildValueGet(Plan plan, List<List<NamedExpression>> rows, List<TExpression> texprs) {
TLogicalValueScan valueScan = new TLogicalValueScan();
if (!rows.isEmpty()) {
List<List<TOperator>> valueLists = new ArrayList<>(rows.size());
for (List<NamedExpression> row : rows) {
List<TOperator> rowCells = new ArrayList<>(row.size());
for (NamedExpression cell : row) {
Expression literalExpr = Doris2HornUtils.unwrapExpr(cell);
rowCells.add(scalarTranslator.translate(literalExpr));
}
valueLists.add(rowCells);
}
valueScan.setValue_lists(valueLists);
}
List<TOperator> outputSchema = new ArrayList<>();
for (Slot slot : plan.getOutput()) {
if (slot instanceof SlotReference) {
outputSchema.add(scalarTranslator.visitSlotReference((SlotReference) slot, null));
}
}
valueScan.setOutput_list(outputSchema);
TLogicalScan scanWrapper = new TLogicalScan();
scanWrapper.setLogical_value_scan(valueScan);
TOperatorUnion opUnion = new TOperatorUnion();
opUnion.setLogical_scan(scanWrapper);
emit(plan, TOperatorType.kLogicalValueGet, opUnion, texprs);
}
/**
* 构建 TTable(schema + col/table stats)供 horn StaticMataProvider,并注册到
* {@link HornOptimizationContext#addTable}。
* <p>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<Column> baseSchema = table.getBaseSchema();
List<TColumn> columns = new ArrayList<>();
boolean allColumnStatsAvailable = true;
long tableRowCount = -1;
for (int i = 0; i < baseSchema.size(); i++) {
Column col = baseSchema.get(i);
ColumnStatistic colStat = statsCache.getColumnStatistics(
catalogId, dbId, tblId, idxId, col.getName(), ctx);
boolean columnStatsAvailable = colStat != null && colStat != ColumnStatistic.UNKNOWN;
if (columnStatsAvailable && tableRowCount < 0) {
tableRowCount = (long) colStat.count;
} else if (!columnStatsAvailable) {
allColumnStatsAvailable = false;
}
TColumn tcol = new TColumn();
tcol.setColumnName(col.getName());
tcol.setColumnType(DorisTypeToHornConverter.convertCatalogType(col.getType()));
tcol.setPosition(i);
if (columnStatsAvailable) {
TColumnStats tcolStats = new TColumnStats();
tcolStats.setAvg_size(colStat.avgSizeByte);
tcolStats.setMax_size(col.getType().getLength());
tcolStats.setNum_distinct_values((long) colStat.ndv);
tcolStats.setNum_nulls((long) colStat.numNulls);
TColumnValue low = new TColumnValue();
low.setDouble_val(colStat.minValue);
tcolStats.setLow_value(low);
TColumnValue high = new TColumnValue();
high.setDouble_val(colStat.maxValue);
tcolStats.setHigh_value(high);
tcol.setCol_stats(tcolStats);
}
columns.add(tcol);
}
ttable.setColumns(columns);
TTableStats tableStats = new TTableStats();
tableStats.setNum_rows(allColumnStatsAvailable ? tableRowCount : -1);
ttable.setTable_stats(tableStats);
// table_id / partition_num 不再传:都是 horn C++ 从不读的死字段。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<TExpression> texprs) {
TOperatorUnion opUnion = new TOperatorUnion();
TLogicalFilter tfilter = new TLogicalFilter();
tfilter.setPredicate_list(
scalarTranslator.translateList(new ArrayList<>(filter.getConjuncts())));
opUnion.setLogical_filter(tfilter);
emit(filter, TOperatorType.kLogicalSelect, opUnion, texprs);
return null;
}
@Override
public Void visitLogicalProject(LogicalProject<? extends Plan> project, List<TExpression> texprs) {
buildProject(project, project.getProjects(), project.getOutput(), texprs);
return null;
}
/**
* 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<TExpression> 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<NamedExpression> projects,
List<Slot> output, List<TExpression> texprs) {
TLogicalProject projectOp = new TLogicalProject();
projectOp.setProject_list(scalarTranslator.translateList(projects));
List<TOperator> rewriteList = new ArrayList<>();
for (Slot slot : output) {
if (slot instanceof SlotReference) {
rewriteList.add(scalarTranslator.visitSlotReference((SlotReference) slot, null));
}
}
projectOp.setRewrite_list(rewriteList);
TOperatorUnion opUnion = new TOperatorUnion();
opUnion.setLogical_project(projectOp);
emit(plan, TOperatorType.kLogicalProject, opUnion, texprs);
}
/**
* Doris LogicalAggregate → horn TLogicalAgg(单层 forward,horn cascades 的 XformSplitAgg
* 自己拆 Local/Global)。HAVING 已被 RBO 上提为独立 LogicalFilter,不在此处理。
*/
@Override
public Void visitLogicalAggregate(LogicalAggregate<? extends Plan> agg, List<TExpression> 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<TOperator> aggFuncs = new ArrayList<>();
List<TOperator> aggComputedCols = new ArrayList<>();
for (NamedExpression out : agg.getOutputExpressions()) {
Expression inner = Doris2HornUtils.unwrapExpr(out);
if (inner instanceof AggregateFunction) {
aggFuncs.add(scalarTranslator.translate(inner));
aggComputedCols.add(scalarTranslator.visitSlotReference(
(SlotReference) out.toSlot(), null));
}
}
tagg.setAggregate_functions(aggFuncs);
tagg.setAgg_computed_cols(aggComputedCols);
// 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<TExpression> 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<TExpression> texprs) {
// intersect 恒 DISTINCT(Doris 绑定期拒 ALL);防御性断言,非 DISTINCT → fallback。
if (intersect.getQualifier() != Qualifier.DISTINCT) {
throw new UnsupportedOperationException(
"Horn: intersect must be DISTINCT (Doris rejects ALL at binding)");
}
emitSetOp(intersect, TOperatorType.kLogicalIntersect, texprs);
return null;
}
/**
* Doris LogicalExcept → horn TLogicalSetOp(kLogicalExcept)。不被 MergeSetOperations 合并,恒嵌套
* 二元;恒 DISTINCT,无 constantExprsList。
*/
@Override
public Void visitLogicalExcept(LogicalExcept except, List<TExpression> texprs) {
// except 恒 DISTINCT(Doris 绑定期拒 ALL);防御性断言,非 DISTINCT → fallback。
if (except.getQualifier() != Qualifier.DISTINCT) {
throw new UnsupportedOperationException(
"Horn: except must be DISTINCT (Doris rejects ALL at binding)");
}
emitSetOp(except, TOperatorType.kLogicalExcept, texprs);
return null;
}
/**
* 所有 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<TExpression> texprs) {
TLogicalSetOp tsetop = new TLogicalSetOp();
List<List<TOperator>> childrenOut = new ArrayList<>();
for (List<SlotReference> oneChild : setOp.getRegularChildrenOutputs()) {
childrenOut.add(scalarTranslator.translateList(oneChild));
}
tsetop.setChildren_output_list(childrenOut);
if (setOp instanceof LogicalUnion) {
List<List<NamedExpression>> constExprsList = ((LogicalUnion) setOp).getConstantExprsList();
if (!constExprsList.isEmpty()) {
List<List<TOperator>> constRows = new ArrayList<>();
for (List<NamedExpression> row : constExprsList) {
constRows.add(scalarTranslator.translateList(row));
}
tsetop.setChildren_const_expr_list(constRows);
}
}
TOperatorUnion opUnion = new TOperatorUnion();
opUnion.setLogical_set_op(tsetop);
emit(setOp, opType, opUnion, texprs);
}
/**
* 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<TExpression> texprs) {
// 1. grouping_set_list
List<List<TOperator>> tGroupingSets = new ArrayList<>(repeat.getGroupingSets().size());
for (List<Expression> set : repeat.getGroupingSets()) {
tGroupingSets.add(scalarTranslator.translateList(set));
}
// 2. grouping_expr_list = flatten distinct
Set<Expression> groupingExprSet = new LinkedHashSet<>();
for (List<Expression> set : repeat.getGroupingSets()) {
groupingExprSet.addAll(set);
}
List<TOperator> tGroupingExprs = new ArrayList<>(groupingExprSet.size());
for (Expression e : groupingExprSet) {
tGroupingExprs.add(scalarTranslator.translate(e));
}
// 3. 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<TOperator> tAggFunInputCols = new ArrayList<>();
for (NamedExpression out : repeat.getOutputExpressions()) {
Expression inner = Doris2HornUtils.unwrapExpr(out);
if (inner instanceof SlotReference && !groupingExprSet.contains(inner)) {
tAggFunInputCols.add(scalarTranslator.visitSlotReference((SlotReference) inner, null));
} else if (inner instanceof GroupingScalarFunction) {
hasGroupingPrefix = true;
}
}
TLogicalGrouping tlg = new TLogicalGrouping();
tlg.setGrouping_set_list(tGroupingSets);
tlg.setGrouping_expr_list(tGroupingExprs);
tlg.setGrouping_id_func(Lists.newArrayList(tGroupingIdFunc));
if (!tAggFunInputCols.isEmpty()) {
tlg.setAgg_fun_input_cols(tAggFunInputCols);
}
TOperatorUnion opUnion = new TOperatorUnion();
opUnion.setLogical_grouping(tlg);
// 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<NamedExpression> projects = new ArrayList<>(repeat.getOutputExpressions());
projects.add(groupingIdSlot);
List<TOperator> projectOutput = new ArrayList<>();
for (NamedExpression project : projects) {
projectOutput.add(scalarTranslator.visitSlotReference((SlotReference) project.toSlot(), null));
}
TLogicalProject projectOp = new TLogicalProject();
projectOp.setProject_list(scalarTranslator.translateList(projects));
projectOp.setRewrite_list(projectOutput);
TOperatorUnion projectUnion = new TOperatorUnion();
projectUnion.setLogical_project(projectOp);
// pre-order:先 add Project texpr(arity=1 不递归),再 emit grouping(自带递归 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<TExpression> texprs) {
List<NamedExpression> windowExpressions = logicalWindow.getWindowExpressions();
if (windowExpressions.isEmpty()) {
throw new UnsupportedOperationException(
"Horn: LogicalWindow with empty windowExpressions");
}
// 多 spec 拆分:按 (partition, order, frame) 等价类分组(复用 createWindowFrameGroups),
// 多组则重写成单 spec LogicalWindow 链、逐个走单 spec 翻译。
List<WindowFrameGroup> sameSpecGroups =
LogicalWindowToPhysicalWindow.createWindowFrameGroups(windowExpressions);
if (sameSpecGroups.size() > 1) {
Plan windowChain = logicalWindow.child();
for (WindowFrameGroup sameSpecGroup : sameSpecGroups) {
windowChain = new LogicalWindow<>(sameSpecGroup.getGroups(), windowChain);
}
windowChain.accept(this, texprs);
return null;
}
// 到这里 windowExpressions 必为同一 spec,partition/order/frame 取第一项套用整个 TLogicalWindow,
// 逐项收集函数体 + 输出列。窗口列恒为 Alias(WindowExpression)。
WindowExpression leaderWindow =
(WindowExpression) Doris2HornUtils.unwrapExpr(windowExpressions.get(0));
List<Expression> partitionKeys = leaderWindow.getPartitionKeys();
List<OrderExpression> orderKeys = leaderWindow.getOrderKeys();
Optional<WindowFrame> windowFrame = leaderWindow.getWindowFrame();
List<TOperator> analyticFunctions = new ArrayList<>(windowExpressions.size());
List<TOperator> analyticComputedColumns = new ArrayList<>(windowExpressions.size());
for (NamedExpression windowExpression : windowExpressions) {
WindowExpression analyticFunction =
(WindowExpression) Doris2HornUtils.unwrapExpr(windowExpression);
analyticFunctions.add(scalarTranslator.translate(analyticFunction.getFunction()));
// Alias.toSlot() 携带 ExprId + name + type —— 跟 agg_computed_cols 同样路径。
analyticComputedColumns.add(scalarTranslator.visitSlotReference(
(SlotReference) windowExpression.toSlot(), null));
}
TLogicalWindow logicalWindowThrift = new TLogicalWindow();
logicalWindowThrift.setAnalytic_functions(analyticFunctions);
logicalWindowThrift.setAnalytic_computed_cols(analyticComputedColumns);
if (!partitionKeys.isEmpty()) {
logicalWindowThrift.setPartition_exprs(scalarTranslator.translateList(partitionKeys));
}
if (!orderKeys.isEmpty()) {
List<OrderKey> windowOrderKeys = new ArrayList<>(orderKeys.size());
for (OrderExpression orderExpr : orderKeys) {
windowOrderKeys.add(orderExpr.getOrderKey());
}
logicalWindowThrift.setOrder_by_exprs(buildOrderSpec(windowOrderKeys));
}
if (windowFrame.isPresent()) {
// 直接调 visitWindowFrame 得到 TOperator(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<TExpression> texprs) {
// sort wrapper:limit=-1 表示纯 ORDER BY,没有 split 概念
buildLimit(sort, -1L, 0L, sort.getOrderKeys(), true, false, texprs);
return null;
}
@Override
public Void visitLogicalLimit(LogicalLimit<? extends Plan> limit, List<TExpression> texprs) {
// 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<TExpression> texprs) {
// Doris LogicalTopN 没有 phase 字段 — 单一形态,相当于 ORIGIN,is_split=false
buildLimit(topn, topn.getLimit(), topn.getOffset(), topn.getOrderKeys(), true, false, texprs);
return null;
}
/** 公共 emit helper:visitLogicalSort/Limit/TopN 三个变体共用同一份 TLogicalLimit wire。 */
private void buildLimit(Plan plan, long limit, long offset,
List<OrderKey> orderKeys, boolean isGlobal, boolean isSplit,
List<TExpression> texprs) {
TLogicalLimit tlimit = new TLogicalLimit();
tlimit.setLimit(limit);
tlimit.setOffset(offset);
tlimit.setIs_global_limit(isGlobal);
tlimit.setIs_split(isSplit);
if (orderKeys != null && !orderKeys.isEmpty()) {
tlimit.setOrder_spec(buildOrderSpec(orderKeys));
}
TOperatorUnion opUnion = new TOperatorUnion();
opUnion.setLogical_limit(tlimit);
emit(plan, TOperatorType.kLogicalLimit, opUnion, texprs);
}
/** 在 scan 输出里按源列名(忽略大小写)找对应 SlotReference,找不到返回 null;供
* base_table_order_spec 与 distribution_spec 把 key/分桶列对应到 scan 输出 slot。
* 按名字匹配对齐 mainline(列名比 length/type 稳定),匹配 getOriginalColumn().getName()。 */
private SlotReference findOutputSlotByColumn(List<Slot> outputSlots, String columnName) {
for (Slot slot : outputSlots) {
if (slot instanceof SlotReference) {
Optional<Column> originalColumn = ((SlotReference) slot).getOriginalColumn();
if (originalColumn.isPresent()
&& originalColumn.get().getName().equalsIgnoreCase(columnName)) {
return (SlotReference) slot;
}
}
}
return null;
}
/** 把 Doris OrderKey 列表翻成 horn TOrderSpec (parallel lists 形态)。 */
private TOrderSpec buildOrderSpec(List<OrderKey> orderKeys) {
TOrderSpec orderSpec = new TOrderSpec();
List<TOperator> exprList = new ArrayList<>(orderKeys.size());
List<Boolean> ascList = new ArrayList<>(orderKeys.size());
List<Boolean> nullsFirstList = new ArrayList<>(orderKeys.size());
for (OrderKey orderKey : orderKeys) {
exprList.add(scalarTranslator.translate(orderKey.getExpr()));
ascList.add(orderKey.isAsc());
nullsFirstList.add(orderKey.isNullFirst());
}
orderSpec.setOrder_by_exprs(exprList);
orderSpec.setIs_asc_order(ascList);
orderSpec.setNulls_first(nullsFirstList);
return orderSpec;
}
@Override
public Void visitLogicalJoin(LogicalJoin<? extends Plan, ? extends Plan> join, List<TExpression> texprs) {
// 全部 10 种 Doris JoinType 映射到对应 horn logical join op_type。
// 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;
}
}