[fix](reverted index) Validate nested variant MATCH predicates (#66207)

Related PR: #61190

Problem Summary: Root VARIANT MATCH predicates were rejected only when
the Match expression was a top-level filter conjunct. A Match nested
under OR could bypass frontend analysis and reach backend execution,
where it failed with an unrelated runtime error. Recursively collect
Match expressions from each filter expression and apply the existing
operand and root Variant validation to every Match node. Add focused FE
unit and regression coverage for a mixed text/root Variant OR predicate.

### Release note

Reject MATCH predicates on VARIANT root columns even when nested in
compound filter expressions.

### Check List (For Author)

- Test: Regression test / Unit Test
- `./run-regression-test.sh --run -d search -s
test_disable_root_variant_match` (failed before the fix and passed after
it)
- `./run-fe-ut.sh --run
org.apache.doris.nereids.rules.rewrite.CheckMatchExpressionTest`
    - `./build.sh --fe`
- Behavior changed: Yes. Nested root VARIANT MATCH predicates now fail
during FE analysis, consistent with direct predicates.
- Does this need documentation: No
This commit is contained in:
lihangyu authored and GitHub committed 2026-08-03 20:36:10 +08:00
1 parent 19c32c56a2
commit 68f54696ee
3 files changed
+45 -17

No files matched your search

@@ -47,20 +47,18 @@ public class CheckMatchExpression extends OneRewriteRuleFactory {
private Plan checkChildren(LogicalFilter<? extends Plan> filter) {
List<Expression> expressions = filter.getExpressions();
for (Expression expr : expressions) {
if (expr instanceof Match) {
Match matchExpression = (Match) expr;
SlotReference slotReference = getSlotFromSlotCastOrAliasChain(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()));
}
List<Match> matchExpressions = ExpressionUtils.collectToList(expressions, Match.class::isInstance);
for (Match matchExpression : matchExpressions) {
SlotReference slotReference = getSlotFromSlotCastOrAliasChain(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;
@@ -23,6 +23,7 @@ import org.apache.doris.nereids.trees.expressions.Alias;
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.Or;
import org.apache.doris.nereids.trees.expressions.SlotReference;
import org.apache.doris.nereids.trees.expressions.literal.IntegerLiteral;
import org.apache.doris.nereids.trees.expressions.literal.StringLiteral;
@@ -63,6 +64,19 @@ class CheckMatchExpressionTest {
exception.getMessage());
}
@Test
void testRejectsRootVariantMatchNestedInOr() {
SlotReference textSlot = new SlotReference("response_body", StringType.INSTANCE, true);
SlotReference rootVariantSlot = new SlotReference("response", VariantType.INSTANCE, true, Arrays.asList());
Or match = new Or(
new MatchAny(textSlot, new StringLiteral("doris")),
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());
@@ -26,10 +26,15 @@ suite("test_disable_root_variant_match", "p0") {
sql """
CREATE TABLE test_disable_root_variant_match_tbl (
`id` INT NOT NULL,
`response_body` TEXT NULL,
`response` variant<
MATCH_NAME 'msg' : string,
properties("variant_max_subcolumns_count" = "16")
> NULL,
INDEX idx_response_body (response_body) USING INVERTED PROPERTIES(
"parser" = "unicode",
"lower_case" = "true"
),
INDEX idx_response (response) USING INVERTED PROPERTIES(
"parser" = "unicode",
"field_pattern" = "msg",
@@ -45,9 +50,9 @@ suite("test_disable_root_variant_match", "p0") {
"""
sql """INSERT INTO test_disable_root_variant_match_tbl VALUES
(1, '{"msg": "doris community"}'),
(2, '{"msg": "apache software"}'),
(3, '{"msg": "doris variant index"}')
(1, 'doris community', '{"msg": "doris community"}'),
(2, 'apache software', '{"msg": "apache software"}'),
(3, 'doris variant index', '{"msg": "doris variant index"}')
"""
sql "sync"
@@ -63,6 +68,17 @@ suite("test_disable_root_variant_match", "p0") {
exception "VARIANT root column does not support MATCH"
}
test {
sql """
SELECT /*+SET_VAR(enable_segment_limit_pushdown=true)*/ id
FROM test_disable_root_variant_match_tbl
WHERE response_body MATCH_ANY 'doris'
OR response MATCH_ANY 'doris'
ORDER BY id
"""
exception "VARIANT root column does not support MATCH"
}
def variantSubcolumnMatchResult = sql """
SELECT /*+SET_VAR(enable_segment_limit_pushdown=true)*/ id
FROM test_disable_root_variant_match_tbl