This is an automated email from the ASF dual-hosted git repository.
yiguolei pushed a commit to branch branch-4.2
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/branch-4.2 by this push:
new f6702049866 branch-4.2: [fix](nereids) Match nested access path roots
case-insensitively (#68360)
f6702049866 is described below
commit f6702049866b35b86d86611b8c2d7b0e602e09c4
Author: daidai <[email protected]>
AuthorDate: Wed Sep 23 08:51:29 2026 +0800
branch-4.2: [fix](nereids) Match nested access path roots
case-insensitively (#68360)
### What problem does this PR solve?
Issue Number: None
Problem Summary:
Nested access paths are collected with a lower-cased column name, while
a whole-column access path is rebuilt from the name the catalog stores.
When a column name is not all lower case, the prefix check that decides
whether the all access paths already cover a predicate access path
compares the two spellings case-sensitively and reports "not covered",
so the sub-field path is added next to the whole-column path:
```sql
CREATE TABLE t (id INT, S STRUCT<City: STRING, Zip: INT>);
SELECT S FROM t WHERE struct_element(S, 'City') = 'x';
-- all access paths: [S], [s.city]
```
BE does not accept both for the same slot.
`ColumnIterator::_get_sub_access_paths` consumes the whole-column path
as the "read this column" marker and removes it from the list, so the
remaining `[city]` is treated as the only sub-column to read and
`StructFileColumnIterator::set_access_paths` marks the siblings
`SKIP_READING`. The projected column then comes back without those
fields, and no error is reported:
| | before | after |
| --- | --- | --- |
| all access paths | `[S]`, `[s.city]` | `[S]` |
| `SELECT S ... WHERE struct_element(S, 'City') = 'x'` | `Zip` is empty
| full struct |
Compare the path components case-insensitively, which is what BE does
when it matches the root (`StringCaseEqual`) and the struct fields
(`to_lower`). A column name that is all lower case was never affected.
Only OLAP scans lose data. External and TVF file scans read the whole
column either way, because `AccessPathParser` marks a one-component path
as `project_all` and then ignores the finer paths.
### Release note
None
### Check List (For Author)
- Test
- [x] Regression test
- [x] Unit Test
- [ ] Manual test (add detailed scripts or steps below)
- [ ] No need to test or manual test. Explain why:
- [ ] This is a refactor/code format and no logic has been changed.
- [ ] Previous test can cover this change.
- [ ] No code files have been changed.
- [ ] Other reason
- Behavior changed:
- [ ] No.
- [x] Yes. A sub-field predicate path is no longer added next to the
whole-column path.
- Does this need documentation?
- [x] No.
- [ ] Yes.
### Check List (For Reviewer who merge this PR)
- [ ] Confirm the release note
- [ ] Confirm test cases
- [ ] Confirm document
- [ ] Add branch pick label
---
.../nereids/rules/rewrite/NestedColumnPruning.java | 20 +++++++-
.../rules/rewrite/PruneNestedColumnTest.java | 22 ++++++++
.../upper_case_nested_column_pruning.groovy | 59 ++++++++++++++++++++++
3 files changed, 100 insertions(+), 1 deletion(-)
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 d60686c2ec6..b45cbc62f0f 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
@@ -897,13 +897,31 @@ public class NestedColumnPruning implements
CustomRewriter {
TColumnAccessPath predicatePath, List<TColumnAccessPath> allPaths)
{
for (TColumnAccessPath allPath : allPaths) {
if (allPath.getType() == predicatePath.getType()
- && pathCoversPrefix(getAccessPathList(allPath),
getAccessPathList(predicatePath))) {
+ && coversPrefixIgnoreCase(getAccessPathList(allPath),
getAccessPathList(predicatePath))) {
return true;
}
}
return false;
}
+ /**
+ * Whether {@code path} is a prefix of {@code longerPath}, ignoring case.
The collected paths
+ * lower-case every component, but a whole-column path is rebuilt from the
catalog column name,
+ * so {@code [S]} still has to cover {@code [s, city]}. A case-sensitive
comparison would append
+ * {@code [s, city]} to the all paths, and BE then reads that field only
and skips its siblings.
+ */
+ private static boolean coversPrefixIgnoreCase(List<String> path,
List<String> longerPath) {
+ if (path.size() > longerPath.size()) {
+ return false;
+ }
+ for (int i = 0; i < path.size(); i++) {
+ if (!path.get(i).equalsIgnoreCase(longerPath.get(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
private static boolean hasStrictPrefix(List<String> path, List<String>
prefix) {
return path.size() > prefix.size() && path.subList(0,
prefix.size()).equals(prefix);
}
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 f3aa4bf42de..297d906c3b5 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
@@ -89,6 +89,11 @@ public class PruneNestedColumnTest extends TestWithFeService
implements MemoPatt
+ ">)\n"
+ "properties ('replication_num'='1')");
+ createTable("create table upper_case_tbl(\n"
+ + " id int,\n"
+ + " S struct<City: string, Zip: int>)\n"
+ + "properties ('replication_num'='1')");
+
createTable("create table tbl2(\n"
+ " id2 int,\n"
+ " value int,\n"
@@ -287,6 +292,23 @@ public class PruneNestedColumnTest extends
TestWithFeService implements MemoPatt
Assertions.assertEquals(ImmutableList.of(path("s", "city")),
ImmutableList.copyOf(predicateAccessPaths));
}
+ @Test
+ public void testWholeUpperCaseColumnOutputKeepsSubFieldPredicatePath()
throws Exception {
+ // The collected paths are lower-cased, the whole-column path keeps
the catalog name. The
+ // predicate path must still count as covered, otherwise it is added
to the all paths and
+ // BE reads that one field and skips its siblings.
+ Pair<PhysicalPlan, List<SlotDescriptor>> result = collectComplexSlots(
+ "select S from upper_case_tbl where struct_element(S, 'City')
= 'x'");
+ TreeSet<TColumnAccessPath> allAccessPaths = new TreeSet<>();
+ TreeSet<TColumnAccessPath> 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 testVariantAccessPath() throws Exception {
assertColumn("select v['a']['B'] from variant_tbl",
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..8c27cc6f3ae
--- /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 next to the whole-column path and the BE reads that
field only, leaving the
+// sibling fields of the projected column empty.
+
+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})
+ """
+
+ // The whole struct is projected and the predicate reads one field of it.
+ def structRows = sql """ SELECT S FROM ucnp_tbl WHERE struct_element(S,
'City') = 'beijing' """
+ assertEquals(1, structRows.size())
+ assertTrue(structRows[0][0].toString().contains("beijing"),
+ "projected struct lost City: ${structRows[0][0]}")
+ assertTrue(structRows[0][0].toString().contains("10001"),
+ "projected struct lost the sibling field Zip: ${structRows[0][0]}")
+
+ // Same shape on a map column: every key and value must survive a
predicate on the values.
+ def mapRows = sql """ SELECT M FROM ucnp_tbl WHERE
array_contains(map_values(M), 1) """
+ assertEquals(1, mapRows.size())
+ def mapValue = mapRows[0][0].toString()
+ for (String entry : ["a", "b", "1", "2"]) {
+ assertTrue(mapValue.contains(entry),
+ "projected map lost ${entry}: ${mapValue}")
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]