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]