diff --git a/.gitignore b/.gitignore index b515321338c74f..5fd54aa4229959 100644 --- a/.gitignore +++ b/.gitignore @@ -147,3 +147,8 @@ compile_commands.json .github .worktrees/ + +# Keep horn4j chunks tracked while the reconstructed jar remains ignored by +# the global *.jar rule. Apache's large-file warning checks individual files. +!/fe/custom_libs/horn4j-1.0.0-SNAPSHOT.jar.parts/ +!/fe/custom_libs/horn4j-1.0.0-SNAPSHOT.jar.parts/* diff --git a/build.sh b/build.sh index eff6b482ec870e..4f3f653ad713ae 100755 --- a/build.sh +++ b/build.sh @@ -571,6 +571,7 @@ if [[ "${BUILD_FE}" -eq 1 ]]; then modules+=("fe-common") modules+=("fe-extension-spi") modules+=("fe-extension-loader") + modules+=("horn4j") modules+=("fe-core") if [[ "${WITH_TDE_DIR}" != "" ]]; then modules+=("fe-${WITH_TDE_DIR}") diff --git a/fe/custom_libs/horn4j-1.0.0-SNAPSHOT.jar.parts/part-00 b/fe/custom_libs/horn4j-1.0.0-SNAPSHOT.jar.parts/part-00 new file mode 100644 index 00000000000000..30206699b9ba26 Binary files /dev/null and b/fe/custom_libs/horn4j-1.0.0-SNAPSHOT.jar.parts/part-00 differ diff --git a/fe/custom_libs/horn4j-1.0.0-SNAPSHOT.jar.parts/part-01 b/fe/custom_libs/horn4j-1.0.0-SNAPSHOT.jar.parts/part-01 new file mode 100644 index 00000000000000..170de2701cb151 Binary files /dev/null and b/fe/custom_libs/horn4j-1.0.0-SNAPSHOT.jar.parts/part-01 differ diff --git a/fe/custom_libs/horn4j-1.0.0-SNAPSHOT.jar.parts/part-02 b/fe/custom_libs/horn4j-1.0.0-SNAPSHOT.jar.parts/part-02 new file mode 100644 index 00000000000000..a803bbb1a61959 Binary files /dev/null and b/fe/custom_libs/horn4j-1.0.0-SNAPSHOT.jar.parts/part-02 differ diff --git a/fe/custom_libs/horn4j-1.0.0-SNAPSHOT.jar.parts/part-03 b/fe/custom_libs/horn4j-1.0.0-SNAPSHOT.jar.parts/part-03 new file mode 100644 index 00000000000000..f1eb958856ee94 Binary files /dev/null and b/fe/custom_libs/horn4j-1.0.0-SNAPSHOT.jar.parts/part-03 differ diff --git a/fe/custom_libs/horn4j-1.0.0-SNAPSHOT.jar.parts/part-04 b/fe/custom_libs/horn4j-1.0.0-SNAPSHOT.jar.parts/part-04 new file mode 100644 index 00000000000000..122cb1bff37bc9 Binary files /dev/null and b/fe/custom_libs/horn4j-1.0.0-SNAPSHOT.jar.parts/part-04 differ diff --git a/fe/custom_libs/horn4j-1.0.0-SNAPSHOT.jar.parts/part-05 b/fe/custom_libs/horn4j-1.0.0-SNAPSHOT.jar.parts/part-05 new file mode 100644 index 00000000000000..129aa8bff8bc01 Binary files /dev/null and b/fe/custom_libs/horn4j-1.0.0-SNAPSHOT.jar.parts/part-05 differ diff --git a/fe/custom_libs/horn4j-1.0.0-SNAPSHOT.jar.parts/part-06 b/fe/custom_libs/horn4j-1.0.0-SNAPSHOT.jar.parts/part-06 new file mode 100644 index 00000000000000..8cecf81f4f788b Binary files /dev/null and b/fe/custom_libs/horn4j-1.0.0-SNAPSHOT.jar.parts/part-06 differ diff --git a/fe/custom_libs/horn4j-1.0.0-SNAPSHOT.jar.parts/part-07 b/fe/custom_libs/horn4j-1.0.0-SNAPSHOT.jar.parts/part-07 new file mode 100644 index 00000000000000..2d13d3fc6547ae Binary files /dev/null and b/fe/custom_libs/horn4j-1.0.0-SNAPSHOT.jar.parts/part-07 differ diff --git a/fe/fe-core/pom.xml b/fe/fe-core/pom.xml index 1f93747458f2f0..b2000ebd4291aa 100644 --- a/fe/fe-core/pom.xml +++ b/fe/fe-core/pom.xml @@ -97,6 +97,11 @@ under the License. fe-extension-loader ${project.version} + + org.apache.horn4j + horn4j + 1.0.0-SNAPSHOT + org.springframework.boot spring-boot-devtools diff --git a/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisLexer.g4 b/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisLexer.g4 index 31fe7628c3f8b0..14d81745a3fe7b 100644 --- a/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisLexer.g4 +++ b/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisLexer.g4 @@ -282,6 +282,7 @@ HELP: 'HELP'; HISTOGRAM: 'HISTOGRAM'; HLL: 'HLL'; HLL_UNION: 'HLL_UNION'; +HORN: 'HORN'; HOSTNAME: 'HOSTNAME'; HOTSPOT: 'HOTSPOT'; HOUR: 'HOUR'; diff --git a/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisParser.g4 b/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisParser.g4 index d5e0a3159ea965..a9d15daa9b4ea4 100644 --- a/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisParser.g4 +++ b/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisParser.g4 @@ -1143,6 +1143,7 @@ planType | SHAPE | MEMO | DISTRIBUTED + | HORN | ALL // default type ; @@ -2104,6 +2105,7 @@ nonReserved | HINT_START | HISTOGRAM | HLL_UNION + | HORN | HOSTNAME | HOTSPOT | HOUR diff --git a/fe/fe-core/src/main/java/org/apache/doris/horn/HornOptimizationContext.java b/fe/fe-core/src/main/java/org/apache/doris/horn/HornOptimizationContext.java new file mode 100644 index 00000000000000..a6b758ba19abfd --- /dev/null +++ b/fe/fe-core/src/main/java/org/apache/doris/horn/HornOptimizationContext.java @@ -0,0 +1,159 @@ +// 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; + + +import org.apache.doris.nereids.CascadesContext; +import org.apache.doris.nereids.hint.LeadingHint; +import org.apache.doris.nereids.trees.expressions.Slot; +import org.apache.doris.nereids.trees.expressions.functions.agg.AggregateFunction; + +import org.apache.horn4j.thrift.TEngineType; +import org.apache.horn4j.thrift.TExplainLevel; +import org.apache.horn4j.thrift.TQueryOptionConfig; +import org.apache.horn4j.thrift.TTable; +import org.apache.horn4j.thrift.TTableBatch; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Horn optimization context shared across the input-translation, JNI, and + * output-rebuild phases. + */ +public class HornOptimizationContext { + + private final CascadesContext cascadesContext; + + /** Table metadata + statistics. */ + private TTableBatch tableBatch = new TTableBatch(); + + /** Horn optimizer configuration. */ + private TQueryOptionConfig queryOptionConfig; + + /** + * forward 每个 LogicalOlapScan 的裁剪后分区集,按 relationId 索引(Horn 只透传 relationId, + * 不进 thrift)。backward DorisPhysicalPlanBuilder.buildScan 用它重建 scan 分区。 + */ + private final Map> scanPrunedPartitions = new HashMap<>(); + + /** + * backward 阶段 horn scalar_unique_id → 反译出的 Doris Slot 全局映射,保证上下游 plan 节点的 + * slot 引用一致({@link org.apache.doris.horn.horn2doris.DorisPhysicalPlanBuilder} 写入、 + * {@link org.apache.doris.horn.horn2doris.DorisExpressionBuilder} 反译 ColumnRef 时按 uid 查)。 + */ + private final Map scalarUidToSlot = new HashMap<>(); + + /** + * backward 阶段 horn agg fn 的 scalar_unique_id → 反译出的 Doris AggregateFunction,专给两阶段 + * agg 用:local agg put、global agg 按 uid get 回同一实例(Doris 用引用关联 phase 间的同一 fn)。 + */ + private final Map scalarUidToAggFunc = new HashMap<>(); + + public HornOptimizationContext(CascadesContext cascadesContext) { + this.cascadesContext = cascadesContext; + this.queryOptionConfig = buildDefaultQueryOptionConfig(); + applyLeadingHint(this.queryOptionConfig); + } + + /** + * 把 SQL 的 {@code LEADING(...)} hint 转发给 horn,让内部 {@code XformLeadingJoin} 按指定顺序重排 join。 + */ + private void applyLeadingHint(TQueryOptionConfig config) { + if (!cascadesContext.isLeadingJoin()) { + return; + } + LeadingHint leading = (LeadingHint) cascadesContext.getHintMap().get("Leading"); + if (leading != null && !leading.getTablelist().isEmpty()) { + + config.setJoin_leading_string(new ArrayList<>(leading.getTablelist())); + } + } + + public CascadesContext getCascadesContext() { + return cascadesContext; + } + + public TTableBatch getTableBatch() { + return tableBatch; + } + + public void setTableBatch(TTableBatch tableBatch) { + this.tableBatch = tableBatch; + } + + public void putScanPrunedPartitions(long relationId, List prunedPartitionIds) { + scanPrunedPartitions.put(relationId, prunedPartitionIds); + } + + public List getScanPrunedPartitions(long relationId) { + return scanPrunedPartitions.get(relationId); + } + + public void addTable(TTable ttable) { + if (tableBatch.getTables() == null) { + tableBatch.setTables(new ArrayList<>()); + } + tableBatch.getTables().add(ttable); + } + + + public Map getScalarUidToSlot() { + return scalarUidToSlot; + } + + public Map getScalarUidToAggFunc() { + return scalarUidToAggFunc; + } + + public TQueryOptionConfig getQueryOptionConfig() { + return queryOptionConfig; + } + + public TExplainLevel getExplainLevel() { + return TExplainLevel.EXTRACTED_PLAN; + } + + private TQueryOptionConfig buildDefaultQueryOptionConfig() { + TQueryOptionConfig config = new TQueryOptionConfig(); + config.setHorn_dphyper_enable(true); + config.setHorn_statistics_default_selectivity(0.4); + config.setHorn_statistics_damping_factor_filter(0.75); + config.setHorn_statistics_damping_factor_groupby(0.75); + config.setHorn_statistics_damping_factor_join(0.1); + // 默认允许无统计的表也走 horn(用 default selectivity 估算)——否则缺 stats 的表 + // 会大量 fallback Nereids。严格 stats 后续由 session var 控制。 + config.setEnable_horn_no_statistics_support(true); + config.setLocal_time_zone(cascadesContext.getConnectContext() + .getSessionVariable().getTimeZone()); + config.setMt_dop(1); + config.setEngine_type(TEngineType.kDoris); + boolean detailInfo = cascadesContext.getConnectContext() + .getSessionVariable().enableHornDetailInfo; + config.setEnable_horn_cascade_log(detailInfo); + config.setEnable_horn_cascade_detail_log(detailInfo); + config.setEnable_horn_stats_log(detailInfo); + config.setHorn_print_memo_after_optimization(detailInfo); // detail 模式下 dump memo + config.setEnable_horn_profile(cascadesContext.getConnectContext() + .getSessionVariable().enableHornProfile); + config.setExplain_level(getExplainLevel()); + return config; + } +} diff --git a/fe/fe-core/src/main/java/org/apache/doris/horn/HornOptimizer.java b/fe/fe-core/src/main/java/org/apache/doris/horn/HornOptimizer.java new file mode 100644 index 00000000000000..743dd2ed4195be --- /dev/null +++ b/fe/fe-core/src/main/java/org/apache/doris/horn/HornOptimizer.java @@ -0,0 +1,109 @@ +// 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; + + +import org.apache.doris.common.UserException; +import org.apache.doris.horn.doris2horn.HornLogicalPlanThriftBuilder; +import org.apache.doris.horn.horn2doris.DorisPhysicalPlanBuilder; +import org.apache.doris.nereids.CascadesContext; +import org.apache.doris.nereids.trees.plans.logical.LogicalPlan; +import org.apache.doris.nereids.trees.plans.physical.PhysicalPlan; + +import org.apache.horn4j.HornNative; +import org.apache.horn4j.thrift.TFlattenedExpression; +import org.apache.horn4j.thrift.THornOptimizeResult; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.thrift.TDeserializer; +import org.apache.thrift.TSerializer; + +/** + * Doris-side entry to the Horn CBO optimizer. + * + * Flow: Doris LogicalPlan → TFlattenedExpression → horn4j JNI → Horn (in-process) + * → THornOptimizeResult → Doris PhysicalPlan. + */ +public class HornOptimizer { + + private static final Logger LOG = LogManager.getLogger(HornOptimizer.class); + + private final CascadesContext cascadesContext; + private String hornExplainString; + private String fallbackReason; + + public HornOptimizer(CascadesContext cascadesContext) { + this.cascadesContext = cascadesContext; + } + + public String getHornExplainString() { + return hornExplainString; + } + + public String getFallbackReason() { + return fallbackReason; + } + + public PhysicalPlan optimize(LogicalPlan logicalPlan) throws UserException { + HornOptimizationContext hornCtx = new HornOptimizationContext(cascadesContext); + + try { + // Step 1: Doris LogicalPlan → TFlattenedExpression + LOG.info("Horn CBO: translating Doris plan to Horn input"); + + HornLogicalPlanThriftBuilder planVisitor = new HornLogicalPlanThriftBuilder(hornCtx); + TFlattenedExpression plan = planVisitor.translate(logicalPlan); + + TSerializer serializer = new TSerializer(); + byte[] planBytes = serializer.serialize(plan); + byte[] tableBatchBytes = serializer.serialize(hornCtx.getTableBatch()); + byte[] queryOptionBytes = serializer.serialize(hornCtx.getQueryOptionConfig()); + + // Step 2: call Horn in-process via JNI (horn4j). + LOG.info("Horn CBO: calling Horn via horn4j JNI"); + + byte[] resultBytes = new HornNative().optimize( + planBytes, tableBatchBytes, queryOptionBytes); + + // Step 3: Horn output → Doris PhysicalPlan. + LOG.info("Horn CBO: rebuilding Doris plan from Horn output"); + + THornOptimizeResult hornResult = new THornOptimizeResult(); + TDeserializer deserializer = new TDeserializer(); + deserializer.deserialize(hornResult, resultBytes); + + hornExplainString = hornResult.getExplain_string(); + if (hornExplainString != null) { + LOG.info("Horn CBO explain:\n{}", hornExplainString); + } + + DorisPhysicalPlanBuilder builder = + new DorisPhysicalPlanBuilder(hornCtx); + PhysicalPlan physicalPlan = builder.build(hornResult.getTfexpr()); + + LOG.info("Horn CBO: optimization complete"); + return physicalPlan; + + } catch (Exception e) { + fallbackReason = e.getMessage(); + LOG.warn("Horn CBO failed, falling back to Nereids: {}", fallbackReason, e); + return null; + } + } + +} diff --git a/fe/fe-core/src/main/java/org/apache/doris/horn/doris2horn/Doris2HornUtils.java b/fe/fe-core/src/main/java/org/apache/doris/horn/doris2horn/Doris2HornUtils.java new file mode 100644 index 00000000000000..39e971e87e4269 --- /dev/null +++ b/fe/fe-core/src/main/java/org/apache/doris/horn/doris2horn/Doris2HornUtils.java @@ -0,0 +1,40 @@ +// 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.nereids.trees.expressions.Alias; +import org.apache.doris.nereids.trees.expressions.Expression; + +/** + * doris2horn 正向翻译路径下的纯静态辅助方法集中点(对称 horn2doris 侧的 + * {@code Horn2DorisUtils})。跟具体 visitor 实例状态无关的小工具放这里复用。 + */ +public final class Doris2HornUtils { + + private Doris2HornUtils() { + } + + /** + * 剥掉一层 {@link Alias} 取内部表达式(Alias 在 horn wire 上透明,exprId/name 由输出列 + * SlotReference 携带);非 Alias 原样返回。 + */ + public static Expression unwrapExpr(Expression expr) { + return (expr instanceof Alias) ? ((Alias) expr).child() : expr; + } +} diff --git a/fe/fe-core/src/main/java/org/apache/doris/horn/doris2horn/DorisTypeToHornConverter.java b/fe/fe-core/src/main/java/org/apache/doris/horn/doris2horn/DorisTypeToHornConverter.java new file mode 100644 index 00000000000000..1a2ed3aed30e54 --- /dev/null +++ b/fe/fe-core/src/main/java/org/apache/doris/horn/doris2horn/DorisTypeToHornConverter.java @@ -0,0 +1,124 @@ +// 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.ScalarType; +import org.apache.doris.catalog.Type; +import org.apache.doris.nereids.types.DataType; + +import org.apache.horn4j.thrift.TColumnType; +import org.apache.horn4j.thrift.TPrimitiveType; +import org.apache.horn4j.thrift.TScalarType; +import org.apache.horn4j.thrift.TTypeNode; +import org.apache.horn4j.thrift.TTypeNodeType; + +import java.util.Collections; + +/** + * Converts Doris Nereids/catalog types to Horn TColumnType (Thrift). Both Nereids {@link DataType} + * and legacy catalog {@link Type} dispatch through one switch on {@code PrimitiveType}; unsupported + * types throw {@link UnsupportedOperationException} to trigger fallback to the Nereids optimizer. + */ +public class DorisTypeToHornConverter { + + /** Convert a Nereids DataType to Horn TColumnType. */ + public static TColumnType convert(DataType dataType) { + return wrap(mapPrimitive(dataType.toCatalogDataType())); + } + + /** Convert a legacy Doris catalog Type to Horn TColumnType (used for table metadata). */ + public static TColumnType convertCatalogType(Type catalogType) { + return wrap(mapPrimitive(catalogType)); + } + + /** + * Map a Doris {@link Type} to a Horn {@link TScalarType}. Throws + * {@link UnsupportedOperationException} for unsupported types so the caller can fall back to Nereids. + */ + private static TScalarType mapPrimitive(Type catalogType) { + TScalarType scalarType = new TScalarType(); + switch (catalogType.getPrimitiveType()) { + case BOOLEAN: + scalarType.setType(TPrimitiveType.BOOLEAN); + return scalarType; + case TINYINT: + scalarType.setType(TPrimitiveType.TINYINT); + return scalarType; + case SMALLINT: + scalarType.setType(TPrimitiveType.SMALLINT); + return scalarType; + case INT: + scalarType.setType(TPrimitiveType.INT); + return scalarType; + case BIGINT: + scalarType.setType(TPrimitiveType.BIGINT); + return scalarType; + case FLOAT: + scalarType.setType(TPrimitiveType.FLOAT); + return scalarType; + case DOUBLE: + scalarType.setType(TPrimitiveType.DOUBLE); + return scalarType; + case DATE: + case DATEV2: + scalarType.setType(TPrimitiveType.DATE); + return scalarType; + case NULL_TYPE: + scalarType.setType(TPrimitiveType.NULL_TYPE); + return scalarType; + case INVALID_TYPE: + // 无值表达式(WindowFrame / FrameBoundary 等)的 placeholder,对齐 kernel 端 + // Scalar 默认 INVALID_TYPE;BE 按 op_type 分发不读 data_type,不影响语义。 + scalarType.setType(TPrimitiveType.INVALID_TYPE); + return scalarType; + case STRING: + scalarType.setType(TPrimitiveType.STRING); + return scalarType; + case CHAR: + scalarType.setType(TPrimitiveType.CHAR); + scalarType.setLen(((ScalarType) catalogType).getLength()); + return scalarType; + case VARCHAR: + scalarType.setType(TPrimitiveType.VARCHAR); + scalarType.setLen(((ScalarType) catalogType).getLength()); + return scalarType; + case DECIMALV2: + case DECIMAL32: + case DECIMAL64: + case DECIMAL128: + scalarType.setType(TPrimitiveType.DECIMAL); + scalarType.setPrecision(((ScalarType) catalogType).getPrecision()); + scalarType.setScale(((ScalarType) catalogType).getScalarScale()); + return scalarType; + default: + // 暂不支持 LARGEINT、DECIMAL256、TIME、TIMESTAMP 等类型 + throw new UnsupportedOperationException( + "Horn: unsupported Doris type: " + catalogType); + } + } + + private static TColumnType wrap(TScalarType scalarType) { + TTypeNode node = new TTypeNode(); + node.setType(TTypeNodeType.SCALAR); + node.setScalar_type(scalarType); + TColumnType result = new TColumnType(); + result.setTypes(Collections.singletonList(node)); + return result; + } +} diff --git a/fe/fe-core/src/main/java/org/apache/doris/horn/doris2horn/HornLogicalPlanThriftBuilder.java b/fe/fe-core/src/main/java/org/apache/doris/horn/doris2horn/HornLogicalPlanThriftBuilder.java new file mode 100644 index 00000000000000..42791b8f0f3981 --- /dev/null +++ b/fe/fe-core/src/main/java/org/apache/doris/horn/doris2horn/HornLogicalPlanThriftBuilder.java @@ -0,0 +1,881 @@ +// 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> { + + 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 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 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 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 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 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 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 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 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 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 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}(cell 为 Alias(Literal) 或裸表达式,剥 Alias 译内层); + * schema 直接从 {@code plan.getOutput()} 抽。 + */ + private void buildValueGet(Plan plan, List> rows, List texprs) { + TLogicalValueScan valueScan = new TLogicalValueScan(); + if (!rows.isEmpty()) { + List> valueLists = new ArrayList<>(rows.size()); + for (List row : rows) { + List 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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