Browse Source

[ES|QL] Add function log(base, value) (#104913)

Add a new scalar function log
Fang Xing 1 year ago
parent
commit
0fb5dee75b

+ 48 - 0
docs/reference/esql/functions/log.asciidoc

@@ -0,0 +1,48 @@
+[discrete]
+[[esql-log]]
+=== `LOG`
+
+*Syntax*
+
+[source,esql]
+----
+LOG([base,] value)
+----
+
+*Parameters*
+
+`base`::
+Numeric expression. If `null`, the function returns `null`. The base is an optional input parameter. If a base is not provided, this function returns the natural logarithm (base e) of a value.
+
+`value`::
+Numeric expression. If `null`, the function returns `null`.
+
+*Description*
+
+Returns the logarithm of a value to a base. The input can be any numeric value, the return value is always a double.
+
+Logs of zero, negative numbers, infinites and base of one return `null` as well as a warning.
+
+*Supported types*
+
+include::types/log.asciidoc[]
+
+*Example*
+
+[source.merge.styled,esql]
+----
+include::{esql-specs}/math.csv-spec[tag=log]
+----
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+include::{esql-specs}/math.csv-spec[tag=log-result]
+|===
+
+[source.merge.styled,esql]
+----
+include::{esql-specs}/math.csv-spec[tag=logUnary]
+----
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+include::{esql-specs}/math.csv-spec[tag=logUnary-result]
+|===

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

@@ -18,6 +18,7 @@
 * <<esql-cosh>>
 * <<esql-e>>
 * <<esql-floor>>
+* <<esql-log>>
 * <<esql-log10>>
 * <<esql-pi>>
 * <<esql-pow>>
@@ -40,6 +41,7 @@ include::cos.asciidoc[]
 include::cosh.asciidoc[]
 include::e.asciidoc[]
 include::floor.asciidoc[]
+include::log.asciidoc[]
 include::log10.asciidoc[]
 include::pi.asciidoc[]
 include::pow.asciidoc[]

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

@@ -0,0 +1 @@
+<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="360" height="46" viewbox="0 0 360 46"><defs><style type="text/css">#guide .c{fill:none;stroke:#222222;}#guide .k{fill:#000000;font-family:Roboto Mono,Sans-serif;font-size:20px;}#guide .s{fill:#e4f4ff;stroke:#222222;}#guide .syn{fill:#8D8D8D;font-family:Roboto Mono,Sans-serif;font-size:20px;}</style></defs><path class="c" d="M0 31h5m56 0h10m32 0h10m68 0h10m32 0h10m80 0h10m32 0h5"/><rect class="s" x="5" y="5" width="56" height="36"/><text class="k" x="15" y="31">LOG</text><rect class="s" x="71" y="5" width="32" height="36" rx="7"/><text class="syn" x="81" y="31">(</text><rect class="s" x="113" y="5" width="68" height="36" rx="7"/><text class="k" x="123" y="31">base</text><rect class="s" x="191" y="5" width="32" height="36" rx="7"/><text class="syn" x="201" y="31">,</text><rect class="s" x="233" y="5" width="80" height="36" rx="7"/><text class="k" x="243" y="31">value</text><rect class="s" x="323" y="5" width="32" height="36" rx="7"/><text class="syn" x="333" y="31">)</text></svg>

+ 20 - 0
docs/reference/esql/functions/types/log.asciidoc

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

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

@@ -210,6 +210,307 @@ salary:integer | s:double
 73851          | 1330201
 ;
 
+log#[skip:-8.12.99,reason:new scalar function added in 8.13]
+// tag::log[]
+ROW base = 2.0, value = 8.0
+| EVAL s = LOG(base, value)
+// end::log[]
+;
+
+// tag::log-result[]
+base: double | value: double | s:double
+2.0          | 8.0           |3.0
+// end::log-result[]
+;
+
+logofNegativeValue#[skip:-8.12.99,reason:new scalar function added in 8.13]
+row base = 2.0, value = -2
+| EVAL s = LOG(base, value);
+
+warning:Line 2:12: evaluation of [LOG(base, value)] failed, treating result as null. Only first 20 failures recorded.
+warning:Line 2:12: java.lang.ArithmeticException: Log of non-positive number
+
+base: double | value: integer | s:double
+2.0          | -2             | null
+;
+
+logofNegativeBase#[skip:-8.12.99,reason:new scalar function added in 8.13]
+row base = -2, value = 2.0
+| EVAL s = LOG(base, value);
+
+warning:Line 2:12: evaluation of [LOG(base, value)] failed, treating result as null. Only first 20 failures recorded.
+warning:Line 2:12: java.lang.ArithmeticException: Log of non-positive number
+
+base: integer | value: double | s:double
+-2            | 2.0           | null
+;
+
+logofBaseOne#[skip:-8.12.99,reason:new scalar function added in 8.13]
+row base = 1, value = 2
+| EVAL s = LOG(base, value);
+
+warning:Line 2:12: evaluation of [LOG(base, value)] failed, treating result as null. Only first 20 failures recorded.
+warning:Line 2:12: java.lang.ArithmeticException: Log of base 1
+
+base: integer | value: integer | s:double
+1             | 2              | null
+;
+
+logofZero#[skip:-8.12.99,reason:new scalar function added in 8.13]
+row base = 2.0, value = 0.0
+| EVAL s = LOG(base, value);
+
+warning:Line 2:12: evaluation of [LOG(base, value)] failed, treating result as null. Only first 20 failures recorded.
+warning:Line 2:12: java.lang.ArithmeticException: Log of non-positive number
+
+base:double | value:double | s:double
+2.0         | 0.0          |null
+;
+
+logofNegativeZero#[skip:-8.12.99,reason:new scalar function added in 8.13]
+row base = 2.0, value = -0.0
+| EVAL s = LOG(base, value);
+
+warning:Line 2:12: evaluation of [LOG(base, value)] failed, treating result as null. Only first 20 failures recorded.
+warning:Line 2:12: java.lang.ArithmeticException: Log of non-positive number
+
+base:double | value:double | s:double
+2.0         | -0.0         |null
+;
+
+logofIntLong#[skip:-8.12.99,reason:new scalar function added in 8.13]
+row base = 10, value = to_long(1000000000000)
+| EVAL s = LOG(base, value);
+
+base:integer | value:long      | s:double
+10           | 1000000000000   | 12
+;
+
+logofLongInt#[skip:-8.12.99,reason:new scalar function added in 8.13]
+row base = to_long(1000000000000), value = 10
+| EVAL s = LOG(base, value);
+
+base:long             | value:integer | s:double
+1000000000000         | 10            | 0.08333333333333333
+;
+
+logofLongLong#[skip:-8.12.99,reason:new scalar function added in 8.13]
+row base = to_long(1000000000000), value = to_long(1000000000000)
+| EVAL s = LOG(base, value);
+
+base:long      | value:long     | s:double
+1000000000000  | 1000000000000  |1.0
+;
+
+logofLongDouble#[skip:-8.12.99,reason:new scalar function added in 8.13]
+row base = to_long(1000000000000), value = 10.0
+| EVAL s = LOG(base, value);
+
+base:long             | value:double | s:double
+1000000000000         | 10.0         | 0.08333333333333333
+;
+
+logofDoubleLong#[skip:-8.12.99,reason:new scalar function added in 8.13]
+row base = 10.0, value = to_long(1000000000000)
+| EVAL s = LOG(base, value);
+
+base:double | value:long     | s:double
+10.0        | 1000000000000  | 12
+;
+
+logofLongUnsignedLong#[skip:-8.12.99,reason:new scalar function added in 8.13]
+row base = to_long(1000000000000), value = to_ul(1000000000000000000)
+| EVAL s = LOG(base, value);
+
+base:long     | value:UNSIGNED_LONG | s:double
+1000000000000 | 1000000000000000000 | 1.5
+;
+
+logofUnsignedLongLong#[skip:-8.12.99,reason:new scalar function added in 8.13]
+row base = to_ul(1000000000000000000), value = to_long(1000000000000)
+| EVAL s = LOG(base, value);
+
+base:UNSIGNED_LONG  | value:long     | s:double
+1000000000000000000 | 1000000000000  | 0.6666666666666666
+;
+
+logofIntUnsignedLong#[skip:-8.12.99,reason:new scalar function added in 8.13]
+row base = 10, value = to_ul(1000000000000000000)
+| EVAL s = LOG(base, value);
+
+base:integer | value:UNSIGNED_LONG | s:double
+10           | 1000000000000000000 | 18.0
+;
+
+logofUnsignedLongInt#[skip:-8.12.99,reason:new scalar function added in 8.13]
+row base = to_ul(1000000000000000000), value = 10
+| EVAL s = LOG(base, value);
+
+base:UNSIGNED_LONG  | value:integer | s:double
+1000000000000000000 | 10            | 0.05555555555555555
+;
+
+logofUnsignedLongUnsignedLong#[skip:-8.12.99,reason:new scalar function added in 8.13]
+row base = to_ul(1000000000000000000), value = to_ul(1000000000000000000)
+| EVAL s = LOG(base, value);
+
+base:UNSIGNED_LONG  | value:UNSIGNED_LONG | s:double
+1000000000000000000 | 1000000000000000000 | 1.0
+;
+
+logofUnsignedLongDouble#[skip:-8.12.99,reason:new scalar function added in 8.13]
+row base = to_ul(1000000000000000000), value = 1000000000.0
+| EVAL s = LOG(base, value);
+
+base:UNSIGNED_LONG  | value:double | s:double
+1000000000000000000 | 1.0E9        | 0.5
+;
+
+logofDoubleUnsignedLong#[skip:-8.12.99,reason:new scalar function added in 8.13]
+row base = 10.0, value = to_ul(1000000000000000000)
+| EVAL s = LOG(base, value);
+
+base:double | value:UNSIGNED_LONG | s:double
+10.0        | 1000000000000000000 | 18.0
+;
+
+logofInt#[skip:-8.12.99,reason:new scalar function added in 8.13]
+// tag::logUnary[]
+row value = 100
+| EVAL s = LOG(value);
+// end::logUnary[]
+
+// tag::logUnary-result[]
+value: integer | s:double
+100            | 4.605170185988092
+// end::logUnary-result[]
+;
+
+logofLong#[skip:-8.12.99,reason:new scalar function added in 8.13]
+row value = to_long(1000000000000)
+| EVAL s = LOG(value);
+
+value: long   | s:double
+1000000000000 | 27.631021115928547
+;
+
+logofUnsignedLong#[skip:-8.12.99,reason:new scalar function added in 8.13]
+row value = to_ul(1000000000000000000)
+| EVAL s = LOG(value);
+
+value: unsigned_long   | s:double
+1000000000000000000    | 41.44653167389282
+;
+
+logofDouble#[skip:-8.12.99,reason:new scalar function added in 8.13]
+row value = 1000000000000.0
+| EVAL s = LOG(value);
+
+value: double   | s:double
+1000000000000.0 | 27.631021115928547
+;
+
+logofNegativeUnary#[skip:-8.12.99,reason:new scalar function added in 8.13]
+row value = -1
+| EVAL s = LOG(value);
+
+warning:Line 2:12: evaluation of [LOG(value)] failed, treating result as null. Only first 20 failures recorded.
+warning:Line 2:12: java.lang.ArithmeticException: Log of non-positive number
+
+value: integer | s:double
+-1             | null
+;
+
+logofZeroUnary#[skip:-8.12.99,reason:new scalar function added in 8.13]
+row value = 0
+| EVAL s = LOG(value);
+
+warning:Line 2:12: evaluation of [LOG(value)] failed, treating result as null. Only first 20 failures recorded.
+warning:Line 2:12: java.lang.ArithmeticException: Log of non-positive number
+
+value: integer | s:double
+0              | null
+;
+
+logofRefs#[skip:-8.12.99,reason:new scalar function added in 8.13]
+from employees
+| EVAL l1 = LOG(languages, salary)
+| EVAL l2 = LOG(avg_worked_seconds)
+| EVAL l3 = l1 + l2
+| sort emp_no
+| keep emp_no, languages, salary, avg_worked_seconds, l1, l2, l3
+| limit 5;
+
+warning:Line 2:13: evaluation of [LOG(languages, salary)] failed, treating result as null. Only first 20 failures recorded.
+warning:Line 2:13: java.lang.ArithmeticException: Log of base 1
+
+emp_no:integer | languages:integer | salary:integer | avg_worked_seconds:long | l1:double          | l2:double          | l3:double
+10001          | 2                 | 57305          | 268728049               | 15.806373402659007 | 19.409210455930772 | 35.21558385858978
+10002          | 5                 | 56371          | 328922887               | 6.797224072039991  | 19.61133389523871  | 26.408557967278703
+10003          | 4                 | 61805          | 200296405               | 7.957717967928651  | 19.115308852397096 | 27.073026820325747
+10004          | 5                 | 36174          | 311267831               | 6.5215910639530374 | 19.55616429245569  | 26.07775535640873
+10005          | 1                 | 63528          | 244294991               | null               | 19.313887032538442 | null
+;
+
+logofRefExps#[skip:-8.12.99,reason:new scalar function added in 8.13]
+from employees
+| EVAL base = languages * 2
+| EVAL l1 = LOG(base, salary * 2)
+| EVAL l2 = LOG(avg_worked_seconds + 10000) / 2
+| EVAL l3 = l1 + l2
+| sort emp_no
+| keep emp_no, languages, salary, avg_worked_seconds, l1, l2, l3
+| limit 5;
+
+emp_no:integer | languages:integer | salary:integer | avg_worked_seconds:long | l1:double          | l2:double         | l3:double
+10001          | 2                 | 57305          | 268728049               | 8.403186701329505  | 9.704623833790084 | 18.10781053511959
+10002          | 5                 | 56371          | 328922887               | 5.052085734770665  | 9.805682148519608 | 14.857767883290272
+10003          | 4                 | 61805          | 200296405               | 5.638478645285767  | 9.55767938857962  | 15.196158033865387
+10004          | 5                 | 36174          | 311267831               | 4.8594265299129775 | 9.778098209306606 | 14.637524739219582
+10005          | 1                 | 63528          | 244294991               | 16.955104980216557 | 9.656963982909355 | 26.612068963125914
+;
+
+logofSort#[skip:-8.12.99,reason:new scalar function added in 8.13]
+from employees
+| EVAL l1 = LOG(languages, salary)
+| EVAL l2 = LOG(avg_worked_seconds)
+| EVAL l3 = l1 + l2
+| sort l1 ASC nulls last, l2 DESC nulls last
+| keep l1, l2, emp_no, languages, salary, avg_worked_seconds, l3
+| limit 5;
+
+warning:Line 2:13: evaluation of [LOG(languages, salary)] failed, treating result as null. Only first 20 failures recorded.
+warning:Line 2:13: java.lang.ArithmeticException: Log of base 1
+
+l1:double         | l2:double          | emp_no:integer | languages:integer | salary:integer | avg_worked_seconds:long | l3:double
+6.300030441266983 | 19.782340222815456 | 10015          | 5                 | 25324          | 390266432               | 26.08237066408244
+6.315083118944484 | 19.132836869278762 | 10035          | 5                 | 25945          | 203838153               | 25.447919988223248
+6.428086413501791 | 19.294546217598917 | 10011          | 5                 | 31120          | 239615525               | 25.72263263110071
+6.443409317219002 | 19.704129344210816 | 10066          | 5                 | 31897          | 360906451               | 26.14753866142982
+6.450671492193451 | 19.538385836155943 | 10087          | 5                 | 32272          | 305782871               | 25.989057328349393
+;
+
+logofRenameSort#[skip:-8.12.99,reason:new scalar function added in 8.13]
+from employees
+| RENAME languages as base1
+| EVAL l1 = LOG(base1, salary)
+| EVAL l2 = LOG(avg_worked_seconds)
+| EVAL l3 = l1 + l2
+| sort l1 ASC nulls first, l2 DESC nulls first
+| keep l1, l2, emp_no, base1, salary, avg_worked_seconds, l3
+| limit 5;
+
+warning:Line 3:13: evaluation of [LOG(base1, salary)] failed, treating result as null. Only first 20 failures recorded.
+warning:Line 3:13: java.lang.ArithmeticException: Log of base 1
+
+l1:double | l2:double          | emp_no:integer | base1:integer | salary:integer | avg_worked_seconds:long | l3:double
+null      | 19.774989878141827 | 10044          | 1             | 39728          | 387408356               | null
+null      | 19.739867371666865 | 10027          | null          | 73851          | 374037782               | null
+null      | 19.73791867352969  | 10020          | null          | 40031          | 373309605               | null
+null      | 19.732442265367403 | 10025          | null          | 47411          | 371270797               | null
+null      | 19.72282600331636  | 10024          | null          | 64675          | 367717671               | null
+;
+
 log10
 // tag::log10[]
 ROW d = 1000.0 
@@ -238,7 +539,7 @@ warning:Line 1:24: evaluation of [log10(d)] failed, treating result as null. Onl
 warning:Line 1:24: java.lang.ArithmeticException: Log of non-positive number
 
 d:double | s:double
-0.0     | null
+0.0      | null
 ;
 
 log10ofNegativeZero

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

@@ -38,6 +38,7 @@ greatest                 |"integer|long|double|boolean|keyword|text|ip|version g
 least                    |"integer|long|double|boolean|keyword|text|ip|version least(first:integer|long|double|boolean|keyword|text|ip|version, rest...:integer|long|double|boolean|keyword|text|ip|version)"        |[first, rest]            |["integer|long|double|boolean|keyword|text|ip|version", "integer|long|double|boolean|keyword|text|ip|version"]            |["", ""]                                            |"integer|long|double|boolean|keyword|text|ip|version"                    | "Returns the minimum value from many columns."                      | [false, false]       | true | false
 left                     |"keyword left(str:keyword|text, length:integer)"                |[str, length]         |["keyword|text", "integer"]            |["", ""]                                            |keyword                    | "Return the substring that extracts length chars from the string starting from the left."                      | [false, false]       | false | false
 length                   |"integer length(str:keyword|text)"                                        |str                     |"keyword|text"                 | ""                                                 |integer                    | "Returns the character length of a string."                      | false                | false | false
+log                      |"double log(?base:integer|unsigned_long|long|double, value:integer|unsigned_long|long|double)"          |[base, value]                        |["integer|unsigned_long|long|double", "integer|unsigned_long|long|double"]| ["", ""]                                                 |double                    | "Returns the logarithm of a value to a base."                      | [true, false]                | false | false
 log10                    |"double log10(n:double|integer|long|unsigned_long)"          |n                        |"double|integer|long|unsigned_long" | ""                                                 |double                    | "Returns the log base 10."                      | false                | false | false
 ltrim                    |"keyword|text ltrim(str:keyword|text)"                  |str                      |"keyword|text"    | ""                                                 |"keyword|text"       |Removes leading whitespaces from a string.| false | false | false
 max                      |"double|integer|long max(field:double|integer|long)"                                           |field                     |"double|integer|long"                 | ""                                                 |"double|integer|long"                    | "The maximum value of a numeric field."                      | false                | false | true
@@ -134,6 +135,7 @@ double e()
 "integer|long|double|boolean|keyword|text|ip|version least(first:integer|long|double|boolean|keyword|text|ip|version, rest...:integer|long|double|boolean|keyword|text|ip|version)"
 "keyword left(str:keyword|text, length:integer)"
 "integer length(str:keyword|text)"
+"double log(?base:integer|unsigned_long|long|double, value:integer|unsigned_long|long|double)"
 "double log10(n:double|integer|long|unsigned_long)"
 "keyword|text ltrim(str:keyword|text)"
 "double|integer|long max(field:double|integer|long)"
@@ -218,5 +220,5 @@ countFunctions#[skip:-8.12.99]
 show functions |  stats  a = count(*), b = count(*), c = count(*) |  mv_expand c;
 
 a:long | b:long | c:long
-89     | 89     | 89
+90     | 90     | 90
 ;

+ 119 - 0
x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/math/LogConstantEvaluator.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 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.core.Releasables;
+import org.elasticsearch.xpack.esql.expression.function.Warnings;
+import org.elasticsearch.xpack.ql.tree.Source;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link Log}.
+ * This class is generated. Do not edit it.
+ */
+public final class LogConstantEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final Warnings warnings;
+
+  private final EvalOperator.ExpressionEvaluator value;
+
+  private final DriverContext driverContext;
+
+  public LogConstantEvaluator(Source source, EvalOperator.ExpressionEvaluator value,
+      DriverContext driverContext) {
+    this.warnings = new Warnings(source);
+    this.value = value;
+    this.driverContext = driverContext;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    try (DoubleBlock valueBlock = (DoubleBlock) value.eval(page)) {
+      DoubleVector valueVector = valueBlock.asVector();
+      if (valueVector == null) {
+        return eval(page.getPositionCount(), valueBlock);
+      }
+      return eval(page.getPositionCount(), valueVector);
+    }
+  }
+
+  public DoubleBlock eval(int positionCount, DoubleBlock valueBlock) {
+    try(DoubleBlock.Builder result = driverContext.blockFactory().newDoubleBlockBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        if (valueBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (valueBlock.getValueCount(p) != 1) {
+          if (valueBlock.getValueCount(p) > 1) {
+            warnings.registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        try {
+          result.appendDouble(Log.process(valueBlock.getDouble(valueBlock.getFirstValueIndex(p))));
+        } catch (ArithmeticException e) {
+          warnings.registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  public DoubleBlock eval(int positionCount, DoubleVector valueVector) {
+    try(DoubleBlock.Builder result = driverContext.blockFactory().newDoubleBlockBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        try {
+          result.appendDouble(Log.process(valueVector.getDouble(p)));
+        } catch (ArithmeticException e) {
+          warnings.registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "LogConstantEvaluator[" + "value=" + value + "]";
+  }
+
+  @Override
+  public void close() {
+    Releasables.closeExpectNoException(value);
+  }
+
+  static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
+    private final Source source;
+
+    private final EvalOperator.ExpressionEvaluator.Factory value;
+
+    public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory value) {
+      this.source = source;
+      this.value = value;
+    }
+
+    @Override
+    public LogConstantEvaluator get(DriverContext context) {
+      return new LogConstantEvaluator(source, value.get(context), context);
+    }
+
+    @Override
+    public String toString() {
+      return "LogConstantEvaluator[" + "value=" + value + "]";
+    }
+  }
+}

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

@@ -0,0 +1,143 @@
+// 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.core.Releasables;
+import org.elasticsearch.xpack.esql.expression.function.Warnings;
+import org.elasticsearch.xpack.ql.tree.Source;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link Log}.
+ * This class is generated. Do not edit it.
+ */
+public final class LogEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final Warnings warnings;
+
+  private final EvalOperator.ExpressionEvaluator base;
+
+  private final EvalOperator.ExpressionEvaluator value;
+
+  private final DriverContext driverContext;
+
+  public LogEvaluator(Source source, EvalOperator.ExpressionEvaluator base,
+      EvalOperator.ExpressionEvaluator value, DriverContext driverContext) {
+    this.warnings = new Warnings(source);
+    this.base = base;
+    this.value = value;
+    this.driverContext = driverContext;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    try (DoubleBlock baseBlock = (DoubleBlock) base.eval(page)) {
+      try (DoubleBlock valueBlock = (DoubleBlock) value.eval(page)) {
+        DoubleVector baseVector = baseBlock.asVector();
+        if (baseVector == null) {
+          return eval(page.getPositionCount(), baseBlock, valueBlock);
+        }
+        DoubleVector valueVector = valueBlock.asVector();
+        if (valueVector == null) {
+          return eval(page.getPositionCount(), baseBlock, valueBlock);
+        }
+        return eval(page.getPositionCount(), baseVector, valueVector);
+      }
+    }
+  }
+
+  public DoubleBlock eval(int positionCount, DoubleBlock baseBlock, DoubleBlock valueBlock) {
+    try(DoubleBlock.Builder result = driverContext.blockFactory().newDoubleBlockBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        if (baseBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (baseBlock.getValueCount(p) != 1) {
+          if (baseBlock.getValueCount(p) > 1) {
+            warnings.registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        if (valueBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (valueBlock.getValueCount(p) != 1) {
+          if (valueBlock.getValueCount(p) > 1) {
+            warnings.registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        try {
+          result.appendDouble(Log.process(baseBlock.getDouble(baseBlock.getFirstValueIndex(p)), valueBlock.getDouble(valueBlock.getFirstValueIndex(p))));
+        } catch (ArithmeticException e) {
+          warnings.registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  public DoubleBlock eval(int positionCount, DoubleVector baseVector, DoubleVector valueVector) {
+    try(DoubleBlock.Builder result = driverContext.blockFactory().newDoubleBlockBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        try {
+          result.appendDouble(Log.process(baseVector.getDouble(p), valueVector.getDouble(p)));
+        } catch (ArithmeticException e) {
+          warnings.registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "LogEvaluator[" + "base=" + base + ", value=" + value + "]";
+  }
+
+  @Override
+  public void close() {
+    Releasables.closeExpectNoException(base, value);
+  }
+
+  static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
+    private final Source source;
+
+    private final EvalOperator.ExpressionEvaluator.Factory base;
+
+    private final EvalOperator.ExpressionEvaluator.Factory value;
+
+    public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory base,
+        EvalOperator.ExpressionEvaluator.Factory value) {
+      this.source = source;
+      this.base = base;
+      this.value = value;
+    }
+
+    @Override
+    public LogEvaluator get(DriverContext context) {
+      return new LogEvaluator(source, base.get(context), value.get(context), context);
+    }
+
+    @Override
+    public String toString() {
+      return "LogEvaluator[" + "base=" + base + ", value=" + value + "]";
+    }
+  }
+}

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

@@ -53,6 +53,7 @@ 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;
 import org.elasticsearch.xpack.esql.expression.function.scalar.math.Floor;
+import org.elasticsearch.xpack.esql.expression.function.scalar.math.Log;
 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;
@@ -136,6 +137,7 @@ public final class EsqlFunctionRegistry extends FunctionRegistry {
                 def(E.class, E::new, "e"),
                 def(Floor.class, Floor::new, "floor"),
                 def(Greatest.class, Greatest::new, "greatest"),
+                def(Log.class, Log::new, "log"),
                 def(Log10.class, Log10::new, "log10"),
                 def(Least.class, Least::new, "least"),
                 def(Pi.class, Pi::new, "pi"),

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

@@ -0,0 +1,138 @@
+/*
+ * 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.ExpressionEvaluator;
+import org.elasticsearch.xpack.esql.evaluator.mapper.EvaluatorMapper;
+import org.elasticsearch.xpack.esql.expression.function.FunctionInfo;
+import org.elasticsearch.xpack.esql.expression.function.Param;
+import org.elasticsearch.xpack.ql.expression.Expression;
+import org.elasticsearch.xpack.ql.expression.function.OptionalArgument;
+import org.elasticsearch.xpack.ql.expression.function.scalar.ScalarFunction;
+import org.elasticsearch.xpack.ql.expression.gen.script.ScriptTemplate;
+import org.elasticsearch.xpack.ql.tree.NodeInfo;
+import org.elasticsearch.xpack.ql.tree.Source;
+import org.elasticsearch.xpack.ql.type.DataType;
+import org.elasticsearch.xpack.ql.type.DataTypes;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Function;
+
+import static org.elasticsearch.xpack.ql.expression.TypeResolutions.ParamOrdinal.FIRST;
+import static org.elasticsearch.xpack.ql.expression.TypeResolutions.ParamOrdinal.SECOND;
+import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isNumeric;
+
+public class Log extends ScalarFunction implements OptionalArgument, EvaluatorMapper {
+
+    private final Expression base, value;
+
+    @FunctionInfo(returnType = "double", description = "Returns the logarithm of a value to a base.")
+    public Log(
+        Source source,
+        @Param(name = "base", type = { "integer", "unsigned_long", "long", "double" }, optional = true) Expression base,
+        @Param(name = "value", type = { "integer", "unsigned_long", "long", "double" }) Expression value
+    ) {
+        super(source, value != null ? Arrays.asList(base, value) : Arrays.asList(base));
+        this.value = value != null ? value : base;
+        this.base = value != null ? base : null;
+    }
+
+    @Override
+    protected TypeResolution resolveType() {
+        if (childrenResolved() == false) {
+            return new TypeResolution("Unresolved children");
+        }
+
+        if (base != null) {
+            TypeResolution resolution = isNumeric(base, sourceText(), FIRST);
+            if (resolution.unresolved()) {
+                return resolution;
+            }
+        }
+
+        return isNumeric(value, sourceText(), base != null ? SECOND : FIRST);
+    }
+
+    @Override
+    public boolean foldable() {
+        return (base == null || base.foldable()) && value.foldable();
+    }
+
+    @Override
+    public Object fold() {
+        return EvaluatorMapper.super.fold();
+    }
+
+    @Evaluator(extraName = "Constant", warnExceptions = { ArithmeticException.class })
+    static double process(double value) throws ArithmeticException {
+        if (value <= 0d) {
+            throw new ArithmeticException("Log of non-positive number");
+        }
+        return Math.log(value);
+    }
+
+    @Evaluator(warnExceptions = { ArithmeticException.class })
+    static double process(double base, double value) throws ArithmeticException {
+        if (base <= 0d || value <= 0d) {
+            throw new ArithmeticException("Log of non-positive number");
+        }
+        if (base == 1d) {
+            throw new ArithmeticException("Log of base 1");
+        }
+        return Math.log10(value) / Math.log10(base);
+    }
+
+    @Override
+    public final Expression replaceChildren(List<Expression> newChildren) {
+        return new Log(source(), newChildren.get(0), newChildren.size() > 1 ? newChildren.get(1) : null);
+    }
+
+    @Override
+    protected NodeInfo<? extends Expression> info() {
+        Expression b = base != null ? base : value;
+        Expression v = base != null ? value : null;
+        return NodeInfo.create(this, Log::new, b, v);
+    }
+
+    @Override
+    public DataType dataType() {
+        return DataTypes.DOUBLE;
+    }
+
+    @Override
+    public ScriptTemplate asScript() {
+        throw new UnsupportedOperationException("functions do not support scripting");
+    }
+
+    @Override
+    public ExpressionEvaluator.Factory toEvaluator(Function<Expression, ExpressionEvaluator.Factory> toEvaluator) {
+        var valueEval = Cast.cast(source(), value.dataType(), DataTypes.DOUBLE, toEvaluator.apply(value));
+        if (base != null) {
+            var baseEval = Cast.cast(source(), base.dataType(), DataTypes.DOUBLE, toEvaluator.apply(base));
+            return new LogEvaluator.Factory(source(), baseEval, valueEval);
+        }
+        return new LogConstantEvaluator.Factory(source(), valueEval);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(base, value);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null || obj.getClass() != getClass()) {
+            return false;
+        }
+        Log other = (Log) obj;
+        return Objects.equals(other.base, base) && Objects.equals(other.value, value);
+    }
+}

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

@@ -76,6 +76,7 @@ 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;
 import org.elasticsearch.xpack.esql.expression.function.scalar.math.Floor;
+import org.elasticsearch.xpack.esql.expression.function.scalar.math.Log;
 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;
@@ -372,6 +373,7 @@ public final class PlanNamedTypes {
             of(ScalarFunction.class, E.class, PlanNamedTypes::writeNoArgScalar, PlanNamedTypes::readNoArgScalar),
             of(ScalarFunction.class, Greatest.class, PlanNamedTypes::writeVararg, PlanNamedTypes::readVarag),
             of(ScalarFunction.class, Least.class, PlanNamedTypes::writeVararg, PlanNamedTypes::readVarag),
+            of(ScalarFunction.class, Log.class, PlanNamedTypes::writeLog, PlanNamedTypes::readLog),
             of(ScalarFunction.class, Now.class, PlanNamedTypes::writeNow, PlanNamedTypes::readNow),
             of(ScalarFunction.class, Pi.class, PlanNamedTypes::writeNoArgScalar, PlanNamedTypes::readNoArgScalar),
             of(ScalarFunction.class, Round.class, PlanNamedTypes::writeRound, PlanNamedTypes::readRound),
@@ -1811,4 +1813,16 @@ public final class PlanNamedTypes {
         out.writeString(dissectParser.pattern());
         out.writeString(dissectParser.appendSeparator());
     }
+
+    static Log readLog(PlanStreamInput in) throws IOException {
+        return new Log(in.readSource(), in.readExpression(), in.readOptionalNamed(Expression.class));
+    }
+
+    static void writeLog(PlanStreamOutput out, Log log) throws IOException {
+        out.writeSource(log.source());
+        List<Expression> fields = log.children();
+        assert fields.size() == 1 || fields.size() == 2;
+        out.writeExpression(fields.get(0));
+        out.writeOptionalWriteable(fields.size() == 2 ? o -> out.writeExpression(fields.get(1)) : null);
+    }
 }

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

@@ -0,0 +1,212 @@
+/*
+ * 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.TestCaseSupplier;
+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;
+
+public class LogTests extends AbstractScalarFunctionTestCase {
+    public LogTests(@Name("TestCase") Supplier<TestCaseSupplier.TestCase> testCaseSupplier) {
+        this.testCase = testCaseSupplier.get();
+    }
+
+    @ParametersFactory
+    public static Iterable<Object[]> parameters() {
+        // Positive base > 1, value >= 1,
+        List<TestCaseSupplier> suppliers = TestCaseSupplier.forBinaryCastingToDouble(
+            "LogEvaluator",
+            "base",
+            "value",
+            (b, l) -> Math.log10(l) / Math.log10(b),
+            2d,
+            Double.POSITIVE_INFINITY,
+            1d,
+            Double.POSITIVE_INFINITY,
+            List.of()
+        );
+
+        // Positive natural logarithm
+        suppliers.addAll(
+            TestCaseSupplier.forUnaryCastingToDouble(
+                "LogConstantEvaluator",
+                "value",
+                Math::log,
+                Math.nextUp(1d),
+                Double.POSITIVE_INFINITY,
+                List.of()
+            )
+        );
+
+        TestCaseSupplier.forUnaryDouble(
+            suppliers,
+            "LogConstantEvaluator[value=Attribute[channel=0]]",
+            DataTypes.DOUBLE,
+            Math::log,
+            Math.nextUp(0d),
+            Math.nextDown(1d),
+            List.of()
+        );
+
+        // Positive 0 < base < 1, 0 < value < 1
+        suppliers.addAll(
+            TestCaseSupplier.forBinaryCastingToDouble(
+                "LogEvaluator",
+                "base",
+                "value",
+                (b, l) -> Math.log10(l) / Math.log10(b),
+                List.of(
+                    new TestCaseSupplier.TypedDataSupplier("<gt0 double>", () -> Math.nextUp(0d), DataTypes.DOUBLE),
+                    new TestCaseSupplier.TypedDataSupplier("<lt1 double>", () -> Math.nextDown(1d), DataTypes.DOUBLE)
+                ),
+                List.of(
+                    new TestCaseSupplier.TypedDataSupplier("<gt0 double>", () -> Math.nextUp(0d), DataTypes.DOUBLE),
+                    new TestCaseSupplier.TypedDataSupplier("<lt1 double>", () -> Math.nextDown(1d), DataTypes.DOUBLE)
+                ),
+                List.of()
+            )
+        );
+
+        // Negative base <=0,
+        suppliers.addAll(
+            TestCaseSupplier.forBinaryCastingToDouble(
+                "LogEvaluator",
+                "base",
+                "value",
+                (b, l) -> null,
+                Double.NEGATIVE_INFINITY,
+                0d,
+                1d,
+                Double.POSITIVE_INFINITY,
+                List.of(
+                    "Line -1:-1: evaluation of [] failed, treating result as null. Only first 20 failures recorded.",
+                    "Line -1:-1: java.lang.ArithmeticException: Log of non-positive number"
+                )
+            )
+        );
+
+        // Negative value <=0,
+        suppliers.addAll(
+            TestCaseSupplier.forBinaryCastingToDouble(
+                "LogEvaluator",
+                "base",
+                "value",
+                (b, l) -> null,
+                2d,
+                Double.POSITIVE_INFINITY,
+                Double.NEGATIVE_INFINITY,
+                0d,
+                List.of(
+                    "Line -1:-1: evaluation of [] failed, treating result as null. Only first 20 failures recorded.",
+                    "Line -1:-1: java.lang.ArithmeticException: Log of non-positive number"
+                )
+            )
+        );
+
+        // Negative base = 1
+        suppliers.addAll(
+            TestCaseSupplier.forBinaryCastingToDouble(
+                "LogEvaluator",
+                "base",
+                "value",
+                (b, l) -> null,
+                1d,
+                1d,
+                1d,
+                Double.POSITIVE_INFINITY,
+                List.of(
+                    "Line -1:-1: evaluation of [] failed, treating result as null. Only first 20 failures recorded.",
+                    "Line -1:-1: java.lang.ArithmeticException: Log of base 1"
+                )
+            )
+        );
+
+        // Negative 0 < base < 1, value > 1
+        suppliers.addAll(
+            TestCaseSupplier.forBinaryCastingToDouble(
+                "LogEvaluator",
+                "base",
+                "value",
+                (b, l) -> Math.log10(l) / Math.log10(b),
+                List.of(
+                    new TestCaseSupplier.TypedDataSupplier("<gt0 double>", () -> Math.nextUp(0d), DataTypes.DOUBLE),
+                    new TestCaseSupplier.TypedDataSupplier("<lt1 double>", () -> Math.nextDown(1d), DataTypes.DOUBLE)
+                ),
+                List.of(
+                    new TestCaseSupplier.TypedDataSupplier("<gt1 double>", () -> Math.nextUp(1d), DataTypes.DOUBLE),
+                    new TestCaseSupplier.TypedDataSupplier("<largest double>", () -> Double.MAX_VALUE, DataTypes.DOUBLE)
+                ),
+                List.of()
+            )
+        );
+
+        // Negative base > 1, 0 < value < 1
+        suppliers.addAll(
+            TestCaseSupplier.forBinaryCastingToDouble(
+                "LogEvaluator",
+                "base",
+                "value",
+                (b, l) -> Math.log10(l) / Math.log10(b),
+                List.of(
+                    new TestCaseSupplier.TypedDataSupplier("<gt0 double>", () -> Math.nextUp(1d), DataTypes.DOUBLE),
+                    new TestCaseSupplier.TypedDataSupplier("<lt1 double>", () -> Double.MAX_VALUE, DataTypes.DOUBLE)
+                ),
+                List.of(
+                    new TestCaseSupplier.TypedDataSupplier("<gt1 double>", () -> Math.nextUp(0d), DataTypes.DOUBLE),
+                    new TestCaseSupplier.TypedDataSupplier("<largest double>", () -> Math.nextDown(1d), DataTypes.DOUBLE)
+                ),
+                List.of()
+            )
+        );
+
+        // Negative Unary value <=0
+        suppliers.addAll(
+            TestCaseSupplier.forUnaryCastingToDouble(
+                "LogConstantEvaluator",
+                "value",
+                v -> null,
+                Double.NEGATIVE_INFINITY,
+                0d,
+                List.of(
+                    "Line -1:-1: evaluation of [] failed, treating result as null. Only first 20 failures recorded.",
+                    "Line -1:-1: java.lang.ArithmeticException: Log of non-positive number"
+                )
+            )
+        );
+
+        // Add null cases before the rest of the error cases, so messages are correct.
+        suppliers = anyNullIsNull(true, suppliers);
+
+        // Negative cases
+        return parameterSuppliersFromTypedData(errorsForCasesWithoutExamples(suppliers));
+    }
+
+    @Override
+    protected DataType expectedType(List<DataType> argTypes) {
+        return DataTypes.DOUBLE;
+    }
+
+    @Override
+    protected List<ArgumentSpec> argSpec() {
+        return List.of(optional(numerics()), required(numerics()));
+    }
+
+    @Override
+    protected Expression build(Source source, List<Expression> args) {
+        return new Log(source, args.get(0), args.size() > 1 ? args.get(1) : null);
+    }
+}