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 afdf8bf31d4 branch-4.2: [fix](NestedColumnPruning): visit
key/value/entry args in visitMapContainsKey/Value/Entry #64440 (#70074)
afdf8bf31d4 is described below
commit afdf8bf31d4c6d1ba06dfcac54a454c0edb25ffb
Author: morrySnow <[email protected]>
AuthorDate: Thu Nov 5 10:16:07 2026 +0800
branch-4.2: [fix](NestedColumnPruning): visit key/value/entry args in
visitMapContainsKey/Value/Entry #64440 (#70074)
picked from master #64440
Co-authored-by: minghong <[email protected]>
---
.../rewrite/AccessPathExpressionCollector.java | 62 +++++-
.../rules/rewrite/PruneNestedColumnTest.java | 23 +++
.../column_pruning/map_contains_arg_pruning.out | 55 ++++++
.../column_pruning/map_contains_arg_pruning.groovy | 219 +++++++++++++++++++++
4 files changed, 356 insertions(+), 3 deletions(-)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/AccessPathExpressionCollector.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/AccessPathExpressionCollector.java
index 01eeb8a6927..bc7440b60f5 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/AccessPathExpressionCollector.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/AccessPathExpressionCollector.java
@@ -484,20 +484,76 @@ public class AccessPathExpressionCollector extends
DefaultExpressionVisitor<Void
@Override
public Void visitMapContainsKey(MapContainsKey mapContainsKey,
CollectorContext context) {
+ // MAP_CONTAINS_KEY(<map>, <key>)
+ //
+ // isFunctionNullCheckPath checks whether the parent of this
expression is IS NULL,
+ // splitting queries into two shapes:
+ //
+ // Shape A (parent is IS NULL):
+ // SQL: SELECT ... WHERE map_contains_key(m, k) IS NULL
+ // map_contains_key(m, k) returns NULL only when m itself is NULL —
so the path
+ // should be m.NULL, not m.KEYS.NULL
+ //
+ // Shape B (regular predicate):
+ // SQL: SELECT ... WHERE map_contains_key(m, element_at(s, 'city'))
+ // AND element_at(s, 'city') IS NULL
+ // We add the KEYS prefix for the map column and visit key arg:
`element_at(s, 'city')` with a
+ // fresh context.
+ // s collects two paths:
+ // [s, city] ← from key arg (fresh context → DATA path)
+ // [s, city, NULL] ← from IS NULL
+ // NestedColumnPruning sees [s, city] and strips [s, city, NULL] in
prune phase.
+ if (isFunctionNullCheckPath(context.accessPathBuilder.accessPath)) {
+ // Shape A: skip KEYS prefix, route NULL directly to the map
column.
+ return continueCollectAccessPath(mapContainsKey.getArgument(0),
context);
+ }
+ // Shape B: map argument — only the key sub-column is needed.
context.accessPathBuilder.addPrefix(AccessPathInfo.ACCESS_MAP_KEYS);
- return continueCollectAccessPath(mapContainsKey.getArgument(0),
context);
+ continueCollectAccessPath(mapContainsKey.getArgument(0), context);
+ // Shape B: key argument — visit with a fresh context to register
full-data paths.
+ Expression keyArg = mapContainsKey.getArgument(1);
+ CollectorContext keyCtx = new
CollectorContext(context.statementContext, context.bottomFilter);
+ continueCollectAccessPath(keyArg, keyCtx);
+ return null;
}
@Override
public Void visitMapContainsValue(MapContainsValue mapContainsValue,
CollectorContext context) {
+ // MAP_CONTAINS_VALUE(<map>, <value>)
+ // Same two-shape logic as visitMapContainsKey; see that method for
the full rationale.
+ //
+ // Shape A (parent is IS NULL): skip VALUES prefix, route NULL to m →
m.NULL.
+ // Shape B (regular predicate): add VALUES prefix for m, visit value
arg with fresh context.
+ if (isFunctionNullCheckPath(context.accessPathBuilder.accessPath)) {
+ return continueCollectAccessPath(mapContainsValue.getArgument(0),
context);
+ }
context.accessPathBuilder.addPrefix(AccessPathInfo.ACCESS_MAP_VALUES);
- return continueCollectAccessPath(mapContainsValue.getArgument(0),
context);
+ continueCollectAccessPath(mapContainsValue.getArgument(0), context);
+ Expression valueArg = mapContainsValue.getArgument(1);
+ CollectorContext valueCtx = new
CollectorContext(context.statementContext, context.bottomFilter);
+ continueCollectAccessPath(valueArg, valueCtx);
+ return null;
}
@Override
public Void visitMapContainsEntry(MapContainsEntry mapContainsEntry,
CollectorContext context) {
+ // MAP_CONTAINS_ENTRY(<map>, <key>, <value>)
+ // Same two-shape logic as visitMapContainsKey; see that method for
the full rationale.
+ //
+ // Shape A (parent is IS NULL): skip sub-column prefix, route NULL to
m → m.NULL.
+ // Shape B (regular predicate): add ACCESS_ALL for m (needs both keys
and values),
+ // visit key/value args with fresh context.
+ if (isFunctionNullCheckPath(context.accessPathBuilder.accessPath)) {
+ return continueCollectAccessPath(mapContainsEntry.getArgument(0),
context);
+ }
context.accessPathBuilder.addPrefix(AccessPathInfo.ACCESS_ALL);
- return continueCollectAccessPath(mapContainsEntry.getArgument(0),
context);
+ continueCollectAccessPath(mapContainsEntry.getArgument(0), context);
+ for (int i = 1; i < mapContainsEntry.arity(); i++) {
+ Expression entryArg = mapContainsEntry.getArgument(i);
+ CollectorContext entryCtx = new
CollectorContext(context.statementContext, context.bottomFilter);
+ continueCollectAccessPath(entryArg, entryCtx);
+ }
+ return null;
}
@Override
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 18ec4114f71..30840a518fc 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
@@ -2430,4 +2430,27 @@ public class PruneNestedColumnTest extends
TestWithFeService implements MemoPatt
ImmutableList.of());
}
+ @Test
+ public void testMapContainsArgumentsKeepPayload() throws Exception {
+ for (String expression : ImmutableList.of(
+ "map_contains_key(map_col, c_struct.f1)",
+ "map_contains_value(map_col, c_struct.f1)",
+ "map_contains_entry(map_col, c_struct.f1, c_struct.f3)")) {
+ assertAllAccessPathsContain("select " + expression
+ + " from str_tbl where c_struct.f1 is null",
+ ImmutableList.of(path("c_struct", "f1")),
ImmutableList.of());
+ }
+ }
+
+ @Test
+ public void testMapContainsNullCheckUsesMapNullPath() throws Exception {
+ for (String expression : ImmutableList.of(
+ "map_contains_key(map_col, 'key')",
+ "map_contains_value(map_col, 'value')",
+ "map_contains_entry(map_col, 'key', 'value')")) {
+ assertAllAccessPathsContain("select " + expression + " is null
from str_tbl",
+ ImmutableList.of(metaPath("map_col", "NULL")),
ImmutableList.of());
+ }
+ }
+
}
diff --git
a/regression-test/data/nereids_rules_p0/column_pruning/map_contains_arg_pruning.out
b/regression-test/data/nereids_rules_p0/column_pruning/map_contains_arg_pruning.out
new file mode 100644
index 00000000000..498c202752f
--- /dev/null
+++
b/regression-test/data/nereids_rules_p0/column_pruning/map_contains_arg_pruning.out
@@ -0,0 +1,55 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !case1 --
+1 false true
+2 false true
+3 true \N
+4 true false
+
+-- !case2 --
+1 false false
+2 false false
+3 false \N
+4 true false
+
+-- !case3 --
+1 false false false
+2 false false false
+3 true false \N
+4 true true false
+
+-- !case4 --
+1 false
+2 false
+3 true
+4 false
+
+-- !case5 --
+1 false
+2 false
+3 true
+4 false
+
+-- !case6 --
+1 false
+2 false
+3 true
+4 false
+
+-- !case7a --
+1 true
+2 true
+3 false
+4 true
+
+-- !case7b --
+1 true
+2 true
+3 false
+4 true
+
+-- !case7c --
+1 true
+2 true
+3 false
+4 true
+
diff --git
a/regression-test/suites/nereids_rules_p0/column_pruning/map_contains_arg_pruning.groovy
b/regression-test/suites/nereids_rules_p0/column_pruning/map_contains_arg_pruning.groovy
new file mode 100644
index 00000000000..331a00ac626
--- /dev/null
+++
b/regression-test/suites/nereids_rules_p0/column_pruning/map_contains_arg_pruning.groovy
@@ -0,0 +1,219 @@
+// 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.
+
+// Regression tests for map_contains_key / map_contains_value /
map_contains_entry
+// when the key/value/entry argument references nested sub-columns.
+//
+// Bug: visitMapContainsKey/Value/Entry only visited the map argument and
skipped
+// the key/value/entry argument. When the key is a nested sub-column expression
+// (e.g. element_at(s, 'a')) whose data path was not registered, and the same
+// sub-column also appears in IS NULL, NestedColumnPruning would prune it to
+// null-only metadata access, causing wrong results.
+
+suite("map_contains_arg_pruning") {
+ // In 4.2, DATA and META paths coexist; match complete DATA path tokens.
+ def hasDataPaths = { String plan, List<String> required ->
+ def paths = plan.readLines().findAll { it.contains("all access paths:
[") }.collectMany {
+ def pathList = it.substring(it.indexOf("all access paths: [") +
"all access paths: [".length())
+ pathList.substring(0, pathList.lastIndexOf("]")).split(",
").toList()
+ }
+ paths.containsAll(required)
+ }
+
+ sql """ DROP TABLE IF EXISTS map_contains_arg_pruning_tbl """
+ sql """
+ CREATE TABLE map_contains_arg_pruning_tbl (
+ id INT,
+ s STRUCT<a: STRING, b: INT> NULL,
+ m MAP<STRING, INT> NULL,
+ v VARIANT NULL
+ ) ENGINE = OLAP
+ DUPLICATE KEY(id)
+ DISTRIBUTED BY HASH(id) BUCKETS 1
+ PROPERTIES ("replication_allocation" = "tag.location.default: 1")
+ """
+ sql """
+ INSERT INTO map_contains_arg_pruning_tbl VALUES
+ (1, named_struct('a', 'hello', 'b', 100), {'hello': 1, 'world':
2}, '{"k": 42}'),
+ (2, named_struct('a', 'doris', 'b', 200), {'doris': 3},
NULL),
+ (3, named_struct('a', null, 'b', 300), NULL,
'{"x": 1}'),
+ (4, NULL, {},
'{}')
+ """
+
+ // ================================================================
+ // Case 1: map_contains_key + element_at IS NULL (original bug)
+ // map_contains_key(m, element_at(s, 'a')) needs full access to s.a
+ // as the key lookup value. Without fix, only [s.a.NULL] from
+ // element_at(s, 'a') IS NULL is registered.
+ // ================================================================
+ explain {
+ sql """
+ SELECT id,
+ element_at(s, 'a') IS NULL,
+ map_contains_key(m, element_at(s, 'a'))
+ FROM map_contains_arg_pruning_tbl ORDER BY id
+ """
+ contains "nested columns"
+ check { String plan -> hasDataPaths(plan, ["s.a"]) }
+ contains "m.KEYS" // map_contains_key needs KEYS
path
+ }
+
+ order_qt_case1 """
+ SELECT id,
+ element_at(s, 'a') IS NULL,
+ map_contains_key(m, element_at(s, 'a'))
+ FROM map_contains_arg_pruning_tbl ORDER BY id
+ """
+
+ // ================================================================
+ // Case 2: map_contains_value, value arg references sub-column
+ // map_contains_value(m, element_at(s, 'b')) needs s.b as value
+ // ================================================================
+ explain {
+ sql """
+ SELECT id,
+ element_at(s, 'b') IS NULL,
+ map_contains_value(m, element_at(s, 'b'))
+ FROM map_contains_arg_pruning_tbl ORDER BY id
+ """
+ contains "nested columns"
+ check { String plan -> hasDataPaths(plan, ["s.b"]) }
+ }
+
+ order_qt_case2 """
+ SELECT id,
+ element_at(s, 'b') IS NULL,
+ map_contains_value(m, element_at(s, 'b'))
+ FROM map_contains_arg_pruning_tbl ORDER BY id
+ """
+
+ // ================================================================
+ // Case 3: map_contains_entry(m, key, value) — ternary function.
+ // visitMapContainsEntry must visit BOTH search arguments (arg1=key,
+ // arg2=value) with fresh contexts. Without the fix, only arg1 was
+ // collected and arg2 was silently skipped, leaving s.b unregistered
+ // so that element_at(s, 'b') IS NULL would prune s.b to null-only.
+ // ================================================================
+ explain {
+ sql """
+ SELECT id,
+ element_at(s, 'a') IS NULL,
+ element_at(s, 'b') IS NULL,
+ map_contains_entry(m, element_at(s, 'a'), element_at(s,
'b'))
+ FROM map_contains_arg_pruning_tbl ORDER BY id
+ """
+ contains "nested columns"
+ check { String plan -> hasDataPaths(plan, ["s.a", "s.b"]) }
+ }
+
+ order_qt_case3 """
+ SELECT id,
+ element_at(s, 'a') IS NULL,
+ element_at(s, 'b') IS NULL,
+ map_contains_entry(m, element_at(s, 'a'), element_at(s, 'b'))
+ FROM map_contains_arg_pruning_tbl ORDER BY id
+ """
+
+ // ================================================================
+ // Case 4: map_contains_key(...) IS NULL — the key argument must NOT
+ // be collected as a full-data path because map_contains_key returns
+ // NULL only when the map itself is NULL. The search key does not
+ // affect nullability.
+ //
+ // When map_contains_key(...) IS NULL is the only expression, the
+ // NULL-only context causes the key argument to be skipped entirely,
+ // so s.a is never registered and no nested column pruning is emitted.
+ // ================================================================
+ explain {
+ sql """
+ SELECT id,
+ map_contains_key(m, element_at(s, 'a')) IS NULL
+ FROM map_contains_arg_pruning_tbl ORDER BY id
+ """
+ // s.a must NOT appear in access paths: the key argument inside
+ // map_contains_key(...) IS NULL is skipped in NULL-only context.
+ notContains "[s.a]"
+ }
+
+ order_qt_case4 """
+ SELECT id,
+ map_contains_key(m, element_at(s, 'a')) IS NULL
+ FROM map_contains_arg_pruning_tbl ORDER BY id
+ """
+
+ // ================================================================
+ // Case 5: map_contains_value(...) IS NULL — same behaviour as
+ // map_contains_key: the value argument does not affect nullability.
+ // ================================================================
+ explain {
+ sql """
+ SELECT id,
+ map_contains_value(m, element_at(s, 'b')) IS NULL
+ FROM map_contains_arg_pruning_tbl ORDER BY id
+ """
+ notContains "[s.b]"
+ }
+
+ order_qt_case5 """
+ SELECT id,
+ map_contains_value(m, element_at(s, 'b')) IS NULL
+ FROM map_contains_arg_pruning_tbl ORDER BY id
+ """
+
+ // ================================================================
+ // Case 6: map_contains_entry(...) IS NULL — both key and value
+ // arguments must be skipped. No nested column pruning is expected
+ // because only the map argument is visited with NULL-only path.
+ // ================================================================
+ explain {
+ sql """
+ SELECT id,
+ map_contains_entry(m, element_at(s, 'a'), element_at(s,
'b')) IS NULL
+ FROM map_contains_arg_pruning_tbl ORDER BY id
+ """
+ notContains "s.a"
+ notContains "s.b"
+ }
+
+ order_qt_case6 """
+ SELECT id,
+ map_contains_entry(m, element_at(s, 'a'), element_at(s, 'b'))
IS NULL
+ FROM map_contains_arg_pruning_tbl ORDER BY id
+ """
+
+ // ================================================================
+ // Case 7: IS NOT NULL variants — NOT(IS NULL) delegates to the
+ // same visitIsNull path, so the NULL-only routing logic applies.
+ // ================================================================
+ order_qt_case7a """
+ SELECT id,
+ map_contains_key(m, element_at(s, 'a')) IS NOT NULL
+ FROM map_contains_arg_pruning_tbl ORDER BY id
+ """
+
+ order_qt_case7b """
+ SELECT id,
+ map_contains_value(m, element_at(s, 'b')) IS NOT NULL
+ FROM map_contains_arg_pruning_tbl ORDER BY id
+ """
+
+ order_qt_case7c """
+ SELECT id,
+ map_contains_entry(m, element_at(s, 'a'), element_at(s, 'b'))
IS NOT NULL
+ FROM map_contains_arg_pruning_tbl ORDER BY id
+ """
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]