Browse Source

Log base 10 for ESQL (ESQL-1358)

Introduces a unary scalar function for base 10 log, which is a thin
wrapper over the Java.Math implementation

---------

Co-authored-by: Abdon Pijpelink <abdon.pijpelink@elastic.co>
Mark Tozzi 2 years ago
parent
commit
985b1949cb

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

@@ -23,6 +23,7 @@ these functions:
 * <<esql-is_nan>>
 * <<esql-is_null>>
 * <<esql-length>>
+* <<esql-log10>>
 * <<esql-mv_avg>>
 * <<esql-mv_concat>>
 * <<esql-mv_count>>
@@ -62,6 +63,7 @@ include::functions/is_infinite.asciidoc[]
 include::functions/is_nan.asciidoc[]
 include::functions/is_null.asciidoc[]
 include::functions/length.asciidoc[]
+include::functions/log10.asciidoc[]
 include::functions/mv_avg.asciidoc[]
 include::functions/mv_concat.asciidoc[]
 include::functions/mv_count.asciidoc[]

+ 15 - 0
docs/reference/esql/functions/log10.asciidoc

@@ -0,0 +1,15 @@
+[[esql-log10]]
+=== `LOG10`
+Returns the log base 10.  The input can be any numeric value, the return value
+is always a double.
+
+Logs of negative numbers are NaN. Logs of infinites are infinite, as is the log of 0.
+
+[source.merge.styled,esql]
+----
+include::{esql-specs}/math.csv-spec[tag=log10]
+----
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+include::{esql-specs}/math.csv-spec[tag=log10-result]
+|===

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

@@ -159,6 +159,60 @@ d:double | s:boolean
 1.0      | false
 ;
 
+isNaNTrue
+row d = 0.0/0.0 | eval s = is_nan(d);
+
+d:double | s:boolean
+NaN      | true
+;
+
+log10
+// tag::log10[]
+ROW d = 1000.0 
+| EVAL s = LOG10(d);
+// end::log10[]
+
+// tag::log10-result[]
+d: double | s:double
+1000.0    | 3.0
+// end::log10-result[]
+;
+
+log10ofNegative
+row d = -1.0 | eval s = is_nan(log10(d));
+
+d:double | s:boolean
+-1.0     | true
+;
+
+log10ofNan
+row d = 0.0/0.0 | eval s = is_nan(log10(d));
+
+d:double | s:boolean
+NaN      | true
+;
+
+log10ofZero
+row d = 0.0 |eval s = is_infinite(log10(d));
+
+d:double | s:boolean
+0.0     | true
+;
+
+log10ofNegativeZero
+row d = -0.0 |eval s = is_infinite(log10(d));
+
+d:double | s:boolean
+-0.0     | true
+;
+
+log10ofInfinite
+row d = 1/0.0 | eval s = is_infinite(log10(d));
+
+d:double | s:boolean
+Infinity | true
+;
+
 powDoubleDouble
 row base = 2.0, exponent = 2.0 | eval s = pow(base, exponent);
 

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

@@ -26,6 +26,7 @@ is_infinite              |is_infinite(arg1)
 is_nan                   |is_nan(arg1)
 is_null                  |is_null(arg1)
 length                   |length(arg1)
+log10                    |log10(arg1)
 max                      |max(arg1)
 median                   |median(arg1)
 median_absolute_deviation|median_absolute_deviation(arg1)

+ 64 - 0
x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/math/Log10Evaluator.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 Log10}.
+ * This class is generated. Do not edit it.
+ */
+public final class Log10Evaluator implements EvalOperator.ExpressionEvaluator {
+  private final EvalOperator.ExpressionEvaluator val;
+
+  public Log10Evaluator(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(Log10.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(Log10.process(valVector.getDouble(p)));
+    }
+    return result.build();
+  }
+
+  @Override
+  public String toString() {
+    return "Log10Evaluator[" + "val=" + val + "]";
+  }
+}

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

@@ -37,6 +37,7 @@ import org.elasticsearch.xpack.esql.expression.function.scalar.math.E;
 import org.elasticsearch.xpack.esql.expression.function.scalar.math.IsFinite;
 import org.elasticsearch.xpack.esql.expression.function.scalar.math.IsInfinite;
 import org.elasticsearch.xpack.esql.expression.function.scalar.math.IsNaN;
+import org.elasticsearch.xpack.esql.expression.function.scalar.math.Log10;
 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;
@@ -92,6 +93,7 @@ public class EsqlFunctionRegistry extends FunctionRegistry {
                 def(IsFinite.class, IsFinite::new, "is_finite"),
                 def(IsInfinite.class, IsInfinite::new, "is_infinite"),
                 def(IsNaN.class, IsNaN::new, "is_nan"),
+                def(Log10.class, Log10::new, "log10"),
                 def(Pi.class, Pi::new, "pi"),
                 def(Pow.class, Pow::new, "pow"),
                 def(Round.class, Round::new, "round"),

+ 1 - 1
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/UnaryScalarFunction.java

@@ -20,7 +20,7 @@ import java.util.Objects;
 import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isNumeric;
 
 public abstract class UnaryScalarFunction extends ScalarFunction {
-    private final Expression field;
+    protected final Expression field;
 
     public UnaryScalarFunction(Source source, Expression field) {
         super(source, Arrays.asList(field));

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

@@ -0,0 +1,66 @@
+/*
+ * 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.expression.function.scalar.UnaryScalarFunction;
+import org.elasticsearch.xpack.esql.planner.Mappable;
+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;
+
+public class Log10 extends UnaryScalarFunction implements Mappable {
+    public Log10(Source source, Expression field) {
+        super(source, field);
+    }
+
+    @Override
+    public Supplier<EvalOperator.ExpressionEvaluator> toEvaluator(
+        Function<Expression, Supplier<EvalOperator.ExpressionEvaluator>> toEvaluator
+    ) {
+        Supplier<EvalOperator.ExpressionEvaluator> field = toEvaluator.apply(field());
+        return () -> new Log10Evaluator(field.get());
+    }
+
+    @Evaluator
+    static double process(double val) {
+        return Math.log10(val);
+    }
+
+    @Override
+    public final Expression replaceChildren(List<Expression> newChildren) {
+        return new Log10(source(), newChildren.get(0));
+    }
+
+    @Override
+    protected NodeInfo<? extends Expression> info() {
+        return NodeInfo.create(this, Log10::new, field());
+    }
+
+    @Override
+    public Object fold() {
+        return Mappable.super.fold();
+    }
+
+    @Override
+    protected TypeResolution resolveType() {
+        if (childrenResolved() == false) {
+            return new Expression.TypeResolution("Unresolved children");
+        }
+
+        return isNumeric(field, sourceText(), DEFAULT);
+    }
+}

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

@@ -47,6 +47,7 @@ import org.elasticsearch.xpack.esql.expression.function.scalar.math.E;
 import org.elasticsearch.xpack.esql.expression.function.scalar.math.IsFinite;
 import org.elasticsearch.xpack.esql.expression.function.scalar.math.IsInfinite;
 import org.elasticsearch.xpack.esql.expression.function.scalar.math.IsNaN;
+import org.elasticsearch.xpack.esql.expression.function.scalar.math.Log10;
 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;
@@ -273,6 +274,7 @@ public final class PlanNamedTypes {
             of(ESQL_UNARY_SCLR_CLS, IsFinite.class, PlanNamedTypes::writeESQLUnaryScalar, PlanNamedTypes::readESQLUnaryScalar),
             of(ESQL_UNARY_SCLR_CLS, IsInfinite.class, PlanNamedTypes::writeESQLUnaryScalar, PlanNamedTypes::readESQLUnaryScalar),
             of(ESQL_UNARY_SCLR_CLS, IsNaN.class, PlanNamedTypes::writeESQLUnaryScalar, PlanNamedTypes::readESQLUnaryScalar),
+            of(ESQL_UNARY_SCLR_CLS, Log10.class, PlanNamedTypes::writeESQLUnaryScalar, PlanNamedTypes::readESQLUnaryScalar),
             of(ScalarFunction.class, Pi.class, PlanNamedTypes::writeNoArgScalar, PlanNamedTypes::readNoArgScalar),
             of(ESQL_UNARY_SCLR_CLS, Metadata.class, PlanNamedTypes::writeESQLUnaryScalar, PlanNamedTypes::readESQLUnaryScalar),
             of(ScalarFunction.class, Tau.class, PlanNamedTypes::writeNoArgScalar, PlanNamedTypes::readNoArgScalar),
@@ -945,6 +947,7 @@ public final class PlanNamedTypes {
         entry(name(IsNaN.class), IsNaN::new),
         entry(name(Length.class), Length::new),
         entry(name(Metadata.class), Metadata::new),
+        entry(name(Log10.class), Log10::new),
         entry(name(ToBoolean.class), ToBoolean::new),
         entry(name(ToDatetime.class), ToDatetime::new),
         entry(name(ToDouble.class), ToDouble::new),

+ 27 - 1
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/AbstractFunctionTestCase.java

@@ -43,7 +43,8 @@ import static org.elasticsearch.xpack.esql.SerializationTestUtils.assertSerializ
 import static org.hamcrest.Matchers.equalTo;
 
 /**
- * Base class for function tests.
+ * Base class for function tests.  Tests based on this class will generally build out a single example evaluation,
+ * which can be automatically tested against several scenarios (null handling, concurrency, etc).
  */
 public abstract class AbstractFunctionTestCase extends ESTestCase {
     /**
@@ -71,20 +72,45 @@ public abstract class AbstractFunctionTestCase extends ESTestCase {
         }, type);
     }
 
+    /**
+     * Used for constructing a sample data point for the function being tested.  This should return a
+     * List of arguments for the Expression, which will be used by {@link AbstractFunctionTestCase#expressionForSimpleData()}
+     * to build the actual expression
+     */
     protected abstract List<Object> simpleData();
 
+    /**
+     * Return an {@link Expression} capable of parsing the data from {@link AbstractFunctionTestCase#simpleData()}
+     */
     protected abstract Expression expressionForSimpleData();
 
     protected abstract DataType expressionForSimpleDataType();
 
+    /**
+     * Return a {@link Matcher} to validate the results of evaluating the function
+     *
+     * @param data a list of the parameters that were passed to the evaluator
+     * @return a matcher to validate correctness against the given data set
+     */
     protected abstract Matcher<Object> resultMatcher(List<Object> data, DataType dataType);
 
     protected Matcher<Object> resultMatcher(List<Object> data) {
         return resultMatcher(data, EsqlDataTypes.fromJava(data.get(0) instanceof List<?> list ? list.get(0) : data.get(0)));
     }
 
+    /**
+     * The expected results for calling {@code toString} on the {@link Expression} created by
+     * {@link AbstractFunctionTestCase#expressionForSimpleData()}.  Generally speaking, this can be implemented by returning
+     * a string literal
+     * @return The expected string representation
+     */
     protected abstract String expectedEvaluatorSimpleToString();
 
+    /**
+     * Build an {@link Expression} that operates on {@link Literal} versions of the given data
+     * @param data a list of the parameters that were passed to the evaluator
+     * @return An {@link Expression} operating only on literals
+     */
     protected abstract Expression constantFoldable(List<Object> data);
 
     protected abstract Expression build(Source source, List<Literal> args);

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

@@ -0,0 +1,68 @@
+/*
+ * 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.xpack.esql.expression.function.scalar.AbstractScalarFunctionTestCase;
+import org.elasticsearch.xpack.ql.expression.Expression;
+import org.elasticsearch.xpack.ql.expression.Literal;
+import org.elasticsearch.xpack.ql.tree.Source;
+import org.elasticsearch.xpack.ql.type.DataType;
+import org.elasticsearch.xpack.ql.type.DataTypes;
+import org.hamcrest.Matcher;
+
+import java.util.List;
+
+import static org.hamcrest.Matchers.equalTo;
+
+public class Log10Tests extends AbstractScalarFunctionTestCase {
+
+    @Override
+    protected List<Object> simpleData() {
+        return List.of(1000.0d);
+    }
+
+    @Override
+    protected Expression expressionForSimpleData() {
+        return new Log10(Source.EMPTY, field("arg", DataTypes.DOUBLE));
+    }
+
+    @Override
+    protected Matcher<Object> resultMatcher(List<Object> data, DataType dataType) {
+        return equalTo(Math.log10((Double) data.get(0)));
+    }
+
+    @Override
+    protected Matcher<Object> resultMatcher(List<Object> data) {
+        return equalTo(Math.log10((Double) data.get(0)));
+    }
+
+    @Override
+    protected String expectedEvaluatorSimpleToString() {
+        return "Log10Evaluator[val=Attribute[channel=0]]";
+    }
+
+    @Override
+    protected Expression constantFoldable(List<Object> data) {
+        return new Log10(Source.EMPTY, new Literal(Source.EMPTY, data.get(0), DataTypes.DOUBLE));
+    }
+
+    @Override
+    protected Expression build(Source source, List<Literal> args) {
+        return new Log10(source, args.get(0));
+    }
+
+    @Override
+    protected List<ArgumentSpec> argSpec() {
+        return List.of(required(numerics()));
+    }
+
+    @Override
+    protected DataType expectedType(List<DataType> argTypes) {
+        return argTypes.get(0);
+    }
+}