This is an automated email from the ASF dual-hosted git repository.

englefly pushed a commit to branch meta-path
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/meta-path by this push:
     new 61cea382e6c (fe)Emit struct-level NULL path for element_at over struct 
IS NULL
61cea382e6c is described below

commit 61cea382e6c584e43f53801c6d95422a7d1a8bdf
Author: minghong <[email protected]>
AuthorDate: Mon Jul 20 14:27:13 2026 +0800

    (fe)Emit struct-level NULL path for element_at over struct IS NULL
---
 .../rewrite/AccessPathExpressionCollector.java     |  41 +++++++
 .../rules/rewrite/PruneNestedColumnTest.java       | 127 ++++++++++++++++++---
 2 files changed, 149 insertions(+), 19 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 8abe242c3e1..c91c8faab92 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
@@ -338,6 +338,23 @@ public class AccessPathExpressionCollector extends 
DefaultExpressionVisitor<Void
             Expression fieldName = arguments.get(1);
             DataType fieldType = fieldName.getDataType();
             if (fieldName.isLiteral() && (fieldType.isIntegerLikeType() || 
fieldType.isStringLikeType())) {
+                // element_at(s, 'field') IS NULL has two-layer null semantics:
+                //   s IS NULL OR s.field IS NULL
+                // Always emit META [s, NULL] for the struct-level null map.
+                // Only emit META [s, field, NULL] when the selected field 
itself is nullable.
+                if (context.type == ColumnAccessPathType.META
+                        && 
isUnderIsNull(context.accessPathBuilder.getPathList())) {
+                    StructField field = resolveStructField(
+                            (StructType) first.getDataType(), fieldName);
+                    // Path 1: struct-level null check — bypass field prefix
+                    continueCollectAccessPath(first, copyContext(context));
+                    if (field != null && !field.isNullable()) {
+                        // Non-nullable leaf: struct-level NULL is sufficient
+                        return null;
+                    }
+                    // Nullable leaf: fall through to add field prefix → META 
[s, field, NULL]
+                }
+
                 if (fieldType.isIntegerLikeType()) {
                     int fieldIndex = ((Number) ((Literal) 
fieldName).getValue()).intValue();
                     List<StructField> fields = ((StructType) 
first.getDataType()).getFields();
@@ -654,6 +671,30 @@ public class AccessPathExpressionCollector extends 
DefaultExpressionVisitor<Void
         return visit(isNull, context);
     }
 
+    /**
+     * Resolve the StructField selected by a constant int/string literal.
+     * Returns null when the selector is not a recognized literal or index is 
out of bounds.
+     */
+    /** VisibleForTesting */
+    static StructField resolveStructField(StructType structType, Expression 
fieldExpr) {
+        if (!fieldExpr.isLiteral()) {
+            return null;
+        }
+        if (fieldExpr.getDataType().isIntegerLikeType()) {
+            int index = ((Number) ((Literal) fieldExpr).getValue()).intValue();
+            List<StructField> fields = structType.getFields();
+            if (index >= 1 && index <= fields.size()) {
+                return fields.get(index - 1);
+            }
+            return null;
+        }
+        if (fieldExpr.getDataType().isStringLikeType()) {
+            String name = ((Literal) fieldExpr).getStringValue().toLowerCase();
+            return structType.getField(name);
+        }
+        return null;
+    }
+
     @Override
     public Void visitIf(If ifExpr, CollectorContext context) {
         if (isUnderIsNull(context.accessPathBuilder.accessPath)) {
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 ca53366e5cc..30c21d97569 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
@@ -142,6 +142,13 @@ public class PruneNestedColumnTest extends 
TestWithFeService implements MemoPatt
                 + "  s struct<`NULL`: string, `OFFSET`: string>\n"
                 + ") properties ('replication_num'='1')");
 
+        // Nested struct for verifying multi-level META NULL path generation.
+        // All fields are nullable by default (no NOT NULL in DDL).
+        createTable("create table nested_struct_tbl(\n"
+                + "  id int,\n"
+                + "  s struct<`outer`: struct<`a`: string, `inner_f`: 
string>>\n"
+                + ") properties ('replication_num'='1')");
+
         
connectContext.getSessionVariable().setDisableNereidsRules(RuleType.PRUNE_EMPTY_PARTITION.name());
         connectContext.getSessionVariable().enableNereidsTimeout = false;
     }
@@ -203,8 +210,8 @@ public class PruneNestedColumnTest extends 
TestWithFeService implements MemoPatt
         assertColumn("select element_at(s, 'city') from tbl "
                         + "where element_at(s, 'city') is null",
                 "struct<city:text>",
-                ImmutableList.of(path("s", "city"), metaPath("s", "city", 
"NULL")),
-                ImmutableList.of(metaPath("s", "city", "NULL")));
+                ImmutableList.of(path("s", "city"), metaPath("s", "NULL"), 
metaPath("s", "city", "NULL")),
+                ImmutableList.of(metaPath("s", "NULL"), metaPath("s", "city", 
"NULL")));
 
         assertColumn("select cardinality(element_at(s, 'data')), element_at(s, 
'data') from tbl",
                 "struct<data:array<map<int,struct<a:int,b:double>>>>",
@@ -623,14 +630,14 @@ public class PruneNestedColumnTest extends 
TestWithFeService implements MemoPatt
 
         assertColumn("select 100 from tbl where element_at(s, 'city') is not 
null",
                 "struct<city:text>",
-                ImmutableList.of(metaPath("s", "city", "NULL")),
-                ImmutableList.of(metaPath("s", "city", "NULL"))
+                ImmutableList.of(metaPath("s", "NULL"), metaPath("s", "city", 
"NULL")),
+                ImmutableList.of(metaPath("s", "NULL"), metaPath("s", "city", 
"NULL"))
         );
 
         assertColumn("select 100 from tbl where element_at(s, 'data') is not 
null",
                 "struct<data:array<map<int,struct<a:int,b:double>>>>",
-                ImmutableList.of(metaPath("s", "data", "NULL")),
-                ImmutableList.of(metaPath("s", "data", "NULL"))
+                ImmutableList.of(metaPath("s", "NULL"), metaPath("s", "data", 
"NULL")),
+                ImmutableList.of(metaPath("s", "NULL"), metaPath("s", "data", 
"NULL"))
         );
         assertColumn("select 100 from tbl where element_at(s, 'data')[1] is 
not null",
                 "struct<data:array<map<int,struct<a:int,b:double>>>>",
@@ -649,8 +656,10 @@ public class PruneNestedColumnTest extends 
TestWithFeService implements MemoPatt
         );
         assertColumn("select 100 from tbl where 
element_at(map_values(element_at(s, 'data')[1])[1], 'a') is not null",
                 "struct<data:array<map<int,struct<a:int>>>>",
-                ImmutableList.of(metaPath("s", "data", "*", "VALUES", "a", 
"NULL")),
-                ImmutableList.of(metaPath("s", "data", "*", "VALUES", "a", 
"NULL"))
+                ImmutableList.of(metaPath("s", "data", "*", "VALUES", "NULL"),
+                        metaPath("s", "data", "*", "VALUES", "a", "NULL")),
+                ImmutableList.of(metaPath("s", "data", "*", "VALUES", "NULL"),
+                        metaPath("s", "data", "*", "VALUES", "a", "NULL"))
         );
         assertColumn("select 100 from tbl where element_at(s, 'data')[1][1] is 
not null",
                 "struct<data:array<map<int,struct<a:int,b:double>>>>",
@@ -659,13 +668,21 @@ public class PruneNestedColumnTest extends 
TestWithFeService implements MemoPatt
         );
         assertColumn("select 100 from tbl where element_at(element_at(s, 
'data')[1][1], 'a') is not null",
                 "struct<data:array<map<int,struct<a:int>>>>",
-                ImmutableList.of(path("s", "data", "*", "KEYS"), metaPath("s", 
"data", "*", "VALUES", "a", "NULL")),
-                ImmutableList.of(path("s", "data", "*", "KEYS"), metaPath("s", 
"data", "*", "VALUES", "a", "NULL"))
+                ImmutableList.of(path("s", "data", "*", "KEYS"),
+                        metaPath("s", "data", "*", "VALUES", "NULL"),
+                        metaPath("s", "data", "*", "VALUES", "a", "NULL")),
+                ImmutableList.of(path("s", "data", "*", "KEYS"),
+                        metaPath("s", "data", "*", "VALUES", "NULL"),
+                        metaPath("s", "data", "*", "VALUES", "a", "NULL"))
         );
         assertColumn("select 100 from tbl where element_at(element_at(s, 
'data')[1][1], 'b') is not null",
                 "struct<data:array<map<int,struct<b:double>>>>",
-                ImmutableList.of(path("s", "data", "*", "KEYS"), metaPath("s", 
"data", "*", "VALUES", "b", "NULL")),
-                ImmutableList.of(path("s", "data", "*", "KEYS"), metaPath("s", 
"data", "*", "VALUES", "b", "NULL"))
+                ImmutableList.of(path("s", "data", "*", "KEYS"),
+                        metaPath("s", "data", "*", "VALUES", "NULL"),
+                        metaPath("s", "data", "*", "VALUES", "b", "NULL")),
+                ImmutableList.of(path("s", "data", "*", "KEYS"),
+                        metaPath("s", "data", "*", "VALUES", "NULL"),
+                        metaPath("s", "data", "*", "VALUES", "b", "NULL"))
         );
     }
 
@@ -703,17 +720,19 @@ public class PruneNestedColumnTest extends 
TestWithFeService implements MemoPatt
                 
"struct<city:text,data:array<map<int,struct<a:int,b:double>>>>",
                 ImmutableList.of(
                         path("s", "data"),
+                        metaPath("s", "NULL"),
                         metaPath("s", "city", "NULL")),
-                ImmutableList.of(metaPath("s", "city", "NULL"))
+                ImmutableList.of(metaPath("s", "NULL"), metaPath("s", "city", 
"NULL"))
         );
 
         assertColumn("select element_at(s, 'data') from tbl where 
element_at(s, 'city') is not null and element_at(s, 'data') is not null",
                 
"struct<city:text,data:array<map<int,struct<a:int,b:double>>>>",
                 ImmutableList.of(
                         path("s", "data"),
+                        metaPath("s", "NULL"),
                         metaPath("s", "city", "NULL"),
                         metaPath("s", "data", "NULL")),
-                ImmutableList.of(metaPath("s", "city", "NULL"), metaPath("s", 
"data", "NULL"))
+                ImmutableList.of(metaPath("s", "NULL"), metaPath("s", "city", 
"NULL"), metaPath("s", "data", "NULL"))
         );
     }
 
@@ -1127,8 +1146,8 @@ public class PruneNestedColumnTest extends 
TestWithFeService implements MemoPatt
     public void testMetadataPathBelowSameNamedStructField() throws Exception {
         assertColumn("select 1 from meta_name_tbl where element_at(s, 'NULL') 
is null",
                 "struct<null:text>",
-                ImmutableList.of(metaPath("s", "null", "NULL")),
-                ImmutableList.of(metaPath("s", "null", "NULL")));
+                ImmutableList.of(metaPath("s", "NULL"), metaPath("s", "null", 
"NULL")),
+                ImmutableList.of(metaPath("s", "NULL"), metaPath("s", "null", 
"NULL")));
 
         assertColumn("select length(element_at(s, 'OFFSET')) from 
meta_name_tbl",
                 "struct<offset:text>",
@@ -1453,6 +1472,76 @@ public class PruneNestedColumnTest extends 
TestWithFeService implements MemoPatt
                 ImmutableList.of(metaPath("s", "NULL")));
     }
 
+    @Test
+    public void testResolveStructFieldNullable() {
+        StructType type = new StructType(ImmutableList.of(
+                new StructField("not_null_f", StringType.INSTANCE, false, ""),
+                new StructField("nullable_f", StringType.INSTANCE, true, "")
+        ));
+        // String-like literal: select by name
+        StructField notNullField = 
AccessPathExpressionCollector.resolveStructField(
+                type, new 
org.apache.doris.nereids.trees.expressions.literal.VarcharLiteral("not_null_f"));
+        Assertions.assertNotNull(notNullField);
+        Assertions.assertFalse(notNullField.isNullable());
+
+        StructField nullableField = 
AccessPathExpressionCollector.resolveStructField(
+                type, new 
org.apache.doris.nereids.trees.expressions.literal.VarcharLiteral("nullable_f"));
+        Assertions.assertNotNull(nullableField);
+        Assertions.assertTrue(nullableField.isNullable());
+
+        // Integer-like literal: select by 1-based index
+        StructField fieldByIndex = 
AccessPathExpressionCollector.resolveStructField(
+                type, new 
org.apache.doris.nereids.trees.expressions.literal.IntegerLiteral(1));
+        Assertions.assertNotNull(fieldByIndex);
+        Assertions.assertEquals("not_null_f", fieldByIndex.getName());
+        Assertions.assertFalse(fieldByIndex.isNullable());
+
+        // Out-of-bounds index returns null
+        StructField outOfBounds = 
AccessPathExpressionCollector.resolveStructField(
+                type, new 
org.apache.doris.nereids.trees.expressions.literal.IntegerLiteral(99));
+        Assertions.assertNull(outOfBounds);
+
+        // Non-literal returns null
+        StructField nonLiteral = 
AccessPathExpressionCollector.resolveStructField(
+                type, NullLiteral.INSTANCE);
+        Assertions.assertNull(nonLiteral);
+    }
+
+    @Test
+    public void testNestedStructFieldNullAccess() throws Exception {
+        // element_at(element_at(s, 'outer'), 'a') IS NULL generates three 
META NULL paths:
+        // one for each nullable level in the chain.
+        // s IS NULL  → META [s, NULL]
+        // s.outer IS NULL → META [s, outer, NULL]
+        // s.outer.a IS NULL → META [s, outer, a, NULL]
+        assertColumn("select 1 from nested_struct_tbl "
+                        + "where element_at(element_at(s, 'outer'), 'a') is 
null",
+                "struct<outer:struct<a:text>>",
+                ImmutableList.of(
+                        metaPath("s", "NULL"),
+                        metaPath("s", "outer", "NULL"),
+                        metaPath("s", "outer", "a", "NULL")),
+                ImmutableList.of(
+                        metaPath("s", "NULL"),
+                        metaPath("s", "outer", "NULL"),
+                        metaPath("s", "outer", "a", "NULL")));
+
+        // With IS NOT NULL on a field that's the only predicate access.
+        // SELECT element_at(s, 'outer') reads the full outer sub-struct → 
both a and inner_f.
+        assertColumn("select element_at(s, 'outer') from nested_struct_tbl "
+                        + "where element_at(element_at(s, 'outer'), 'inner_f') 
is not null",
+                "struct<outer:struct<a:text,inner_f:text>>",
+                ImmutableList.of(
+                        path("s", "outer"),
+                        metaPath("s", "NULL"),
+                        metaPath("s", "outer", "NULL"),
+                        metaPath("s", "outer", "inner_f", "NULL")),
+                ImmutableList.of(
+                        metaPath("s", "NULL"),
+                        metaPath("s", "outer", "NULL"),
+                        metaPath("s", "outer", "inner_f", "NULL")));
+    }
+
     @Test
     public void testScalarIsNullProducesMetaPath() {
         SlotReference slot = rewriteAndFindScanSlot("select 1 from tbl where 
id is null", "id", false);
@@ -1475,7 +1564,7 @@ public class PruneNestedColumnTest extends 
TestWithFeService implements MemoPatt
         assertColumn("select s from tbl where element_at(s, 'city') is null",
                 
"struct<city:text,data:array<map<int,struct<a:int,b:double>>>>",
                 ImmutableList.of(path("s")),
-                ImmutableList.of(metaPath("s", "city", "NULL")));
+                ImmutableList.of(metaPath("s", "NULL"), metaPath("s", "city", 
"NULL")));
 
         // This shape is closer to the production bug: one predicate needs the 
parent
         // null map, another predicate needs a child null map, and the 
projection needs
@@ -1591,10 +1680,10 @@ public class PruneNestedColumnTest extends 
TestWithFeService implements MemoPatt
         SlotReference nestedNormalSlot = rewriteAndFindScanSlot(
                 "select 1 from tbl where element_at(s, 'city') is not null", 
"s", false);
         Assertions.assertEquals(
-                new TreeSet<>(ImmutableList.of(metaPath("s", "city", "NULL"))),
+                new TreeSet<>(ImmutableList.of(metaPath("s", "NULL"), 
metaPath("s", "city", "NULL"))),
                 new TreeSet<>(nestedNormalSlot.getAllAccessPaths().get()));
         Assertions.assertEquals(
-                new TreeSet<>(ImmutableList.of(metaPath("s", "city", "NULL"))),
+                new TreeSet<>(ImmutableList.of(metaPath("s", "NULL"), 
metaPath("s", "city", "NULL"))),
                 new 
TreeSet<>(nestedNormalSlot.getPredicateAccessPaths().get()));
 
         // MV fragment: IS NULL degrades to element_at via default visitor,


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to