-
Notifications
You must be signed in to change notification settings - Fork 24.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
SQL: Implement null handling for IN(v1, v2, ...)
#34750
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -46,9 +46,15 @@ public static double doubleValueOf(Expression e) { | |
} | ||
|
||
public static <T> List<T> valuesOf(List<Expression> list, DataType to) { | ||
return valuesOf(list, to, false); | ||
} | ||
|
||
public static <T> List<T> valuesOf(List<Expression> list, DataType to, boolean removeNulls) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure why nulls need to be removed - if the expression in the list is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please check my comment here: #34750 (comment) |
||
List<T> l = new ArrayList<>(list.size()); | ||
for (Expression e : list) { | ||
l.add(valueOf(e, to)); | ||
if (!removeNulls || e.dataType() != DataType.NULL) { | ||
l.add(valueOf(e, to)); | ||
} | ||
} | ||
return l; | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -82,16 +82,20 @@ public boolean foldable() { | |
} | ||
|
||
@Override | ||
public Object fold() { | ||
public Boolean fold() { | ||
Object foldedLeftValue = value.fold(); | ||
|
||
Boolean result = false; | ||
for (Expression rightValue : list) { | ||
Boolean compResult = Comparisons.eq(foldedLeftValue, rightValue.fold()); | ||
if (compResult != null && compResult) { | ||
if (compResult == null) { | ||
// if (value().dataType() == DataType.NULL || rightValue.dataType() == DataType.NULL) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This comments are confusing (not sure if this is the final version or not). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry about that, some mixup with git stash. |
||
result = null; | ||
// } | ||
} else if (compResult) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
return result; | ||
} | ||
|
||
@Override | ||
|
@@ -118,15 +122,18 @@ public ScriptTemplate asScript() { | |
String scriptPrefix = leftScript + "=="; | ||
LinkedHashSet<Object> values = list.stream().map(Expression::fold).collect(Collectors.toCollection(LinkedHashSet::new)); | ||
for (Object valueFromList : values) { | ||
if (valueFromList instanceof Expression) { | ||
ScriptTemplate rightScript = asScript((Expression) valueFromList); | ||
sj.add(scriptPrefix + rightScript.template()); | ||
rightParams.add(rightScript.params()); | ||
} else { | ||
if (valueFromList instanceof String) { | ||
sj.add(scriptPrefix + '"' + valueFromList + '"'); | ||
// if checked against null => false | ||
if (valueFromList != null) { | ||
if (valueFromList instanceof Expression) { | ||
ScriptTemplate rightScript = asScript((Expression) valueFromList); | ||
sj.add(scriptPrefix + rightScript.template()); | ||
rightParams.add(rightScript.params()); | ||
} else { | ||
sj.add(scriptPrefix + valueFromList.toString()); | ||
if (valueFromList instanceof String) { | ||
sj.add(scriptPrefix + '"' + valueFromList + '"'); | ||
} else { | ||
sj.add(scriptPrefix + valueFromList.toString()); | ||
} | ||
} | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -40,14 +40,18 @@ public final void writeTo(StreamOutput out) throws IOException { | |
@Override | ||
public Object process(Object input) { | ||
Object leftValue = processsors.get(processsors.size() - 1).process(input); | ||
Boolean result = false; | ||
|
||
for (int i = 0; i < processsors.size() - 1; i++) { | ||
Boolean compResult = Comparisons.eq(leftValue, processsors.get(i).process(input)); | ||
if (compResult == null) { | ||
result = null; | ||
} | ||
if (compResult != null && compResult) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
return true; | ||
} | ||
} | ||
return false; | ||
return result; | ||
} | ||
|
||
@Override | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,7 +24,7 @@ public class TermsQuery extends LeafQuery { | |
public TermsQuery(Location location, String term, List<Expression> values) { | ||
super(location); | ||
this.term = term; | ||
this.values = new LinkedHashSet<>(Foldables.valuesOf(values, values.get(0).dataType())); | ||
this.values = new LinkedHashSet<>(Foldables.valuesOf(values, values.get(0).dataType(), true)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To avoid having the overloaded method, it might be more convenient to use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I did that only to avoid a second iteration on the list (to remove the nulls), but I can change it. what do you think? I guess it can make a difference only if the list is really long. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yup. I wouldn't worry about it. I'm with you regarding avoiding the iteration but again, the Foldable method looks obscure with the null exclusion. |
||
} | ||
|
||
@Override | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
/* | ||
* 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.sql.expression.predicate; | ||
|
||
import org.elasticsearch.test.ESTestCase; | ||
import org.elasticsearch.xpack.sql.expression.Literal; | ||
|
||
import java.util.Arrays; | ||
|
||
import static org.elasticsearch.xpack.sql.tree.Location.EMPTY; | ||
|
||
public class InTests extends ESTestCase { | ||
|
||
private static final Literal ONE = L(1); | ||
private static final Literal TWO = L(2); | ||
private static final Literal THREE = L(3); | ||
private static final Literal NULL = L(null); | ||
|
||
public void testInWithContainedValue() { | ||
In in = new In(EMPTY, TWO, Arrays.asList(ONE, TWO, THREE)); | ||
assertTrue(in.fold()); | ||
} | ||
|
||
public void testInWithNotContainedValue() { | ||
In in = new In(EMPTY, THREE, Arrays.asList(ONE, TWO)); | ||
assertFalse(in.fold()); | ||
} | ||
|
||
public void testHandleNullOnLeftValue() { | ||
In in = new In(EMPTY, NULL, Arrays.asList(ONE, TWO, THREE)); | ||
assertNull(in.fold()); | ||
in = new In(EMPTY, NULL, Arrays.asList(ONE, NULL, THREE)); | ||
assertNull(in.fold()); | ||
|
||
} | ||
|
||
public void testHandleNullsOnRightValue() { | ||
In in = new In(EMPTY, THREE, Arrays.asList(ONE, NULL, THREE)); | ||
assertTrue(in.fold()); | ||
in = new In(EMPTY, ONE, Arrays.asList(TWO, NULL, THREE)); | ||
assertNull(in.fold()); | ||
} | ||
|
||
private static Literal L(Object value) { | ||
return Literal.of(EMPTY, value); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -173,6 +173,19 @@ public void testTranslateInExpression_WhereClause() throws IOException { | |
assertEquals("keyword:(bar foo lala)", tq.asBuilder().toQuery(createShardContext()).toString()); | ||
} | ||
|
||
public void testTranslateInExpression_WhereClauseAndNullHAndling() throws IOException { | ||
LogicalPlan p = plan("SELECT * FROM test WHERE keyword IN ('foo', null, 'lala', null, 'foo', concat('la', 'la'))"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I tried to copy the behaviour to Postgres:
MySQL behaves the same way too. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Makes sense. |
||
assertTrue(p instanceof Project); | ||
assertTrue(p.children().get(0) instanceof Filter); | ||
Expression condition = ((Filter) p.children().get(0)).condition(); | ||
assertFalse(condition.foldable()); | ||
QueryTranslation translation = QueryTranslator.toQuery(condition, false); | ||
Query query = translation.query; | ||
assertTrue(query instanceof TermsQuery); | ||
TermsQuery tq = (TermsQuery) query; | ||
assertEquals("keyword:(foo lala)", tq.asBuilder().toQuery(createShardContext()).toString()); | ||
} | ||
|
||
public void testTranslateInExpressionInvalidValues_WhereClause() { | ||
LogicalPlan p = plan("SELECT * FROM test WHERE keyword IN ('foo', 'bar', keyword)"); | ||
assertTrue(p instanceof Project); | ||
|
@@ -196,4 +209,17 @@ public void testTranslateInExpression_HavingClause_Painless() { | |
assertEquals("InternalSqlScriptUtils.nullSafeFilter(params.a0==10 || params.a0==20)", sq.script().toString()); | ||
assertThat(sq.script().params().toString(), startsWith("[{a=MAX(int){a->")); | ||
} | ||
|
||
public void testTranslateInExpression_HavingClauseAndNullHandling_Painless() { | ||
LogicalPlan p = plan("SELECT keyword, max(int) FROM test GROUP BY keyword HAVING max(int) in (10, null, 20, null, 30 - 10)"); | ||
assertTrue(p instanceof Project); | ||
assertTrue(p.children().get(0) instanceof Filter); | ||
Expression condition = ((Filter) p.children().get(0)).condition(); | ||
assertFalse(condition.foldable()); | ||
QueryTranslation translation = QueryTranslator.toQuery(condition, false); | ||
assertTrue(translation.query instanceof ScriptQuery); | ||
ScriptQuery sq = (ScriptQuery) translation.query; | ||
assertEquals("InternalSqlScriptUtils.nullSafeFilter(params.a0==10 || params.a0==20)", sq.script().toString()); | ||
assertThat(sq.script().params().toString(), startsWith("[{a=MAX(int){a->")); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All these conditions that return
true
wouldn't look better if there is oneif ()
only?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did it only to be more readable, can combine them in one if.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe use one
if
with one condition evaluation per line - best of both worlds.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good to me