diff --git a/docs/reference/query-languages/esql/_snippets/functions/description/scalb.md b/docs/reference/query-languages/esql/_snippets/functions/description/scalb.md new file mode 100644 index 0000000000000..c71133d8ae925 --- /dev/null +++ b/docs/reference/query-languages/esql/_snippets/functions/description/scalb.md @@ -0,0 +1,11 @@ +% This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it. + +**Description** + +Returns the result of `d * 2 ^ scaleFactor`. + +::::{note} +Similar to Java's `scalb` function. Result is rounded as if performed by a single correctly rounded floating-point multiply to a member of the double value set. +:::: + + diff --git a/docs/reference/query-languages/esql/_snippets/functions/examples/scalb.md b/docs/reference/query-languages/esql/_snippets/functions/examples/scalb.md new file mode 100644 index 0000000000000..b5927cea9fbb8 --- /dev/null +++ b/docs/reference/query-languages/esql/_snippets/functions/examples/scalb.md @@ -0,0 +1,16 @@ +% This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it. + +**Examples** + +```esql +null +``` + +The scaleFactor can be a fraction, which is similar to performing a root. +For example, the scaleFactor of `0.5` will give the square root of the d: + +```esql +null +``` + + diff --git a/docs/reference/query-languages/esql/_snippets/functions/layout/scalb.md b/docs/reference/query-languages/esql/_snippets/functions/layout/scalb.md new file mode 100644 index 0000000000000..15022863d204b --- /dev/null +++ b/docs/reference/query-languages/esql/_snippets/functions/layout/scalb.md @@ -0,0 +1,23 @@ +% This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it. + +## `SCALB` [esql-scalb] + +**Syntax** + +:::{image} ../../../images/functions/scalb.svg +:alt: Embedded +:class: text-center +::: + + +:::{include} ../parameters/scalb.md +::: + +:::{include} ../description/scalb.md +::: + +:::{include} ../types/scalb.md +::: + +:::{include} ../examples/scalb.md +::: diff --git a/docs/reference/query-languages/esql/_snippets/functions/parameters/scalb.md b/docs/reference/query-languages/esql/_snippets/functions/parameters/scalb.md new file mode 100644 index 0000000000000..4e4e726c372dc --- /dev/null +++ b/docs/reference/query-languages/esql/_snippets/functions/parameters/scalb.md @@ -0,0 +1,10 @@ +% This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it. + +**Parameters** + +`d` +: Numeric expression for the multiplier. If `null`, the function returns `null`. + +`scaleFactor` +: Numeric expression for the scale factor. If `null`, the function returns `null`. + diff --git a/docs/reference/query-languages/esql/_snippets/functions/types/scalb.md b/docs/reference/query-languages/esql/_snippets/functions/types/scalb.md new file mode 100644 index 0000000000000..4fab8db35a531 --- /dev/null +++ b/docs/reference/query-languages/esql/_snippets/functions/types/scalb.md @@ -0,0 +1,15 @@ +% This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it. + +**Supported types** + +| d | scaleFactor | result | +| --- | --- | --- | +| double | integer | double | +| double | long | double | +| integer | integer | double | +| integer | long | double | +| long | integer | double | +| long | long | double | +| unsigned_long | integer | double | +| unsigned_long | long | double | + diff --git a/docs/reference/query-languages/esql/images/functions/scalb.svg b/docs/reference/query-languages/esql/images/functions/scalb.svg new file mode 100644 index 0000000000000..161e05c163c91 --- /dev/null +++ b/docs/reference/query-languages/esql/images/functions/scalb.svg @@ -0,0 +1 @@ +SCALB(d,scaleFactor) \ No newline at end of file diff --git a/docs/reference/query-languages/esql/kibana/definition/functions/scalb.json b/docs/reference/query-languages/esql/kibana/definition/functions/scalb.json new file mode 100644 index 0000000000000..4a122159af939 --- /dev/null +++ b/docs/reference/query-languages/esql/kibana/definition/functions/scalb.json @@ -0,0 +1,159 @@ +{ + "comment" : "This is generated by ESQL’s AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.", + "type" : "scalar", + "name" : "scalb", + "description" : "Returns the result of `d * 2 ^ scaleFactor`.", + "note" : "Similar to Java's `scalb` function. Result is rounded as if performed by a single correctly rounded floating-point multiply to a member of the double value set.", + "signatures" : [ + { + "params" : [ + { + "name" : "d", + "type" : "double", + "optional" : false, + "description" : "Numeric expression for the multiplier. If `null`, the function returns `null`." + }, + { + "name" : "scaleFactor", + "type" : "integer", + "optional" : false, + "description" : "Numeric expression for the scale factor. If `null`, the function returns `null`." + } + ], + "variadic" : false, + "returnType" : "double" + }, + { + "params" : [ + { + "name" : "d", + "type" : "double", + "optional" : false, + "description" : "Numeric expression for the multiplier. If `null`, the function returns `null`." + }, + { + "name" : "scaleFactor", + "type" : "long", + "optional" : false, + "description" : "Numeric expression for the scale factor. If `null`, the function returns `null`." + } + ], + "variadic" : false, + "returnType" : "double" + }, + { + "params" : [ + { + "name" : "d", + "type" : "integer", + "optional" : false, + "description" : "Numeric expression for the multiplier. If `null`, the function returns `null`." + }, + { + "name" : "scaleFactor", + "type" : "integer", + "optional" : false, + "description" : "Numeric expression for the scale factor. If `null`, the function returns `null`." + } + ], + "variadic" : false, + "returnType" : "double" + }, + { + "params" : [ + { + "name" : "d", + "type" : "integer", + "optional" : false, + "description" : "Numeric expression for the multiplier. If `null`, the function returns `null`." + }, + { + "name" : "scaleFactor", + "type" : "long", + "optional" : false, + "description" : "Numeric expression for the scale factor. If `null`, the function returns `null`." + } + ], + "variadic" : false, + "returnType" : "double" + }, + { + "params" : [ + { + "name" : "d", + "type" : "long", + "optional" : false, + "description" : "Numeric expression for the multiplier. If `null`, the function returns `null`." + }, + { + "name" : "scaleFactor", + "type" : "integer", + "optional" : false, + "description" : "Numeric expression for the scale factor. If `null`, the function returns `null`." + } + ], + "variadic" : false, + "returnType" : "double" + }, + { + "params" : [ + { + "name" : "d", + "type" : "long", + "optional" : false, + "description" : "Numeric expression for the multiplier. If `null`, the function returns `null`." + }, + { + "name" : "scaleFactor", + "type" : "long", + "optional" : false, + "description" : "Numeric expression for the scale factor. If `null`, the function returns `null`." + } + ], + "variadic" : false, + "returnType" : "double" + }, + { + "params" : [ + { + "name" : "d", + "type" : "unsigned_long", + "optional" : false, + "description" : "Numeric expression for the multiplier. If `null`, the function returns `null`." + }, + { + "name" : "scaleFactor", + "type" : "integer", + "optional" : false, + "description" : "Numeric expression for the scale factor. If `null`, the function returns `null`." + } + ], + "variadic" : false, + "returnType" : "double" + }, + { + "params" : [ + { + "name" : "d", + "type" : "unsigned_long", + "optional" : false, + "description" : "Numeric expression for the multiplier. If `null`, the function returns `null`." + }, + { + "name" : "scaleFactor", + "type" : "long", + "optional" : false, + "description" : "Numeric expression for the scale factor. If `null`, the function returns `null`." + } + ], + "variadic" : false, + "returnType" : "double" + } + ], + "examples" : [ + null, + null + ], + "preview" : false, + "snapshot_only" : false +} diff --git a/docs/reference/query-languages/esql/kibana/docs/functions/scalb.md b/docs/reference/query-languages/esql/kibana/docs/functions/scalb.md new file mode 100644 index 0000000000000..d857f3ec9acf8 --- /dev/null +++ b/docs/reference/query-languages/esql/kibana/docs/functions/scalb.md @@ -0,0 +1,9 @@ +% This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it. + +### SCALB +Returns the result of `d * 2 ^ scaleFactor`. + +```esql +null +``` +Note: Similar to Java's `scalb` function. Result is rounded as if performed by a single correctly rounded floating-point multiply to a member of the double value set. diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/floats.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/floats.csv-spec index 0e5b28b6966af..675118432504d 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/floats.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/floats.csv-spec @@ -530,7 +530,7 @@ required_capability: agg_values mvAppend required_capability: fn_mv_append -FROM employees + FROM employees | WHERE emp_no == 10008 OR emp_no == 10021 | EVAL d = mv_append(salary_change, salary_change), i = mv_append(salary_change.int, salary_change.int), @@ -592,3 +592,42 @@ s:double | emp_no:integer | salary:integer | salary_change:double -1.0 | 10004 | 36174 | [-0.35, 1.13, 3.65, 13.48] -1.0 | 10005 | 63528 | [-2.14, 13.07] ; + +scalb +required_capability: fn_scalb + +row x = 3.0, y = 10 | eval z = scalb(x, y); + +x:double | y:integer | z:double +3.0 | 10 | 3072.0 +; + +scalbWithNonFoldableArgs +required_capability: fn_scalb + +from employees +| eval s = scalb(mv_min(salary_change), emp_no / 1000) +| keep s +| limit 3; + +s:double +1218.56 +-7403.52 +13127.68 +; + +scalbWithEvalAndSort +required_capability: fn_scalb +from employees +| eval s = scalb(mv_min(salary_change), 5) +| where scalb(mv_max(salary_change), 5) > 100 +| keep s, emp_no, salary, salary_change +| sort s, emp_no +| limit 3; + +s:double | emp_no:integer | salary:integer | salary_change:double +-313.92 | 10065 | 50249 | [-9.81, -1.47, 14.44] +-296.96 | 10042 | 30404 | [-9.28, 9.42] +-295.36 | 10091 | 38645 | [-9.23, 5.19, 5.85, 7.5] + +; diff --git a/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/math/ScalbConstantEvaluator.java b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/math/ScalbConstantEvaluator.java new file mode 100644 index 0000000000000..70629305e3e9f --- /dev/null +++ b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/math/ScalbConstantEvaluator.java @@ -0,0 +1,139 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License +// 2.0; you may not use this file except in compliance with the Elastic License +// 2.0. +package org.elasticsearch.xpack.esql.expression.function.scalar.math; + +import java.lang.ArithmeticException; +import java.lang.IllegalArgumentException; +import java.lang.Override; +import java.lang.String; +import org.elasticsearch.compute.data.Block; +import org.elasticsearch.compute.data.DoubleBlock; +import org.elasticsearch.compute.data.DoubleVector; +import org.elasticsearch.compute.data.Page; +import org.elasticsearch.compute.operator.DriverContext; +import org.elasticsearch.compute.operator.EvalOperator; +import org.elasticsearch.compute.operator.Warnings; +import org.elasticsearch.core.Releasables; +import org.elasticsearch.xpack.esql.core.tree.Source; + +/** + * {@link EvalOperator.ExpressionEvaluator} implementation for {@link Scalb}. + * This class is generated. Edit {@code EvaluatorImplementer} instead. + */ +public final class ScalbConstantEvaluator implements EvalOperator.ExpressionEvaluator { + private final Source source; + + private final EvalOperator.ExpressionEvaluator d; + + private final int scaleFactor; + + private final DriverContext driverContext; + + private Warnings warnings; + + public ScalbConstantEvaluator(Source source, EvalOperator.ExpressionEvaluator d, int scaleFactor, + DriverContext driverContext) { + this.source = source; + this.d = d; + this.scaleFactor = scaleFactor; + this.driverContext = driverContext; + } + + @Override + public Block eval(Page page) { + try (DoubleBlock dBlock = (DoubleBlock) d.eval(page)) { + DoubleVector dVector = dBlock.asVector(); + if (dVector == null) { + return eval(page.getPositionCount(), dBlock); + } + return eval(page.getPositionCount(), dVector); + } + } + + public DoubleBlock eval(int positionCount, DoubleBlock dBlock) { + try(DoubleBlock.Builder result = driverContext.blockFactory().newDoubleBlockBuilder(positionCount)) { + position: for (int p = 0; p < positionCount; p++) { + if (dBlock.isNull(p)) { + result.appendNull(); + continue position; + } + if (dBlock.getValueCount(p) != 1) { + if (dBlock.getValueCount(p) > 1) { + warnings().registerException(new IllegalArgumentException("single-value function encountered multi-value")); + } + result.appendNull(); + continue position; + } + try { + result.appendDouble(Scalb.processConstant(dBlock.getDouble(dBlock.getFirstValueIndex(p)), this.scaleFactor)); + } catch (ArithmeticException e) { + warnings().registerException(e); + result.appendNull(); + } + } + return result.build(); + } + } + + public DoubleBlock eval(int positionCount, DoubleVector dVector) { + try(DoubleBlock.Builder result = driverContext.blockFactory().newDoubleBlockBuilder(positionCount)) { + position: for (int p = 0; p < positionCount; p++) { + try { + result.appendDouble(Scalb.processConstant(dVector.getDouble(p), this.scaleFactor)); + } catch (ArithmeticException e) { + warnings().registerException(e); + result.appendNull(); + } + } + return result.build(); + } + } + + @Override + public String toString() { + return "ScalbConstantEvaluator[" + "d=" + d + ", scaleFactor=" + scaleFactor + "]"; + } + + @Override + public void close() { + Releasables.closeExpectNoException(d); + } + + private Warnings warnings() { + if (warnings == null) { + this.warnings = Warnings.createWarnings( + driverContext.warningsMode(), + source.source().getLineNumber(), + source.source().getColumnNumber(), + source.text() + ); + } + return warnings; + } + + static class Factory implements EvalOperator.ExpressionEvaluator.Factory { + private final Source source; + + private final EvalOperator.ExpressionEvaluator.Factory d; + + private final int scaleFactor; + + public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory d, int scaleFactor) { + this.source = source; + this.d = d; + this.scaleFactor = scaleFactor; + } + + @Override + public ScalbConstantEvaluator get(DriverContext context) { + return new ScalbConstantEvaluator(source, d.get(context), scaleFactor, context); + } + + @Override + public String toString() { + return "ScalbConstantEvaluator[" + "d=" + d + ", scaleFactor=" + scaleFactor + "]"; + } + } +} diff --git a/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/math/ScalbIntEvaluator.java b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/math/ScalbIntEvaluator.java new file mode 100644 index 0000000000000..510f736f55a32 --- /dev/null +++ b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/math/ScalbIntEvaluator.java @@ -0,0 +1,159 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License +// 2.0; you may not use this file except in compliance with the Elastic License +// 2.0. +package org.elasticsearch.xpack.esql.expression.function.scalar.math; + +import java.lang.ArithmeticException; +import java.lang.IllegalArgumentException; +import java.lang.Override; +import java.lang.String; +import org.elasticsearch.compute.data.Block; +import org.elasticsearch.compute.data.DoubleBlock; +import org.elasticsearch.compute.data.DoubleVector; +import org.elasticsearch.compute.data.IntBlock; +import org.elasticsearch.compute.data.IntVector; +import org.elasticsearch.compute.data.Page; +import org.elasticsearch.compute.operator.DriverContext; +import org.elasticsearch.compute.operator.EvalOperator; +import org.elasticsearch.compute.operator.Warnings; +import org.elasticsearch.core.Releasables; +import org.elasticsearch.xpack.esql.core.tree.Source; + +/** + * {@link EvalOperator.ExpressionEvaluator} implementation for {@link Scalb}. + * This class is generated. Edit {@code EvaluatorImplementer} instead. + */ +public final class ScalbIntEvaluator implements EvalOperator.ExpressionEvaluator { + private final Source source; + + private final EvalOperator.ExpressionEvaluator d; + + private final EvalOperator.ExpressionEvaluator scaleFactor; + + private final DriverContext driverContext; + + private Warnings warnings; + + public ScalbIntEvaluator(Source source, EvalOperator.ExpressionEvaluator d, + EvalOperator.ExpressionEvaluator scaleFactor, DriverContext driverContext) { + this.source = source; + this.d = d; + this.scaleFactor = scaleFactor; + this.driverContext = driverContext; + } + + @Override + public Block eval(Page page) { + try (DoubleBlock dBlock = (DoubleBlock) d.eval(page)) { + try (IntBlock scaleFactorBlock = (IntBlock) scaleFactor.eval(page)) { + DoubleVector dVector = dBlock.asVector(); + if (dVector == null) { + return eval(page.getPositionCount(), dBlock, scaleFactorBlock); + } + IntVector scaleFactorVector = scaleFactorBlock.asVector(); + if (scaleFactorVector == null) { + return eval(page.getPositionCount(), dBlock, scaleFactorBlock); + } + return eval(page.getPositionCount(), dVector, scaleFactorVector); + } + } + } + + public DoubleBlock eval(int positionCount, DoubleBlock dBlock, IntBlock scaleFactorBlock) { + try(DoubleBlock.Builder result = driverContext.blockFactory().newDoubleBlockBuilder(positionCount)) { + position: for (int p = 0; p < positionCount; p++) { + if (dBlock.isNull(p)) { + result.appendNull(); + continue position; + } + if (dBlock.getValueCount(p) != 1) { + if (dBlock.getValueCount(p) > 1) { + warnings().registerException(new IllegalArgumentException("single-value function encountered multi-value")); + } + result.appendNull(); + continue position; + } + if (scaleFactorBlock.isNull(p)) { + result.appendNull(); + continue position; + } + if (scaleFactorBlock.getValueCount(p) != 1) { + if (scaleFactorBlock.getValueCount(p) > 1) { + warnings().registerException(new IllegalArgumentException("single-value function encountered multi-value")); + } + result.appendNull(); + continue position; + } + try { + result.appendDouble(Scalb.process(dBlock.getDouble(dBlock.getFirstValueIndex(p)), scaleFactorBlock.getInt(scaleFactorBlock.getFirstValueIndex(p)))); + } catch (ArithmeticException e) { + warnings().registerException(e); + result.appendNull(); + } + } + return result.build(); + } + } + + public DoubleBlock eval(int positionCount, DoubleVector dVector, IntVector scaleFactorVector) { + try(DoubleBlock.Builder result = driverContext.blockFactory().newDoubleBlockBuilder(positionCount)) { + position: for (int p = 0; p < positionCount; p++) { + try { + result.appendDouble(Scalb.process(dVector.getDouble(p), scaleFactorVector.getInt(p))); + } catch (ArithmeticException e) { + warnings().registerException(e); + result.appendNull(); + } + } + return result.build(); + } + } + + @Override + public String toString() { + return "ScalbIntEvaluator[" + "d=" + d + ", scaleFactor=" + scaleFactor + "]"; + } + + @Override + public void close() { + Releasables.closeExpectNoException(d, scaleFactor); + } + + private Warnings warnings() { + if (warnings == null) { + this.warnings = Warnings.createWarnings( + driverContext.warningsMode(), + source.source().getLineNumber(), + source.source().getColumnNumber(), + source.text() + ); + } + return warnings; + } + + static class Factory implements EvalOperator.ExpressionEvaluator.Factory { + private final Source source; + + private final EvalOperator.ExpressionEvaluator.Factory d; + + private final EvalOperator.ExpressionEvaluator.Factory scaleFactor; + + public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory d, + EvalOperator.ExpressionEvaluator.Factory scaleFactor) { + this.source = source; + this.d = d; + this.scaleFactor = scaleFactor; + } + + @Override + public ScalbIntEvaluator get(DriverContext context) { + return new ScalbIntEvaluator(source, d.get(context), scaleFactor.get(context), context); + } + + @Override + public String toString() { + return "ScalbIntEvaluator[" + "d=" + d + ", scaleFactor=" + scaleFactor + "]"; + } + } +} diff --git a/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/math/ScalbLongEvaluator.java b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/math/ScalbLongEvaluator.java new file mode 100644 index 0000000000000..7e24b28332dc3 --- /dev/null +++ b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/math/ScalbLongEvaluator.java @@ -0,0 +1,159 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License +// 2.0; you may not use this file except in compliance with the Elastic License +// 2.0. +package org.elasticsearch.xpack.esql.expression.function.scalar.math; + +import java.lang.ArithmeticException; +import java.lang.IllegalArgumentException; +import java.lang.Override; +import java.lang.String; +import org.elasticsearch.compute.data.Block; +import org.elasticsearch.compute.data.DoubleBlock; +import org.elasticsearch.compute.data.DoubleVector; +import org.elasticsearch.compute.data.LongBlock; +import org.elasticsearch.compute.data.LongVector; +import org.elasticsearch.compute.data.Page; +import org.elasticsearch.compute.operator.DriverContext; +import org.elasticsearch.compute.operator.EvalOperator; +import org.elasticsearch.compute.operator.Warnings; +import org.elasticsearch.core.Releasables; +import org.elasticsearch.xpack.esql.core.tree.Source; + +/** + * {@link EvalOperator.ExpressionEvaluator} implementation for {@link Scalb}. + * This class is generated. Edit {@code EvaluatorImplementer} instead. + */ +public final class ScalbLongEvaluator implements EvalOperator.ExpressionEvaluator { + private final Source source; + + private final EvalOperator.ExpressionEvaluator d; + + private final EvalOperator.ExpressionEvaluator scaleFactor; + + private final DriverContext driverContext; + + private Warnings warnings; + + public ScalbLongEvaluator(Source source, EvalOperator.ExpressionEvaluator d, + EvalOperator.ExpressionEvaluator scaleFactor, DriverContext driverContext) { + this.source = source; + this.d = d; + this.scaleFactor = scaleFactor; + this.driverContext = driverContext; + } + + @Override + public Block eval(Page page) { + try (DoubleBlock dBlock = (DoubleBlock) d.eval(page)) { + try (LongBlock scaleFactorBlock = (LongBlock) scaleFactor.eval(page)) { + DoubleVector dVector = dBlock.asVector(); + if (dVector == null) { + return eval(page.getPositionCount(), dBlock, scaleFactorBlock); + } + LongVector scaleFactorVector = scaleFactorBlock.asVector(); + if (scaleFactorVector == null) { + return eval(page.getPositionCount(), dBlock, scaleFactorBlock); + } + return eval(page.getPositionCount(), dVector, scaleFactorVector); + } + } + } + + public DoubleBlock eval(int positionCount, DoubleBlock dBlock, LongBlock scaleFactorBlock) { + try(DoubleBlock.Builder result = driverContext.blockFactory().newDoubleBlockBuilder(positionCount)) { + position: for (int p = 0; p < positionCount; p++) { + if (dBlock.isNull(p)) { + result.appendNull(); + continue position; + } + if (dBlock.getValueCount(p) != 1) { + if (dBlock.getValueCount(p) > 1) { + warnings().registerException(new IllegalArgumentException("single-value function encountered multi-value")); + } + result.appendNull(); + continue position; + } + if (scaleFactorBlock.isNull(p)) { + result.appendNull(); + continue position; + } + if (scaleFactorBlock.getValueCount(p) != 1) { + if (scaleFactorBlock.getValueCount(p) > 1) { + warnings().registerException(new IllegalArgumentException("single-value function encountered multi-value")); + } + result.appendNull(); + continue position; + } + try { + result.appendDouble(Scalb.process(dBlock.getDouble(dBlock.getFirstValueIndex(p)), scaleFactorBlock.getLong(scaleFactorBlock.getFirstValueIndex(p)))); + } catch (ArithmeticException e) { + warnings().registerException(e); + result.appendNull(); + } + } + return result.build(); + } + } + + public DoubleBlock eval(int positionCount, DoubleVector dVector, LongVector scaleFactorVector) { + try(DoubleBlock.Builder result = driverContext.blockFactory().newDoubleBlockBuilder(positionCount)) { + position: for (int p = 0; p < positionCount; p++) { + try { + result.appendDouble(Scalb.process(dVector.getDouble(p), scaleFactorVector.getLong(p))); + } catch (ArithmeticException e) { + warnings().registerException(e); + result.appendNull(); + } + } + return result.build(); + } + } + + @Override + public String toString() { + return "ScalbLongEvaluator[" + "d=" + d + ", scaleFactor=" + scaleFactor + "]"; + } + + @Override + public void close() { + Releasables.closeExpectNoException(d, scaleFactor); + } + + private Warnings warnings() { + if (warnings == null) { + this.warnings = Warnings.createWarnings( + driverContext.warningsMode(), + source.source().getLineNumber(), + source.source().getColumnNumber(), + source.text() + ); + } + return warnings; + } + + static class Factory implements EvalOperator.ExpressionEvaluator.Factory { + private final Source source; + + private final EvalOperator.ExpressionEvaluator.Factory d; + + private final EvalOperator.ExpressionEvaluator.Factory scaleFactor; + + public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory d, + EvalOperator.ExpressionEvaluator.Factory scaleFactor) { + this.source = source; + this.d = d; + this.scaleFactor = scaleFactor; + } + + @Override + public ScalbLongEvaluator get(DriverContext context) { + return new ScalbLongEvaluator(source, d.get(context), scaleFactor.get(context), context); + } + + @Override + public String toString() { + return "ScalbLongEvaluator[" + "d=" + d + ", scaleFactor=" + scaleFactor + "]"; + } + } +} diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java index 07990a72e99cc..133cd55312ead 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java @@ -213,6 +213,11 @@ public enum Cap { */ FN_ROUND_UL_FIXES, + /** + * Support for function {@code SCALB}. + */ + FN_SCALB, + /** * Fixes for multiple functions not serializing their source, and emitting warnings with wrong line number and text. */ diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/ExpressionWritables.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/ExpressionWritables.java index 4ffa778b8287a..7e17d25f67e72 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/ExpressionWritables.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/ExpressionWritables.java @@ -45,6 +45,7 @@ import org.elasticsearch.xpack.esql.expression.function.scalar.math.Exp; import org.elasticsearch.xpack.esql.expression.function.scalar.math.Floor; import org.elasticsearch.xpack.esql.expression.function.scalar.math.Log10; +import org.elasticsearch.xpack.esql.expression.function.scalar.math.Scalb; import org.elasticsearch.xpack.esql.expression.function.scalar.math.Signum; import org.elasticsearch.xpack.esql.expression.function.scalar.math.Sin; import org.elasticsearch.xpack.esql.expression.function.scalar.math.Sinh; @@ -169,6 +170,7 @@ public static List unaryScalars() { entries.add(Not.ENTRY); entries.add(RLike.ENTRY); entries.add(RTrim.ENTRY); + entries.add(Scalb.ENTRY); entries.add(Signum.ENTRY); entries.add(Sin.ENTRY); entries.add(Sinh.ENTRY); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java index b2d85d809c058..67d11197efe5e 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java @@ -100,6 +100,7 @@ import org.elasticsearch.xpack.esql.expression.function.scalar.math.Pi; import org.elasticsearch.xpack.esql.expression.function.scalar.math.Pow; import org.elasticsearch.xpack.esql.expression.function.scalar.math.Round; +import org.elasticsearch.xpack.esql.expression.function.scalar.math.Scalb; import org.elasticsearch.xpack.esql.expression.function.scalar.math.Signum; import org.elasticsearch.xpack.esql.expression.function.scalar.math.Sin; import org.elasticsearch.xpack.esql.expression.function.scalar.math.Sinh; @@ -325,6 +326,7 @@ private static FunctionDefinition[][] functions() { def(Pi.class, Pi::new, "pi"), def(Pow.class, Pow::new, "pow"), def(Round.class, Round::new, "round"), + def(Scalb.class, Scalb::new, "scalb"), def(Signum.class, Signum::new, "signum"), def(Sin.class, Sin::new, "sin"), def(Sinh.class, Sinh::new, "sinh"), diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/Round.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/Round.java index 7c977fd1ce5a6..70882573b518d 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/Round.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/Round.java @@ -47,7 +47,6 @@ public class Round extends EsqlScalarFunction implements OptionalArgument { private final Expression field, decimals; - // @TODO: add support for "integer", "long", "unsigned_long" once tests are fixed @FunctionInfo(returnType = { "double", "integer", "long", "unsigned_long" }, description = """ Rounds a number to the specified number of decimal places. Defaults to 0, which returns the nearest integer. If the diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/Scalb.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/Scalb.java new file mode 100644 index 0000000000000..fea0e284b5545 --- /dev/null +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/Scalb.java @@ -0,0 +1,162 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.esql.expression.function.scalar.math; + +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.compute.ann.Evaluator; +import org.elasticsearch.compute.ann.Fixed; +import org.elasticsearch.compute.operator.EvalOperator.ExpressionEvaluator; +import org.elasticsearch.xpack.esql.core.expression.Expression; +import org.elasticsearch.xpack.esql.core.tree.NodeInfo; +import org.elasticsearch.xpack.esql.core.tree.Source; +import org.elasticsearch.xpack.esql.core.type.DataType; +import org.elasticsearch.xpack.esql.core.util.NumericUtils; +import org.elasticsearch.xpack.esql.expression.function.Example; +import org.elasticsearch.xpack.esql.expression.function.FunctionInfo; +import org.elasticsearch.xpack.esql.expression.function.Param; +import org.elasticsearch.xpack.esql.expression.function.scalar.EsqlScalarFunction; +import org.elasticsearch.xpack.esql.io.stream.PlanStreamInput; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.FIRST; +import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.SECOND; +import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isNumeric; + +public class Scalb extends EsqlScalarFunction { + public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(Expression.class, "Scalb", Scalb::new); + + private final Expression d; + private final Expression scaleFactor; + + @FunctionInfo( + returnType = "double", + description = "Returns the result of `d * 2 ^ scaleFactor`.", + note = "Similar to Java's `scalb` function. Result is rounded as if performed by a single " + + "correctly rounded floating-point multiply to a member of the double value set.", + examples = { @Example(file = "math", tag = "ScalbDI"), @Example(file = "math", tag = "ScalbID-sqrt", description = """ + The scaleFactor can be a fraction, which is similar to performing a root. + For example, the scaleFactor of `0.5` will give the square root of the d:"""), } + ) + public Scalb( + Source source, + @Param( + name = "d", + type = { "double", "integer", "long", "unsigned_long" }, + description = "Numeric expression for the multiplier. If `null`, the function returns `null`." + ) Expression d, + @Param( + name = "scaleFactor", + type = { "integer", "long" }, + description = "Numeric expression for the scale factor. If `null`, the function returns `null`." + ) Expression scaleFactor + ) { + super(source, Arrays.asList(d, scaleFactor)); + this.d = d; + this.scaleFactor = scaleFactor; + } + + private Scalb(StreamInput in) throws IOException { + this(Source.readFrom((PlanStreamInput) in), in.readNamedWriteable(Expression.class), in.readNamedWriteable(Expression.class)); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + source().writeTo(out); + out.writeNamedWriteable(d); + out.writeNamedWriteable(scaleFactor); + } + + @Override + public String getWriteableName() { + return ENTRY.name; + } + + @Override + protected TypeResolution resolveType() { + if (childrenResolved() == false) { + return new TypeResolution("Unresolved children"); + } + + TypeResolution resolution = isNumeric(d, sourceText(), FIRST); + if (resolution.unresolved()) { + return resolution; + } + + return isNumeric(scaleFactor, sourceText(), SECOND); + } + + @Override + public boolean foldable() { + return d.foldable() && scaleFactor.foldable(); + } + + @Evaluator(extraName = "Int", warnExceptions = { ArithmeticException.class }) + static double process(double d, int scaleFactor) { + return NumericUtils.asFiniteNumber(Math.scalb(d, scaleFactor)); + } + + @Evaluator(extraName = "Long", warnExceptions = { ArithmeticException.class }) + static double process(double d, long scaleFactor) { + return NumericUtils.asFiniteNumber(Math.scalb(d, Math.toIntExact(scaleFactor))); + } + + @Evaluator(extraName = "Constant", warnExceptions = { ArithmeticException.class }) + static double processConstant(double d, @Fixed int scaleFactor) { + return NumericUtils.asFiniteNumber(Math.scalb(d, scaleFactor)); + } + + @Override + public final Expression replaceChildren(List newChildren) { + return new Scalb(source(), newChildren.get(0), newChildren.get(1)); + } + + @Override + protected NodeInfo info() { + return NodeInfo.create(this, Scalb::new, d(), scaleFactor()); + } + + public Expression d() { + return d; + } + + public Expression scaleFactor() { + return scaleFactor; + } + + @Override + public DataType dataType() { + return DataType.DOUBLE; + } + + @Override + public ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvaluator) { + var dEval = Cast.cast(source(), d.dataType(), DataType.DOUBLE, toEvaluator.apply(d)); + if (scaleFactor.foldable()) { + Integer factor = switch (scaleFactor.dataType()) { + case DataType.INTEGER -> (Integer) scaleFactor.fold(toEvaluator.foldCtx()); + case DataType.LONG -> Math.toIntExact((Long) scaleFactor.fold(toEvaluator.foldCtx())); + default -> throw new IllegalStateException("Invalid type for scaleFactor, should be int or long."); + }; + return new ScalbConstantEvaluator.Factory(source(), dEval, factor); + } + var scaleFactorEval = toEvaluator.apply(scaleFactor); + return switch (scaleFactor.dataType()) { + case DataType.INTEGER -> new ScalbIntEvaluator.Factory(source(), dEval, scaleFactorEval); + case DataType.LONG -> new ScalbLongEvaluator.Factory(source(), dEval, scaleFactorEval); + case DataType type -> throw new IllegalStateException( + String.format("Invalid type for scaleFactor: %s, should be int or long.", type) + ); + }; + + } +} diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/ScalbTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/ScalbTests.java new file mode 100644 index 0000000000000..5ae4946a22c18 --- /dev/null +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/ScalbTests.java @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.esql.expression.function.scalar.math; + +import com.carrotsearch.randomizedtesting.annotations.Name; +import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; + +import org.elasticsearch.common.Strings; +import org.elasticsearch.xpack.esql.core.expression.Expression; +import org.elasticsearch.xpack.esql.core.tree.Source; +import org.elasticsearch.xpack.esql.core.type.DataType; +import org.elasticsearch.xpack.esql.expression.function.AbstractScalarFunctionTestCase; +import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier; + +import java.util.List; +import java.util.function.Supplier; + +import static org.hamcrest.Matchers.equalTo; + +public class ScalbTests extends AbstractScalarFunctionTestCase { + public ScalbTests(@Name("TestCase") Supplier testCaseSupplier) { + this.testCase = testCaseSupplier.get(); + } + + @ParametersFactory + public static Iterable parameters() { + return parameterSuppliersFromTypedDataWithDefaultChecksNoErrors( + true, + List.of( + // Int scale factor + test(DataType.DOUBLE, DataType.INTEGER, 3.14d, 6, 200.96d), + test(DataType.INTEGER, DataType.INTEGER, 666, 6, 42624.0), + test(DataType.LONG, DataType.INTEGER, 23L, 5, 736.d), + test(DataType.UNSIGNED_LONG, DataType.INTEGER, 24325342L, 34, 1.584563250289466E29), + + // Long scale factor + test(DataType.DOUBLE, DataType.LONG, 3.14d, 6L, 200.96d), + test(DataType.INTEGER, DataType.LONG, 666, 6L, 42624.0), + test(DataType.LONG, DataType.LONG, 23L, 5L, 736.d), + test(DataType.UNSIGNED_LONG, DataType.LONG, 24325342L, 34L, 1.584563250289466E29), + + // null values + test(DataType.LONG, DataType.LONG, null, 34L, null), + test(DataType.LONG, DataType.LONG, 23L, null, null), + + // Overflow + test( + DataType.LONG, + DataType.LONG, + 24325342L, + 344444L, + null, + List.of( + "Line 1:1: evaluation of [source] failed, treating result as null. Only first 20 failures recorded.", + "Line 1:1: java.lang.ArithmeticException: not a finite double number: Infinity" + ) + ) + ) + ); + } + + private static TestCaseSupplier test(DataType dType, DataType scaleType, Number d, Number scaleFactor, Number res) { + return test(dType, scaleType, d, scaleFactor, res, List.of()); + } + + private static TestCaseSupplier test( + DataType dType, + DataType scaleType, + Number d, + Number scaleFactor, + Number res, + Iterable warns + ) { + var supplier = new TestCaseSupplier.TestCase( + List.of(new TestCaseSupplier.TypedData(d, dType, "d"), new TestCaseSupplier.TypedData(scaleFactor, scaleType, "scaleFactor")), + String.format( + "Scalb%sEvaluator[d=%s, scaleFactor=Attribute[channel=1]]", + pascalize(scaleType), + cast(dType) + + ), + DataType.DOUBLE, + equalTo(res) + ); + for (var warn : warns) { + supplier = supplier.withWarning(warn); + } + var finalSupplier = supplier; + return new TestCaseSupplier( + String.format("<%s>, <%s>", dType.typeName(), scaleType.typeName()), + List.of(dType, scaleType), + () -> finalSupplier + ); + } + + private static String cast(DataType from) { + return from == DataType.DOUBLE + ? "Attribute[channel=0]" + : String.format("Cast%sToDoubleEvaluator[v=Attribute[channel=0]]", pascalize(from)); + } + + private static String pascalize(DataType type) { + return switch (type) { + case DataType.UNSIGNED_LONG -> "UnsignedLong"; + case DataType.INTEGER -> "Int"; + default -> Strings.capitalize(type.typeName()); + }; + } + + @Override + protected Expression build(Source source, List args) { + return new Scalb(source, args.get(0), args.get(1)); + } +}