Explorar el Código

Integrate LIKE/RLIKE LIST with ReplaceStringCasingWithInsensitiveRegexMatch rule (#131531)

Allow LIKE LIST and RLIKE LIST to take advantage of ReplaceStringCasingWithInsensitiveRegexMatch for optimizations.
Add unit tests to confirm the change works and the results are correct.

The following is pushed down as just first_name RLIKE ("G.*")

FROM employees 
| WHERE TO_UPPER(first_name) RLIKE ("G.*", "a.*", "bE.*") 
The following is pushed down as just first_name LIKE ("G.*")

FROM employees 
| WHERE TO_UPPER(TO_LOWER(TO_UPPER(first_name))) LIKE ("G.*", "a.*", "bE.*")
Julian Kiryakov hace 3 meses
padre
commit
631cbac07a

+ 6 - 0
docs/changelog/131531.yaml

@@ -0,0 +1,6 @@
+pr: 131531
+summary: Integrate LIKE/RLIKE LIST with `ReplaceStringCasingWithInsensitiveRegexMatch`
+  rule
+area: ES|QL
+type: enhancement
+issues: []

+ 9 - 0
x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/predicate/regex/RegexMatch.java

@@ -15,6 +15,7 @@ import org.elasticsearch.xpack.esql.core.tree.Source;
 import org.elasticsearch.xpack.esql.core.type.DataType;
 
 import java.util.Objects;
+import java.util.function.Predicate;
 
 import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.DEFAULT;
 import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isStringAndExact;
@@ -67,6 +68,14 @@ public abstract class RegexMatch<T extends StringPattern> extends UnaryScalarFun
         throw new UnsupportedOperationException();
     }
 
+    /**
+     * Returns an equivalent optimized expression taking into account the case of the pattern(s)
+     * @param unwrappedField the field with to_upper/to_lower function removed
+     * @param matchesCaseFn a predicate to check if a pattern matches the case
+     * @return an optimized equivalent Expression or this if no optimization is possible
+     */
+    public abstract Expression optimizeStringCasingWithInsensitiveRegexMatch(Expression unwrappedField, Predicate<String> matchesCaseFn);
+
     @Override
     public boolean equals(Object obj) {
         if (super.equals(obj)) {

+ 131 - 0
x-pack/plugin/esql/qa/testFixtures/src/main/resources/where-like.csv-spec

@@ -534,6 +534,114 @@ emp_no:integer | first_name:keyword
 10055          | Georgy    
 ;
 
+likeListWithUpperAllLower
+required_capability: like_with_list_of_patterns
+FROM employees 
+| WHERE TO_UPPER(first_name) LIKE ("geor*", "wei*") 
+| KEEP emp_no, first_name 
+| SORT emp_no;
+
+emp_no:integer | first_name:keyword
+;
+
+likeListWithUpperAllUpper
+required_capability: like_with_list_of_patterns
+FROM employees 
+| WHERE TO_UPPER(first_name) LIKE ("GEOR*", "WEI*")
+| KEEP emp_no, first_name 
+| SORT emp_no;
+
+emp_no:integer | first_name:keyword
+10001          | Georgi
+10040          | Weiyi
+10055          | Georgy
+;
+
+likeListWithUpperMixedCase
+required_capability: like_with_list_of_patterns
+FROM employees 
+| WHERE TO_UPPER(first_name) LIKE ("GeOr*", "wEiY*", "bErNi")
+| KEEP emp_no, first_name 
+| SORT emp_no;
+
+emp_no:integer | first_name:keyword
+;
+
+likeListWithUpperMultiplePatternsMixedCase
+required_capability: like_with_list_of_patterns
+FROM employees 
+| WHERE TO_UPPER(first_name) LIKE ("geor*", "WEIYI*", "bErnI*") 
+| KEEP emp_no, first_name 
+| SORT emp_no;
+
+emp_no:integer | first_name:keyword
+10040          | Weiyi
+;
+
+likeListWithUpperNoMatch
+required_capability: like_with_list_of_patterns
+FROM employees 
+| WHERE TO_UPPER(first_name) LIKE ("notaname*") 
+| KEEP emp_no, first_name 
+| SORT emp_no;
+
+emp_no:integer | first_name:keyword
+;
+
+rlikeListWithUpperAllLower
+required_capability: rlike_with_list_of_patterns
+FROM employees 
+| WHERE TO_UPPER(first_name) RLIKE ("geor.*", "wei.*") 
+| KEEP emp_no, first_name 
+| SORT emp_no;
+
+emp_no:integer | first_name:keyword
+;
+
+rlikeListWithUpperAllUpper
+required_capability: rlike_with_list_of_patterns
+FROM employees 
+| WHERE TO_UPPER(first_name) RLIKE ("GEOR.*", "WEI.*")
+| KEEP emp_no, first_name 
+| SORT emp_no;
+
+emp_no:integer | first_name:keyword
+10001          | Georgi
+10040          | Weiyi
+10055          | Georgy
+;
+
+rlikeListWithUpperMixedCase
+required_capability: rlike_with_list_of_patterns
+FROM employees 
+| WHERE TO_UPPER(first_name) RLIKE ("GeOr.*", "wEiY.*", "bErNi")
+| KEEP emp_no, first_name 
+| SORT emp_no;
+
+emp_no:integer | first_name:keyword
+;
+
+rlikeListWithUpperMultiplePatternsMixedCase
+required_capability: rlike_with_list_of_patterns
+FROM employees 
+| WHERE TO_UPPER(first_name) RLIKE ("geor*", "WEIYI.*", "bErnI.*") 
+| KEEP emp_no, first_name 
+| SORT emp_no;
+
+emp_no:integer | first_name:keyword
+10040          | Weiyi
+;
+
+rlikeListWithUpperNoMatch
+required_capability: rlike_with_list_of_patterns
+FROM employees 
+| WHERE TO_UPPER(first_name) RLIKE ("notaname.*") 
+| KEEP emp_no, first_name 
+| SORT emp_no;
+
+emp_no:integer | first_name:keyword
+;
+
 rlikeListEmptyArgWildcard
 required_capability: rlike_with_list_of_patterns
 FROM employees 
@@ -1336,3 +1444,26 @@ ROW x = "abc" | EVAL bool = x RLIKE "#"
 x:keyword | bool:boolean
 abc       | false
 ;
+
+rlikeWithLowerTurnedInsensitiveUnicode#[skip:-8.12.99]
+FROM airport_city_boundaries
+| WHERE TO_UPPER(region) RLIKE ".*Л.*" and abbrev == "FRU"
+| KEEP region
+| LIMIT 1
+;
+
+region:text
+Свердлов району
+;
+
+rlikeListWithLowerTurnedInsensitiveUnicode
+required_capability: rlike_with_list_of_patterns
+FROM airport_city_boundaries
+| WHERE TO_UPPER(region) RLIKE (".*Л.*", ".*NOT EXISTS.*") and abbrev == "FRU"
+| KEEP region
+| LIMIT 1
+;
+
+region:text
+Свердлов району
+;

+ 14 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/regex/RLike.java

@@ -11,6 +11,7 @@ import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
 import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
 import org.elasticsearch.xpack.esql.core.expression.Expression;
+import org.elasticsearch.xpack.esql.core.expression.Literal;
 import org.elasticsearch.xpack.esql.core.expression.predicate.regex.RLikePattern;
 import org.elasticsearch.xpack.esql.core.querydsl.query.Query;
 import org.elasticsearch.xpack.esql.core.querydsl.query.RegexQuery;
@@ -24,6 +25,7 @@ import org.elasticsearch.xpack.esql.optimizer.rules.physical.local.LucenePushdow
 import org.elasticsearch.xpack.esql.planner.TranslatorHandler;
 
 import java.io.IOException;
+import java.util.function.Predicate;
 
 public class RLike extends RegexMatch<RLikePattern> {
     public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(Expression.class, "RLike", RLike::new);
@@ -108,4 +110,16 @@ public class RLike extends RegexMatch<RLikePattern> {
         // TODO: see whether escaping is needed
         return new RegexQuery(source(), handler.nameOf(fa.exactAttribute()), pattern().asJavaRegex(), caseInsensitive());
     }
+
+    /**
+     * Pushes down string casing optimization for a single pattern using the provided predicate.
+     * Returns a new RLike with case insensitivity or a Literal.FALSE if not matched.
+     */
+    @Override
+    public Expression optimizeStringCasingWithInsensitiveRegexMatch(Expression unwrappedField, Predicate<String> matchesCaseFn) {
+        if (matchesCaseFn.test(pattern().pattern()) == false) {
+            return Literal.of(this, Boolean.FALSE);
+        }
+        return new RLike(source(), unwrappedField, pattern(), true);
+    }
 }

+ 20 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/regex/RLikeList.java

@@ -17,6 +17,7 @@ import org.elasticsearch.index.mapper.MappedFieldType;
 import org.elasticsearch.index.query.SearchExecutionContext;
 import org.elasticsearch.xpack.esql.core.expression.Expression;
 import org.elasticsearch.xpack.esql.core.expression.FieldAttribute;
+import org.elasticsearch.xpack.esql.core.expression.Literal;
 import org.elasticsearch.xpack.esql.core.expression.predicate.regex.RLikePattern;
 import org.elasticsearch.xpack.esql.core.expression.predicate.regex.RLikePatternList;
 import org.elasticsearch.xpack.esql.core.querydsl.query.Query;
@@ -29,6 +30,8 @@ import org.elasticsearch.xpack.esql.optimizer.rules.physical.local.LucenePushdow
 import org.elasticsearch.xpack.esql.planner.TranslatorHandler;
 
 import java.io.IOException;
+import java.util.List;
+import java.util.function.Predicate;
 import java.util.function.Supplier;
 import java.util.stream.Collectors;
 
@@ -145,6 +148,23 @@ public class RLikeList extends RegexMatch<RLikePatternList> {
         );
     }
 
+    /**
+     * Pushes down string casing optimization by filtering patterns using the provided predicate.
+     * Returns a new RegexMatch or a Literal.FALSE if none match.
+     */
+    @Override
+    public Expression optimizeStringCasingWithInsensitiveRegexMatch(Expression unwrappedField, Predicate<String> matchesCaseFn) {
+        List<RLikePattern> filtered = pattern().patternList()
+            .stream()
+            .filter(p -> matchesCaseFn.test(p.pattern()))
+            .collect(Collectors.toList());
+        // none of the patterns matches the case of the field, return false
+        if (filtered.isEmpty()) {
+            return Literal.of(this, Boolean.FALSE);
+        }
+        return new RLikeList(source(), unwrappedField, new RLikePatternList(filtered), true);
+    }
+
     @Override
     protected NodeInfo<? extends Expression> info() {
         return NodeInfo.create(this, RLikeList::new, field(), pattern(), caseInsensitive());

+ 14 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/regex/WildcardLike.java

@@ -12,6 +12,7 @@ import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
 import org.elasticsearch.xpack.esql.core.expression.Expression;
 import org.elasticsearch.xpack.esql.core.expression.FieldAttribute;
+import org.elasticsearch.xpack.esql.core.expression.Literal;
 import org.elasticsearch.xpack.esql.core.expression.predicate.regex.WildcardPattern;
 import org.elasticsearch.xpack.esql.core.querydsl.query.Query;
 import org.elasticsearch.xpack.esql.core.querydsl.query.WildcardQuery;
@@ -25,6 +26,7 @@ import org.elasticsearch.xpack.esql.optimizer.rules.physical.local.LucenePushdow
 import org.elasticsearch.xpack.esql.planner.TranslatorHandler;
 
 import java.io.IOException;
+import java.util.function.Predicate;
 
 public class WildcardLike extends RegexMatch<WildcardPattern> {
     public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(
@@ -132,4 +134,16 @@ public class WildcardLike extends RegexMatch<WildcardPattern> {
     private Query translateField(String targetFieldName, boolean forceStringMatch) {
         return new WildcardQuery(source(), targetFieldName, pattern().asLuceneWildcard(), caseInsensitive(), forceStringMatch);
     }
+
+    /**
+     * Pushes down string casing optimization for a single pattern using the provided predicate.
+     * Returns a new WildcardLike with case insensitivity or a Literal.FALSE if not matched.
+     */
+    @Override
+    public Expression optimizeStringCasingWithInsensitiveRegexMatch(Expression unwrappedField, Predicate<String> matchesCaseFn) {
+        if (matchesCaseFn.test(pattern().pattern()) == false) {
+            return Literal.of(this, Boolean.FALSE);
+        }
+        return new WildcardLike(source(), unwrappedField, pattern(), true);
+    }
 }

+ 20 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/regex/WildcardLikeList.java

@@ -18,6 +18,7 @@ import org.elasticsearch.index.mapper.MappedFieldType;
 import org.elasticsearch.index.query.SearchExecutionContext;
 import org.elasticsearch.xpack.esql.core.expression.Expression;
 import org.elasticsearch.xpack.esql.core.expression.FieldAttribute;
+import org.elasticsearch.xpack.esql.core.expression.Literal;
 import org.elasticsearch.xpack.esql.core.expression.predicate.regex.WildcardPattern;
 import org.elasticsearch.xpack.esql.core.expression.predicate.regex.WildcardPatternList;
 import org.elasticsearch.xpack.esql.core.querydsl.query.Query;
@@ -31,6 +32,8 @@ import org.elasticsearch.xpack.esql.optimizer.rules.physical.local.LucenePushdow
 import org.elasticsearch.xpack.esql.planner.TranslatorHandler;
 
 import java.io.IOException;
+import java.util.List;
+import java.util.function.Predicate;
 import java.util.function.Supplier;
 import java.util.stream.Collectors;
 
@@ -177,4 +180,21 @@ public class WildcardLikeList extends RegexMatch<WildcardPatternList> {
     private Query translateField(String targetFieldName) {
         return new ExpressionQuery(source(), targetFieldName, this);
     }
+
+    /**
+     * Pushes down string casing optimization by filtering patterns using the provided predicate.
+     * Returns a new RegexMatch or a Literal.FALSE if none match.
+     */
+    @Override
+    public Expression optimizeStringCasingWithInsensitiveRegexMatch(Expression unwrappedField, Predicate<String> matchesCaseFn) {
+        List<WildcardPattern> filtered = pattern().patternList()
+            .stream()
+            .filter(p -> matchesCaseFn.test(p.pattern()))
+            .collect(Collectors.toList());
+        // none of the patterns matches the case of the field, return false
+        if (filtered.isEmpty()) {
+            return Literal.of(this, Boolean.FALSE);
+        }
+        return new WildcardLikeList(source(), unwrappedField, new WildcardPatternList(filtered), true);
+    }
 }

+ 6 - 26
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/ReplaceStringCasingWithInsensitiveRegexMatch.java

@@ -8,16 +8,13 @@
 package org.elasticsearch.xpack.esql.optimizer.rules.logical;
 
 import org.elasticsearch.xpack.esql.core.expression.Expression;
-import org.elasticsearch.xpack.esql.core.expression.Literal;
-import org.elasticsearch.xpack.esql.core.expression.predicate.regex.RLikePatternList;
 import org.elasticsearch.xpack.esql.core.expression.predicate.regex.RegexMatch;
 import org.elasticsearch.xpack.esql.core.expression.predicate.regex.StringPattern;
-import org.elasticsearch.xpack.esql.core.expression.predicate.regex.WildcardPatternList;
 import org.elasticsearch.xpack.esql.expression.function.scalar.string.ChangeCase;
-import org.elasticsearch.xpack.esql.expression.function.scalar.string.regex.RLike;
-import org.elasticsearch.xpack.esql.expression.function.scalar.string.regex.WildcardLike;
 import org.elasticsearch.xpack.esql.optimizer.LogicalOptimizerContext;
 
+import java.util.function.Predicate;
+
 import static org.elasticsearch.xpack.esql.optimizer.rules.logical.ReplaceStringCasingWithInsensitiveEquals.unwrapCase;
 
 public class ReplaceStringCasingWithInsensitiveRegexMatch extends OptimizerRules.OptimizerExpressionRule<
@@ -29,29 +26,12 @@ public class ReplaceStringCasingWithInsensitiveRegexMatch extends OptimizerRules
 
     @Override
     protected Expression rule(RegexMatch<? extends StringPattern> regexMatch, LogicalOptimizerContext unused) {
-        Expression e = regexMatch;
-        if (regexMatch.pattern() instanceof WildcardPatternList || regexMatch.pattern() instanceof RLikePatternList) {
-            // This optimization is not supported for WildcardPatternList and RLikePatternList for now
-            return e;
-        }
         if (regexMatch.field() instanceof ChangeCase changeCase) {
-            var pattern = regexMatch.pattern().pattern();
-            e = changeCase.caseType().matchesCase(pattern) ? insensitiveRegexMatch(regexMatch) : Literal.of(regexMatch, Boolean.FALSE);
+            Predicate<String> matchesCase = changeCase.caseType()::matchesCase;
+            Expression unwrappedField = unwrapCase(regexMatch.field());
+            return regexMatch.optimizeStringCasingWithInsensitiveRegexMatch(unwrappedField, matchesCase);
         }
-        return e;
-    }
-
-    private static Expression insensitiveRegexMatch(RegexMatch<? extends StringPattern> regexMatch) {
-        return switch (regexMatch) {
-            case RLike rlike -> new RLike(rlike.source(), unwrapCase(rlike.field()), rlike.pattern(), true);
-            case WildcardLike wildcardLike -> new WildcardLike(
-                wildcardLike.source(),
-                unwrapCase(wildcardLike.field()),
-                wildcardLike.pattern(),
-                true
-            );
-            default -> regexMatch;
-        };
+        return regexMatch;
     }
 
 }

+ 104 - 0
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalLogicalPlanOptimizerTests.java

@@ -39,7 +39,9 @@ import org.elasticsearch.xpack.esql.expression.function.scalar.conditional.Case;
 import org.elasticsearch.xpack.esql.expression.function.scalar.nulls.Coalesce;
 import org.elasticsearch.xpack.esql.expression.function.scalar.string.StartsWith;
 import org.elasticsearch.xpack.esql.expression.function.scalar.string.regex.RLike;
+import org.elasticsearch.xpack.esql.expression.function.scalar.string.regex.RLikeList;
 import org.elasticsearch.xpack.esql.expression.function.scalar.string.regex.WildcardLike;
+import org.elasticsearch.xpack.esql.expression.function.scalar.string.regex.WildcardLikeList;
 import org.elasticsearch.xpack.esql.expression.predicate.logical.And;
 import org.elasticsearch.xpack.esql.expression.predicate.nulls.IsNotNull;
 import org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic.Add;
@@ -673,6 +675,26 @@ public class LocalLogicalPlanOptimizerTests extends ESTestCase {
         var source = as(filter.child(), EsRelation.class);
     }
 
+    /*
+     *Limit[1000[INTEGER],false]
+     * \_Filter[RLikeList(first_name{f}#4, "("VALÜ*", "TEST*")", true)]
+     *  \_EsRelation[test][_meta_field{f}#9, emp_no{f}#3, first_name{f}#4, gen..]
+     */
+    public void testReplaceUpperStringCasinqWithInsensitiveRLikeList() {
+        var plan = localPlan("FROM test | WHERE TO_UPPER(TO_LOWER(TO_UPPER(first_name))) RLIKE (\"VALÜ*\", \"TEST*\")");
+
+        var limit = as(plan, Limit.class);
+        var filter = as(limit.child(), Filter.class);
+        var rLikeList = as(filter.condition(), RLikeList.class);
+        var field = as(rLikeList.field(), FieldAttribute.class);
+        assertThat(field.fieldName().string(), is("first_name"));
+        assertEquals(2, rLikeList.pattern().patternList().size());
+        assertThat(rLikeList.pattern().patternList().get(0).pattern(), is("VALÜ*"));
+        assertThat(rLikeList.pattern().patternList().get(1).pattern(), is("TEST*"));
+        assertThat(rLikeList.caseInsensitive(), is(true));
+        var source = as(filter.child(), EsRelation.class);
+    }
+
     // same plan as above, but lower case pattern
     public void testReplaceLowerStringCasingWithInsensitiveRLike() {
         var plan = localPlan("FROM test | WHERE TO_LOWER(TO_UPPER(first_name)) RLIKE \"valü*\"");
@@ -687,6 +709,35 @@ public class LocalLogicalPlanOptimizerTests extends ESTestCase {
         var source = as(filter.child(), EsRelation.class);
     }
 
+    // same plan as above, but lower case pattern and list of patterns
+    public void testReplaceLowerStringCasingWithInsensitiveRLikeList() {
+        var plan = localPlan("FROM test | WHERE TO_LOWER(TO_UPPER(first_name)) RLIKE (\"valü*\", \"test*\")");
+        var limit = as(plan, Limit.class);
+        var filter = as(limit.child(), Filter.class);
+        var rLikeList = as(filter.condition(), RLikeList.class);
+        var field = as(rLikeList.field(), FieldAttribute.class);
+        assertThat(field.fieldName().string(), is("first_name"));
+        assertEquals(2, rLikeList.pattern().patternList().size());
+        assertThat(rLikeList.pattern().patternList().get(0).pattern(), is("valü*"));
+        assertThat(rLikeList.pattern().patternList().get(1).pattern(), is("test*"));
+        assertThat(rLikeList.caseInsensitive(), is(true));
+        var source = as(filter.child(), EsRelation.class);
+    }
+
+    // same plan as above, but lower case pattern and list of patterns, one of which is upper case
+    public void testReplaceLowerStringCasingWithMixedCaseRLikeList() {
+        var plan = localPlan("FROM test | WHERE TO_LOWER(TO_UPPER(first_name)) RLIKE (\"valü*\", \"TEST*\")");
+        var limit = as(plan, Limit.class);
+        var filter = as(limit.child(), Filter.class);
+        var rLikeList = as(filter.condition(), RLikeList.class);
+        var field = as(rLikeList.field(), FieldAttribute.class);
+        assertThat(field.fieldName().string(), is("first_name"));
+        assertEquals(1, rLikeList.pattern().patternList().size());
+        assertThat(rLikeList.pattern().patternList().get(0).pattern(), is("valü*"));
+        assertThat(rLikeList.caseInsensitive(), is(true));
+        var source = as(filter.child(), EsRelation.class);
+    }
+
     /**
      * LocalRelation[[_meta_field{f}#9, emp_no{f}#3, first_name{f}#4, gender{f}#5, hire_date{f}#10, job{f}#11, job.raw{f}#12, langu
      *   ages{f}#6, last_name{f}#7, long_noidx{f}#13, salary{f}#8],EMPTY]
@@ -712,6 +763,37 @@ public class LocalLogicalPlanOptimizerTests extends ESTestCase {
         var source = as(filter.child(), EsRelation.class);
     }
 
+    // same plan as in testReplaceUpperStringCasingWithInsensitiveRLikeList, but with LIKE instead of RLIKE
+    public void testReplaceUpperStringCasingWithInsensitiveLikeList() {
+        var plan = localPlan("FROM test | WHERE TO_UPPER(TO_LOWER(TO_UPPER(first_name))) LIKE (\"VALÜ*\", \"TEST*\")");
+
+        var limit = as(plan, Limit.class);
+        var filter = as(limit.child(), Filter.class);
+        var likeList = as(filter.condition(), WildcardLikeList.class);
+        var field = as(likeList.field(), FieldAttribute.class);
+        assertThat(field.fieldName().string(), is("first_name"));
+        assertEquals(2, likeList.pattern().patternList().size());
+        assertThat(likeList.pattern().patternList().get(0).pattern(), is("VALÜ*"));
+        assertThat(likeList.pattern().patternList().get(1).pattern(), is("TEST*"));
+        assertThat(likeList.caseInsensitive(), is(true));
+        var source = as(filter.child(), EsRelation.class);
+    }
+
+    // same plan as above, but mixed case pattern and list of patterns
+    public void testReplaceLowerStringCasingWithMixedCaseLikeList() {
+        var plan = localPlan("FROM test | WHERE TO_LOWER(TO_UPPER(first_name)) LIKE (\"TEST*\", \"valü*\", \"vaLü*\")");
+        var limit = as(plan, Limit.class);
+        var filter = as(limit.child(), Filter.class);
+        var likeList = as(filter.condition(), WildcardLikeList.class);
+        var field = as(likeList.field(), FieldAttribute.class);
+        assertThat(field.fieldName().string(), is("first_name"));
+        // only the all lowercase pattern is kept, the mixed case and all uppercase patterns are ignored
+        assertEquals(1, likeList.pattern().patternList().size());
+        assertThat(likeList.pattern().patternList().get(0).pattern(), is("valü*"));
+        assertThat(likeList.caseInsensitive(), is(true));
+        var source = as(filter.child(), EsRelation.class);
+    }
+
     // same plan as above, but lower case pattern
     public void testReplaceLowerStringCasingWithInsensitiveLike() {
         var plan = localPlan("FROM test | WHERE TO_LOWER(TO_UPPER(first_name)) LIKE \"valü*\"");
@@ -737,6 +819,28 @@ public class LocalLogicalPlanOptimizerTests extends ESTestCase {
         assertThat(local.supplier(), equalTo(EmptyLocalSupplier.EMPTY));
     }
 
+    /**
+     * LocalRelation[[_meta_field{f}#9, emp_no{f}#3, first_name{f}#4, gender{f}#5, hire_date{f}#10, job{f}#11, job.raw{f}#12, langu
+     *   ages{f}#6, last_name{f}#7, long_noidx{f}#13, salary{f}#8],EMPTY]
+     */
+    public void testReplaceStringCasingAndLikeListWithLocalRelation() {
+        var plan = localPlan("FROM test | WHERE TO_LOWER(TO_UPPER(first_name)) LIKE (\"VALÜ*\", \"TEST*\")");
+
+        var local = as(plan, LocalRelation.class);
+        assertThat(local.supplier(), equalTo(EmptyLocalSupplier.EMPTY));
+    }
+
+    /**
+     * LocalRelation[[_meta_field{f}#9, emp_no{f}#3, first_name{f}#4, gender{f}#5, hire_date{f}#10, job{f}#11, job.raw{f}#12, langu
+     *   ages{f}#6, last_name{f}#7, long_noidx{f}#13, salary{f}#8],EMPTY]
+     */
+    public void testReplaceStringCasingAndRLikeListWithLocalRelation() {
+        var plan = localPlan("FROM test | WHERE TO_LOWER(TO_UPPER(first_name)) RLIKE (\"VALÜ*\", \"TEST*\")");
+
+        var local = as(plan, LocalRelation.class);
+        assertThat(local.supplier(), equalTo(EmptyLocalSupplier.EMPTY));
+    }
+
     /**
      * Limit[1000[INTEGER],false]
      * \_Aggregate[[],[SUM($$integer_long_field$converted_to$long{f$}#5,true[BOOLEAN]) AS sum(integer_long_field::long)#3]]