Browse Source

SQL: Escaped wildcard (*) not accepted in LIKE (#63428)

For a query like `SELECT name FROM test WHERE name LIKE ''%c*'` ES SQL
generates an error. `*` is not a special character in a `LIKE` construct
and it's expected to not needing to be escaped, so the previous query
should work as is.
In the LIKE pattern any `*` character was treated as invalid character
and the usage of `%` or `_` was suggested instead. But `*` is a valid,
acceptable non-wildcard on the right side of the `LIKE` operator.

Fix: #55108
Andras Palinkas 5 years ago
parent
commit
190d9fe3de

+ 3 - 2
docs/reference/sql/functions/like-rlike.asciidoc

@@ -33,8 +33,9 @@ with the `LIKE` operator:
 * The percent sign (%)
 * The underscore (_)
 
-The percent sign represents zero, one or multiple characters. The underscore represents a single number or character. These symbols can be
-used in combinations.
+The percent sign represents zero, one or multiple characters. The underscore represents a single number or character. These symbols can be used in combinations.
+
+NOTE: No other characters have special meaning or act as wildcard. Characters often used as wildcards in other languages (`*` or `?`) are treated as normal characters.
 
 [source, sql]
 ----

+ 3 - 9
x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/parser/ExpressionBuilder.java

@@ -270,12 +270,6 @@ abstract class ExpressionBuilder extends IdentifierBuilder {
         if (pattern == null) {
             throw new ParsingException(source(ctx.value), "Pattern must not be [null]");
         }
-        int pos = pattern.indexOf('*');
-        if (pos >= 0) {
-            throw new ParsingException(source(ctx.value),
-                    "Invalid char [*] found in pattern [{}] at position {}; use [%] or [_] instead",
-                    pattern, pos);
-        }
 
         char escape = 0;
         PatternEscapeContext escapeCtx = ctx.patternEscape();
@@ -288,7 +282,7 @@ abstract class ExpressionBuilder extends IdentifierBuilder {
             } else if (escapeString.length() == 1) {
                 escape = escapeString.charAt(0);
                 // these chars already have a meaning
-                if (escape == '*' || escape == '%' || escape == '_') {
+                if (escape == '%' || escape == '_') {
                     throw new ParsingException(source(escapeCtx.escape), "Char [{}] cannot be used for escaping", escape);
                 }
                 // lastly validate that escape chars (if present) are followed by special chars
@@ -303,8 +297,8 @@ abstract class ExpressionBuilder extends IdentifierBuilder {
                         char next = pattern.charAt(i + 1);
                         if (next != '%' && next != '_') {
                             throw new ParsingException(source(ctx.value),
-                                    "Pattern [{}] is invalid as escape char [{}] at position {} can only escape wildcard chars; found [{}]",
-                                    pattern, escape, i, next);
+                                    "Pattern [{}] is invalid as escape char [{}] at position {} can only escape "
+                                    + "wildcard chars [%_]; found [{}]", pattern, escape, i, next);
                         }
                     }
                 }

+ 36 - 10
x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/parser/LikeEscapingParsingTests.java

@@ -21,6 +21,12 @@ public class LikeEscapingParsingTests extends ESTestCase {
 
     private final SqlParser parser = new SqlParser();
 
+    private static LikePattern patternOfLike(Expression exp) {
+        assertThat(exp, instanceOf(Like.class));
+        Like l = (Like) exp;
+        return l.pattern();
+    }
+
     private String error(String pattern) {
         ParsingException ex = expectThrows(ParsingException.class,
                 () -> parser.createExpression(format(null, "exp LIKE {}", pattern)));
@@ -36,9 +42,11 @@ public class LikeEscapingParsingTests extends ESTestCase {
         } else {
             exp = parser.createExpression(format(null, "exp LIKE '{}'", pattern));
         }
-        assertThat(exp, instanceOf(Like.class));
-        Like l = (Like) exp;
-        return l.pattern();
+        return patternOfLike(exp);
+    }
+
+    private LikePattern like(String pattern, Character escapeChar) {
+        return patternOfLike(parser.createExpression(format(null, "exp LIKE '{}' ESCAPE '{}'", pattern, escapeChar)));
     }
 
     public void testNoEscaping() {
@@ -55,16 +63,34 @@ public class LikeEscapingParsingTests extends ESTestCase {
 
     public void testEscapingWrongChar() {
         assertThat(error("'|string' ESCAPE '|'"),
-                is("line 1:11: Pattern [|string] is invalid as escape char [|] at position 0 can only escape wildcard chars; found [s]"));
+                is("line 1:11: Pattern [|string] is invalid as escape char [|] at position 0 can only escape "
+                   + "wildcard chars [%_]; found [s]"));
+    }
+
+    public void testEscapingTheEscapeCharacter() {
+        assertThat(error("'||string' ESCAPE '|'"),
+            is("line 1:11: Pattern [||string] is invalid as escape char [|] at position 0 can only escape wildcard chars [%_]; found [|]"));
     }
 
-    public void testInvalidChar() {
-        assertThat(error("'%string' ESCAPE '%'"),
-                is("line 1:28: Char [%] cannot be used for escaping"));
+    public void testEscapingWildcards() {
+        assertThat(error("'string' ESCAPE '%'"),
+                is("line 1:27: Char [%] cannot be used for escaping"));
+        assertThat(error("'string' ESCAPE '_'"),
+            is("line 1:27: Char [_] cannot be used for escaping"));
     }
 
-    public void testCannotUseStar() {
-        assertThat(error("'|*string' ESCAPE '|'"),
-                is("line 1:11: Invalid char [*] found in pattern [|*string] at position 1; use [%] or [_] instead"));
+    public void testCanUseStarWithoutEscaping() {
+        LikePattern like = like("%string*");
+        assertThat(like.pattern(), is("%string*"));
+        assertThat(like.asJavaRegex(), is("^.*string\\*$"));
+        assertThat(like.asLuceneWildcard(), is("*string\\*"));
     }
+
+    public void testEscapingWithStar() {
+        LikePattern like = like("*%%*__string", '*');
+        assertThat(like.pattern(), is("*%%*__string"));
+        assertThat(like.asJavaRegex(), is("^%.*_.string$"));
+        assertThat(like.asLuceneWildcard(), is("%*_?string"));
+    }
+
 }

+ 5 - 1
x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/util/LikeConversionTests.java

@@ -81,6 +81,10 @@ public class LikeConversionTests extends ESTestCase {
         assertEquals("foo\\*bar*", wildcard("foo*bar%"));
     }
 
+    public void testStarLiteralWithWildcards() {
+        assertEquals("\\**\\*?foo\\*\\*?*", wildcard("*%*_foo**_%"));
+    }
+
     public void testWildcardEscapedWildcard() {
         assertEquals("foo\\*bar%", wildcard("foo*bar|%"));
     }
@@ -129,4 +133,4 @@ public class LikeConversionTests extends ESTestCase {
         assertEquals("foo|_bar|", unescape("foo|||_bar||"));
     }
 
-}
+}