From e16850864ecaf8cac333e19e977cec043420e0d6 Mon Sep 17 00:00:00 2001 From: morrySnow Date: Thu, 10 Sep 2026 18:52:38 +0800 Subject: [PATCH] [fix](nereids) Preserve variant sub-path order ### What problem does this PR solve? Problem Summary: Multi-level VARIANT sub-paths are represented in canonical root-to-leaf order. Two rewrite paths rebuilt ElementAt expressions in reverse order, so a UNION ALL constant branch could read a different nested value when nested-column pruning was disabled. The change centralizes root-to-leaf ElementAt construction and uses it for UNION constant expressions and projected VARIANT expressions. Unit coverage checks the construction/extraction invariant, and regression coverage verifies UNION and project plans with nested-column pruning both disabled and enabled. ### Release note Fix incorrect values for multi-level VARIANT sub-paths in UNION ALL constant branches. ### Check List (For Author) - Test: Unit Test and Regression Test - Behavior changed: Yes. Multi-level VARIANT sub-paths now preserve SQL access order across rewritten branches. - Does this need documentation: No --- .../rules/rewrite/VariantSubPathPruning.java | 20 +++--- .../rules/rewrite/PruneNestedColumnTest.java | 14 ++++ .../test_variant_sub_path_order.out | 11 +++ .../test_variant_sub_path_order.groovy | 72 +++++++++++++++++++ 4 files changed, 108 insertions(+), 9 deletions(-) create mode 100644 regression-test/data/variant_p0/test_variant_sub_path_order.out create mode 100644 regression-test/suites/variant_p0/test_variant_sub_path_order.groovy diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/VariantSubPathPruning.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/VariantSubPathPruning.java index 5b081ce2c2477c..cbbd44baefcc9b 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/VariantSubPathPruning.java +++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/VariantSubPathPruning.java @@ -332,10 +332,7 @@ public Plan visitLogicalUnion(LogicalUnion union, Context context) { } else { pushDownExpr = constExpr; } - for (int sp = entry.getKey().size() - 1; sp >= 0; sp--) { - VarcharLiteral path = new VarcharLiteral(entry.getKey().get(sp)); - pushDownExpr = new ElementAt(pushDownExpr, path); - } + pushDownExpr = constructElementAt(pushDownExpr, entry.getKey()); constExprs.get(j).add(new Alias(pushDownExpr)); } @@ -610,11 +607,7 @@ private List pushDownToProject(Context context, NamedExpression Set> subPaths = context.slotToSubPathsMap .get((SlotReference) projection.toSlot()); for (List subPath : subPaths) { - Expression pushDownExpr = child; - for (int i = subPath.size() - 1; i >= 0; i--) { - VarcharLiteral path = new VarcharLiteral(subPath.get(i)); - pushDownExpr = new ElementAt(pushDownExpr, path); - } + Expression pushDownExpr = constructElementAt(child, subPath); Alias alias = new Alias(pushDownExpr); newProjections.add(alias); subPathToSlot.put(subPath, (SlotReference) alias.toSlot()); @@ -781,6 +774,15 @@ public Void visit(Plan plan, Context context) { } } + /** Build nested ElementAt expressions from a canonical root-to-leaf sub-path. */ + protected static Expression constructElementAt(Expression root, List subPath) { + Expression result = root; + for (String path : subPath) { + result = new ElementAt(result, new VarcharLiteral(path)); + } + return result; + } + protected static Pair> extractSlotToSubPathPair(ElementAt elementAt) { List subPath = Lists.newArrayList(); while (true) { diff --git a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/PruneNestedColumnTest.java b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/PruneNestedColumnTest.java index 7e66abb5925969..a3e82f92d0c1d6 100644 --- a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/PruneNestedColumnTest.java +++ b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/PruneNestedColumnTest.java @@ -1153,6 +1153,20 @@ public void testPushDownThroughUnion() { ); } + @Test + public void testVariantSubPathConstructionOrder() { + SlotReference root = new SlotReference("v", VariantType.INSTANCE); + List subPath = ImmutableList.of("a", "b", "c"); + + Expression expression = VariantSubPathPruning.constructElementAt(root, subPath); + + Assertions.assertInstanceOf(ElementAt.class, expression); + Pair> extracted = VariantSubPathPruning.extractSlotToSubPathPair( + (ElementAt) expression); + Assertions.assertEquals(root, extracted.first); + Assertions.assertEquals(subPath, extracted.second); + } + @Test public void testDataTypeAccessTree() { List> trees = getDataTypeAccessTrees( diff --git a/regression-test/data/variant_p0/test_variant_sub_path_order.out b/regression-test/data/variant_p0/test_variant_sub_path_order.out new file mode 100644 index 00000000000000..94e3d6d4e25c0d --- /dev/null +++ b/regression-test/data/variant_p0/test_variant_sub_path_order.out @@ -0,0 +1,11 @@ +-- This file is automatically generated. You should know what you did if you want to edit this +-- !union_constant_sub_path -- +1 table 1 +2 constant 1 + +-- !project_sub_path -- +1 1 + +-- !union_constant_sub_path_with_nested_pruning -- +1 table 1 +2 constant 1 diff --git a/regression-test/suites/variant_p0/test_variant_sub_path_order.groovy b/regression-test/suites/variant_p0/test_variant_sub_path_order.groovy new file mode 100644 index 00000000000000..925c1f628c51f4 --- /dev/null +++ b/regression-test/suites/variant_p0/test_variant_sub_path_order.groovy @@ -0,0 +1,72 @@ +// 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. + +suite("test_variant_sub_path_order", "p0") { + sql "DROP TABLE IF EXISTS variant_sub_path_order" + sql """ + CREATE TABLE variant_sub_path_order ( + id INT NOT NULL, + v VARIANT NULL + ) ENGINE = OLAP + DUPLICATE KEY(id) + DISTRIBUTED BY HASH(id) BUCKETS 1 + PROPERTIES ("replication_num" = "1") + """ + sql """ + INSERT INTO variant_sub_path_order VALUES + (1, '{"a":{"b":1},"b":{"a":2}}') + """ + + sql "SET experimental_enable_prune_nested_column = false" + + order_qt_union_constant_sub_path """ + WITH u AS ( + SELECT id, 'table' AS branch_name, v AS c + FROM variant_sub_path_order + UNION ALL + SELECT 2 AS id, 'constant' AS branch_name, + CAST('{"a":{"b":1},"b":{"a":2}}' AS VARIANT) AS c + ) + SELECT id, branch_name, CAST(c['a']['b'] AS INT) AS value + FROM u + ORDER BY id + """ + + order_qt_project_sub_path """ + SELECT id, CAST(c['a']['b'] AS INT) AS value + FROM ( + SELECT id, IF(id > 0, v, CAST('{}' AS VARIANT)) AS c + FROM variant_sub_path_order + ) projected + ORDER BY id + """ + + sql "SET experimental_enable_prune_nested_column = true" + + order_qt_union_constant_sub_path_with_nested_pruning """ + WITH u AS ( + SELECT id, 'table' AS branch_name, v AS c + FROM variant_sub_path_order + UNION ALL + SELECT 2 AS id, 'constant' AS branch_name, + CAST('{"a":{"b":1},"b":{"a":2}}' AS VARIANT) AS c + ) + SELECT id, branch_name, CAST(c['a']['b'] AS INT) AS value + FROM u + ORDER BY id + """ +}