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;
|
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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.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());
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
Loading…
Reference in New Issue