SQL: Fix issue with CAST and NULL checking. (#50371)

Previously, during expression optimisation, CAST would be considered
nullable if the casted expression resulted to a NULL literal, and would
be always non-nullable otherwise. As a result if CASE was wrapped by a
null check function like IS NULL or IS NOT NULL it was simplified to
TRUE/FALSE, eliminating the actual casting operation. So in case of an
expression with an erroneous casting like CAST('foo' AS DATETIME) IS NULL
it would be simplified to FALSE instead of throwing an Exception signifying
the attempt to cast 'foo' to a DATETIME type.

CAST now always returns Nullability.UKNOWN except from the case that
its result evaluated to a constant NULL, where it returns Nullability.TRUE.
This way the IS NULL/IS NOT NULL don't get simplified to FALSE/TRUE
and the CAST actually gets evaluated resulting to a thrown Exception.

Fixes: #50191
(cherry picked from commit 671e07a931cd828661e226cba22a5d38804a17a5)
This commit is contained in:
Marios Trivyzas 2019-12-20 00:19:22 +02:00
parent cb73fb0f9b
commit f1a6b675f7
3 changed files with 51 additions and 8 deletions

View File

@ -66,7 +66,7 @@ public class Cast extends UnaryScalarFunction {
if (from().isNull()) { if (from().isNull()) {
return Nullability.TRUE; return Nullability.TRUE;
} }
return field().nullable(); return Nullability.UNKNOWN;
} }
@Override @Override

View File

@ -9,15 +9,9 @@ import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
import org.elasticsearch.common.io.stream.Writeable.Reader; import org.elasticsearch.common.io.stream.Writeable.Reader;
import org.elasticsearch.test.AbstractWireSerializingTestCase; import org.elasticsearch.test.AbstractWireSerializingTestCase;
import org.elasticsearch.xpack.sql.expression.function.scalar.Processors; import org.elasticsearch.xpack.sql.expression.function.scalar.Processors;
import org.elasticsearch.xpack.sql.expression.gen.processor.ConstantProcessor;
import org.elasticsearch.xpack.sql.expression.gen.processor.Processor;
public class CheckNullProcessorTests extends AbstractWireSerializingTestCase<CheckNullProcessor> { public class CheckNullProcessorTests extends AbstractWireSerializingTestCase<CheckNullProcessor> {
private static final Processor FALSE = new ConstantProcessor(false);
private static final Processor TRUE = new ConstantProcessor(true);
private static final Processor NULL = new ConstantProcessor((Object) null);
public static CheckNullProcessor randomProcessor() { public static CheckNullProcessor randomProcessor() {
return new CheckNullProcessor(randomFrom(CheckNullProcessor.CheckNullOperation.values())); return new CheckNullProcessor(randomFrom(CheckNullProcessor.CheckNullOperation.values()));
} }

View File

@ -6,6 +6,7 @@
package org.elasticsearch.xpack.sql.optimizer; package org.elasticsearch.xpack.sql.optimizer;
import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.xpack.sql.SqlIllegalArgumentException;
import org.elasticsearch.xpack.sql.analysis.analyzer.Analyzer.PruneSubqueryAliases; import org.elasticsearch.xpack.sql.analysis.analyzer.Analyzer.PruneSubqueryAliases;
import org.elasticsearch.xpack.sql.analysis.index.EsIndex; import org.elasticsearch.xpack.sql.analysis.index.EsIndex;
import org.elasticsearch.xpack.sql.expression.Alias; import org.elasticsearch.xpack.sql.expression.Alias;
@ -438,10 +439,46 @@ public class OptimizerTests extends ESTestCase {
assertEquals(false, foldNull.rule(new IsNull(EMPTY, TRUE)).fold()); assertEquals(false, foldNull.rule(new IsNull(EMPTY, TRUE)).fold());
} }
public void testNullFoldingIsNullWithCast() {
FoldNull foldNull = new FoldNull();
Cast cast = new Cast(EMPTY, L("foo"), DataType.DATE);
IsNull isNull = new IsNull(EMPTY, cast);
final IsNull isNullOpt = (IsNull) foldNull.rule(isNull);
assertEquals(isNull, isNullOpt);
SqlIllegalArgumentException sqlIAE =
expectThrows(SqlIllegalArgumentException.class, () -> isNullOpt.asPipe().asProcessor().process(null));
assertEquals("cannot cast [foo] to [date]: Text 'foo' could not be parsed at index 0", sqlIAE.getMessage());
isNull = new IsNull(EMPTY, new Cast(EMPTY, NULL, randomFrom(DataType.values())));
assertTrue((Boolean) ((IsNull) foldNull.rule(isNull)).asPipe().asProcessor().process(null));
}
public void testNullFoldingIsNotNull() { public void testNullFoldingIsNotNull() {
FoldNull foldNull = new FoldNull(); FoldNull foldNull = new FoldNull();
assertEquals(true, foldNull.rule(new IsNotNull(EMPTY, TRUE)).fold()); assertEquals(true, foldNull.rule(new IsNotNull(EMPTY, TRUE)).fold());
assertEquals(false, foldNull.rule(new IsNotNull(EMPTY, NULL)).fold()); assertEquals(false, foldNull.rule(new IsNotNull(EMPTY, NULL)).fold());
Cast cast = new Cast(EMPTY, L("foo"), DataType.DATE);
IsNotNull isNotNull = new IsNotNull(EMPTY, cast);
assertEquals(isNotNull, foldNull.rule(isNotNull));
}
public void testNullFoldingIsNotNullWithCast() {
FoldNull foldNull = new FoldNull();
Cast cast = new Cast(EMPTY, L("foo"), DataType.DATE);
IsNotNull isNotNull = new IsNotNull(EMPTY, cast);
final IsNotNull isNotNullOpt = (IsNotNull) foldNull.rule(isNotNull);
assertEquals(isNotNull, isNotNullOpt);
SqlIllegalArgumentException sqlIAE =
expectThrows(SqlIllegalArgumentException.class, () -> isNotNullOpt.asPipe().asProcessor().process(null));
assertEquals("cannot cast [foo] to [date]: Text 'foo' could not be parsed at index 0", sqlIAE.getMessage());
isNotNull = new IsNotNull(EMPTY, new Cast(EMPTY, NULL, randomFrom(DataType.values())));
assertFalse((Boolean) ((IsNotNull) foldNull.rule(isNotNull)).asPipe().asProcessor().process(null));
} }
public void testGenericNullableExpression() { public void testGenericNullableExpression() {
@ -461,6 +498,18 @@ public class OptimizerTests extends ESTestCase {
assertNullLiteral(rule.rule(new RLike(EMPTY, NULL, "123"))); assertNullLiteral(rule.rule(new RLike(EMPTY, NULL, "123")));
} }
public void testNullFoldingOnCast() {
FoldNull foldNull = new FoldNull();
Cast cast = new Cast(EMPTY, NULL, randomFrom(DataType.values()));
assertEquals(Nullability.TRUE, cast.nullable());
assertNull(foldNull.rule(cast).fold());
cast = new Cast(EMPTY, L("foo"), DataType.DATE);
assertEquals(Nullability.UNKNOWN, cast.nullable());
assertEquals(cast, foldNull.rule(cast));
}
public void testNullFoldingDoesNotApplyOnLogicalExpressions() { public void testNullFoldingDoesNotApplyOnLogicalExpressions() {
FoldNull rule = new FoldNull(); FoldNull rule = new FoldNull();