SQL: Check null in processor (elastic/x-pack-elasticsearch#3494)

Make Processors resilient to NULL values
Check null only in functions not constants

Original commit: elastic/x-pack-elasticsearch@dd8bd16d49
This commit is contained in:
Costin Leau 2018-01-09 16:18:16 +02:00 committed by GitHub
parent e114a86750
commit 25a00a3b55
11 changed files with 108 additions and 11 deletions

View File

@ -47,7 +47,8 @@ public abstract class CsvSpecTestCase extends SpecBaseIntegrationTestCase {
readScriptSpec("/agg.csv-spec", parser), readScriptSpec("/agg.csv-spec", parser),
readScriptSpec("/columns.csv-spec", parser), readScriptSpec("/columns.csv-spec", parser),
readScriptSpec("/datetime.csv-spec", parser), readScriptSpec("/datetime.csv-spec", parser),
readScriptSpec("/alias.csv-spec", parser) readScriptSpec("/alias.csv-spec", parser),
readScriptSpec("/nulls.csv-spec", parser)
); );
} }

View File

@ -7,6 +7,7 @@ package org.elasticsearch.xpack.qa.sql.jdbc;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.relique.jdbc.csv.CsvResultSet; import org.relique.jdbc.csv.CsvResultSet;
import java.sql.JDBCType; import java.sql.JDBCType;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.ResultSetMetaData; import java.sql.ResultSetMetaData;
@ -112,20 +113,28 @@ public class JdbcAssert {
for (int column = 1; column <= columns; column++) { for (int column = 1; column <= columns; column++) {
Object expectedObject = expected.getObject(column); Object expectedObject = expected.getObject(column);
Object actualObject = actual.getObject(column); Object actualObject = actual.getObject(column);
int type = metaData.getColumnType(column); int type = metaData.getColumnType(column);
String msg = "Different result for column [" + metaData.getColumnName(column) + "], entry [" + count + "]"; String msg = "Different result for column [" + metaData.getColumnName(column) + "], entry [" + count + "]";
if (type == Types.TIMESTAMP || type == Types.TIMESTAMP_WITH_TIMEZONE) { // handle nulls first
if (expectedObject == null || actualObject == null) {
assertEquals(expectedObject, actualObject);
}
// then timestamp
else if (type == Types.TIMESTAMP || type == Types.TIMESTAMP_WITH_TIMEZONE) {
assertEquals(getTime(expected, column), getTime(actual, column)); assertEquals(getTime(expected, column), getTime(actual, column));
} }
// and floats/doubles
else if (type == Types.DOUBLE) { else if (type == Types.DOUBLE) {
// the 1d/1f difference is used due to rounding/flooring // the 1d/1f difference is used due to rounding/flooring
assertEquals(msg, (double) expectedObject, (double) actualObject, 1d); assertEquals(msg, (double) expectedObject, (double) actualObject, 1d);
} else if (type == Types.FLOAT) { } else if (type == Types.FLOAT) {
assertEquals(msg, (float) expectedObject, (float) actualObject, 1f); assertEquals(msg, (float) expectedObject, (float) actualObject, 1f);
} else { }
// finally the actual comparison
else {
assertEquals(msg, expectedObject, actualObject); assertEquals(msg, expectedObject, actualObject);
} }
} }

View File

@ -0,0 +1,25 @@
//
// Null expressions
//
nullDate
SELECT YEAR(CAST(NULL AS DATE)) d;
d:i
null
;
nullAdd
SELECT CAST(NULL AS INT) + CAST(NULL AS FLOAT) AS n;
n:d
null
;
nullDiv
SELECT 5 / CAST(NULL AS FLOAT) + 10 AS n;
n:d
null
;

View File

@ -13,6 +13,7 @@ import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definiti
import org.elasticsearch.xpack.sql.expression.function.scalar.script.ScriptTemplate; import org.elasticsearch.xpack.sql.expression.function.scalar.script.ScriptTemplate;
import org.elasticsearch.xpack.sql.tree.Location; import org.elasticsearch.xpack.sql.tree.Location;
import org.elasticsearch.xpack.sql.type.DataType; import org.elasticsearch.xpack.sql.type.DataType;
import org.elasticsearch.xpack.sql.type.DataTypeConversion;
import java.util.Locale; import java.util.Locale;
@ -34,8 +35,7 @@ public abstract class ArithmeticFunction extends BinaryScalarFunction {
@Override @Override
public DataType dataType() { public DataType dataType() {
// left or right have to be compatible so either one works return DataTypeConversion.commonType(left().dataType(), right().dataType());
return left().dataType();
} }
@Override @Override
@ -72,6 +72,7 @@ public abstract class ArithmeticFunction extends BinaryScalarFunction {
.build(), dataType()); .build(), dataType());
} }
@Override
protected final BinaryArithmeticProcessorDefinition makeProcessorDefinition() { protected final BinaryArithmeticProcessorDefinition makeProcessorDefinition() {
return new BinaryArithmeticProcessorDefinition(this, return new BinaryArithmeticProcessorDefinition(this,
ProcessorDefinitions.toProcessorDefinition(left()), ProcessorDefinitions.toProcessorDefinition(left()),

View File

@ -12,6 +12,10 @@ package org.elasticsearch.xpack.sql.expression.function.scalar.arithmetic;
abstract class Arithmetics { abstract class Arithmetics {
static Number add(Number l, Number r) { static Number add(Number l, Number r) {
if (l == null || r == null) {
return null;
}
if (l instanceof Double || r instanceof Double) { if (l instanceof Double || r instanceof Double) {
return Double.valueOf(l.doubleValue() + r.doubleValue()); return Double.valueOf(l.doubleValue() + r.doubleValue());
} }
@ -26,6 +30,10 @@ abstract class Arithmetics {
} }
static Number sub(Number l, Number r) { static Number sub(Number l, Number r) {
if (l == null || r == null) {
return null;
}
if (l instanceof Double || r instanceof Double) { if (l instanceof Double || r instanceof Double) {
return Double.valueOf(l.doubleValue() - r.doubleValue()); return Double.valueOf(l.doubleValue() - r.doubleValue());
} }
@ -40,6 +48,10 @@ abstract class Arithmetics {
} }
static Number mul(Number l, Number r) { static Number mul(Number l, Number r) {
if (l == null || r == null) {
return null;
}
if (l instanceof Double || r instanceof Double) { if (l instanceof Double || r instanceof Double) {
return Double.valueOf(l.doubleValue() * r.doubleValue()); return Double.valueOf(l.doubleValue() * r.doubleValue());
} }
@ -54,6 +66,10 @@ abstract class Arithmetics {
} }
static Number div(Number l, Number r) { static Number div(Number l, Number r) {
if (l == null || r == null) {
return null;
}
if (l instanceof Double || r instanceof Double) { if (l instanceof Double || r instanceof Double) {
return l.doubleValue() / r.doubleValue(); return l.doubleValue() / r.doubleValue();
} }
@ -68,6 +84,10 @@ abstract class Arithmetics {
} }
static Number mod(Number l, Number r) { static Number mod(Number l, Number r) {
if (l == null || r == null) {
return null;
}
if (l instanceof Long || r instanceof Long) { if (l instanceof Long || r instanceof Long) {
return Long.valueOf(Math.floorMod(l.longValue(), r.longValue())); return Long.valueOf(Math.floorMod(l.longValue(), r.longValue()));
} }
@ -82,6 +102,10 @@ abstract class Arithmetics {
} }
static Number negate(Number n) { static Number negate(Number n) {
if (n == null) {
return null;
}
if (n instanceof Double) { if (n instanceof Double) {
double d = n.doubleValue(); double d = n.doubleValue();
if (d == Double.MIN_VALUE) { if (d == Double.MIN_VALUE) {

View File

@ -7,6 +7,7 @@ package org.elasticsearch.xpack.sql.expression.function.scalar.arithmetic;
import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.xpack.sql.SqlIllegalArgumentException;
import org.elasticsearch.xpack.sql.expression.function.scalar.processor.runtime.BinaryProcessor; import org.elasticsearch.xpack.sql.expression.function.scalar.processor.runtime.BinaryProcessor;
import org.elasticsearch.xpack.sql.expression.function.scalar.processor.runtime.Processor; import org.elasticsearch.xpack.sql.expression.function.scalar.processor.runtime.Processor;
@ -75,6 +76,16 @@ public class BinaryArithmeticProcessor extends BinaryProcessor {
@Override @Override
protected Object doProcess(Object left, Object right) { protected Object doProcess(Object left, Object right) {
if (left == null || right == null) {
return null;
}
if (!(left instanceof Number)) {
throw new SqlIllegalArgumentException("A number is required; received %s", left);
}
if (!(right instanceof Number)) {
throw new SqlIllegalArgumentException("A number is required; received %s", right);
}
return operation.apply((Number) left, (Number) right); return operation.apply((Number) left, (Number) right);
} }

View File

@ -58,6 +58,10 @@ public class UnaryArithmeticProcessor implements Processor {
@Override @Override
public Object process(Object input) { public Object process(Object input) {
if (input == null) {
return null;
}
if (input instanceof Number) { if (input instanceof Number) {
return operation.apply((Number) input); return operation.apply((Number) input);
} }

View File

@ -51,6 +51,11 @@ public abstract class DateTimeFunction extends UnaryScalarFunction {
return field().foldable(); return field().foldable();
} }
@Override
public Object fold() {
return field().fold();
}
@Override @Override
protected TypeResolution resolveType() { protected TypeResolution resolveType() {
if (field().dataType().same(DataTypes.DATE)) { if (field().dataType().same(DataTypes.DATE)) {

View File

@ -12,6 +12,7 @@ import org.elasticsearch.xpack.sql.expression.function.scalar.processor.runtime.
import java.io.IOException; import java.io.IOException;
import java.util.function.DoubleFunction; import java.util.function.DoubleFunction;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Supplier;
public class MathProcessor implements Processor { public class MathProcessor implements Processor {
@ -35,13 +36,13 @@ public class MathProcessor implements Processor {
COS(Math::cos), COS(Math::cos),
COSH(Math::cosh), COSH(Math::cosh),
DEGREES(Math::toDegrees), DEGREES(Math::toDegrees),
E((Object l) -> Math.E), E(() -> Math.E),
EXP(Math::exp), EXP(Math::exp),
EXPM1(Math::expm1), EXPM1(Math::expm1),
FLOOR(Math::floor), FLOOR(Math::floor),
LOG(Math::log), LOG(Math::log),
LOG10(Math::log10), LOG10(Math::log10),
PI((Object l) -> Math.PI), PI(() -> Math.PI),
RADIANS(Math::toRadians), RADIANS(Math::toRadians),
ROUND((DoubleFunction<Object>) Math::round), ROUND((DoubleFunction<Object>) Math::round),
SIN(Math::sin), SIN(Math::sin),
@ -52,11 +53,15 @@ public class MathProcessor implements Processor {
private final Function<Object, Object> apply; private final Function<Object, Object> apply;
MathOperation(Function<Object, Object> apply) { MathOperation(Function<Object, Object> apply) {
this.apply = apply; this.apply = l -> l == null ? null : apply.apply(l);
} }
MathOperation(DoubleFunction<Object> apply) { MathOperation(DoubleFunction<Object> apply) {
this.apply = (Object l) -> apply.apply(((Number) l).doubleValue()); this.apply = (Object l) -> l == null ? null : apply.apply(((Number) l).doubleValue());
}
MathOperation(Supplier<Double> supplier) {
this.apply = l -> supplier.get();
} }
public final Object apply(Object l) { public final Object apply(Object l) {

View File

@ -306,6 +306,9 @@ public abstract class DataTypeConversion {
if (detectedType.equals(dataType)) { if (detectedType.equals(dataType)) {
return value; return value;
} }
if (value == null) {
return value;
}
return conversionFor(detectedType, dataType).convert(value); return conversionFor(detectedType, dataType).convert(value);
} }

View File

@ -20,7 +20,7 @@ public class BinaryArithmeticProcessorTests extends AbstractWireSerializingTestC
public static BinaryArithmeticProcessor randomProcessor() { public static BinaryArithmeticProcessor randomProcessor() {
return new BinaryArithmeticProcessor( return new BinaryArithmeticProcessor(
new ConstantProcessor(randomLong()), new ConstantProcessor(randomLong()),
new ConstantProcessor(randomLong()), new ConstantProcessor(randomLong()),
randomFrom(BinaryArithmeticProcessor.BinaryArithmeticOperation.values())); randomFrom(BinaryArithmeticProcessor.BinaryArithmeticOperation.values()));
} }
@ -82,6 +82,15 @@ public class BinaryArithmeticProcessorTests extends AbstractWireSerializingTestC
Processor proc = mod.makeProcessorDefinition().asProcessor(); Processor proc = mod.makeProcessorDefinition().asProcessor();
assertEquals(1, proc.process(null)); assertEquals(1, proc.process(null));
} }
public void testHandleNull() {
assertNull(new Add(EMPTY, l(null), l(3)).makeProcessorDefinition().asProcessor().process(null));
assertNull(new Sub(EMPTY, l(null), l(3)).makeProcessorDefinition().asProcessor().process(null));
assertNull(new Mul(EMPTY, l(null), l(3)).makeProcessorDefinition().asProcessor().process(null));
assertNull(new Div(EMPTY, l(null), l(3)).makeProcessorDefinition().asProcessor().process(null));
assertNull(new Mod(EMPTY, l(null), l(3)).makeProcessorDefinition().asProcessor().process(null));
assertNull(new Neg(EMPTY, l(null)).makeProcessorDefinition().asProcessor().process(null));
}
private static Literal l(Object value) { private static Literal l(Object value) {
return Literal.of(EMPTY, value); return Literal.of(EMPTY, value);