Skip to content

Commit eaa8ead

Browse files
authored
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
1 parent 01eee1a commit eaa8ead

File tree

11 files changed

+92
-41
lines changed

11 files changed

+92
-41
lines changed

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

-13
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66
package org.elasticsearch.xpack.ql.expression.predicate.regex;
77

88
import org.elasticsearch.xpack.ql.expression.Expression;
9-
import org.elasticsearch.xpack.ql.expression.gen.processor.Processor;
10-
import org.elasticsearch.xpack.ql.expression.predicate.regex.RegexProcessor.RegexOperation;
119
import org.elasticsearch.xpack.ql.tree.NodeInfo;
1210
import org.elasticsearch.xpack.ql.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/ql/src/main/java/org/elasticsearch/xpack/ql/expression/predicate/regex/LikePattern.java

+3-5
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/ql/src/main/java/org/elasticsearch/xpack/ql/expression/predicate/regex/RLike.java

+2-15
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,12 @@
66
package org.elasticsearch.xpack.ql.expression.predicate.regex;
77

88
import org.elasticsearch.xpack.ql.expression.Expression;
9-
import org.elasticsearch.xpack.ql.expression.gen.processor.Processor;
10-
import org.elasticsearch.xpack.ql.expression.predicate.regex.RegexProcessor.RegexOperation;
119
import org.elasticsearch.xpack.ql.tree.NodeInfo;
1210
import org.elasticsearch.xpack.ql.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
}
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.ql.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/ql/src/main/java/org/elasticsearch/xpack/ql/expression/predicate/regex/RegexMatch.java

+28-2
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,19 @@
1010
import org.elasticsearch.xpack.ql.expression.Expressions;
1111
import org.elasticsearch.xpack.ql.expression.Nullability;
1212
import org.elasticsearch.xpack.ql.expression.function.scalar.UnaryScalarFunction;
13+
import org.elasticsearch.xpack.ql.expression.gen.processor.Processor;
14+
import org.elasticsearch.xpack.ql.expression.gen.script.ScriptTemplate;
1315
import org.elasticsearch.xpack.ql.tree.Source;
1416
import org.elasticsearch.xpack.ql.type.DataType;
1517
import org.elasticsearch.xpack.ql.type.DataTypes;
1618

1719
import java.util.Objects;
1820

21+
import static org.elasticsearch.common.logging.LoggerMessageFormat.format;
1922
import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isStringAndExact;
23+
import static org.elasticsearch.xpack.ql.expression.gen.script.ParamsBuilder.paramsBuilder;
2024

21-
public abstract class RegexMatch<T> extends UnaryScalarFunction {
25+
public abstract class RegexMatch<T extends StringPattern> extends UnaryScalarFunction {
2226

2327
private final T pattern;
2428

@@ -54,8 +58,30 @@ public boolean foldable() {
5458
// right() is not directly foldable in any context but Like can fold it.
5559
return field().foldable();
5660
}
57-
61+
62+
@Override
63+
public Boolean fold() {
64+
Object val = field().fold();
65+
return RegexProcessor.RegexOperation.match(val, pattern().asJavaRegex());
66+
}
67+
5868
@Override
69+
protected Processor makeProcessor() {
70+
return new RegexProcessor(pattern().asJavaRegex());
71+
}
72+
73+
@Override
74+
public ScriptTemplate asScript() {
75+
ScriptTemplate fieldAsScript = asScript(field());
76+
return new ScriptTemplate(
77+
formatTemplate(format("{sql}.", "regex({},{})", fieldAsScript.template())),
78+
paramsBuilder()
79+
.script(fieldAsScript.params())
80+
.variable(pattern.asJavaRegex())
81+
.build(),
82+
dataType());
83+
}
84+
5985
public boolean equals(Object obj) {
6086
return super.equals(obj) && Objects.equals(((RegexMatch<?>) obj).pattern(), pattern());
6187
}
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.ql.expression.predicate.regex;
7+
8+
interface StringPattern {
9+
/**
10+
* Returns the pattern in (Java) regex format.
11+
*/
12+
String asJavaRegex();
13+
}

x-pack/plugin/ql/src/main/java/org/elasticsearch/xpack/ql/planner/ExpressionTranslators.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ public static Query doTranslate(RegexMatch e, TranslatorHandler handler) {
111111
}
112112

113113
if (e instanceof RLike) {
114-
String pattern = ((RLike) e).pattern();
114+
String pattern = ((RLike) e).pattern().asJavaRegex();
115115
q = new RegexQuery(e.source(), targetFieldName, pattern);
116116
}
117117

@@ -351,4 +351,4 @@ private static Query boolQuery(Source source, Query left, Query right, boolean i
351351
}
352352
return new BoolQuery(source, isAnd, left, right);
353353
}
354-
}
354+
}

x-pack/plugin/ql/src/test/java/org/elasticsearch/xpack/ql/optimizer/OptimizerRulesTests.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.elasticsearch.xpack.ql.expression.predicate.regex.Like;
3434
import org.elasticsearch.xpack.ql.expression.predicate.regex.LikePattern;
3535
import org.elasticsearch.xpack.ql.expression.predicate.regex.RLike;
36+
import org.elasticsearch.xpack.ql.expression.predicate.regex.RLikePattern;
3637
import org.elasticsearch.xpack.ql.optimizer.OptimizerRules.BooleanLiteralsOnTheRight;
3738
import org.elasticsearch.xpack.ql.optimizer.OptimizerRules.BooleanSimplification;
3839
import org.elasticsearch.xpack.ql.optimizer.OptimizerRules.CombineBinaryComparisons;
@@ -48,13 +49,13 @@
4849
import java.util.List;
4950

5051
import static java.util.Collections.emptyMap;
52+
import static org.elasticsearch.xpack.ql.TestUtils.of;
5153
import static org.elasticsearch.xpack.ql.expression.Literal.FALSE;
5254
import static org.elasticsearch.xpack.ql.expression.Literal.NULL;
5355
import static org.elasticsearch.xpack.ql.expression.Literal.TRUE;
5456
import static org.elasticsearch.xpack.ql.tree.Source.EMPTY;
5557
import static org.elasticsearch.xpack.ql.type.DataTypes.BOOLEAN;
5658
import static org.elasticsearch.xpack.ql.type.DataTypes.INTEGER;
57-
import static org.elasticsearch.xpack.ql.TestUtils.of;
5859

5960
public class OptimizerRulesTests extends ESTestCase {
6061

@@ -189,7 +190,7 @@ public void testConstantFoldingLikes() {
189190
new ConstantFolding().rule(new Like(EMPTY, of("test_emp"), new LikePattern("test%", (char) 0)))
190191
.canonical());
191192
assertEquals(TRUE,
192-
new ConstantFolding().rule(new RLike(EMPTY, of("test_emp"), "test.emp")).canonical());
193+
new ConstantFolding().rule(new RLike(EMPTY, of("test_emp"), new RLikePattern("test.emp"))).canonical());
193194
}
194195

195196
public void testArithmeticFolding() {

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import org.elasticsearch.xpack.ql.expression.predicate.regex.Like;
4545
import org.elasticsearch.xpack.ql.expression.predicate.regex.LikePattern;
4646
import org.elasticsearch.xpack.ql.expression.predicate.regex.RLike;
47+
import org.elasticsearch.xpack.ql.expression.predicate.regex.RLikePattern;
4748
import org.elasticsearch.xpack.ql.tree.Source;
4849
import org.elasticsearch.xpack.ql.type.DataType;
4950
import org.elasticsearch.xpack.ql.type.DataTypes;
@@ -235,7 +236,7 @@ public Expression visitPredicated(PredicatedContext ctx) {
235236
e = new Like(source, exp, visitPattern(pCtx.pattern()));
236237
break;
237238
case SqlBaseParser.RLIKE:
238-
e = new RLike(source, exp, string(pCtx.regex));
239+
e = new RLike(source, exp, new RLikePattern(string(pCtx.regex)));
239240
break;
240241
case SqlBaseParser.NULL:
241242
// shortcut to avoid double negation later on (since there's no IsNull (missing in ES is a negated exists))

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import org.elasticsearch.xpack.ql.expression.predicate.operator.comparison.NotEquals;
4040
import org.elasticsearch.xpack.ql.expression.predicate.operator.comparison.NullEquals;
4141
import org.elasticsearch.xpack.ql.expression.predicate.regex.RLike;
42+
import org.elasticsearch.xpack.ql.expression.predicate.regex.RLikePattern;
4243
import org.elasticsearch.xpack.ql.index.EsIndex;
4344
import org.elasticsearch.xpack.ql.optimizer.OptimizerRules.BooleanLiteralsOnTheRight;
4445
import org.elasticsearch.xpack.ql.optimizer.OptimizerRules.BooleanSimplification;
@@ -369,7 +370,7 @@ public void testGenericNullableExpression() {
369370
// comparison
370371
assertNullLiteral(rule.rule(new GreaterThan(EMPTY, getFieldAttribute(), NULL)));
371372
// regex
372-
assertNullLiteral(rule.rule(new RLike(EMPTY, NULL, "123")));
373+
assertNullLiteral(rule.rule(new RLike(EMPTY, NULL, new RLikePattern("123"))));
373374
}
374375

375376
public void testNullFoldingOnCast() {

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

+17
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,23 @@ private void assertDifferentRLikeAndNotRLikePatterns(String firstPattern, String
547547
assertEquals("keyword", rqsq.field());
548548
}
549549

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

0 commit comments

Comments
 (0)