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:
parent
e2effa9fab
commit
1272ae411e
|
@ -6,8 +6,6 @@
|
|||
package org.elasticsearch.xpack.ql.expression.predicate.regex;
|
||||
|
||||
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.Source;
|
||||
|
||||
|
@ -26,15 +24,4 @@ public class Like extends RegexMatch<LikePattern> {
|
|||
protected Like replaceChild(Expression newLeft) {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ import java.util.Objects;
|
|||
*
|
||||
* 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 char escape;
|
||||
|
@ -43,9 +43,7 @@ public class LikePattern {
|
|||
return escape;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the pattern in (Java) regex format.
|
||||
*/
|
||||
@Override
|
||||
public String asJavaRegex() {
|
||||
return regex;
|
||||
}
|
||||
|
@ -83,4 +81,4 @@ public class LikePattern {
|
|||
return Objects.equals(pattern, other.pattern)
|
||||
&& escape == other.escape;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,14 +6,12 @@
|
|||
package org.elasticsearch.xpack.ql.expression.predicate.regex;
|
||||
|
||||
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.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);
|
||||
}
|
||||
|
||||
|
@ -26,15 +24,4 @@ public class RLike extends RegexMatch<String> {
|
|||
protected RLike replaceChild(Expression newChild) {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -10,15 +10,19 @@ import org.elasticsearch.xpack.ql.expression.Expression;
|
|||
import org.elasticsearch.xpack.ql.expression.Expressions;
|
||||
import org.elasticsearch.xpack.ql.expression.Nullability;
|
||||
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.type.DataType;
|
||||
import org.elasticsearch.xpack.ql.type.DataTypes;
|
||||
|
||||
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.gen.script.ParamsBuilder.paramsBuilder;
|
||||
|
||||
public abstract class RegexMatch<T> extends UnaryScalarFunction {
|
||||
public abstract class RegexMatch<T extends StringPattern> extends UnaryScalarFunction {
|
||||
|
||||
private final T pattern;
|
||||
|
||||
|
@ -54,8 +58,30 @@ public abstract class RegexMatch<T> extends UnaryScalarFunction {
|
|||
// right() is not directly foldable in any context but Like can fold it.
|
||||
return field().foldable();
|
||||
}
|
||||
|
||||
|
||||
@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) {
|
||||
return super.equals(obj) && Objects.equals(((RegexMatch<?>) obj).pattern(), pattern());
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -112,7 +112,7 @@ public final class ExpressionTranslators {
|
|||
}
|
||||
|
||||
if (e instanceof RLike) {
|
||||
String pattern = ((RLike) e).pattern();
|
||||
String pattern = ((RLike) e).pattern().asJavaRegex();
|
||||
q = new RegexQuery(e.source(), targetFieldName, pattern);
|
||||
}
|
||||
|
||||
|
@ -352,4 +352,4 @@ public final class ExpressionTranslators {
|
|||
}
|
||||
return new BoolQuery(source, isAnd, left, right);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.LikePattern;
|
||||
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.BooleanSimplification;
|
||||
import org.elasticsearch.xpack.ql.optimizer.OptimizerRules.CombineBinaryComparisons;
|
||||
|
@ -48,13 +49,13 @@ import java.util.Collections;
|
|||
import java.util.List;
|
||||
|
||||
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.NULL;
|
||||
import static org.elasticsearch.xpack.ql.expression.Literal.TRUE;
|
||||
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.INTEGER;
|
||||
import static org.elasticsearch.xpack.ql.TestUtils.of;
|
||||
|
||||
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)))
|
||||
.canonical());
|
||||
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() {
|
||||
|
|
|
@ -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.LikePattern;
|
||||
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.type.DataType;
|
||||
import org.elasticsearch.xpack.ql.type.DataTypes;
|
||||
|
@ -235,7 +236,7 @@ abstract class ExpressionBuilder extends IdentifierBuilder {
|
|||
e = new Like(source, exp, visitPattern(pCtx.pattern()));
|
||||
break;
|
||||
case SqlBaseParser.RLIKE:
|
||||
e = new RLike(source, exp, string(pCtx.regex));
|
||||
e = new RLike(source, exp, new RLikePattern(string(pCtx.regex)));
|
||||
break;
|
||||
case SqlBaseParser.NULL:
|
||||
// shortcut to avoid double negation later on (since there's no IsNull (missing in ES is a negated exists))
|
||||
|
|
|
@ -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.NullEquals;
|
||||
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.optimizer.OptimizerRules.BooleanLiteralsOnTheRight;
|
||||
import org.elasticsearch.xpack.ql.optimizer.OptimizerRules.BooleanSimplification;
|
||||
|
@ -369,7 +370,7 @@ public class OptimizerTests extends ESTestCase {
|
|||
// comparison
|
||||
assertNullLiteral(rule.rule(new GreaterThan(EMPTY, getFieldAttribute(), NULL)));
|
||||
// regex
|
||||
assertNullLiteral(rule.rule(new RLike(EMPTY, NULL, "123")));
|
||||
assertNullLiteral(rule.rule(new RLike(EMPTY, NULL, new RLikePattern("123"))));
|
||||
}
|
||||
|
||||
public void testNullFoldingOnCast() {
|
||||
|
|
|
@ -547,6 +547,23 @@ public class QueryTranslatorTests extends ESTestCase {
|
|||
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() {
|
||||
LogicalPlan p = plan("SELECT * FROM test WHERE NOT(POSITION('x', keyword) = 0)");
|
||||
assertTrue(p instanceof Project);
|
||||
|
|
Loading…
Reference in New Issue