Sfoglia il codice sorgente

ESQL: Add `CEIL` function (#98847)

Add the unary scalar function CEIL.

Analogously to FLOOR,  it rounds up its argument.

- Implement CEIL, add it to the function registry and make sure it is serializable.
- Add csv tests, unit tests and docs.
- Add additional csv tests with different data types and some edge cases for both CEIL and FLOOR
- Add unit tests and update docs for FLOOR.
Alexander Spies 2 anni fa
parent
commit
ca3dc3a882

+ 5 - 0
docs/changelog/98847.yaml

@@ -0,0 +1,5 @@
+pr: 98847
+summary: "ESQL: Add `CEIL` function"
+area: ES|QL
+type: enhancement
+issues: []

+ 2 - 0
docs/reference/esql/esql-functions.asciidoc

@@ -15,6 +15,7 @@ these functions:
 * <<esql-atan2>>
 * <<esql-auto_bucket>>
 * <<esql-case>>
+* <<esql-ceil>>
 * <<esql-cidr_match>>
 * <<esql-coalesce>>
 * <<esql-concat>>
@@ -75,6 +76,7 @@ include::functions/atan.asciidoc[]
 include::functions/atan2.asciidoc[]
 include::functions/auto_bucket.asciidoc[]
 include::functions/case.asciidoc[]
+include::functions/ceil.asciidoc[]
 include::functions/cidr_match.asciidoc[]
 include::functions/coalesce.asciidoc[]
 include::functions/concat.asciidoc[]

+ 22 - 0
docs/reference/esql/functions/ceil.asciidoc

@@ -0,0 +1,22 @@
+[[esql-ceil]]
+=== `CEIL`
+[.text-center]
+image::signature/ceil.svg[]
+Round a number up to the nearest integer.
+
+[source.merge.styled,esql]
+----
+include::{esql-specs}/math.csv-spec[tag=ceil]
+----
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+include::{esql-specs}/math.csv-spec[tag=ceil-result]
+|===
+
+NOTE: This is a noop for `long` (including unsigned) and `integer`.
+      For `double` this picks the the closest `double` value to the integer ala
+      {javadoc}/java.base/java/lang/Math.html#ceil(double)[Math.ceil].
+
+Supported types:
+
+include::types/ceil.asciidoc[]

+ 9 - 2
docs/reference/esql/functions/floor.asciidoc

@@ -1,5 +1,8 @@
 [[esql-floor]]
 === `FLOOR`
+[.text-center]
+image::signature/floor.svg[]
+
 Round a number down to the nearest integer.
 
 [source.merge.styled,esql]
@@ -11,6 +14,10 @@ include::{esql-specs}/math.csv-spec[tag=floor]
 include::{esql-specs}/math.csv-spec[tag=floor-result]
 |===
 
-NOTE: This is a noop for `long` and `integer`. For `double` this picks the
-      the closest `double` value to the integer ala
+NOTE: This is a noop for `long` (including unsigned) and `integer`.
+      For `double` this picks the the closest `double` value to the integer ala
       {javadoc}/java.base/java/lang/Math.html#floor(double)[Math.floor].
+
+Supported types:
+
+include::types/floor.asciidoc[]

+ 1 - 0
docs/reference/esql/functions/signature/ceil.svg

@@ -0,0 +1 @@
+<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="226" height="43" viewbox="0 0 226 43"><defs><style type="text/css">.c{fill:none;stroke:#222222;}.k{fill:#000000;font-family:Dialog,Sans-serif;font-size:20px;}.s{fill:#e4f4ff;stroke:#222222;}.syn{fill:#8D8D8D;font-family:Dialog,Sans-serif;font-size:20px;}</style></defs><path class="c" d="M0 28h5m64 0h10m28 0h10m66 0h10m28 0h5"/><rect class="s" x="5" y="5" width="64" height="33"/><text class="k" x="15" y="28">CEIL</text><rect class="s" x="79" y="5" width="28" height="33" rx="7"/><text class="syn" x="89" y="28">(</text><rect class="s" x="117" y="5" width="66" height="33" rx="7"/><text class="k" x="127" y="28">arg1</text><rect class="s" x="193" y="5" width="28" height="33" rx="7"/><text class="syn" x="203" y="28">)</text></svg>

+ 8 - 0
docs/reference/esql/functions/types/ceil.asciidoc

@@ -0,0 +1,8 @@
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+arg1 | result
+double | double
+integer | integer
+long | long
+unsigned_long | unsigned_long
+|===

+ 3 - 0
docs/reference/esql/functions/types/floor.asciidoc

@@ -2,4 +2,7 @@
 |===
 arg1 | result
 double | double
+integer | integer
+long | long
+unsigned_long | unsigned_long
 |===

+ 51 - 1
x-pack/plugin/esql/qa/testFixtures/src/main/resources/math.csv-spec

@@ -930,9 +930,23 @@ TAU():double
 // end::tau-result[]
 ;
 
+ceil
+// tag::ceil[]
+ROW a=1.8
+| EVAL a=CEIL(a)
+// end::ceil[]
+;
+
+// tag::ceil-result[]
+a:double
+2
+// end::ceil-result[]
+;
+
 floor
 // tag::floor[]
-ROW a=1.8 | EVAL a=FLOOR(a)
+ROW a=1.8
+| EVAL a=FLOOR(a)
 // end::floor[]
 ;
 
@@ -942,6 +956,42 @@ a:double
 // end::floor-result[]
 ;
 
+ceilFloorOfInfinite
+row i = 1.0/0.0 | eval c = ceil(i), f = floor(i);
+
+i:double | c:double | f:double
+Infinity | Infinity | Infinity
+;
+
+ceilFloorOfNegativeInfinite
+row i = -1.0/0.0 | eval c = ceil(i), f = floor(i);
+
+i:double  | c:double  | f:double
+-Infinity | -Infinity | -Infinity
+;
+
+
+ceilFloorOfInteger
+row i = 1 | eval c = ceil(i), f = floor(i);
+
+i:integer | c:integer | f:integer
+1         | 1         | 1
+;
+
+ceilFloorOfLong
+row i = to_long(1000000000000) | eval c = ceil(i), f = floor(i);
+
+i:long        | c:long        | f:long
+1000000000000 | 1000000000000 | 1000000000000
+;
+
+ceilFloorOfUnsignedLong
+row i = to_ul(1000000000000000000) | eval c = ceil(i), f = floor(i);
+
+i:ul                | c:ul                | f:ul
+1000000000000000000 | 1000000000000000000 | 1000000000000000000
+;
+
 sqrt
 // tag::sqrt[]
 ROW d = 100.0

+ 1 - 0
x-pack/plugin/esql/qa/testFixtures/src/main/resources/show.csv-spec

@@ -17,6 +17,7 @@ atan2                    |atan2(arg1, arg2)
 auto_bucket              |auto_bucket(arg1, arg2, arg3, arg4)
 avg                      |avg(arg1)
 case                     |case(arg1, arg2...)
+ceil                     |ceil(arg1)
 cidr_match               |cidr_match(arg1, arg2...)
 coalesce                 |coalesce(arg1, arg2...)
 concat                   |concat(arg1, arg2...)

+ 64 - 0
x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/math/CeilDoubleEvaluator.java

@@ -0,0 +1,64 @@
+// 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.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.EvalOperator;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link Ceil}.
+ * This class is generated. Do not edit it.
+ */
+public final class CeilDoubleEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final EvalOperator.ExpressionEvaluator val;
+
+  public CeilDoubleEvaluator(EvalOperator.ExpressionEvaluator val) {
+    this.val = val;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    Block valUncastBlock = val.eval(page);
+    if (valUncastBlock.areAllValuesNull()) {
+      return Block.constantNullBlock(page.getPositionCount());
+    }
+    DoubleBlock valBlock = (DoubleBlock) valUncastBlock;
+    DoubleVector valVector = valBlock.asVector();
+    if (valVector == null) {
+      return eval(page.getPositionCount(), valBlock);
+    }
+    return eval(page.getPositionCount(), valVector).asBlock();
+  }
+
+  public DoubleBlock eval(int positionCount, DoubleBlock valBlock) {
+    DoubleBlock.Builder result = DoubleBlock.newBlockBuilder(positionCount);
+    position: for (int p = 0; p < positionCount; p++) {
+      if (valBlock.isNull(p) || valBlock.getValueCount(p) != 1) {
+        result.appendNull();
+        continue position;
+      }
+      result.appendDouble(Ceil.process(valBlock.getDouble(valBlock.getFirstValueIndex(p))));
+    }
+    return result.build();
+  }
+
+  public DoubleVector eval(int positionCount, DoubleVector valVector) {
+    DoubleVector.Builder result = DoubleVector.newVectorBuilder(positionCount);
+    position: for (int p = 0; p < positionCount; p++) {
+      result.appendDouble(Ceil.process(valVector.getDouble(p)));
+    }
+    return result.build();
+  }
+
+  @Override
+  public String toString() {
+    return "CeilDoubleEvaluator[" + "val=" + val + "]";
+  }
+}

+ 2 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java

@@ -42,6 +42,7 @@ import org.elasticsearch.xpack.esql.expression.function.scalar.math.Asin;
 import org.elasticsearch.xpack.esql.expression.function.scalar.math.Atan;
 import org.elasticsearch.xpack.esql.expression.function.scalar.math.Atan2;
 import org.elasticsearch.xpack.esql.expression.function.scalar.math.AutoBucket;
+import org.elasticsearch.xpack.esql.expression.function.scalar.math.Ceil;
 import org.elasticsearch.xpack.esql.expression.function.scalar.math.Cos;
 import org.elasticsearch.xpack.esql.expression.function.scalar.math.Cosh;
 import org.elasticsearch.xpack.esql.expression.function.scalar.math.E;
@@ -112,6 +113,7 @@ public class EsqlFunctionRegistry extends FunctionRegistry {
                 def(Atan.class, Atan::new, "atan"),
                 def(Atan2.class, Atan2::new, "atan2"),
                 def(AutoBucket.class, AutoBucket::new, "auto_bucket"),
+                def(Ceil.class, Ceil::new, "ceil"),
                 def(Cos.class, Cos::new, "cos"),
                 def(Cosh.class, Cosh::new, "cosh"),
                 def(E.class, E::new, "e"),

+ 76 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/Ceil.java

@@ -0,0 +1,76 @@
+/*
+ * 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.compute.ann.Evaluator;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.xpack.esql.evaluator.mapper.EvaluatorMapper;
+import org.elasticsearch.xpack.esql.expression.function.scalar.UnaryScalarFunction;
+import org.elasticsearch.xpack.ql.expression.Expression;
+import org.elasticsearch.xpack.ql.tree.NodeInfo;
+import org.elasticsearch.xpack.ql.tree.Source;
+
+import java.util.List;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+import static org.elasticsearch.xpack.ql.expression.TypeResolutions.ParamOrdinal.DEFAULT;
+import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isNumeric;
+
+/**
+ * Round a number up to the nearest integer.
+ * <p>
+ *     Note that doubles are rounded up to the nearest valid double that is
+ *     an integer ala {@link Math#ceil}.
+ * </p>
+ */
+public class Ceil extends UnaryScalarFunction implements EvaluatorMapper {
+    public Ceil(Source source, Expression field) {
+        super(source, field);
+    }
+
+    @Override
+    public Supplier<EvalOperator.ExpressionEvaluator> toEvaluator(
+        Function<Expression, Supplier<EvalOperator.ExpressionEvaluator>> toEvaluator
+    ) {
+        if (dataType().isInteger()) {
+            return toEvaluator.apply(field());
+        }
+        Supplier<EvalOperator.ExpressionEvaluator> fieldEval = toEvaluator.apply(field());
+        return () -> new CeilDoubleEvaluator(fieldEval.get());
+    }
+
+    @Override
+    public Object fold() {
+        return EvaluatorMapper.super.fold();
+    }
+
+    @Override
+    protected TypeResolution resolveType() {
+        if (childrenResolved() == false) {
+            return new TypeResolution("Unresolved children");
+        }
+
+        return isNumeric(field, sourceText(), DEFAULT);
+    }
+
+    @Override
+    public Expression replaceChildren(List<Expression> newChildren) {
+        return new Ceil(source(), newChildren.get(0));
+    }
+
+    @Override
+    protected NodeInfo<? extends Expression> info() {
+        return NodeInfo.create(this, Ceil::new, field());
+    }
+
+    @Evaluator(extraName = "Double")
+    static double process(double val) {
+        return Math.ceil(val);
+    }
+}

+ 14 - 3
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/package-info.java

@@ -38,9 +38,11 @@
  *         Open Elasticsearch in IntelliJ.
  *     </li>
  *     <li>
- *         Open {@code x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/CsvTests.java}
- *         and run it. IntelliJ will take a few minutes to compile everything but the test itself
- *         should take only a few seconds. This is a fast path to running ESQL's integration tests.
+ *         Run the csv tests (see {@code x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/CsvTests.java})
+ *         from within Intellij or, alternatively, via Gradle:
+ *         {@code ./gradlew -p x-pack/plugin/esql test --tests "org.elasticsearch.xpack.esql.CsvTests"}
+ *         IntelliJ will take a few minutes to compile everything but the test itself should take only a few seconds.
+ *         This is a fast path to running ESQL's integration tests.
  *     </li>
  *     <li>
  *         Pick one of the csv-spec files in {@code x-pack/plugin/esql/qa/testFixtures/src/main/resources/}
@@ -121,6 +123,15 @@
  *         asciidoc ceremony to make the result look right in the rendered docs.
  *     </li>
  *     <li>
+ *         Auto-generate a syntax diagram and a table with supported types by running
+ *         {@code ./gradlew x-pack:plugin:esql:copyGeneratedDocs}
+ *         The generated files can be found here
+ *         {@code docs/reference/esql/functions/signature/myfunction.svg }
+ *         and here
+ *         {@code docs/reference/esql/functions/types/myfunction.asciidoc}
+ *         Make sure to commit them and reference them in your doc file.
+ *     </li>
+ *     <li>
  *          Build the docs by cloning the <a href="https://github.com/elastic/docs">docs repo</a>
  *          and running:
  *          <pre>{@code

+ 3 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/io/stream/PlanNamedTypes.java

@@ -52,6 +52,7 @@ import org.elasticsearch.xpack.esql.expression.function.scalar.math.Asin;
 import org.elasticsearch.xpack.esql.expression.function.scalar.math.Atan;
 import org.elasticsearch.xpack.esql.expression.function.scalar.math.Atan2;
 import org.elasticsearch.xpack.esql.expression.function.scalar.math.AutoBucket;
+import org.elasticsearch.xpack.esql.expression.function.scalar.math.Ceil;
 import org.elasticsearch.xpack.esql.expression.function.scalar.math.Cos;
 import org.elasticsearch.xpack.esql.expression.function.scalar.math.Cosh;
 import org.elasticsearch.xpack.esql.expression.function.scalar.math.E;
@@ -294,6 +295,7 @@ public final class PlanNamedTypes {
             of(ESQL_UNARY_SCLR_CLS, Acos.class, PlanNamedTypes::writeESQLUnaryScalar, PlanNamedTypes::readESQLUnaryScalar),
             of(ESQL_UNARY_SCLR_CLS, Asin.class, PlanNamedTypes::writeESQLUnaryScalar, PlanNamedTypes::readESQLUnaryScalar),
             of(ESQL_UNARY_SCLR_CLS, Atan.class, PlanNamedTypes::writeESQLUnaryScalar, PlanNamedTypes::readESQLUnaryScalar),
+            of(ESQL_UNARY_SCLR_CLS, Ceil.class, PlanNamedTypes::writeESQLUnaryScalar, PlanNamedTypes::readESQLUnaryScalar),
             of(ESQL_UNARY_SCLR_CLS, Cos.class, PlanNamedTypes::writeESQLUnaryScalar, PlanNamedTypes::readESQLUnaryScalar),
             of(ESQL_UNARY_SCLR_CLS, Cosh.class, PlanNamedTypes::writeESQLUnaryScalar, PlanNamedTypes::readESQLUnaryScalar),
             of(ESQL_UNARY_SCLR_CLS, Floor.class, PlanNamedTypes::writeESQLUnaryScalar, PlanNamedTypes::readESQLUnaryScalar),
@@ -1033,6 +1035,7 @@ public final class PlanNamedTypes {
         entry(name(Acos.class), Acos::new),
         entry(name(Asin.class), Asin::new),
         entry(name(Atan.class), Atan::new),
+        entry(name(Ceil.class), Ceil::new),
         entry(name(Cos.class), Cos::new),
         entry(name(Cosh.class), Cosh::new),
         entry(name(Floor.class), Floor::new),

+ 75 - 0
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/CeilTests.java

@@ -0,0 +1,75 @@
+/*
+ * 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.xpack.esql.expression.function.scalar.AbstractScalarFunctionTestCase;
+import org.elasticsearch.xpack.ql.expression.Expression;
+import org.elasticsearch.xpack.ql.tree.Source;
+import org.elasticsearch.xpack.ql.type.DataType;
+import org.elasticsearch.xpack.ql.type.DataTypes;
+
+import java.util.List;
+import java.util.function.Supplier;
+
+import static org.hamcrest.Matchers.equalTo;
+
+public class CeilTests extends AbstractScalarFunctionTestCase {
+    public CeilTests(@Name("TestCase") Supplier<TestCase> testCaseSupplier) {
+        this.testCase = testCaseSupplier.get();
+    }
+
+    @ParametersFactory
+    public static Iterable<Object[]> parameters() {
+        return parameterSuppliersFromTypedData(List.of(new TestCaseSupplier("large double value", () -> {
+            double arg = 1 / randomDouble();
+            return new TestCase(
+                List.of(new TypedData(arg, DataTypes.DOUBLE, "arg")),
+                "CeilDoubleEvaluator[val=Attribute[channel=0]]",
+                DataTypes.DOUBLE,
+                equalTo(Math.ceil(arg))
+            );
+        }), new TestCaseSupplier("integer value", () -> {
+            int arg = randomInt();
+            return new TestCase(
+                List.of(new TypedData(arg, DataTypes.INTEGER, "arg")),
+                "Attribute[channel=0]",
+                DataTypes.INTEGER,
+                equalTo(arg)
+            );
+        }), new TestCaseSupplier("long value", () -> {
+            long arg = randomLong();
+            return new TestCase(List.of(new TypedData(arg, DataTypes.LONG, "arg")), "Attribute[channel=0]", DataTypes.LONG, equalTo(arg));
+        }), new TestCaseSupplier("unsigned long value", () -> {
+            long arg = randomLong();
+            return new TestCase(
+                List.of(new TypedData(arg, DataTypes.UNSIGNED_LONG, "arg")),
+                "Attribute[channel=0]",
+                DataTypes.UNSIGNED_LONG,
+                equalTo(arg)
+            );
+        })));
+    }
+
+    @Override
+    protected DataType expectedType(List<DataType> argTypes) {
+        return argTypes.get(0);
+    }
+
+    @Override
+    protected List<ArgumentSpec> argSpec() {
+        return List.of(required(numerics()));
+    }
+
+    @Override
+    protected Expression build(Source source, List<Expression> args) {
+        return new Ceil(source, args.get(0));
+    }
+}

+ 19 - 0
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/FloorTests.java

@@ -36,6 +36,25 @@ public class FloorTests extends AbstractScalarFunctionTestCase {
                 DataTypes.DOUBLE,
                 equalTo(Math.floor(arg))
             );
+        }), new TestCaseSupplier("integer value", () -> {
+            int arg = randomInt();
+            return new TestCase(
+                List.of(new TypedData(arg, DataTypes.INTEGER, "arg")),
+                "Attribute[channel=0]",
+                DataTypes.INTEGER,
+                equalTo(arg)
+            );
+        }), new TestCaseSupplier("long value", () -> {
+            long arg = randomLong();
+            return new TestCase(List.of(new TypedData(arg, DataTypes.LONG, "arg")), "Attribute[channel=0]", DataTypes.LONG, equalTo(arg));
+        }), new TestCaseSupplier("unsigned long value", () -> {
+            long arg = randomLong();
+            return new TestCase(
+                List.of(new TypedData(arg, DataTypes.UNSIGNED_LONG, "arg")),
+                "Attribute[channel=0]",
+                DataTypes.UNSIGNED_LONG,
+                equalTo(arg)
+            );
         })));
     }