Skip to content

Commit 972e248

Browse files
committed
SQL: Fix issue with LIKE/RLIKE as painless script (#53495)
Add missing asScript() implementation for LIKE/RLIKE expressions. When LIKE/RLIKE are used for example in GROUP BY or are wrapped with scalar functions in a WHERE clause, the translation must produce a painless script which will be executed to implement the correct behaviour and previously this was completely missing, and as a consquence wrong results were silently (no error) returned. Fixes: #53486 (cherry picked from commit eaa8ead)
1 parent 9cf063e commit 972e248

File tree

10 files changed

+93
-43
lines changed

10 files changed

+93
-43
lines changed

x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/predicate/regex/Like.java

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66
package org.elasticsearch.xpack.sql.expression.predicate.regex;
77

88
import org.elasticsearch.xpack.sql.expression.Expression;
9-
import org.elasticsearch.xpack.sql.expression.gen.processor.Processor;
10-
import org.elasticsearch.xpack.sql.expression.predicate.regex.RegexProcessor.RegexOperation;
119
import org.elasticsearch.xpack.sql.tree.NodeInfo;
1210
import org.elasticsearch.xpack.sql.tree.Source;
1311

@@ -26,15 +24,4 @@ protected NodeInfo<Like> info() {
2624
protected Like replaceChild(Expression newLeft) {
2725
return new Like(source(), newLeft, pattern());
2826
}
29-
30-
@Override
31-
public Boolean fold() {
32-
Object val = field().fold();
33-
return RegexOperation.match(val, pattern().asJavaRegex());
34-
}
35-
36-
@Override
37-
protected Processor makeProcessor() {
38-
return new RegexProcessor(pattern().asJavaRegex());
39-
}
4027
}

x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/predicate/regex/LikePattern.java

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
*
1818
* To prevent conflicts with ES, the string and char must be validated to not contain '*'.
1919
*/
20-
public class LikePattern {
20+
public class LikePattern implements StringPattern {
2121

2222
private final String pattern;
2323
private final char escape;
@@ -43,9 +43,7 @@ public char escape() {
4343
return escape;
4444
}
4545

46-
/**
47-
* Returns the pattern in (Java) regex format.
48-
*/
46+
@Override
4947
public String asJavaRegex() {
5048
return regex;
5149
}
@@ -83,4 +81,4 @@ public boolean equals(Object obj) {
8381
return Objects.equals(pattern, other.pattern)
8482
&& escape == other.escape;
8583
}
86-
}
84+
}

x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/predicate/regex/RLike.java

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,12 @@
66
package org.elasticsearch.xpack.sql.expression.predicate.regex;
77

88
import org.elasticsearch.xpack.sql.expression.Expression;
9-
import org.elasticsearch.xpack.sql.expression.gen.processor.Processor;
10-
import org.elasticsearch.xpack.sql.expression.predicate.regex.RegexProcessor.RegexOperation;
11-
import org.elasticsearch.xpack.sql.tree.Source;
129
import org.elasticsearch.xpack.sql.tree.NodeInfo;
10+
import org.elasticsearch.xpack.sql.tree.Source;
1311

14-
public class RLike extends RegexMatch<String> {
12+
public class RLike extends RegexMatch<RLikePattern> {
1513

16-
public RLike(Source source, Expression value, String pattern) {
14+
public RLike(Source source, Expression value, RLikePattern pattern) {
1715
super(source, value, pattern);
1816
}
1917

@@ -26,15 +24,4 @@ protected NodeInfo<RLike> info() {
2624
protected RLike replaceChild(Expression newChild) {
2725
return new RLike(source(), newChild, pattern());
2826
}
29-
30-
@Override
31-
public Boolean fold() {
32-
Object val = field().fold();
33-
return RegexOperation.match(val, pattern());
34-
}
35-
36-
@Override
37-
protected Processor makeProcessor() {
38-
return new RegexProcessor(pattern());
39-
}
4027
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
package org.elasticsearch.xpack.sql.expression.predicate.regex;
7+
8+
public class RLikePattern implements StringPattern {
9+
10+
private final String regexpPattern;
11+
12+
public RLikePattern(String regexpPattern) {
13+
this.regexpPattern = regexpPattern;
14+
}
15+
16+
@Override
17+
public String asJavaRegex() {
18+
return regexpPattern;
19+
}
20+
}

x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/predicate/regex/RegexMatch.java

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,26 @@
1010
import org.elasticsearch.xpack.sql.expression.Expressions;
1111
import org.elasticsearch.xpack.sql.expression.Nullability;
1212
import org.elasticsearch.xpack.sql.expression.function.scalar.UnaryScalarFunction;
13+
import org.elasticsearch.xpack.sql.expression.gen.processor.Processor;
14+
import org.elasticsearch.xpack.sql.expression.gen.script.ScriptTemplate;
1315
import org.elasticsearch.xpack.sql.tree.Source;
1416
import org.elasticsearch.xpack.sql.type.DataType;
1517

1618
import java.util.Objects;
1719

20+
import static org.elasticsearch.common.logging.LoggerMessageFormat.format;
1821
import static org.elasticsearch.xpack.sql.expression.TypeResolutions.isStringAndExact;
22+
import static org.elasticsearch.xpack.sql.expression.gen.script.ParamsBuilder.paramsBuilder;
1923

20-
public abstract class RegexMatch<T> extends UnaryScalarFunction {
24+
public abstract class RegexMatch<T extends StringPattern> extends UnaryScalarFunction {
2125

2226
private final T pattern;
23-
27+
2428
protected RegexMatch(Source source, Expression value, T pattern) {
2529
super(source, value);
2630
this.pattern = pattern;
2731
}
28-
32+
2933
public T pattern() {
3034
return pattern;
3135
}
@@ -53,8 +57,30 @@ public boolean foldable() {
5357
// right() is not directly foldable in any context but Like can fold it.
5458
return field().foldable();
5559
}
56-
60+
61+
@Override
62+
public Boolean fold() {
63+
Object val = field().fold();
64+
return RegexProcessor.RegexOperation.match(val, pattern().asJavaRegex());
65+
}
66+
5767
@Override
68+
protected Processor makeProcessor() {
69+
return new RegexProcessor(pattern().asJavaRegex());
70+
}
71+
72+
@Override
73+
public ScriptTemplate asScript() {
74+
ScriptTemplate fieldAsScript = asScript(field());
75+
return new ScriptTemplate(
76+
formatTemplate(format("{sql}.", "regex({},{})", fieldAsScript.template())),
77+
paramsBuilder()
78+
.script(fieldAsScript.params())
79+
.variable(pattern.asJavaRegex())
80+
.build(),
81+
dataType());
82+
}
83+
5884
public boolean equals(Object obj) {
5985
return super.equals(obj) && Objects.equals(((RegexMatch<?>) obj).pattern(), pattern());
6086
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
package org.elasticsearch.xpack.sql.expression.predicate.regex;
7+
8+
interface StringPattern {
9+
/**
10+
* Returns the pattern in (Java) regex format.
11+
*/
12+
String asJavaRegex();
13+
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
import org.elasticsearch.xpack.sql.expression.predicate.regex.Like;
6161
import org.elasticsearch.xpack.sql.expression.predicate.regex.LikePattern;
6262
import org.elasticsearch.xpack.sql.expression.predicate.regex.RLike;
63+
import org.elasticsearch.xpack.sql.expression.predicate.regex.RLikePattern;
6364
import org.elasticsearch.xpack.sql.parser.SqlBaseParser.ArithmeticBinaryContext;
6465
import org.elasticsearch.xpack.sql.parser.SqlBaseParser.ArithmeticUnaryContext;
6566
import org.elasticsearch.xpack.sql.parser.SqlBaseParser.BooleanLiteralContext;
@@ -234,7 +235,7 @@ public Expression visitPredicated(PredicatedContext ctx) {
234235
e = new Like(source, exp, visitPattern(pCtx.pattern()));
235236
break;
236237
case SqlBaseParser.RLIKE:
237-
e = new RLike(source, exp, string(pCtx.regex));
238+
e = new RLike(source, exp, new RLikePattern(string(pCtx.regex)));
238239
break;
239240
case SqlBaseParser.NULL:
240241
// shortcut to avoid double negation later on (since there's no IsNull (missing in ES is a negated exists))

x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/planner/QueryTranslator.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ protected QueryTranslation asQuery(RegexMatch e, boolean onAggs) {
341341
}
342342

343343
if (e instanceof RLike) {
344-
String pattern = ((RLike) e).pattern();
344+
String pattern = ((RLike) e).pattern().asJavaRegex();
345345
q = new RegexQuery(e.source(), targetFieldName, pattern);
346346
}
347347

@@ -864,4 +864,4 @@ protected static Query wrapIfNested(Query query, Expression exp) {
864864
return query;
865865
}
866866
}
867-
}
867+
}

x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/optimizer/OptimizerTests.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@
9393
import org.elasticsearch.xpack.sql.expression.predicate.regex.Like;
9494
import org.elasticsearch.xpack.sql.expression.predicate.regex.LikePattern;
9595
import org.elasticsearch.xpack.sql.expression.predicate.regex.RLike;
96+
import org.elasticsearch.xpack.sql.expression.predicate.regex.RLikePattern;
9697
import org.elasticsearch.xpack.sql.optimizer.Optimizer.BinaryComparisonSimplification;
9798
import org.elasticsearch.xpack.sql.optimizer.Optimizer.BooleanLiteralsOnTheRight;
9899
import org.elasticsearch.xpack.sql.optimizer.Optimizer.BooleanSimplification;
@@ -361,7 +362,7 @@ public void testConstantFoldingLikes() {
361362
new ConstantFolding().rule(new Like(EMPTY, of(EMPTY, "test_emp"), new LikePattern("test%", (char) 0)))
362363
.canonical());
363364
assertEquals(TRUE,
364-
new ConstantFolding().rule(new RLike(EMPTY, of(EMPTY, "test_emp"), "test.emp")).canonical());
365+
new ConstantFolding().rule(new RLike(EMPTY, of(EMPTY, "test_emp"), new RLikePattern("test.emp"))).canonical());
365366
}
366367

367368
public void testConstantFoldingDatetime() {
@@ -495,7 +496,7 @@ public void testGenericNullableExpression() {
495496
// comparison
496497
assertNullLiteral(rule.rule(new GreaterThan(EMPTY, getFieldAttribute(), NULL)));
497498
// regex
498-
assertNullLiteral(rule.rule(new RLike(EMPTY, NULL, "123")));
499+
assertNullLiteral(rule.rule(new RLike(EMPTY, NULL, new RLikePattern("123"))));
499500
}
500501

501502
public void testNullFoldingOnCast() {

x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/planner/QueryTranslatorTests.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,23 @@ private void assertDifferentRLikeAndNotRLikePatterns(String firstPattern, String
540540
assertEquals("keyword", rqsq.field());
541541
}
542542

543+
public void testLikeRLikeAsPainlessScripts() {
544+
LogicalPlan p = plan("SELECT count(*), CASE WHEN keyword LIKE '%foo%' THEN 1 WHEN keyword RLIKE '.*bar.*' THEN 2 " +
545+
"ELSE 3 END AS t FROM test GROUP BY t");
546+
assertTrue(p instanceof Aggregate);
547+
Expression condition = ((Aggregate) p).groupings().get(0);
548+
assertFalse(condition.foldable());
549+
GroupingContext groupingContext = QueryFolder.FoldAggregate.groupBy(((Aggregate) p).groupings());
550+
assertNotNull(groupingContext);
551+
ScriptTemplate scriptTemplate = groupingContext.tail.script();
552+
assertEquals("InternalSqlScriptUtils.caseFunction([InternalSqlScriptUtils.regex(InternalSqlScriptUtils.docValue(" +
553+
"doc,params.v0),params.v1),params.v2,InternalSqlScriptUtils.regex(InternalSqlScriptUtils.docValue(" +
554+
"doc,params.v3),params.v4),params.v5,params.v6])",
555+
scriptTemplate.toString());
556+
assertEquals("[{v=keyword}, {v=^.*foo.*$}, {v=1}, {v=keyword}, {v=.*bar.*}, {v=2}, {v=3}]",
557+
scriptTemplate.params().toString());
558+
}
559+
543560
public void testTranslateNotExpression_WhereClause_Painless() {
544561
LogicalPlan p = plan("SELECT * FROM test WHERE NOT(POSITION('x', keyword) = 0)");
545562
assertTrue(p instanceof Project);

0 commit comments

Comments
 (0)