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 eaa8ead6742a8e7dcf343bcbaff8de031550fd77)
This commit is contained in:
Marios Trivyzas 2020-03-16 11:55:08 +01:00
parent e2effa9fab
commit 1272ae411e
11 changed files with 92 additions and 41 deletions

View File

@ -6,8 +6,6 @@
package org.elasticsearch.xpack.ql.expression.predicate.regex; package org.elasticsearch.xpack.ql.expression.predicate.regex;
import org.elasticsearch.xpack.ql.expression.Expression; import org.elasticsearch.xpack.ql.expression.Expression;
import org.elasticsearch.xpack.ql.expression.gen.processor.Processor;
import org.elasticsearch.xpack.ql.expression.predicate.regex.RegexProcessor.RegexOperation;
import org.elasticsearch.xpack.ql.tree.NodeInfo; import org.elasticsearch.xpack.ql.tree.NodeInfo;
import org.elasticsearch.xpack.ql.tree.Source; import org.elasticsearch.xpack.ql.tree.Source;
@ -26,15 +24,4 @@ public class Like extends RegexMatch<LikePattern> {
protected Like replaceChild(Expression newLeft) { protected Like replaceChild(Expression newLeft) {
return new Like(source(), newLeft, pattern()); return new Like(source(), newLeft, pattern());
} }
@Override
public Boolean fold() {
Object val = field().fold();
return RegexOperation.match(val, pattern().asJavaRegex());
}
@Override
protected Processor makeProcessor() {
return new RegexProcessor(pattern().asJavaRegex());
}
} }

View File

@ -17,7 +17,7 @@ import java.util.Objects;
* *
* To prevent conflicts with ES, the string and char must be validated to not contain '*'. * To prevent conflicts with ES, the string and char must be validated to not contain '*'.
*/ */
public class LikePattern { public class LikePattern implements StringPattern {
private final String pattern; private final String pattern;
private final char escape; private final char escape;
@ -43,9 +43,7 @@ public class LikePattern {
return escape; return escape;
} }
/** @Override
* Returns the pattern in (Java) regex format.
*/
public String asJavaRegex() { public String asJavaRegex() {
return regex; return regex;
} }

View File

@ -6,14 +6,12 @@
package org.elasticsearch.xpack.ql.expression.predicate.regex; package org.elasticsearch.xpack.ql.expression.predicate.regex;
import org.elasticsearch.xpack.ql.expression.Expression; import org.elasticsearch.xpack.ql.expression.Expression;
import org.elasticsearch.xpack.ql.expression.gen.processor.Processor;
import org.elasticsearch.xpack.ql.expression.predicate.regex.RegexProcessor.RegexOperation;
import org.elasticsearch.xpack.ql.tree.NodeInfo; import org.elasticsearch.xpack.ql.tree.NodeInfo;
import org.elasticsearch.xpack.ql.tree.Source; import org.elasticsearch.xpack.ql.tree.Source;
public class RLike extends RegexMatch<String> { public class RLike extends RegexMatch<RLikePattern> {
public RLike(Source source, Expression value, String pattern) { public RLike(Source source, Expression value, RLikePattern pattern) {
super(source, value, pattern); super(source, value, pattern);
} }
@ -26,15 +24,4 @@ public class RLike extends RegexMatch<String> {
protected RLike replaceChild(Expression newChild) { protected RLike replaceChild(Expression newChild) {
return new RLike(source(), newChild, pattern()); return new RLike(source(), newChild, pattern());
} }
@Override
public Boolean fold() {
Object val = field().fold();
return RegexOperation.match(val, pattern());
}
@Override
protected Processor makeProcessor() {
return new RegexProcessor(pattern());
}
} }

View File

@ -0,0 +1,20 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.ql.expression.predicate.regex;
public class RLikePattern implements StringPattern {
private final String regexpPattern;
public RLikePattern(String regexpPattern) {
this.regexpPattern = regexpPattern;
}
@Override
public String asJavaRegex() {
return regexpPattern;
}
}

View File

@ -10,15 +10,19 @@ import org.elasticsearch.xpack.ql.expression.Expression;
import org.elasticsearch.xpack.ql.expression.Expressions; import org.elasticsearch.xpack.ql.expression.Expressions;
import org.elasticsearch.xpack.ql.expression.Nullability; import org.elasticsearch.xpack.ql.expression.Nullability;
import org.elasticsearch.xpack.ql.expression.function.scalar.UnaryScalarFunction; import org.elasticsearch.xpack.ql.expression.function.scalar.UnaryScalarFunction;
import org.elasticsearch.xpack.ql.expression.gen.processor.Processor;
import org.elasticsearch.xpack.ql.expression.gen.script.ScriptTemplate;
import org.elasticsearch.xpack.ql.tree.Source; import org.elasticsearch.xpack.ql.tree.Source;
import org.elasticsearch.xpack.ql.type.DataType; import org.elasticsearch.xpack.ql.type.DataType;
import org.elasticsearch.xpack.ql.type.DataTypes; import org.elasticsearch.xpack.ql.type.DataTypes;
import java.util.Objects; import java.util.Objects;
import static org.elasticsearch.common.logging.LoggerMessageFormat.format;
import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isStringAndExact; import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isStringAndExact;
import static org.elasticsearch.xpack.ql.expression.gen.script.ParamsBuilder.paramsBuilder;
public abstract class RegexMatch<T> extends UnaryScalarFunction { public abstract class RegexMatch<T extends StringPattern> extends UnaryScalarFunction {
private final T pattern; private final T pattern;
@ -56,6 +60,28 @@ public abstract class RegexMatch<T> extends UnaryScalarFunction {
} }
@Override @Override
public Boolean fold() {
Object val = field().fold();
return RegexProcessor.RegexOperation.match(val, pattern().asJavaRegex());
}
@Override
protected Processor makeProcessor() {
return new RegexProcessor(pattern().asJavaRegex());
}
@Override
public ScriptTemplate asScript() {
ScriptTemplate fieldAsScript = asScript(field());
return new ScriptTemplate(
formatTemplate(format("{sql}.", "regex({},{})", fieldAsScript.template())),
paramsBuilder()
.script(fieldAsScript.params())
.variable(pattern.asJavaRegex())
.build(),
dataType());
}
public boolean equals(Object obj) { public boolean equals(Object obj) {
return super.equals(obj) && Objects.equals(((RegexMatch<?>) obj).pattern(), pattern()); return super.equals(obj) && Objects.equals(((RegexMatch<?>) obj).pattern(), pattern());
} }

View File

@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.ql.expression.predicate.regex;
interface StringPattern {
/**
* Returns the pattern in (Java) regex format.
*/
String asJavaRegex();
}

View File

@ -112,7 +112,7 @@ public final class ExpressionTranslators {
} }
if (e instanceof RLike) { if (e instanceof RLike) {
String pattern = ((RLike) e).pattern(); String pattern = ((RLike) e).pattern().asJavaRegex();
q = new RegexQuery(e.source(), targetFieldName, pattern); q = new RegexQuery(e.source(), targetFieldName, pattern);
} }

View File

@ -33,6 +33,7 @@ import org.elasticsearch.xpack.ql.expression.predicate.operator.comparison.NullE
import org.elasticsearch.xpack.ql.expression.predicate.regex.Like; import org.elasticsearch.xpack.ql.expression.predicate.regex.Like;
import org.elasticsearch.xpack.ql.expression.predicate.regex.LikePattern; import org.elasticsearch.xpack.ql.expression.predicate.regex.LikePattern;
import org.elasticsearch.xpack.ql.expression.predicate.regex.RLike; import org.elasticsearch.xpack.ql.expression.predicate.regex.RLike;
import org.elasticsearch.xpack.ql.expression.predicate.regex.RLikePattern;
import org.elasticsearch.xpack.ql.optimizer.OptimizerRules.BooleanLiteralsOnTheRight; import org.elasticsearch.xpack.ql.optimizer.OptimizerRules.BooleanLiteralsOnTheRight;
import org.elasticsearch.xpack.ql.optimizer.OptimizerRules.BooleanSimplification; import org.elasticsearch.xpack.ql.optimizer.OptimizerRules.BooleanSimplification;
import org.elasticsearch.xpack.ql.optimizer.OptimizerRules.CombineBinaryComparisons; import org.elasticsearch.xpack.ql.optimizer.OptimizerRules.CombineBinaryComparisons;
@ -48,13 +49,13 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import static java.util.Collections.emptyMap; import static java.util.Collections.emptyMap;
import static org.elasticsearch.xpack.ql.TestUtils.of;
import static org.elasticsearch.xpack.ql.expression.Literal.FALSE; import static org.elasticsearch.xpack.ql.expression.Literal.FALSE;
import static org.elasticsearch.xpack.ql.expression.Literal.NULL; import static org.elasticsearch.xpack.ql.expression.Literal.NULL;
import static org.elasticsearch.xpack.ql.expression.Literal.TRUE; import static org.elasticsearch.xpack.ql.expression.Literal.TRUE;
import static org.elasticsearch.xpack.ql.tree.Source.EMPTY; import static org.elasticsearch.xpack.ql.tree.Source.EMPTY;
import static org.elasticsearch.xpack.ql.type.DataTypes.BOOLEAN; import static org.elasticsearch.xpack.ql.type.DataTypes.BOOLEAN;
import static org.elasticsearch.xpack.ql.type.DataTypes.INTEGER; import static org.elasticsearch.xpack.ql.type.DataTypes.INTEGER;
import static org.elasticsearch.xpack.ql.TestUtils.of;
public class OptimizerRulesTests extends ESTestCase { public class OptimizerRulesTests extends ESTestCase {
@ -189,7 +190,7 @@ public class OptimizerRulesTests extends ESTestCase {
new ConstantFolding().rule(new Like(EMPTY, of("test_emp"), new LikePattern("test%", (char) 0))) new ConstantFolding().rule(new Like(EMPTY, of("test_emp"), new LikePattern("test%", (char) 0)))
.canonical()); .canonical());
assertEquals(TRUE, assertEquals(TRUE,
new ConstantFolding().rule(new RLike(EMPTY, of("test_emp"), "test.emp")).canonical()); new ConstantFolding().rule(new RLike(EMPTY, of("test_emp"), new RLikePattern("test.emp"))).canonical());
} }
public void testArithmeticFolding() { public void testArithmeticFolding() {

View File

@ -44,6 +44,7 @@ import org.elasticsearch.xpack.ql.expression.predicate.operator.comparison.NullE
import org.elasticsearch.xpack.ql.expression.predicate.regex.Like; import org.elasticsearch.xpack.ql.expression.predicate.regex.Like;
import org.elasticsearch.xpack.ql.expression.predicate.regex.LikePattern; import org.elasticsearch.xpack.ql.expression.predicate.regex.LikePattern;
import org.elasticsearch.xpack.ql.expression.predicate.regex.RLike; import org.elasticsearch.xpack.ql.expression.predicate.regex.RLike;
import org.elasticsearch.xpack.ql.expression.predicate.regex.RLikePattern;
import org.elasticsearch.xpack.ql.tree.Source; import org.elasticsearch.xpack.ql.tree.Source;
import org.elasticsearch.xpack.ql.type.DataType; import org.elasticsearch.xpack.ql.type.DataType;
import org.elasticsearch.xpack.ql.type.DataTypes; import org.elasticsearch.xpack.ql.type.DataTypes;
@ -235,7 +236,7 @@ abstract class ExpressionBuilder extends IdentifierBuilder {
e = new Like(source, exp, visitPattern(pCtx.pattern())); e = new Like(source, exp, visitPattern(pCtx.pattern()));
break; break;
case SqlBaseParser.RLIKE: case SqlBaseParser.RLIKE:
e = new RLike(source, exp, string(pCtx.regex)); e = new RLike(source, exp, new RLikePattern(string(pCtx.regex)));
break; break;
case SqlBaseParser.NULL: case SqlBaseParser.NULL:
// shortcut to avoid double negation later on (since there's no IsNull (missing in ES is a negated exists)) // shortcut to avoid double negation later on (since there's no IsNull (missing in ES is a negated exists))

View File

@ -39,6 +39,7 @@ import org.elasticsearch.xpack.ql.expression.predicate.operator.comparison.LessT
import org.elasticsearch.xpack.ql.expression.predicate.operator.comparison.NotEquals; import org.elasticsearch.xpack.ql.expression.predicate.operator.comparison.NotEquals;
import org.elasticsearch.xpack.ql.expression.predicate.operator.comparison.NullEquals; import org.elasticsearch.xpack.ql.expression.predicate.operator.comparison.NullEquals;
import org.elasticsearch.xpack.ql.expression.predicate.regex.RLike; import org.elasticsearch.xpack.ql.expression.predicate.regex.RLike;
import org.elasticsearch.xpack.ql.expression.predicate.regex.RLikePattern;
import org.elasticsearch.xpack.ql.index.EsIndex; import org.elasticsearch.xpack.ql.index.EsIndex;
import org.elasticsearch.xpack.ql.optimizer.OptimizerRules.BooleanLiteralsOnTheRight; import org.elasticsearch.xpack.ql.optimizer.OptimizerRules.BooleanLiteralsOnTheRight;
import org.elasticsearch.xpack.ql.optimizer.OptimizerRules.BooleanSimplification; import org.elasticsearch.xpack.ql.optimizer.OptimizerRules.BooleanSimplification;
@ -369,7 +370,7 @@ public class OptimizerTests extends ESTestCase {
// comparison // comparison
assertNullLiteral(rule.rule(new GreaterThan(EMPTY, getFieldAttribute(), NULL))); assertNullLiteral(rule.rule(new GreaterThan(EMPTY, getFieldAttribute(), NULL)));
// regex // regex
assertNullLiteral(rule.rule(new RLike(EMPTY, NULL, "123"))); assertNullLiteral(rule.rule(new RLike(EMPTY, NULL, new RLikePattern("123"))));
} }
public void testNullFoldingOnCast() { public void testNullFoldingOnCast() {

View File

@ -547,6 +547,23 @@ public class QueryTranslatorTests extends ESTestCase {
assertEquals("keyword", rqsq.field()); assertEquals("keyword", rqsq.field());
} }
public void testLikeRLikeAsPainlessScripts() {
LogicalPlan p = plan("SELECT count(*), CASE WHEN keyword LIKE '%foo%' THEN 1 WHEN keyword RLIKE '.*bar.*' THEN 2 " +
"ELSE 3 END AS t FROM test GROUP BY t");
assertTrue(p instanceof Aggregate);
Expression condition = ((Aggregate) p).groupings().get(0);
assertFalse(condition.foldable());
GroupingContext groupingContext = QueryFolder.FoldAggregate.groupBy(((Aggregate) p).groupings());
assertNotNull(groupingContext);
ScriptTemplate scriptTemplate = groupingContext.tail.script();
assertEquals("InternalSqlScriptUtils.caseFunction([InternalSqlScriptUtils.regex(InternalSqlScriptUtils.docValue(" +
"doc,params.v0),params.v1),params.v2,InternalSqlScriptUtils.regex(InternalSqlScriptUtils.docValue(" +
"doc,params.v3),params.v4),params.v5,params.v6])",
scriptTemplate.toString());
assertEquals("[{v=keyword}, {v=^.*foo.*$}, {v=1}, {v=keyword}, {v=.*bar.*}, {v=2}, {v=3}]",
scriptTemplate.params().toString());
}
public void testTranslateNotExpression_WhereClause_Painless() { public void testTranslateNotExpression_WhereClause_Painless() {
LogicalPlan p = plan("SELECT * FROM test WHERE NOT(POSITION('x', keyword) = 0)"); LogicalPlan p = plan("SELECT * FROM test WHERE NOT(POSITION('x', keyword) = 0)");
assertTrue(p instanceof Project); assertTrue(p instanceof Project);