This is an automated email from the ASF dual-hosted git repository.
morningman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new eefcd845c77 [fix](nereids) Match nested access path roots
case-insensitively (#69024)
eefcd845c77 is described below
commit eefcd845c77ab01cd2fff96cbdc5b57639e0eef5
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Wed Oct 14 22:31:58 2026 +0800
[fix](nereids) Match nested access path roots case-insensitively (#69024)
### What problem does this PR solve?
Issue Number: None
Related PR: #68360
Problem Summary:
**In short.** This ports branch-4.2 #68360 to master. When a nested
column's name is not all lower case, Nereids decided that the
whole-column access path did not cover a predicate's sub-field path, and
sent both to BE. The check now compares path components
case-insensitively, so FE sends only the whole-column path.
**Background.** For a scan of a nested column, `NestedColumnPruning`
sends BE two lists: the *all* access paths (what the scan must produce)
and the *predicate* access paths (what the predicates read first). Paths
are collected with lower-cased components, while
`buildColumnAccessPaths` rebuilds a whole-column path from the column
name as the catalog stores it. `addPredicatePathsToFinalAllAccessPaths`
then keeps the all paths a superset of the predicate paths: a predicate
path that no all path covers is appended.
**The problem, and what it cost.** `isPrefixPath` compared the
components case-sensitively, so for a column named `S` the whole-column
path `[S]` did not cover `[s, city]`:
| `SELECT S FROM t WHERE struct_element(S, 'City') = 'beijing'` | master
| with this PR |
|---|---|---|
| all access paths | `[S, s.city]` | `[S]` |
| predicate access paths | `[s.city]` | `[s.city]` |
| map column `M`, `WHERE array_contains(map_values(M), 1)` | `[M,
m.VALUES]` | `[M]` |
On branch-4.x the extra path made BE read only `city` and return the
struct with its other fields empty, which is what #68360 fixed there.
The BE on master reads every child for a whole-column path, so the query
results on master are already correct (checked on a local cluster); what
remains is that FE sends a sub-field path next to the whole column it
already covers, which the BE has to tolerate.
**How this PR fixes it.** `isPrefixPath` becomes
`isPrefixPathIgnoreCase` and compares components with
`equalsIgnoreCase`, as #68360 does on branch-4.2. All collected
components are already lower case, so in practice this only changes the
comparison of the root, the one component that keeps the catalog
spelling.
**Results.** The table above is the EXPLAIN output on a local cluster
before and after this change. The FE UT of #68360 passes with this
change and fails without it (`expected: <[DATA:S]> but was: <[DATA:S,
DATA:s.city]>`); the other 68 `PruneNestedColumnTest` cases still pass.
The regression case checks the access paths and the returned rows, and
its `.out` file was generated by the script.
Co-authored-by: daidai <[email protected]>
---
.../nereids/rules/rewrite/NestedColumnPruning.java | 13 +++--
.../rules/rewrite/PruneNestedColumnTest.java | 23 +++++++++
.../upper_case_nested_column_pruning.out | 7 +++
.../upper_case_nested_column_pruning.groovy | 59 ++++++++++++++++++++++
4 files changed, 99 insertions(+), 3 deletions(-)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/NestedColumnPruning.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/NestedColumnPruning.java
index 41f1b945e8b..1b07ffe8d49 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/NestedColumnPruning.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/NestedColumnPruning.java
@@ -363,19 +363,26 @@ public class NestedColumnPruning implements
CustomRewriter {
private static boolean isCoveredByAllPath(ColumnAccessPath predicatePath,
List<ColumnAccessPath> allPaths) {
for (ColumnAccessPath allPath : allPaths) {
if (allPath.getType() == predicatePath.getType()
- && isPrefixPath(allPath.getPath(),
predicatePath.getPath())) {
+ && isPrefixPathIgnoreCase(allPath.getPath(),
predicatePath.getPath())) {
return true;
}
}
return false;
}
- private static boolean isPrefixPath(List<String> prefix, List<String>
path) {
+ /**
+ * Whether {@code prefix} is a prefix of {@code path}, ignoring case. The
collected access paths
+ * lower-case every component, but buildColumnAccessPaths rebuilds a
whole-column path from the
+ * catalog column name, so for a column named {@code S} the all path
{@code [S]} must still cover
+ * the predicate path {@code [s, city]}. Compared case-sensitively, {@code
[s, city]} would be
+ * appended next to {@code [S]}.
+ */
+ private static boolean isPrefixPathIgnoreCase(List<String> prefix,
List<String> path) {
if (prefix.size() > path.size()) {
return false;
}
for (int i = 0; i < prefix.size(); ++i) {
- if (!prefix.get(i).equals(path.get(i))) {
+ if (!prefix.get(i).equalsIgnoreCase(path.get(i))) {
return false;
}
}
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 b83ad8af87f..86ce28b43be 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
@@ -189,6 +189,13 @@ public class PruneNestedColumnTest extends
TestWithFeService implements MemoPatt
+ " s struct<f: int, g: int>\n"
+ ") properties ('replication_num'='1')");
+ // The column name is not all lower case: access paths are collected
lower-cased, while the
+ // whole-column path keeps the catalog name.
+ createTable("create table upper_case_tbl(\n"
+ + " id int,\n"
+ + " S struct<City: string, Zip: int>\n"
+ + ") properties ('replication_num'='1')");
+
connectContext.getSessionVariable().setDisableNereidsRules(RuleType.PRUNE_EMPTY_PARTITION.name());
connectContext.getSessionVariable().enableNereidsTimeout = false;
}
@@ -317,6 +324,22 @@ public class PruneNestedColumnTest extends
TestWithFeService implements MemoPatt
ImmutableList.of());
}
+ @Test
+ public void testWholeUpperCaseColumnOutputKeepsSubFieldPredicatePath()
throws Exception {
+ // [S] (catalog name) must cover the lower-cased predicate path [s,
city]; otherwise
+ // [s, city] is appended to the all access paths next to the whole
column.
+ Pair<PhysicalPlan, List<SlotDescriptor>> result = collectComplexSlots(
+ "select S from upper_case_tbl where struct_element(S, 'City')
= 'x'");
+ TreeSet<ColumnAccessPath> allAccessPaths = new TreeSet<>();
+ TreeSet<ColumnAccessPath> predicateAccessPaths = new TreeSet<>();
+ for (SlotDescriptor slotDescriptor : result.second) {
+ allAccessPaths.addAll(slotDescriptor.getAllAccessPaths());
+
predicateAccessPaths.addAll(slotDescriptor.getPredicateAccessPaths());
+ }
+ Assertions.assertEquals(ImmutableList.of(path("S")),
ImmutableList.copyOf(allAccessPaths));
+ Assertions.assertEquals(ImmutableList.of(path("s", "city")),
ImmutableList.copyOf(predicateAccessPaths));
+ }
+
@Test
public void
testCardinalityMapElementOffsetPredicateStaysOutOfAllAccessPaths() throws
Exception {
Pair<PhysicalPlan, List<SlotDescriptor>> result = collectComplexSlots(
diff --git
a/regression-test/data/nereids_rules_p0/column_pruning/upper_case_nested_column_pruning.out
b/regression-test/data/nereids_rules_p0/column_pruning/upper_case_nested_column_pruning.out
new file mode 100644
index 00000000000..21aac942d48
--- /dev/null
+++
b/regression-test/data/nereids_rules_p0/column_pruning/upper_case_nested_column_pruning.out
@@ -0,0 +1,7 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !struct --
+1 {"city":"beijing", "zip":10001}
+
+-- !map --
+1 {"a":1, "b":2}
+
diff --git
a/regression-test/suites/nereids_rules_p0/column_pruning/upper_case_nested_column_pruning.groovy
b/regression-test/suites/nereids_rules_p0/column_pruning/upper_case_nested_column_pruning.groovy
new file mode 100644
index 00000000000..bbc3688df3c
--- /dev/null
+++
b/regression-test/suites/nereids_rules_p0/column_pruning/upper_case_nested_column_pruning.groovy
@@ -0,0 +1,59 @@
+// 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.
+
+// Access paths are collected with a lower-cased column name, while a
whole-column access path
+// keeps the name as the catalog stores it. When a column name is not all
lower case, the two
+// spellings must still be recognized as the same column; otherwise the
sub-field path of the
+// predicate is added to the all access paths next to the whole-column path.
+
+suite("upper_case_nested_column_pruning") {
+ sql """ SET enable_prune_nested_column = true """
+ sql """ DROP TABLE IF EXISTS ucnp_tbl """
+ sql """
+ CREATE TABLE ucnp_tbl (
+ id INT,
+ S STRUCT<City: STRING, Zip: INT> NULL,
+ M MAP<STRING, INT> NULL
+ ) ENGINE = OLAP
+ DUPLICATE KEY(id)
+ DISTRIBUTED BY HASH(id) BUCKETS 1
+ PROPERTIES ("replication_allocation" = "tag.location.default: 1")
+ """
+
+ sql """
+ INSERT INTO ucnp_tbl VALUES
+ (1, named_struct('city', 'beijing', 'zip', 10001), {'a': 1, 'b':
2}),
+ (2, named_struct('city', 'shanghai', 'zip', 20002), {'c': 3})
+ """
+
+ // The whole struct is projected and the predicate reads one field of it:
the whole-column
+ // path alone covers the predicate path, and every field of the struct is
returned.
+ explain {
+ sql "SELECT S FROM ucnp_tbl WHERE struct_element(S, 'City') =
'beijing'"
+ contains "all access paths: [S]"
+ contains "predicate access paths: [s.city]"
+ }
+ order_qt_struct """ SELECT id, S FROM ucnp_tbl WHERE struct_element(S,
'City') = 'beijing' """
+
+ // Same shape on a map column: every key and value must survive a
predicate on the values.
+ explain {
+ sql "SELECT M FROM ucnp_tbl WHERE array_contains(map_values(M), 1)"
+ contains "all access paths: [M]"
+ contains "predicate access paths: [m.VALUES]"
+ }
+ order_qt_map """ SELECT id, M FROM ucnp_tbl WHERE
array_contains(map_values(M), 1) """
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]