Add ES|QL bit_length function (#115792)

This commit is contained in:
Tim Grein 2024-11-07 08:51:26 +01:00 committed by GitHub
parent 30feced762
commit 81fd1de76b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 472 additions and 5 deletions

View file

@ -0,0 +1,5 @@
pr: 115792
summary: Add ES|QL `bit_length` function
area: ES|QL
type: enhancement
issues: []

View file

@ -0,0 +1,5 @@
// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
*Description*
Returns the bit length of a string.

View file

@ -0,0 +1,13 @@
// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
*Example*
[source.merge.styled,esql]
----
include::{esql-specs}/docs.csv-spec[tag=bitLength]
----
[%header.monospaced.styled,format=dsv,separator=|]
|===
include::{esql-specs}/docs.csv-spec[tag=bitLength-result]
|===

View file

@ -0,0 +1,12 @@
<!--
This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
-->
### BIT_LENGTH
Returns the bit length of a string.
```
FROM employees
| KEEP first_name, last_name
| EVAL fn_bit_length = BIT_LENGTH(first_name)
```

View file

@ -0,0 +1,15 @@
// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
[discrete]
[[esql-bit_length]]
=== `BIT_LENGTH`
*Syntax*
[.text-center]
image::esql/functions/signature/bit_length.svg[Embedded,opts=inline]
include::../parameters/bit_length.asciidoc[]
include::../description/bit_length.asciidoc[]
include::../types/bit_length.asciidoc[]
include::../examples/bit_length.asciidoc[]

View file

@ -0,0 +1,6 @@
// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
*Parameters*
`string`::
String expression. If `null`, the function returns `null`.

View file

@ -0,0 +1 @@
<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="336" height="46" viewbox="0 0 336 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 31h5m140 0h10m32 0h10m92 0h10m32 0h5"/><rect class="s" x="5" y="5" width="140" height="36"/><text class="k" x="15" y="31">BIT_LENGTH</text><rect class="s" x="155" y="5" width="32" height="36" rx="7"/><text class="syn" x="165" y="31">(</text><rect class="s" x="197" y="5" width="92" height="36" rx="7"/><text class="k" x="207" y="31">string</text><rect class="s" x="299" y="5" width="32" height="36" rx="7"/><text class="syn" x="309" y="31">)</text></svg>

After

Width:  |  Height:  |  Size: 887 B

View file

@ -8,6 +8,7 @@
{esql} supports these string functions: {esql} supports these string functions:
// tag::string_list[] // tag::string_list[]
* <<esql-bit_length>>
* <<esql-concat>> * <<esql-concat>>
* <<esql-ends_with>> * <<esql-ends_with>>
* <<esql-from_base64>> * <<esql-from_base64>>
@ -30,6 +31,7 @@
* <<esql-trim>> * <<esql-trim>>
// end::string_list[] // end::string_list[]
include::layout/bit_length.asciidoc[]
include::layout/concat.asciidoc[] include::layout/concat.asciidoc[]
include::layout/ends_with.asciidoc[] include::layout/ends_with.asciidoc[]
include::layout/from_base64.asciidoc[] include::layout/from_base64.asciidoc[]

View file

@ -0,0 +1,10 @@
// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
*Supported types*
[%header.monospaced.styled,format=dsv,separator=|]
|===
string | result
keyword | integer
text | integer
|===

View file

@ -82,8 +82,10 @@ tasks.named("precommit").configure {
tasks.named("yamlRestCompatTestTransform").configure({ task -> tasks.named("yamlRestCompatTestTransform").configure({ task ->
task.skipTest("security/10_forbidden/Test bulk response with invalid credentials", "warning does not exist for compatibility") task.skipTest("security/10_forbidden/Test bulk response with invalid credentials", "warning does not exist for compatibility")
task.skipTest("inference/inference_crud/Test get all", "Assertions on number of inference models break due to default configs")
task.skipTest("esql/60_usage/Basic ESQL usage output (telemetry)", "The telemetry output changed. We dropped a column. That's safe.") task.skipTest("esql/60_usage/Basic ESQL usage output (telemetry)", "The telemetry output changed. We dropped a column. That's safe.")
task.skipTest("inference/inference_crud/Test get all", "Assertions on number of inference models break due to default configs")
task.skipTest("esql/60_usage/Basic ESQL usage output (telemetry) snapshot version", "The number of functions is constantly increasing")
task.skipTest("esql/60_usage/Basic ESQL usage output (telemetry) non-snapshot version", "The number of functions is constantly increasing")
task.skipTest("esql/80_text/reverse text", "The output type changed from TEXT to KEYWORD.") task.skipTest("esql/80_text/reverse text", "The output type changed from TEXT to KEYWORD.")
task.skipTest("esql/80_text/values function", "The output type changed from TEXT to KEYWORD.") task.skipTest("esql/80_text/values function", "The output type changed from TEXT to KEYWORD.")
}) })

View file

@ -656,3 +656,22 @@ FROM sample_data
@timestamp:date | client_ip:ip | event_duration:long | message:keyword @timestamp:date | client_ip:ip | event_duration:long | message:keyword
; ;
docsBitLength
required_capability: fn_bit_length
// tag::bitLength[]
FROM employees
| KEEP first_name, last_name
| EVAL fn_bit_length = BIT_LENGTH(first_name)
// end::bitLength[]
| SORT first_name
| LIMIT 3
;
// tag::bitLength-result[]
first_name:keyword | last_name:keyword | fn_bit_length:integer
Alejandro |McAlpine |72
Amabile |Gomatam |56
Anneke |Preusig |48
// end::bitLength-result[]
;

View file

@ -38,6 +38,56 @@ emp_no:integer | l:integer
10003 | 5 10003 | 5
; ;
bitLength
required_capability: fn_bit_length
row a = "hello", b = "" | eval y = bit_length(a) + bit_length(b);
a:keyword | b:keyword | y:integer
hello | | 40
;
bitLengthWithNonAsciiChars
required_capability: fn_bit_length
row a = "¡", b = "❗️" | eval y = bit_length(a) | eval z = bit_length(b);
a:keyword | b:keyword | y:integer | z:integer
¡ | ❗️ | 16 | 48
;
foldBitLength
required_capability: fn_bit_length
row a = 1 | eval b = bit_length("hello");
a:integer | b:integer
1 | 40
;
bitLengthAndSourceQuoting
required_capability: fn_bit_length
from "employees" | sort emp_no | limit 3 | eval l = bit_length(first_name) | keep emp_no, l;
emp_no:integer | l:integer
10001 | 48
10002 | 56
10003 | 40
;
bitLengthInsideOtherFunction
required_capability: fn_bit_length
row a = "abc", b = "de" | eval g = greatest(bit_length(a), bit_length(b), bit_length("fghi"));
a:keyword | b:keyword | g:integer
abc | de | 32
;
bitLengthNull
required_capability: fn_bit_length
row a = "abc" | eval l = bit_length(null);
a:string | l:integer
abc | null
;
startsWithConstant startsWithConstant
from employees | sort emp_no | limit 10 | eval f_S = starts_with(first_name, "S") | keep emp_no, first_name, f_S; from employees | sort emp_no | limit 10 | eval f_S = starts_with(first_name, "S") | keep emp_no, first_name, f_S;

View file

@ -0,0 +1,137 @@
// 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.string;
import java.lang.ArithmeticException;
import java.lang.IllegalArgumentException;
import java.lang.Override;
import java.lang.String;
import org.apache.lucene.util.BytesRef;
import org.elasticsearch.compute.data.Block;
import org.elasticsearch.compute.data.BytesRefBlock;
import org.elasticsearch.compute.data.BytesRefVector;
import org.elasticsearch.compute.data.IntBlock;
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 BitLength}.
* This class is generated. Do not edit it.
*/
public final class BitLengthEvaluator implements EvalOperator.ExpressionEvaluator {
private final Source source;
private final EvalOperator.ExpressionEvaluator val;
private final DriverContext driverContext;
private Warnings warnings;
public BitLengthEvaluator(Source source, EvalOperator.ExpressionEvaluator val,
DriverContext driverContext) {
this.source = source;
this.val = val;
this.driverContext = driverContext;
}
@Override
public Block eval(Page page) {
try (BytesRefBlock valBlock = (BytesRefBlock) val.eval(page)) {
BytesRefVector valVector = valBlock.asVector();
if (valVector == null) {
return eval(page.getPositionCount(), valBlock);
}
return eval(page.getPositionCount(), valVector);
}
}
public IntBlock eval(int positionCount, BytesRefBlock valBlock) {
try(IntBlock.Builder result = driverContext.blockFactory().newIntBlockBuilder(positionCount)) {
BytesRef valScratch = new BytesRef();
position: for (int p = 0; p < positionCount; p++) {
if (valBlock.isNull(p)) {
result.appendNull();
continue position;
}
if (valBlock.getValueCount(p) != 1) {
if (valBlock.getValueCount(p) > 1) {
warnings().registerException(new IllegalArgumentException("single-value function encountered multi-value"));
}
result.appendNull();
continue position;
}
try {
result.appendInt(BitLength.process(valBlock.getBytesRef(valBlock.getFirstValueIndex(p), valScratch)));
} catch (ArithmeticException e) {
warnings().registerException(e);
result.appendNull();
}
}
return result.build();
}
}
public IntBlock eval(int positionCount, BytesRefVector valVector) {
try(IntBlock.Builder result = driverContext.blockFactory().newIntBlockBuilder(positionCount)) {
BytesRef valScratch = new BytesRef();
position: for (int p = 0; p < positionCount; p++) {
try {
result.appendInt(BitLength.process(valVector.getBytesRef(p, valScratch)));
} catch (ArithmeticException e) {
warnings().registerException(e);
result.appendNull();
}
}
return result.build();
}
}
@Override
public String toString() {
return "BitLengthEvaluator[" + "val=" + val + "]";
}
@Override
public void close() {
Releasables.closeExpectNoException(val);
}
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 val;
public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory val) {
this.source = source;
this.val = val;
}
@Override
public BitLengthEvaluator get(DriverContext context) {
return new BitLengthEvaluator(source, val.get(context), context);
}
@Override
public String toString() {
return "BitLengthEvaluator[" + "val=" + val + "]";
}
}
}

View file

@ -27,6 +27,12 @@ import java.util.Set;
*/ */
public class EsqlCapabilities { public class EsqlCapabilities {
public enum Cap { public enum Cap {
/**
* Support for function {@code BIT_LENGTH}. Done in #115792
*/
FN_BIT_LENGTH,
/** /**
* Support for function {@code REVERSE}. * Support for function {@code REVERSE}.
*/ */

View file

@ -117,6 +117,7 @@ import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.SpatialWi
import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StDistance; import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StDistance;
import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StX; import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StX;
import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StY; import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StY;
import org.elasticsearch.xpack.esql.expression.function.scalar.string.BitLength;
import org.elasticsearch.xpack.esql.expression.function.scalar.string.Concat; import org.elasticsearch.xpack.esql.expression.function.scalar.string.Concat;
import org.elasticsearch.xpack.esql.expression.function.scalar.string.EndsWith; import org.elasticsearch.xpack.esql.expression.function.scalar.string.EndsWith;
import org.elasticsearch.xpack.esql.expression.function.scalar.string.LTrim; import org.elasticsearch.xpack.esql.expression.function.scalar.string.LTrim;
@ -305,6 +306,7 @@ public class EsqlFunctionRegistry {
def(Tau.class, Tau::new, "tau") }, def(Tau.class, Tau::new, "tau") },
// string // string
new FunctionDefinition[] { new FunctionDefinition[] {
def(BitLength.class, BitLength::new, "bit_length"),
def(Concat.class, Concat::new, "concat"), def(Concat.class, Concat::new, "concat"),
def(EndsWith.class, EndsWith::new, "ends_with"), def(EndsWith.class, EndsWith::new, "ends_with"),
def(LTrim.class, LTrim::new, "ltrim"), def(LTrim.class, LTrim::new, "ltrim"),

View file

@ -38,6 +38,7 @@ import org.elasticsearch.xpack.esql.expression.function.scalar.math.Round;
import org.elasticsearch.xpack.esql.expression.function.scalar.math.Tau; import org.elasticsearch.xpack.esql.expression.function.scalar.math.Tau;
import org.elasticsearch.xpack.esql.expression.function.scalar.nulls.Coalesce; import org.elasticsearch.xpack.esql.expression.function.scalar.nulls.Coalesce;
import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.BinarySpatialFunction; import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.BinarySpatialFunction;
import org.elasticsearch.xpack.esql.expression.function.scalar.string.BitLength;
import org.elasticsearch.xpack.esql.expression.function.scalar.string.Concat; import org.elasticsearch.xpack.esql.expression.function.scalar.string.Concat;
import org.elasticsearch.xpack.esql.expression.function.scalar.string.EndsWith; import org.elasticsearch.xpack.esql.expression.function.scalar.string.EndsWith;
import org.elasticsearch.xpack.esql.expression.function.scalar.string.Left; import org.elasticsearch.xpack.esql.expression.function.scalar.string.Left;
@ -74,6 +75,7 @@ public abstract class EsqlScalarFunction extends ScalarFunction implements Evalu
List<NamedWriteableRegistry.Entry> entries = new ArrayList<>(); List<NamedWriteableRegistry.Entry> entries = new ArrayList<>();
entries.add(And.ENTRY); entries.add(And.ENTRY);
entries.add(Atan2.ENTRY); entries.add(Atan2.ENTRY);
entries.add(BitLength.ENTRY);
entries.add(Bucket.ENTRY); entries.add(Bucket.ENTRY);
entries.add(Case.ENTRY); entries.add(Case.ENTRY);
entries.add(Categorize.ENTRY); entries.add(Categorize.ENTRY);

View file

@ -0,0 +1,100 @@
/*
* 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.string;
import org.apache.lucene.util.BytesRef;
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.operator.EvalOperator;
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.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.UnaryScalarFunction;
import org.elasticsearch.xpack.esql.io.stream.PlanStreamInput;
import java.io.IOException;
import java.util.List;
import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.DEFAULT;
import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isString;
public class BitLength extends UnaryScalarFunction {
public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(
Expression.class,
"BitLength",
BitLength::new
);
@FunctionInfo(
returnType = "integer",
description = "Returns the bit length of a string.",
examples = @Example(file = "docs", tag = "bitLength")
)
public BitLength(
Source source,
@Param(
name = "string",
type = { "keyword", "text" },
description = "String expression. If `null`, the function returns `null`."
) Expression field
) {
super(source, field);
}
private BitLength(StreamInput in) throws IOException {
this(Source.readFrom((PlanStreamInput) in), in.readNamedWriteable(Expression.class));
}
@Override
public void writeTo(StreamOutput out) throws IOException {
source().writeTo(out);
out.writeNamedWriteable(field());
}
@Override
public String getWriteableName() {
return ENTRY.name;
}
@Override
public DataType dataType() {
return DataType.INTEGER;
}
@Override
protected TypeResolution resolveType() {
return childrenResolved() == false ? new TypeResolution("Unresolved children") : isString(field(), sourceText(), DEFAULT);
}
@Evaluator(warnExceptions = { ArithmeticException.class })
static int process(BytesRef val) {
return Math.multiplyExact(val.length, Byte.SIZE);
}
@Override
public Expression replaceChildren(List<Expression> newChildren) {
return new BitLength(source(), newChildren.get(0));
}
@Override
protected NodeInfo<? extends Expression> info() {
return NodeInfo.create(this, BitLength::new, field());
}
@Override
public EvalOperator.ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvaluator) {
return new BitLengthEvaluator.Factory(source(), toEvaluator.apply(field()));
}
}

View file

@ -0,0 +1,19 @@
/*
* 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.string;
import org.elasticsearch.xpack.esql.core.expression.Expression;
import org.elasticsearch.xpack.esql.core.tree.Source;
import org.elasticsearch.xpack.esql.expression.AbstractUnaryScalarSerializationTests;
public class BitLengthSerializationTests extends AbstractUnaryScalarSerializationTests<BitLength> {
@Override
protected BitLength create(Source source, Expression child) {
return new BitLength(source, child);
}
}

View file

@ -0,0 +1,61 @@
/*
* 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.string;
import com.carrotsearch.randomizedtesting.annotations.Name;
import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
import org.apache.lucene.util.BytesRef;
import org.elasticsearch.common.lucene.BytesRefs;
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.ArrayList;
import java.util.List;
import java.util.function.Supplier;
import static org.hamcrest.Matchers.equalTo;
public class BitLengthTests extends AbstractScalarFunctionTestCase {
public BitLengthTests(@Name("TestCase") Supplier<TestCaseSupplier.TestCase> testCaseSupplier) {
this.testCase = testCaseSupplier.get();
}
@ParametersFactory
public static Iterable<Object[]> parameters() {
List<TestCaseSupplier> suppliers = new ArrayList<>();
for (DataType stringType : DataType.stringTypes()) {
for (var supplier : TestCaseSupplier.stringCases(stringType)) {
suppliers.add(makeSupplier(supplier));
}
}
return parameterSuppliersFromTypedDataWithDefaultChecks(true, suppliers, (v, p) -> "string");
}
@Override
protected Expression build(Source source, List<Expression> args) {
return new BitLength(source, args.get(0));
}
private static TestCaseSupplier makeSupplier(TestCaseSupplier.TypedDataSupplier fieldSupplier) {
return new TestCaseSupplier(fieldSupplier.name(), List.of(fieldSupplier.type()), () -> {
var fieldTypedData = fieldSupplier.get();
String evaluatorToString = "BitLengthEvaluator[val=Attribute[channel=0]]";
BytesRef value = BytesRefs.toBytesRef(fieldTypedData.data());
var expectedValue = value.length * Byte.SIZE;
return new TestCaseSupplier.TestCase(List.of(fieldTypedData), evaluatorToString, DataType.INTEGER, equalTo(expectedValue));
});
}
}

View file

@ -30,7 +30,7 @@ setup:
- method: POST - method: POST
path: /_query path: /_query
parameters: [] parameters: []
capabilities: [ snapshot_test_for_telemetry ] capabilities: [ snapshot_test_for_telemetry, fn_bit_length ]
reason: "Test that should only be executed on snapshot versions" reason: "Test that should only be executed on snapshot versions"
- do: {xpack.usage: {}} - do: {xpack.usage: {}}
@ -91,7 +91,7 @@ setup:
- match: {esql.functions.cos: $functions_cos} - match: {esql.functions.cos: $functions_cos}
- gt: {esql.functions.to_long: $functions_to_long} - gt: {esql.functions.to_long: $functions_to_long}
- match: {esql.functions.coalesce: $functions_coalesce} - match: {esql.functions.coalesce: $functions_coalesce}
- length: {esql.functions: 117} # check the "sister" test below for a likely update to the same esql.functions length check - length: {esql.functions: 118} # check the "sister" test below for a likely update to the same esql.functions length check
--- ---
"Basic ESQL usage output (telemetry) non-snapshot version": "Basic ESQL usage output (telemetry) non-snapshot version":
@ -101,7 +101,7 @@ setup:
- method: POST - method: POST
path: /_query path: /_query
parameters: [] parameters: []
capabilities: [ non_snapshot_test_for_telemetry ] capabilities: [ non_snapshot_test_for_telemetry, fn_bit_length ]
reason: "Test that should only be executed on release versions" reason: "Test that should only be executed on release versions"
- do: {xpack.usage: {}} - do: {xpack.usage: {}}
@ -162,4 +162,4 @@ setup:
- match: {esql.functions.cos: $functions_cos} - match: {esql.functions.cos: $functions_cos}
- gt: {esql.functions.to_long: $functions_to_long} - gt: {esql.functions.to_long: $functions_to_long}
- match: {esql.functions.coalesce: $functions_coalesce} - match: {esql.functions.coalesce: $functions_coalesce}
- length: {esql.functions: 115} # check the "sister" test above for a likely update to the same esql.functions length check - length: {esql.functions: 118} # check the "sister" test above for a likely update to the same esql.functions length check