diff --git a/x-pack/plugin/sql/qa/src/main/resources/agg.csv-spec b/x-pack/plugin/sql/qa/src/main/resources/agg.csv-spec index 4e6d0d0d79fa4..d73bef6fab2e1 100644 --- a/x-pack/plugin/sql/qa/src/main/resources/agg.csv-spec +++ b/x-pack/plugin/sql/qa/src/main/resources/agg.csv-spec @@ -192,6 +192,23 @@ ROUND(SQRT(CAST(EXP(languages) AS SMALLINT)),2):d| COUNT(1):l null |10 ; +groupByRoundWithTwoParams +SELECT ROUND(YEAR("birth_date"), -2) FROM test_emp GROUP BY ROUND(YEAR("birth_date"), -2); + +ROUND(YEAR(birth_date [Z]),-2) +----------------------------- +null +2000 +; + +groupByTruncateWithTwoParams +SELECT TRUNCATE(YEAR("birth_date"), -2) FROM test_emp GROUP BY TRUNCATE(YEAR("birth_date"), -2); + +TRUNCATE(YEAR(birth_date [Z]),-2) +-------------------------------- +null +1900 +; // // Grouping functions diff --git a/x-pack/plugin/sql/qa/src/main/resources/agg.sql-spec b/x-pack/plugin/sql/qa/src/main/resources/agg.sql-spec index 21dd7bf530e3d..08becf46141f2 100644 --- a/x-pack/plugin/sql/qa/src/main/resources/agg.sql-spec +++ b/x-pack/plugin/sql/qa/src/main/resources/agg.sql-spec @@ -50,6 +50,31 @@ SELECT emp_no * 2 AS e FROM test_emp GROUP BY e ORDER BY e; groupByModScalar SELECT (emp_no % 3) + 1 AS e FROM test_emp GROUP BY e ORDER BY e; +// group by nested functions with no alias +//https://github.com/elastic/elasticsearch/issues/40239 +groupByTruncate-Ignore +SELECT CAST(TRUNCATE(EXTRACT(YEAR FROM "birth_date")) AS INTEGER) AS x FROM test_emp GROUP BY CAST(TRUNCATE(EXTRACT(YEAR FROM "birth_date")) AS INTEGER) ORDER BY CAST(TRUNCATE(EXTRACT(YEAR FROM "birth_date")) AS INTEGER); +//https://github.com/elastic/elasticsearch/issues/40239 +groupByRound-Ignore +SELECT CAST(ROUND(EXTRACT(YEAR FROM "birth_date")) AS INTEGER) AS x FROM test_emp GROUP BY CAST(ROUND(EXTRACT(YEAR FROM "birth_date")) AS INTEGER) ORDER BY CAST(ROUND(EXTRACT(YEAR FROM "birth_date")) AS INTEGER); +groupByAtan2 +SELECT ATAN2(YEAR("birth_date"), 5) AS x FROM test_emp GROUP BY ATAN2(YEAR("birth_date"), 5) ORDER BY ATAN2(YEAR("birth_date"), 5); +groupByPower +SELECT POWER(YEAR("birth_date"), 2) AS x FROM test_emp GROUP BY POWER(YEAR("birth_date"), 2) ORDER BY POWER(YEAR("birth_date"), 2); +//https://github.com/elastic/elasticsearch/issues/40239 +groupByPowerWithCast-Ignore +SELECT CAST(POWER(YEAR("birth_date"), 2) AS DOUBLE) AS x FROM test_emp GROUP BY CAST(POWER(YEAR("birth_date"), 2) AS DOUBLE) ORDER BY CAST(POWER(YEAR("birth_date"), 2) AS DOUBLE); +groupByConcat +SELECT LEFT(CONCAT("first_name", "last_name"), 3) AS x FROM test_emp GROUP BY LEFT(CONCAT("first_name", "last_name"), 3) ORDER BY LEFT(CONCAT("first_name", "last_name"), 3) LIMIT 15; +groupByLocateWithTwoParams +SELECT LOCATE('a', CONCAT("first_name", "last_name")) AS x FROM test_emp GROUP BY LOCATE('a', CONCAT("first_name", "last_name")) ORDER BY LOCATE('a', CONCAT("first_name", "last_name")); +groupByLocateWithThreeParams +SELECT LOCATE('a', CONCAT("first_name", "last_name"), 3) AS x FROM test_emp GROUP BY LOCATE('a', CONCAT("first_name", "last_name"), 3) ORDER BY LOCATE('a', CONCAT("first_name", "last_name"), 3); +groupByRoundAndTruncateWithTwoParams +SELECT ROUND(SIN(TRUNCATE("salary", 2)), 2) AS x FROM "test_emp" GROUP BY ROUND(SIN(TRUNCATE("salary", 2)), 2) ORDER BY ROUND(SIN(TRUNCATE("salary", 2)), 2) LIMIT 5; +groupByRoundAndTruncateWithOneParam +SELECT ROUND(SIN(TRUNCATE(languages))) AS x FROM "test_emp" GROUP BY ROUND(SIN(TRUNCATE(languages))) ORDER BY ROUND(SIN(TRUNCATE(languages))) LIMIT 5; + // multiple group by groupByMultiOnText SELECT gender g, languages l FROM "test_emp" GROUP BY gender, languages ORDER BY gender ASC, languages ASC; diff --git a/x-pack/plugin/sql/qa/src/main/resources/math.csv-spec b/x-pack/plugin/sql/qa/src/main/resources/math.csv-spec index 2df93b3795443..cfa17d0779845 100644 --- a/x-pack/plugin/sql/qa/src/main/resources/math.csv-spec +++ b/x-pack/plugin/sql/qa/src/main/resources/math.csv-spec @@ -15,7 +15,7 @@ null |10 truncateWithNoSecondParameterWithAsciiHavingAndOrderBy SELECT TRUNCATE(ASCII(LEFT(first_name, 1))), COUNT(*) count FROM test_emp GROUP BY ASCII(LEFT(first_name, 1)) HAVING COUNT(*) > 5 ORDER BY TRUNCATE(ASCII(LEFT(first_name, 1))) DESC; -TRUNCATE(ASCII(LEFT(first_name,1)),0):i| count:l +TRUNCATE(ASCII(LEFT(first_name,1))):i| count:l ---------------------------------------+--------------- null |10 66 |7 @@ -163,7 +163,7 @@ ROUND(AVG(salary),2):d| AVG(salary):d | COUNT(1):l groupByAndOrderByRoundWithNoSecondParameter SELECT ROUND(AVG(salary)), ROUND(salary) rounded, AVG(salary), COUNT(*) FROM test_emp GROUP BY rounded ORDER BY rounded DESC LIMIT 10; -ROUND(AVG(salary),0):d| rounded:i | AVG(salary):d | COUNT(1):l +ROUND(AVG(salary)):d| rounded:i | AVG(salary):d | COUNT(1):l ----------------------+---------------+---------------+--------------- 74999.0 |74999 |74999.0 |1 74970.0 |74970 |74970.0 |1 diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/Processors.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/Processors.java index e605b82d6b9ed..f5e1a3ece38e9 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/Processors.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/Processors.java @@ -12,6 +12,7 @@ import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.NonIsoDateTimeProcessor; import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.QuarterProcessor; import org.elasticsearch.xpack.sql.expression.function.scalar.math.BinaryMathProcessor; +import org.elasticsearch.xpack.sql.expression.function.scalar.math.BinaryOptionalMathProcessor; import org.elasticsearch.xpack.sql.expression.function.scalar.math.MathProcessor; import org.elasticsearch.xpack.sql.expression.function.scalar.string.BinaryStringNumericProcessor; import org.elasticsearch.xpack.sql.expression.function.scalar.string.BinaryStringStringProcessor; @@ -68,7 +69,6 @@ public static List getNamedWriteables() { // arithmetic entries.add(new Entry(Processor.class, BinaryArithmeticProcessor.NAME, BinaryArithmeticProcessor::new)); entries.add(new Entry(Processor.class, UnaryArithmeticProcessor.NAME, UnaryArithmeticProcessor::new)); - entries.add(new Entry(Processor.class, BinaryMathProcessor.NAME, BinaryMathProcessor::new)); // comparators entries.add(new Entry(Processor.class, BinaryComparisonProcessor.NAME, BinaryComparisonProcessor::new)); entries.add(new Entry(Processor.class, InProcessor.NAME, InProcessor::new)); @@ -82,6 +82,8 @@ public static List getNamedWriteables() { entries.add(new Entry(Processor.class, NonIsoDateTimeProcessor.NAME, NonIsoDateTimeProcessor::new)); entries.add(new Entry(Processor.class, QuarterProcessor.NAME, QuarterProcessor::new)); // math + entries.add(new Entry(Processor.class, BinaryMathProcessor.NAME, BinaryMathProcessor::new)); + entries.add(new Entry(Processor.class, BinaryOptionalMathProcessor.NAME, BinaryOptionalMathProcessor::new)); entries.add(new Entry(Processor.class, MathProcessor.NAME, MathProcessor::new)); // string entries.add(new Entry(Processor.class, StringProcessor.NAME, StringProcessor::new)); diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/BinaryMathProcessor.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/BinaryMathProcessor.java index 397f84b4cf840..f66dcf185fcb7 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/BinaryMathProcessor.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/BinaryMathProcessor.java @@ -25,26 +25,7 @@ public enum BinaryMathOperation implements BiFunction { ATAN2((l, r) -> Math.atan2(l.doubleValue(), r.doubleValue())), MOD(Arithmetics::mod), - POWER((l, r) -> Math.pow(l.doubleValue(), r.doubleValue())), - ROUND((l, r) -> { - if (r instanceof Float || r instanceof Double) { - throw new SqlIllegalArgumentException("An integer number is required; received [{}] as second parameter", r); - } - - double tenAtScale = Math.pow(10., r.longValue()); - double middleResult = l.doubleValue() * tenAtScale; - int sign = middleResult > 0 ? 1 : -1; - return Math.round(Math.abs(middleResult)) / tenAtScale * sign; - }), - TRUNCATE((l, r) -> { - if (r instanceof Float || r instanceof Double) { - throw new SqlIllegalArgumentException("An integer number is required; received [{}] as second parameter", r); - } - - double tenAtScale = Math.pow(10., r.longValue()); - double g = l.doubleValue() * tenAtScale; - return (((l.doubleValue() < 0) ? Math.ceil(g) : Math.floor(g)) / tenAtScale); - }); + POWER((l, r) -> Math.pow(l.doubleValue(), r.doubleValue())); private final BiFunction process; @@ -79,7 +60,7 @@ public String getWriteableName() { @Override protected void checkParameter(Object param) { if (!(param instanceof Number)) { - throw new SqlIllegalArgumentException("A number is required; received {}", param); + throw new SqlIllegalArgumentException("A number is required; received [{}]", param); } } } \ No newline at end of file diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/BinaryNumericFunction.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/BinaryNumericFunction.java index 1738f01f52ee6..82381dba0eed3 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/BinaryNumericFunction.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/BinaryNumericFunction.java @@ -69,6 +69,6 @@ public boolean equals(Object obj) { BinaryNumericFunction other = (BinaryNumericFunction) obj; return Objects.equals(other.left(), left()) && Objects.equals(other.right(), right()) - && Objects.equals(other.operation, operation); + && Objects.equals(other.operation, operation); } } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/BinaryOptionalMathPipe.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/BinaryOptionalMathPipe.java new file mode 100644 index 0000000000000..b4315aa1ef370 --- /dev/null +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/BinaryOptionalMathPipe.java @@ -0,0 +1,115 @@ +/* + * 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.function.scalar.math; + +import org.elasticsearch.xpack.sql.execution.search.SqlSourceBuilder; +import org.elasticsearch.xpack.sql.expression.Expression; +import org.elasticsearch.xpack.sql.expression.function.scalar.math.BinaryOptionalMathProcessor.BinaryOptionalMathOperation; +import org.elasticsearch.xpack.sql.expression.gen.pipeline.Pipe; +import org.elasticsearch.xpack.sql.tree.NodeInfo; +import org.elasticsearch.xpack.sql.tree.Location; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +public class BinaryOptionalMathPipe extends Pipe { + + private final Pipe left, right; + private final BinaryOptionalMathOperation operation; + + public BinaryOptionalMathPipe(Location location, Expression expression, Pipe left, Pipe right, BinaryOptionalMathOperation operation) { + super(location, expression, right == null ? Arrays.asList(left) : Arrays.asList(left, right)); + this.left = left; + this.right = right; + this.operation = operation; + } + + @Override + public final Pipe replaceChildren(List newChildren) { + int childrenSize = newChildren.size(); + if (childrenSize > 2 || childrenSize < 1) { + throw new IllegalArgumentException("expected [1 or 2] children but received [" + newChildren.size() + "]"); + } + return replaceChildren(newChildren.get(0), childrenSize == 1 ? null : newChildren.get(1)); + } + + @Override + public final Pipe resolveAttributes(AttributeResolver resolver) { + Pipe newLeft = left.resolveAttributes(resolver); + Pipe newRight = right == null ? right : right.resolveAttributes(resolver); + if (newLeft == left && newRight == right) { + return this; + } + return replaceChildren(newLeft, newRight); + } + + @Override + public boolean supportedByAggsOnlyQuery() { + return right == null ? left.supportedByAggsOnlyQuery() : left.supportedByAggsOnlyQuery() || right.supportedByAggsOnlyQuery(); + } + + @Override + public boolean resolved() { + return left.resolved() && (right == null || right.resolved()); + } + + protected Pipe replaceChildren(Pipe newLeft, Pipe newRight) { + return new BinaryOptionalMathPipe(location(), expression(), newLeft, newRight, operation); + } + + @Override + public final void collectFields(SqlSourceBuilder sourceBuilder) { + left.collectFields(sourceBuilder); + if (right != null) { + right.collectFields(sourceBuilder); + } + } + + @Override + protected NodeInfo info() { + return NodeInfo.create(this, BinaryOptionalMathPipe::new, expression(), left, right, operation); + } + + @Override + public BinaryOptionalMathProcessor asProcessor() { + return new BinaryOptionalMathProcessor(left.asProcessor(), right == null ? null : right.asProcessor(), operation); + } + + public Pipe right() { + return right; + } + + public Pipe left() { + return left; + } + + public BinaryOptionalMathOperation operation() { + return operation; + } + + @Override + public int hashCode() { + return Objects.hash(left, right, operation); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + BinaryOptionalMathPipe other = (BinaryOptionalMathPipe) obj; + return Objects.equals(left, other.left) + && Objects.equals(right, other.right) + && Objects.equals(operation, other.operation); + } +} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/BinaryOptionalMathProcessor.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/BinaryOptionalMathProcessor.java new file mode 100644 index 0000000000000..dc89b6ce5cff1 --- /dev/null +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/BinaryOptionalMathProcessor.java @@ -0,0 +1,154 @@ +/* + * 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.function.scalar.math; + +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.xpack.sql.SqlIllegalArgumentException; +import org.elasticsearch.xpack.sql.expression.gen.processor.Processor; + +import java.io.IOException; +import java.util.Objects; +import java.util.function.BiFunction; + +/** + * Processor for binary mathematical operations that have a second optional parameter. + */ +public class BinaryOptionalMathProcessor implements Processor { + + public enum BinaryOptionalMathOperation implements BiFunction { + + ROUND((l, r) -> { + double tenAtScale = Math.pow(10., r.longValue()); + double middleResult = l.doubleValue() * tenAtScale; + int sign = middleResult > 0 ? 1 : -1; + return Math.round(Math.abs(middleResult)) / tenAtScale * sign; + }), + TRUNCATE((l, r) -> { + double tenAtScale = Math.pow(10., r.longValue()); + double g = l.doubleValue() * tenAtScale; + return (((l.doubleValue() < 0) ? Math.ceil(g) : Math.floor(g)) / tenAtScale); + }); + + private final BiFunction process; + + BinaryOptionalMathOperation(BiFunction process) { + this.process = process; + } + + @Override + public final Number apply(Number left, Number right) { + if (left == null) { + return null; + } + if (!(left instanceof Number)) { + throw new SqlIllegalArgumentException("A number is required; received [{}]", left); + } + + if (right != null) { + if (!(right instanceof Number)) { + throw new SqlIllegalArgumentException("A number is required; received [{}]", right); + } + if (right instanceof Float || right instanceof Double) { + throw new SqlIllegalArgumentException("An integer number is required; received [{}] as second parameter", right); + } + } else { + right = 0; + } + + return process.apply(left, right); + } + } + + private final Processor left, right; + private final BinaryOptionalMathOperation operation; + public static final String NAME = "mob"; + + public BinaryOptionalMathProcessor(Processor left, Processor right, BinaryOptionalMathOperation operation) { + this.left = left; + this.right = right; + this.operation = operation; + } + + public BinaryOptionalMathProcessor(StreamInput in) throws IOException { + left = in.readNamedWriteable(Processor.class); + right = in.readOptionalNamedWriteable(Processor.class); + operation = in.readEnum(BinaryOptionalMathOperation.class); + } + + @Override + public final void writeTo(StreamOutput out) throws IOException { + out.writeNamedWriteable(left); + out.writeOptionalNamedWriteable(right); + out.writeEnum(operation); + } + + @Override + public Object process(Object input) { + return doProcess(left().process(input), right() == null ? null : right().process(input)); + } + + public Number doProcess(Object left, Object right) { + if (left == null) { + return null; + } + if (!(left instanceof Number)) { + throw new SqlIllegalArgumentException("A number is required; received [{}]", left); + } + + if (right != null) { + if (!(right instanceof Number)) { + throw new SqlIllegalArgumentException("A number is required; received [{}]", right); + } + if (right instanceof Float || right instanceof Double) { + throw new SqlIllegalArgumentException("An integer number is required; received [{}] as second parameter", right); + } + } else { + right = 0; + } + + return operation().apply((Number) left, (Number) right); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + BinaryOptionalMathProcessor other = (BinaryOptionalMathProcessor) obj; + return Objects.equals(left(), other.left()) + && Objects.equals(right(), other.right()) + && Objects.equals(operation(), other.operation()); + } + + @Override + public int hashCode() { + return Objects.hash(left(), right(), operation()); + } + + public Processor left() { + return left; + } + + public Processor right() { + return right; + } + + public BinaryOptionalMathOperation operation() { + return operation; + } + + @Override + public String getWriteableName() { + return NAME; + } +} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/BinaryOptionalNumericFunction.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/BinaryOptionalNumericFunction.java new file mode 100644 index 0000000000000..916f0965033a0 --- /dev/null +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/BinaryOptionalNumericFunction.java @@ -0,0 +1,134 @@ +/* + * 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.function.scalar.math; + +import org.elasticsearch.xpack.sql.expression.Expression; +import org.elasticsearch.xpack.sql.expression.Expressions; +import org.elasticsearch.xpack.sql.expression.Expressions.ParamOrdinal; +import org.elasticsearch.xpack.sql.expression.function.scalar.ScalarFunction; +import org.elasticsearch.xpack.sql.expression.function.scalar.math.BinaryOptionalMathProcessor.BinaryOptionalMathOperation; +import org.elasticsearch.xpack.sql.expression.gen.pipeline.Pipe; +import org.elasticsearch.xpack.sql.expression.gen.script.ScriptTemplate; +import org.elasticsearch.xpack.sql.tree.Location; +import org.elasticsearch.xpack.sql.type.DataType; + +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Objects; + +import static java.lang.String.format; +import static org.elasticsearch.xpack.sql.expression.TypeResolutions.isInteger; +import static org.elasticsearch.xpack.sql.expression.TypeResolutions.isNumeric; +import static org.elasticsearch.xpack.sql.expression.gen.script.ParamsBuilder.paramsBuilder; + +public abstract class BinaryOptionalNumericFunction extends ScalarFunction { + + private final Expression left, right; + + public BinaryOptionalNumericFunction(Location location, Expression left, Expression right) { + super(location, right != null ? Arrays.asList(left, right) : Arrays.asList(left)); + this.left = left; + this.right = right; + } + + @Override + protected TypeResolution resolveType() { + if (!childrenResolved()) { + return new TypeResolution("Unresolved children"); + } + + TypeResolution resolution = isNumeric(left, functionName(), ParamOrdinal.FIRST); + if (resolution.unresolved()) { + return resolution; + + } + + return right == null ? TypeResolution.TYPE_RESOLVED : isInteger(right, functionName(), ParamOrdinal.SECOND); + } + + @Override + protected Pipe makePipe() { + return new BinaryOptionalMathPipe(location(), this, + Expressions.pipe(left), + right == null ? null : Expressions.pipe(right), + operation()); + } + + protected abstract BinaryOptionalMathOperation operation(); + + @Override + public boolean foldable() { + return left.foldable() + && (right == null || right.foldable()); + } + + @Override + public Object fold() { + return operation().apply((Number) left.fold(), (right == null ? null : (Number) right.fold())); + } + + @Override + public Expression replaceChildren(List newChildren) { + if (right() != null && newChildren.size() != 2) { + throw new IllegalArgumentException("expected [2] children but received [" + newChildren.size() + "]"); + } else if (right() == null && newChildren.size() != 1) { + throw new IllegalArgumentException("expected [1] child but received [" + newChildren.size() + "]"); + } + + return replacedChildrenInstance(newChildren); + } + + protected abstract Expression replacedChildrenInstance(List newChildren); + + @Override + public ScriptTemplate asScript() { + ScriptTemplate leftScript = asScript(left); + ScriptTemplate rightScript = asOptionalScript(right); + + return asScriptFrom(leftScript, rightScript); + } + + private ScriptTemplate asScriptFrom(ScriptTemplate leftScript, ScriptTemplate rightScript) { + return new ScriptTemplate(format(Locale.ROOT, formatTemplate("{sql}.%s(%s,%s)"), + operation().name().toLowerCase(Locale.ROOT), + leftScript.template(), + rightScript.template()), + paramsBuilder() + .script(leftScript.params()).script(rightScript.params()) + .build(), dataType()); + } + + @Override + public DataType dataType() { + return left().dataType(); + } + + protected Expression left() { + return left; + } + + protected Expression right() { + return right; + } + + @Override + public int hashCode() { + return Objects.hash(left(), right(), operation()); + } + + @Override + public boolean equals(Object obj) { + if (obj == null || obj.getClass() != getClass()) { + return false; + } + BinaryOptionalNumericFunction other = (BinaryOptionalNumericFunction) obj; + return Objects.equals(other.left(), left()) + && Objects.equals(other.right(), right()) + && Objects.equals(other.operation(), operation()); + } +} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/Round.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/Round.java index 05244c2a74e95..a3f379cfe32de 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/Round.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/Round.java @@ -6,11 +6,11 @@ package org.elasticsearch.xpack.sql.expression.function.scalar.math; import org.elasticsearch.xpack.sql.expression.Expression; -import org.elasticsearch.xpack.sql.expression.Literal; -import org.elasticsearch.xpack.sql.expression.function.scalar.math.BinaryMathProcessor.BinaryMathOperation; -import org.elasticsearch.xpack.sql.tree.Location; +import org.elasticsearch.xpack.sql.expression.function.scalar.math.BinaryOptionalMathProcessor.BinaryOptionalMathOperation; import org.elasticsearch.xpack.sql.tree.NodeInfo; -import org.elasticsearch.xpack.sql.type.DataType; +import org.elasticsearch.xpack.sql.tree.Location; + +import java.util.List; /** * Function that takes two parameters: one is the field/value itself, the other is a non-floating point numeric @@ -18,24 +18,24 @@ * count digits after the decimal point. If negative, it will round the number till that paramter count * digits before the decimal point, starting at the decimal point. */ -public class Round extends BinaryNumericFunction { +public class Round extends BinaryOptionalNumericFunction { public Round(Location location, Expression left, Expression right) { - super(location, left, right == null ? Literal.of(left.location(), 0) : right, BinaryMathOperation.ROUND); + super(location, left, right); } @Override - protected NodeInfo info() { + protected NodeInfo info() { return NodeInfo.create(this, Round::new, left(), right()); } @Override - protected Round replaceChildren(Expression newLeft, Expression newRight) { - return new Round(location(), newLeft, newRight); + protected BinaryOptionalMathOperation operation() { + return BinaryOptionalMathOperation.ROUND; } @Override - public DataType dataType() { - return left().dataType(); + protected final Expression replacedChildrenInstance(List newChildren) { + return new Round(location(), newChildren.get(0), right() == null ? null : newChildren.get(1)); } } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/Truncate.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/Truncate.java index 2dedd75d5b323..77421898a6f6f 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/Truncate.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/Truncate.java @@ -6,11 +6,11 @@ package org.elasticsearch.xpack.sql.expression.function.scalar.math; import org.elasticsearch.xpack.sql.expression.Expression; -import org.elasticsearch.xpack.sql.expression.Literal; -import org.elasticsearch.xpack.sql.expression.function.scalar.math.BinaryMathProcessor.BinaryMathOperation; -import org.elasticsearch.xpack.sql.tree.Location; +import org.elasticsearch.xpack.sql.expression.function.scalar.math.BinaryOptionalMathProcessor.BinaryOptionalMathOperation; import org.elasticsearch.xpack.sql.tree.NodeInfo; -import org.elasticsearch.xpack.sql.type.DataType; +import org.elasticsearch.xpack.sql.tree.Location; + +import java.util.List; /** * Function that takes two parameters: one is the field/value itself, the other is a non-floating point numeric @@ -18,24 +18,24 @@ * parameter count digits after the decimal point. If negative, it will truncate the number till that parameter * count digits before the decimal point, starting at the decimal point. */ -public class Truncate extends BinaryNumericFunction { +public class Truncate extends BinaryOptionalNumericFunction { public Truncate(Location location, Expression left, Expression right) { - super(location, left, right == null ? Literal.of(left.location(), 0) : right, BinaryMathOperation.TRUNCATE); + super(location, left, right); } @Override - protected NodeInfo info() { + protected NodeInfo info() { return NodeInfo.create(this, Truncate::new, left(), right()); } @Override - protected Truncate replaceChildren(Expression newLeft, Expression newRight) { - return new Truncate(location(), newLeft, newRight); + protected BinaryOptionalMathOperation operation() { + return BinaryOptionalMathOperation.TRUNCATE; } - + @Override - public DataType dataType() { - return left().dataType(); + protected final Expression replacedChildrenInstance(List newChildren) { + return new Truncate(location(), newChildren.get(0), right() == null ? null : newChildren.get(1)); } } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/whitelist/InternalSqlScriptUtils.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/whitelist/InternalSqlScriptUtils.java index 01d56188ed2ed..9ff9f0592d10b 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/whitelist/InternalSqlScriptUtils.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/whitelist/InternalSqlScriptUtils.java @@ -13,6 +13,7 @@ import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.NonIsoDateTimeProcessor.NonIsoDateTimeExtractor; import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.QuarterProcessor; import org.elasticsearch.xpack.sql.expression.function.scalar.math.BinaryMathProcessor.BinaryMathOperation; +import org.elasticsearch.xpack.sql.expression.function.scalar.math.BinaryOptionalMathProcessor.BinaryOptionalMathOperation; import org.elasticsearch.xpack.sql.expression.function.scalar.math.MathProcessor.MathOperation; import org.elasticsearch.xpack.sql.expression.function.scalar.string.BinaryStringNumericProcessor.BinaryStringNumericOperation; import org.elasticsearch.xpack.sql.expression.function.scalar.string.BinaryStringStringProcessor.BinaryStringStringOperation; @@ -197,11 +198,11 @@ public static Object sub(Object left, Object right) { } public static Number round(Number v, Number s) { - return BinaryMathOperation.ROUND.apply(v, s); + return BinaryOptionalMathOperation.ROUND.apply(v, s); } public static Number truncate(Number v, Number s) { - return BinaryMathOperation.TRUNCATE.apply(v, s); + return BinaryOptionalMathOperation.TRUNCATE.apply(v, s); } public static Double abs(Number value) { @@ -219,6 +220,10 @@ public static Double asin(Number value) { public static Double atan(Number value) { return MathOperation.ATAN.apply(value); } + + public static Number atan2(Number left, Number right) { + return BinaryMathOperation.ATAN2.apply(left, right); + } public static Double cbrt(Number value) { return MathOperation.CBRT.apply(value); @@ -271,6 +276,10 @@ public static Double log10(Number value) { public static Double pi(Number value) { return MathOperation.PI.apply(value); } + + public static Number power(Number left, Number right) { + return BinaryMathOperation.POWER.apply(left, right); + } public static Double radians(Number value) { return MathOperation.RADIANS.apply(value); diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/gen/script/ScriptWeaver.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/gen/script/ScriptWeaver.java index 074518f6b7d7c..8c6d140fdac66 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/gen/script/ScriptWeaver.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/gen/script/ScriptWeaver.java @@ -11,6 +11,7 @@ import org.elasticsearch.xpack.sql.expression.Expression; import org.elasticsearch.xpack.sql.expression.Expressions; import org.elasticsearch.xpack.sql.expression.FieldAttribute; +import org.elasticsearch.xpack.sql.expression.Literal; import org.elasticsearch.xpack.sql.expression.function.aggregate.AggregateFunctionAttribute; import org.elasticsearch.xpack.sql.expression.function.grouping.GroupingFunctionAttribute; import org.elasticsearch.xpack.sql.expression.function.scalar.ScalarFunctionAttribute; @@ -48,6 +49,13 @@ default ScriptTemplate asScript(Expression exp) { throw new SqlIllegalArgumentException("Cannot evaluate script for expression {}", exp); } + /* + * To be used when the function has an optional parameter. + */ + default ScriptTemplate asOptionalScript(Expression exp) { + return exp == null ? asScript(Literal.NULL) : asScript(exp); + } + DataType dataType(); default ScriptTemplate scriptWithFoldable(Expression foldable) { diff --git a/x-pack/plugin/sql/src/main/resources/org/elasticsearch/xpack/sql/plugin/sql_whitelist.txt b/x-pack/plugin/sql/src/main/resources/org/elasticsearch/xpack/sql/plugin/sql_whitelist.txt index 4e9fc1475e302..f628d1be868c3 100644 --- a/x-pack/plugin/sql/src/main/resources/org/elasticsearch/xpack/sql/plugin/sql_whitelist.txt +++ b/x-pack/plugin/sql/src/main/resources/org/elasticsearch/xpack/sql/plugin/sql_whitelist.txt @@ -64,7 +64,9 @@ class org.elasticsearch.xpack.sql.expression.function.scalar.whitelist.InternalS def div(Object, Object) def mod(Object, Object) def mul(Object, Object) + Number atan2(Number, Number) Number neg(Number) + Number power(Number, Number) Number round(Number, Number) Number truncate(Number, Number) diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/analysis/analyzer/VerifierErrorMessagesTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/analysis/analyzer/VerifierErrorMessagesTests.java index 545afeb5ba1f1..1b6ffae03e6b2 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/analysis/analyzer/VerifierErrorMessagesTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/analysis/analyzer/VerifierErrorMessagesTests.java @@ -450,7 +450,7 @@ public void testInvalidTypeForStringFunction_WithTwoArgs() { public void testInvalidTypeForNumericFunction_WithTwoArgs() { assertEquals("1:8: first argument of [TRUNCATE] must be [numeric], found value [foo] type [keyword]", error("SELECT TRUNCATE('foo', 2)")); - assertEquals("1:8: second argument of [TRUNCATE] must be [numeric], found value [bar] type [keyword]", + assertEquals("1:8: second argument of [TRUNCATE] must be [integer], found value [bar] type [keyword]", error("SELECT TRUNCATE(1.2, 'bar')")); } diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/BinaryMathProcessorTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/BinaryMathProcessorTests.java index 84ca662bebac6..b373bbe619b53 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/BinaryMathProcessorTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/BinaryMathProcessorTests.java @@ -71,10 +71,10 @@ public void testRoundFunctionWithEdgeCasesInputs() { public void testRoundInputValidation() { SqlIllegalArgumentException siae = expectThrows(SqlIllegalArgumentException.class, () -> new Round(EMPTY, l(5), l("foobarbar")).makePipe().asProcessor().process(null)); - assertEquals("A number is required; received foobarbar", siae.getMessage()); + assertEquals("A number is required; received [foobarbar]", siae.getMessage()); siae = expectThrows(SqlIllegalArgumentException.class, () -> new Round(EMPTY, l("bla"), l(0)).makePipe().asProcessor().process(null)); - assertEquals("A number is required; received bla", siae.getMessage()); + assertEquals("A number is required; received [bla]", siae.getMessage()); siae = expectThrows(SqlIllegalArgumentException.class, () -> new Round(EMPTY, l(123.34), l(0.1)).makePipe().asProcessor().process(null)); assertEquals("An integer number is required; received [0.1] as second parameter", siae.getMessage()); @@ -103,10 +103,10 @@ public void testTruncateFunctionWithEdgeCasesInputs() { public void testTruncateInputValidation() { SqlIllegalArgumentException siae = expectThrows(SqlIllegalArgumentException.class, () -> new Truncate(EMPTY, l(5), l("foobarbar")).makePipe().asProcessor().process(null)); - assertEquals("A number is required; received foobarbar", siae.getMessage()); + assertEquals("A number is required; received [foobarbar]", siae.getMessage()); siae = expectThrows(SqlIllegalArgumentException.class, () -> new Truncate(EMPTY, l("bla"), l(0)).makePipe().asProcessor().process(null)); - assertEquals("A number is required; received bla", siae.getMessage()); + assertEquals("A number is required; received [bla]", siae.getMessage()); siae = expectThrows(SqlIllegalArgumentException.class, () -> new Truncate(EMPTY, l(123.34), l(0.1)).makePipe().asProcessor().process(null)); assertEquals("An integer number is required; received [0.1] as second parameter", siae.getMessage()); diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/planner/QueryTranslatorTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/planner/QueryTranslatorTests.java index 5b17011ca8fb5..9527b7612396b 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/planner/QueryTranslatorTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/planner/QueryTranslatorTests.java @@ -19,9 +19,11 @@ import org.elasticsearch.xpack.sql.analysis.index.IndexResolution; import org.elasticsearch.xpack.sql.expression.Expression; import org.elasticsearch.xpack.sql.expression.FieldAttribute; +import org.elasticsearch.xpack.sql.expression.Literal; import org.elasticsearch.xpack.sql.expression.function.FunctionRegistry; import org.elasticsearch.xpack.sql.expression.function.grouping.Histogram; import org.elasticsearch.xpack.sql.expression.function.scalar.math.MathProcessor.MathOperation; +import org.elasticsearch.xpack.sql.expression.function.scalar.math.Round; import org.elasticsearch.xpack.sql.expression.gen.script.ScriptTemplate; import org.elasticsearch.xpack.sql.optimizer.Optimizer; import org.elasticsearch.xpack.sql.parser.SqlParser; @@ -415,6 +417,50 @@ public void testTranslateMathFunction_HavingClause_Painless() { assertThat(aggFilter.scriptTemplate().params().toString(), startsWith("[{a=MAX(int){a->")); assertThat(aggFilter.scriptTemplate().params().toString(), endsWith(", {v=10}]")); } + + public void testTranslateRoundWithOneParameter() { + LogicalPlan p = plan("SELECT ROUND(YEAR(date)) FROM test GROUP BY ROUND(YEAR(date))"); + + assertTrue(p instanceof Aggregate); + assertEquals(1, ((Aggregate) p).groupings().size()); + assertEquals(1, ((Aggregate) p).aggregates().size()); + assertTrue(((Aggregate) p).groupings().get(0) instanceof Round); + assertTrue(((Aggregate) p).aggregates().get(0) instanceof Round); + + Round groupingRound = (Round) ((Aggregate) p).groupings().get(0); + assertEquals(1, groupingRound.children().size()); + + QueryTranslator.GroupingContext groupingContext = QueryTranslator.groupBy(((Aggregate) p).groupings()); + assertNotNull(groupingContext); + ScriptTemplate scriptTemplate = groupingContext.tail.script(); + assertEquals("InternalSqlScriptUtils.round(InternalSqlScriptUtils.dateTimeChrono(InternalSqlScriptUtils.docValue(doc,params.v0), " + + "params.v1, params.v2),params.v3)", + scriptTemplate.toString()); + assertEquals("[{v=date}, {v=Z}, {v=YEAR}, {v=null}]", scriptTemplate.params().toString()); + } + + public void testTranslateRoundWithTwoParameters() { + LogicalPlan p = plan("SELECT ROUND(YEAR(date), -2) FROM test GROUP BY ROUND(YEAR(date), -2)"); + + assertTrue(p instanceof Aggregate); + assertEquals(1, ((Aggregate) p).groupings().size()); + assertEquals(1, ((Aggregate) p).aggregates().size()); + assertTrue(((Aggregate) p).groupings().get(0) instanceof Round); + assertTrue(((Aggregate) p).aggregates().get(0) instanceof Round); + + Round groupingRound = (Round) ((Aggregate) p).groupings().get(0); + assertEquals(2, groupingRound.children().size()); + assertTrue(groupingRound.children().get(1) instanceof Literal); + assertEquals(-2, ((Literal) groupingRound.children().get(1)).value()); + + QueryTranslator.GroupingContext groupingContext = QueryTranslator.groupBy(((Aggregate) p).groupings()); + assertNotNull(groupingContext); + ScriptTemplate scriptTemplate = groupingContext.tail.script(); + assertEquals("InternalSqlScriptUtils.round(InternalSqlScriptUtils.dateTimeChrono(InternalSqlScriptUtils.docValue(doc,params.v0), " + + "params.v1, params.v2),params.v3)", + scriptTemplate.toString()); + assertEquals("[{v=date}, {v=Z}, {v=YEAR}, {v=-2}]", scriptTemplate.params().toString()); + } public void testGroupByAndHavingWithFunctionOnTopOfAggregation() { LogicalPlan p = plan("SELECT keyword, MAX(int) FROM test GROUP BY 1 HAVING ABS(MAX(int)) > 10");