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

eldenmoon 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 07cff332a3a [fix](variant) reject root variant match predicates 
(#61190)
07cff332a3a is described below

commit 07cff332a3a63c8a1d1c1ea70776f14be888bd74
Author: lihangyu <[email protected]>
AuthorDate: Wed Mar 11 21:03:10 2026 +0800

    [fix](variant) reject root variant match predicates (#61190)
    
    VARIANT root columns do not have inverted indexes built on them
    directly;
    only typed sub-columns extracted via `response['field']` carry per-field
    indexes. Issuing `response MATCH 'xxx'` on a root VARIANT slot silently
    falls through to a full scan without any index acceleration, returning
    unexpected results or errors.
    
    Add a check in `CheckMatchExpression` that inspects the resolved
    SlotReference after unwinding any Cast chain. If the slot is a
    VARIANT type and has no sub-column path, the planner now throws an
    AnalysisException with a clear message suggesting the correct
    `column['field'] MATCH 'xxx'` syntax.
    
    Refactor `isSlotOrCastChainOnSlot` → `getSlotFromSlotOrCastChain` to
    return the actual SlotReference (or null) so the caller can inspect the
    slot's type and sub-column path without a second traversal.
    Add unit tests (CheckMatchExpressionTest) covering:
    - Root VARIANT slot → rejected
    - CAST(root VARIANT) → rejected
    - VARIANT sub-column slot → allowed
    Add regression test (test_disable_root_variant_match) that creates a
    VARIANT column with a MATCH_NAME inverted index, verifies root MATCH is
    rejected with the expected error, and confirms sub-column MATCH still
    returns correct results.
---
 .../rules/rewrite/CheckMatchExpression.java        | 12 ++-
 .../rules/rewrite/CheckMatchExpressionTest.java    | 91 ++++++++++++++++++++++
 .../test_variant_empty_index_file.groovy           |  2 +-
 .../search/test_disable_root_variant_match.groovy  | 73 +++++++++++++++++
 4 files changed, 174 insertions(+), 4 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/CheckMatchExpression.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/CheckMatchExpression.java
index aefd8070ad9..feaad409995 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/CheckMatchExpression.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/CheckMatchExpression.java
@@ -49,21 +49,27 @@ public class CheckMatchExpression extends 
OneRewriteRuleFactory {
         for (Expression expr : expressions) {
             if (expr instanceof Match) {
                 Match matchExpression = (Match) expr;
-                if (!isSlotOrCastChainOnSlot(matchExpression.left())
+                SlotReference slotReference = 
getSlotFromSlotOrCastChain(matchExpression.left());
+                if (slotReference == null
                         || !(matchExpression.right() instanceof Literal)) {
                     throw new AnalysisException(String.format("Only support 
match left operand is SlotRef,"
                             + " right operand is Literal. But meet expression 
%s", matchExpression));
                 }
+                if (slotReference.getDataType().isVariantType() && 
!slotReference.hasSubColPath()) {
+                    throw new AnalysisException(String.format("VARIANT root 
column does not support MATCH predicates. "
+                                    + "Please query a subcolumn instead, for 
example %s['field'] MATCH 'xxx'",
+                            slotReference.getName()));
+                }
             }
         }
         return filter;
     }
 
-    private boolean isSlotOrCastChainOnSlot(Expression expression) {
+    private SlotReference getSlotFromSlotOrCastChain(Expression expression) {
         Expression current = expression;
         while (current instanceof Cast) {
             current = current.child(0);
         }
-        return current instanceof SlotReference;
+        return current instanceof SlotReference ? (SlotReference) current : 
null;
     }
 }
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/CheckMatchExpressionTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/CheckMatchExpressionTest.java
new file mode 100644
index 00000000000..fbaa2d97a12
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/CheckMatchExpressionTest.java
@@ -0,0 +1,91 @@
+// 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.
+
+package org.apache.doris.nereids.rules.rewrite;
+
+import org.apache.doris.nereids.exceptions.AnalysisException;
+import org.apache.doris.nereids.trees.expressions.Cast;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.MatchAny;
+import org.apache.doris.nereids.trees.expressions.SlotReference;
+import org.apache.doris.nereids.trees.expressions.literal.StringLiteral;
+import org.apache.doris.nereids.trees.plans.logical.LogicalFilter;
+import org.apache.doris.nereids.trees.plans.logical.LogicalOlapScan;
+import org.apache.doris.nereids.types.StringType;
+import org.apache.doris.nereids.types.VariantType;
+import org.apache.doris.nereids.util.PlanConstructor;
+
+import com.google.common.collect.ImmutableSet;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+
+class CheckMatchExpressionTest {
+
+    private CheckMatchExpression checkMatchExpression;
+    private Method checkChildrenMethod;
+
+    @BeforeEach
+    void setUp() throws Exception {
+        checkMatchExpression = new CheckMatchExpression();
+        checkChildrenMethod = 
CheckMatchExpression.class.getDeclaredMethod("checkChildren", 
LogicalFilter.class);
+        checkChildrenMethod.setAccessible(true);
+    }
+
+    @Test
+    void testRejectsRootVariantMatch() {
+        SlotReference rootVariantSlot = new SlotReference("response", 
VariantType.INSTANCE, true, Arrays.asList());
+        MatchAny match = new MatchAny(rootVariantSlot, new 
StringLiteral("doris"));
+
+        AnalysisException exception = 
Assertions.assertThrows(AnalysisException.class, () -> invokeCheck(match));
+        Assertions.assertTrue(exception.getMessage().contains("VARIANT root 
column does not support MATCH"),
+                exception.getMessage());
+    }
+
+    @Test
+    void testRejectsCastOnRootVariantMatch() {
+        SlotReference rootVariantSlot = new SlotReference("response", 
VariantType.INSTANCE, true, Arrays.asList());
+        MatchAny match = new MatchAny(new Cast(rootVariantSlot, 
StringType.INSTANCE), new StringLiteral("doris"));
+
+        AnalysisException exception = 
Assertions.assertThrows(AnalysisException.class, () -> invokeCheck(match));
+        Assertions.assertTrue(exception.getMessage().contains("VARIANT root 
column does not support MATCH"),
+                exception.getMessage());
+    }
+
+    @Test
+    void testAllowsVariantSubcolumnMatch() {
+        SlotReference variantSubcolumnSlot = new SlotReference("response", 
VariantType.INSTANCE, true, Arrays.asList())
+                .withSubPath(Arrays.asList("msg"));
+        MatchAny match = new MatchAny(variantSubcolumnSlot, new 
StringLiteral("doris"));
+
+        Assertions.assertDoesNotThrow(() -> invokeCheck(match));
+    }
+
+    private void invokeCheck(Expression expression) throws Throwable {
+        LogicalOlapScan scan = PlanConstructor.newLogicalOlapScan(0, "t1", 0);
+        LogicalFilter<LogicalOlapScan> filter = new 
LogicalFilter<>(ImmutableSet.of(expression), scan);
+        try {
+            checkChildrenMethod.invoke(checkMatchExpression, filter);
+        } catch (InvocationTargetException e) {
+            throw e.getCause();
+        }
+    }
+}
diff --git 
a/regression-test/suites/inverted_index_p0/test_variant_empty_index_file.groovy 
b/regression-test/suites/inverted_index_p0/test_variant_empty_index_file.groovy
index 50fc8fe2826..93e4cf7b521 100644
--- 
a/regression-test/suites/inverted_index_p0/test_variant_empty_index_file.groovy
+++ 
b/regression-test/suites/inverted_index_p0/test_variant_empty_index_file.groovy
@@ -57,6 +57,6 @@ suite("test_variant_empty_index_file", "p0") {
         sql """ select /*+ SET_VAR(enable_match_without_inverted_index = 0) */ 
 * from ${tableName} where v match 'abcd';  """
     } catch (Exception e) {
         log.info(e.getMessage());
-        assertTrue(e.getMessage().contains("match_any not support 
execute_match"))
+        assertTrue(e.getMessage().contains("VARIANT root column does not 
support MATCH predicates"))
     }
 }
\ No newline at end of file
diff --git 
a/regression-test/suites/search/test_disable_root_variant_match.groovy 
b/regression-test/suites/search/test_disable_root_variant_match.groovy
new file mode 100644
index 00000000000..f800e8e2341
--- /dev/null
+++ b/regression-test/suites/search/test_disable_root_variant_match.groovy
@@ -0,0 +1,73 @@
+// 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_disable_root_variant_match", "p0") {
+    sql """ set enable_match_without_inverted_index = false """
+    sql """ set enable_common_expr_pushdown = true """
+    sql """ set default_variant_enable_typed_paths_to_sparse = false """
+    sql """ set default_variant_enable_doc_mode = false """
+
+    sql "DROP TABLE IF EXISTS test_disable_root_variant_match_tbl"
+
+    sql """
+        CREATE TABLE test_disable_root_variant_match_tbl (
+            `id` INT NOT NULL,
+            `response` variant<
+                MATCH_NAME 'msg' : string,
+                properties("variant_max_subcolumns_count" = "16")
+            > NULL,
+            INDEX idx_response (response) USING INVERTED PROPERTIES(
+                "parser" = "unicode",
+                "field_pattern" = "msg",
+                "lower_case" = "true"
+            )
+        ) ENGINE=OLAP
+        DUPLICATE KEY(`id`)
+        DISTRIBUTED BY HASH(`id`) BUCKETS 1
+        PROPERTIES (
+            "replication_allocation" = "tag.location.default: 1",
+            "disable_auto_compaction" = "true"
+        )
+    """
+
+    sql """INSERT INTO test_disable_root_variant_match_tbl VALUES
+        (1, '{"msg": "doris community"}'),
+        (2, '{"msg": "apache software"}'),
+        (3, '{"msg": "doris variant index"}')
+    """
+
+    sql "sync"
+    Thread.sleep(5000)
+
+    test {
+        sql """
+            SELECT /*+SET_VAR(enable_common_expr_pushdown=true)*/ id
+            FROM test_disable_root_variant_match_tbl
+            WHERE response MATCH 'doris'
+            ORDER BY id
+        """
+        exception "VARIANT root column does not support MATCH"
+    }
+
+    def variantSubcolumnMatchResult = sql """
+        SELECT /*+SET_VAR(enable_common_expr_pushdown=true)*/ id
+        FROM test_disable_root_variant_match_tbl
+        WHERE response['msg'] MATCH 'doris'
+        ORDER BY id
+    """
+    assertEquals([[1], [3]], variantSubcolumnMatchResult)
+}


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

Reply via email to