diff --git a/docs/reference/query-languages/esql/_snippets/functions/description/present.md b/docs/reference/query-languages/esql/_snippets/functions/description/present.md new file mode 100644 index 0000000000000..64d0ea09e22ac --- /dev/null +++ b/docs/reference/query-languages/esql/_snippets/functions/description/present.md @@ -0,0 +1,6 @@ +% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it. + +**Description** + +Returns true if the input expression yields any non-null values within the current aggregation context. Otherwise it returns false. + diff --git a/docs/reference/query-languages/esql/_snippets/functions/examples/present.md b/docs/reference/query-languages/esql/_snippets/functions/examples/present.md new file mode 100644 index 0000000000000..45c27f180480a --- /dev/null +++ b/docs/reference/query-languages/esql/_snippets/functions/examples/present.md @@ -0,0 +1,30 @@ +% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it. + +**Examples** + +```esql +FROM employees +| STATS is_present = PRESENT(languages) +``` + +| is_present:boolean | +| --- | +| true | + +To check for the presence inside a group use `PRESENT()` and `BY` clauses + +```esql +FROM employees +| STATS is_present = PRESENT(salary) BY languages +``` + +| is_present:boolean | languages:integer | +| --- | --- | +| true | 1 | +| true | 2 | +| true | 3 | +| true | 4 | +| true | 5 | +| true | null | + + diff --git a/docs/reference/query-languages/esql/_snippets/functions/layout/present.md b/docs/reference/query-languages/esql/_snippets/functions/layout/present.md new file mode 100644 index 0000000000000..6662a790982f3 --- /dev/null +++ b/docs/reference/query-languages/esql/_snippets/functions/layout/present.md @@ -0,0 +1,23 @@ +% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it. + +## `PRESENT` [esql-present] + +**Syntax** + +:::{image} ../../../images/functions/present.svg +:alt: Embedded +:class: text-center +::: + + +:::{include} ../parameters/present.md +::: + +:::{include} ../description/present.md +::: + +:::{include} ../types/present.md +::: + +:::{include} ../examples/present.md +::: diff --git a/docs/reference/query-languages/esql/_snippets/functions/parameters/present.md b/docs/reference/query-languages/esql/_snippets/functions/parameters/present.md new file mode 100644 index 0000000000000..26d9bb18c9fb5 --- /dev/null +++ b/docs/reference/query-languages/esql/_snippets/functions/parameters/present.md @@ -0,0 +1,7 @@ +% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it. + +**Parameters** + +`field` +: Expression that outputs values to be checked for presence. + diff --git a/docs/reference/query-languages/esql/_snippets/functions/types/present.md b/docs/reference/query-languages/esql/_snippets/functions/types/present.md new file mode 100644 index 0000000000000..626f34b097399 --- /dev/null +++ b/docs/reference/query-languages/esql/_snippets/functions/types/present.md @@ -0,0 +1,25 @@ +% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it. + +**Supported types** + +| field | result | +| --- | --- | +| boolean | boolean | +| cartesian_point | boolean | +| cartesian_shape | boolean | +| date | boolean | +| date_nanos | boolean | +| double | boolean | +| geo_point | boolean | +| geo_shape | boolean | +| geohash | boolean | +| geohex | boolean | +| geotile | boolean | +| integer | boolean | +| ip | boolean | +| keyword | boolean | +| long | boolean | +| text | boolean | +| unsigned_long | boolean | +| version | boolean | + diff --git a/docs/reference/query-languages/esql/_snippets/lists/aggregation-functions.md b/docs/reference/query-languages/esql/_snippets/lists/aggregation-functions.md index 3138a6f97ad69..867eb96d13339 100644 --- a/docs/reference/query-languages/esql/_snippets/lists/aggregation-functions.md +++ b/docs/reference/query-languages/esql/_snippets/lists/aggregation-functions.md @@ -14,3 +14,4 @@ * [`TOP`](../../functions-operators/aggregation-functions.md#esql-top) * [preview] [`VALUES`](../../functions-operators/aggregation-functions.md#esql-values) * [`WEIGHTED_AVG`](../../functions-operators/aggregation-functions.md#esql-weighted_avg) +* [`PRESENT`](../../functions-operators/aggregation-functions.md#esql-present) diff --git a/docs/reference/query-languages/esql/functions-operators/aggregation-functions.md b/docs/reference/query-languages/esql/functions-operators/aggregation-functions.md index c067d46ec6890..07afc26ea6ed8 100644 --- a/docs/reference/query-languages/esql/functions-operators/aggregation-functions.md +++ b/docs/reference/query-languages/esql/functions-operators/aggregation-functions.md @@ -59,3 +59,6 @@ The [`STATS`](/reference/query-languages/esql/commands/stats-by.md) command supp :::{include} ../_snippets/functions/layout/weighted_avg.md ::: + +:::{include} ../_snippets/functions/layout/present.md +::: diff --git a/docs/reference/query-languages/esql/images/functions/present.svg b/docs/reference/query-languages/esql/images/functions/present.svg new file mode 100644 index 0000000000000..fac5187f7c198 --- /dev/null +++ b/docs/reference/query-languages/esql/images/functions/present.svg @@ -0,0 +1 @@ +PRESENT(field) \ No newline at end of file diff --git a/docs/reference/query-languages/esql/kibana/definition/functions/present.json b/docs/reference/query-languages/esql/kibana/definition/functions/present.json new file mode 100644 index 0000000000000..584843213c5a4 --- /dev/null +++ b/docs/reference/query-languages/esql/kibana/definition/functions/present.json @@ -0,0 +1,230 @@ +{ + "comment" : "This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it.", + "type" : "agg", + "name" : "present", + "description" : "Returns true if the input expression yields any non-null values within the current aggregation context. Otherwise it returns false.", + "signatures" : [ + { + "params" : [ + { + "name" : "field", + "type" : "boolean", + "optional" : false, + "description" : "Expression that outputs values to be checked for presence." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "cartesian_point", + "optional" : false, + "description" : "Expression that outputs values to be checked for presence." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "cartesian_shape", + "optional" : false, + "description" : "Expression that outputs values to be checked for presence." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "date", + "optional" : false, + "description" : "Expression that outputs values to be checked for presence." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "date_nanos", + "optional" : false, + "description" : "Expression that outputs values to be checked for presence." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "double", + "optional" : false, + "description" : "Expression that outputs values to be checked for presence." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "geo_point", + "optional" : false, + "description" : "Expression that outputs values to be checked for presence." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "geo_shape", + "optional" : false, + "description" : "Expression that outputs values to be checked for presence." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "geohash", + "optional" : false, + "description" : "Expression that outputs values to be checked for presence." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "geohex", + "optional" : false, + "description" : "Expression that outputs values to be checked for presence." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "geotile", + "optional" : false, + "description" : "Expression that outputs values to be checked for presence." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "integer", + "optional" : false, + "description" : "Expression that outputs values to be checked for presence." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "ip", + "optional" : false, + "description" : "Expression that outputs values to be checked for presence." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "keyword", + "optional" : false, + "description" : "Expression that outputs values to be checked for presence." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "long", + "optional" : false, + "description" : "Expression that outputs values to be checked for presence." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "text", + "optional" : false, + "description" : "Expression that outputs values to be checked for presence." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "unsigned_long", + "optional" : false, + "description" : "Expression that outputs values to be checked for presence." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "version", + "optional" : false, + "description" : "Expression that outputs values to be checked for presence." + } + ], + "variadic" : false, + "returnType" : "boolean" + } + ], + "examples" : [ + "FROM employees\n| STATS is_present = PRESENT(languages)", + "FROM employees\n| STATS is_present = PRESENT(salary) BY languages" + ], + "preview" : false, + "snapshot_only" : false +} diff --git a/docs/reference/query-languages/esql/kibana/docs/functions/present.md b/docs/reference/query-languages/esql/kibana/docs/functions/present.md new file mode 100644 index 0000000000000..7f314a3075862 --- /dev/null +++ b/docs/reference/query-languages/esql/kibana/docs/functions/present.md @@ -0,0 +1,9 @@ +% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it. + +### PRESENT +Returns true if the input expression yields any non-null values within the current aggregation context. Otherwise it returns false. + +```esql +FROM employees +| STATS is_present = PRESENT(languages) +``` diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/PresentAggregatorFunction.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/PresentAggregatorFunction.java new file mode 100644 index 0000000000000..44ffde34238d1 --- /dev/null +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/PresentAggregatorFunction.java @@ -0,0 +1,133 @@ +/* + * 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.compute.aggregation; + +import org.elasticsearch.compute.data.Block; +import org.elasticsearch.compute.data.BooleanBlock; +import org.elasticsearch.compute.data.BooleanVector; +import org.elasticsearch.compute.data.ElementType; +import org.elasticsearch.compute.data.Page; +import org.elasticsearch.compute.operator.DriverContext; + +import java.util.List; + +public class PresentAggregatorFunction implements AggregatorFunction { + public static AggregatorFunctionSupplier supplier() { + return new AggregatorFunctionSupplier() { + @Override + public List nonGroupingIntermediateStateDesc() { + return PresentAggregatorFunction.intermediateStateDesc(); + } + + @Override + public List groupingIntermediateStateDesc() { + return PresentGroupingAggregatorFunction.intermediateStateDesc(); + } + + @Override + public AggregatorFunction aggregator(DriverContext driverContext, List channels) { + return PresentAggregatorFunction.create(channels); + } + + @Override + public GroupingAggregatorFunction groupingAggregator(DriverContext driverContext, List channels) { + return PresentGroupingAggregatorFunction.create(driverContext, channels); + } + + @Override + public String describe() { + return "present"; + } + }; + } + + private static final List INTERMEDIATE_STATE_DESC = List.of( + new IntermediateStateDesc("present", ElementType.BOOLEAN) + ); + + public static List intermediateStateDesc() { + return INTERMEDIATE_STATE_DESC; + } + + private final List channels; + + private boolean state; + + public static PresentAggregatorFunction create(List inputChannels) { + return new PresentAggregatorFunction(inputChannels, false); + } + + private PresentAggregatorFunction(List channels, boolean state) { + this.channels = channels; + this.state = state; + } + + @Override + public int intermediateBlockCount() { + return intermediateStateDesc().size(); + } + + private int blockIndex() { + return channels.get(0); + } + + @Override + public void addRawInput(Page page, BooleanVector mask) { + if (mask.isConstant() && mask.getBoolean(0) == false) return; + + Block block = page.getBlock(blockIndex()); + this.state = mask.isConstant() ? block.getTotalValueCount() > 0 : presentMasked(block, mask); + } + + private boolean presentMasked(Block block, BooleanVector mask) { + for (int p = 0; p < block.getPositionCount(); p++) { + if (mask.getBoolean(p)) { + return true; + } + } + return false; + } + + @Override + public void addIntermediateInput(Page page) { + assert channels.size() == intermediateBlockCount(); + var blockIndex = blockIndex(); + assert page.getBlockCount() >= blockIndex + intermediateStateDesc().size(); + Block uncastBlock = page.getBlock(channels.get(0)); + if (uncastBlock.areAllValuesNull()) { + return; + } + BooleanVector present = page.getBlock(channels.get(0)).asVector(); + assert present.getPositionCount() == 1; + if (present.getBoolean(0)) { + this.state = true; + } + } + + @Override + public void evaluateIntermediate(Block[] blocks, int offset, DriverContext driverContext) { + evaluateFinal(blocks, offset, driverContext); + } + + @Override + public void evaluateFinal(Block[] blocks, int offset, DriverContext driverContext) { + blocks[offset] = driverContext.blockFactory().newConstantBooleanBlockWith(state, 1); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(this.getClass().getSimpleName()).append("["); + sb.append("channels=").append(channels); + sb.append("]"); + return sb.toString(); + } + + @Override + public void close() {} +} diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/PresentGroupingAggregatorFunction.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/PresentGroupingAggregatorFunction.java new file mode 100644 index 0000000000000..3b09e8085828a --- /dev/null +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/PresentGroupingAggregatorFunction.java @@ -0,0 +1,204 @@ +/* + * 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.compute.aggregation; + +import org.elasticsearch.common.util.BitArray; +import org.elasticsearch.compute.data.Block; +import org.elasticsearch.compute.data.BooleanBlock; +import org.elasticsearch.compute.data.BooleanVector; +import org.elasticsearch.compute.data.ElementType; +import org.elasticsearch.compute.data.IntArrayBlock; +import org.elasticsearch.compute.data.IntBigArrayBlock; +import org.elasticsearch.compute.data.IntVector; +import org.elasticsearch.compute.data.Page; +import org.elasticsearch.compute.operator.DriverContext; + +import java.util.List; + +public class PresentGroupingAggregatorFunction implements GroupingAggregatorFunction { + + private static final List INTERMEDIATE_STATE_DESC = List.of( + new IntermediateStateDesc("present", ElementType.BOOLEAN) + ); + + private final BitArray state; + private final List channels; + private final DriverContext driverContext; + + public static PresentGroupingAggregatorFunction create(DriverContext driverContext, List inputChannels) { + return new PresentGroupingAggregatorFunction(inputChannels, new BitArray(1, driverContext.bigArrays()), driverContext); + } + + public static List intermediateStateDesc() { + return INTERMEDIATE_STATE_DESC; + } + + private PresentGroupingAggregatorFunction(List channels, BitArray state, DriverContext driverContext) { + this.channels = channels; + this.state = state; + this.driverContext = driverContext; + } + + private int blockIndex() { + return channels.get(0); + } + + @Override + public int intermediateBlockCount() { + return intermediateStateDesc().size(); + } + + @Override + public AddInput prepareProcessRawInputPage(SeenGroupIds seenGroupIds, Page page) { + Block valuesBlock = page.getBlock(blockIndex()); + + return new AddInput() { + @Override + public void add(int positionOffset, IntArrayBlock groupIds) { + addRawInput(positionOffset, groupIds, valuesBlock); + } + + @Override + public void add(int positionOffset, IntBigArrayBlock groupIds) { + addRawInput(positionOffset, groupIds, valuesBlock); + } + + @Override + public void add(int positionOffset, IntVector groupIds) { + addRawInput(positionOffset, groupIds, valuesBlock); + } + + @Override + public void close() {} + }; + } + + private void addRawInput(int positionOffset, IntVector groups, Block values) { + int position = positionOffset; + for (int groupPosition = 0; groupPosition < groups.getPositionCount(); groupPosition++, position++) { + if (values.isNull(position)) { + continue; + } + state.set(groups.getInt(groupPosition), true); + } + } + + private void addRawInput(int positionOffset, IntArrayBlock groups, Block values) { + int position = positionOffset; + for (int groupPosition = 0; groupPosition < groups.getPositionCount(); groupPosition++, position++) { + if (groups.isNull(groupPosition) || values.isNull(position)) { + continue; + } + int groupStart = groups.getFirstValueIndex(groupPosition); + int groupEnd = groupStart + groups.getValueCount(groupPosition); + for (int g = groupStart; g < groupEnd; g++) { + state.set(groups.getInt(g), true); + } + } + } + + private void addRawInput(int positionOffset, IntBigArrayBlock groups, Block values) { + int position = positionOffset; + for (int groupPosition = 0; groupPosition < groups.getPositionCount(); groupPosition++, position++) { + if (groups.isNull(groupPosition) || values.isNull(position)) { + continue; + } + int groupStart = groups.getFirstValueIndex(groupPosition); + int groupEnd = groupStart + groups.getValueCount(groupPosition); + for (int g = groupStart; g < groupEnd; g++) { + state.set(groups.getInt(g), true); + } + } + } + + @Override + public void selectedMayContainUnseenGroups(SeenGroupIds seenGroupIds) {} + + @Override + public void addIntermediateInput(int positionOffset, IntArrayBlock groups, Page page) { + assert channels.size() == intermediateBlockCount(); + assert page.getBlockCount() >= blockIndex() + intermediateStateDesc().size(); + BooleanVector present = page.getBlock(channels.get(0)).asVector(); + for (int groupPosition = 0; groupPosition < groups.getPositionCount(); groupPosition++) { + if (groups.isNull(groupPosition) || present.getBoolean(groupPosition + positionOffset) == false) { + continue; + } + int groupStart = groups.getFirstValueIndex(groupPosition); + int groupEnd = groupStart + groups.getValueCount(groupPosition); + for (int g = groupStart; g < groupEnd; g++) { + state.set(groups.getInt(g), true); + } + } + } + + @Override + public void addIntermediateInput(int positionOffset, IntBigArrayBlock groups, Page page) { + assert channels.size() == intermediateBlockCount(); + assert page.getBlockCount() >= blockIndex() + intermediateStateDesc().size(); + BooleanVector present = page.getBlock(channels.get(0)).asVector(); + for (int groupPosition = 0; groupPosition < groups.getPositionCount(); groupPosition++) { + if (groups.isNull(groupPosition) || present.getBoolean(groupPosition + positionOffset) == false) { + continue; + } + + int groupStart = groups.getFirstValueIndex(groupPosition); + int groupEnd = groupStart + groups.getValueCount(groupPosition); + for (int g = groupStart; g < groupEnd; g++) { + state.set(groups.getInt(g), true); + } + } + } + + @Override + public void addIntermediateInput(int positionOffset, IntVector groups, Page page) { + assert channels.size() == intermediateBlockCount(); + assert page.getBlockCount() >= blockIndex() + intermediateStateDesc().size(); + BooleanVector present = page.getBlock(channels.get(0)).asVector(); + for (int groupPosition = 0; groupPosition < groups.getPositionCount(); groupPosition++) { + if (present.getBoolean(groupPosition + positionOffset)) { + state.set(groups.getInt(groupPosition), true); + } + } + } + + @Override + public void evaluateIntermediate(Block[] blocks, int offset, IntVector selected) { + try (BooleanVector.FixedBuilder builder = driverContext.blockFactory().newBooleanVectorFixedBuilder(selected.getPositionCount())) { + for (int i = 0; i < selected.getPositionCount(); i++) { + int group = selected.getInt(i); + builder.appendBoolean(state.get(group)); + } + blocks[offset] = builder.build().asBlock(); + } + } + + @Override + public void evaluateFinal(Block[] blocks, int offset, IntVector selected, GroupingAggregatorEvaluationContext evaluationContext) { + try (BooleanVector.Builder builder = evaluationContext.blockFactory().newBooleanVectorFixedBuilder(selected.getPositionCount())) { + for (int i = 0; i < selected.getPositionCount(); i++) { + int si = selected.getInt(i); + builder.appendBoolean(state.get(si)); + } + blocks[offset] = builder.build().asBlock(); + } + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(this.getClass().getSimpleName()).append("["); + sb.append("channels=").append(channels); + sb.append("]"); + return sb.toString(); + } + + @Override + public void close() { + state.close(); + } +} diff --git a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/PresentAggregatorFunctionTests.java b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/PresentAggregatorFunctionTests.java new file mode 100644 index 0000000000000..979583d69fc8d --- /dev/null +++ b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/PresentAggregatorFunctionTests.java @@ -0,0 +1,50 @@ +/* + * 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.compute.aggregation; + +import org.elasticsearch.compute.data.Block; +import org.elasticsearch.compute.data.BlockFactory; +import org.elasticsearch.compute.data.BooleanBlock; +import org.elasticsearch.compute.data.Page; +import org.elasticsearch.compute.operator.SourceOperator; +import org.elasticsearch.compute.test.SequenceLongBlockSourceOperator; + +import java.util.List; +import java.util.stream.LongStream; + +import static org.elasticsearch.compute.test.BlockTestUtils.valuesAtPositions; +import static org.hamcrest.Matchers.equalTo; + +public class PresentAggregatorFunctionTests extends AggregatorFunctionTestCase { + @Override + protected SourceOperator simpleInput(BlockFactory blockFactory, int size) { + return new SequenceLongBlockSourceOperator(blockFactory, LongStream.range(0, size).map(l -> randomLong())); + } + + @Override + protected AggregatorFunctionSupplier aggregatorFunction() { + return PresentAggregatorFunction.supplier(); + } + + @Override + protected String expectedDescriptionOfAggregator() { + return "present"; + } + + @Override + protected void assertSimpleOutput(List input, Block result) { + boolean present = input.stream().flatMapToLong(p -> allLongs(p.getBlock(0))).findAny().isPresent(); + assertThat(((BooleanBlock) result).getBoolean(0), equalTo(present)); + } + + @Override + protected void assertOutputFromEmpty(Block b) { + assertThat(b.getPositionCount(), equalTo(1)); + assertThat(valuesAtPositions(b, 0, 1), equalTo(List.of(List.of(false)))); + } +} diff --git a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/PresentGroupingAggregatorFunctionTests.java b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/PresentGroupingAggregatorFunctionTests.java new file mode 100644 index 0000000000000..f58a8933be3fe --- /dev/null +++ b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/PresentGroupingAggregatorFunctionTests.java @@ -0,0 +1,72 @@ +/* + * 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.compute.aggregation; + +import org.elasticsearch.compute.data.Block; +import org.elasticsearch.compute.data.BlockFactory; +import org.elasticsearch.compute.data.BooleanBlock; +import org.elasticsearch.compute.data.BooleanVector; +import org.elasticsearch.compute.data.ElementType; +import org.elasticsearch.compute.data.Page; +import org.elasticsearch.compute.operator.LongDoubleTupleBlockSourceOperator; +import org.elasticsearch.compute.operator.SourceOperator; +import org.elasticsearch.compute.test.TupleLongLongBlockSourceOperator; +import org.elasticsearch.core.Tuple; + +import java.util.List; +import java.util.stream.LongStream; + +import static org.hamcrest.Matchers.equalTo; + +public class PresentGroupingAggregatorFunctionTests extends GroupingAggregatorFunctionTestCase { + @Override + protected AggregatorFunctionSupplier aggregatorFunction() { + return PresentAggregatorFunction.supplier(); + } + + @Override + protected String expectedDescriptionOfAggregator() { + return "present"; + } + + @Override + protected SourceOperator simpleInput(BlockFactory blockFactory, int size) { + if (randomBoolean()) { + return new TupleLongLongBlockSourceOperator( + blockFactory, + LongStream.range(0, size).mapToObj(l -> Tuple.tuple(randomLongBetween(0, 4), randomLong())) + ); + } + return new LongDoubleTupleBlockSourceOperator( + blockFactory, + LongStream.range(0, size).mapToObj(l -> Tuple.tuple(randomLongBetween(0, 4), randomDouble())) + ); + } + + @Override + protected void assertSimpleGroup(List input, Block result, int position, Long group) { + boolean present = input.stream().flatMapToInt(p -> allValueOffsets(p, group)).findAny().isPresent(); + assertThat(((BooleanBlock) result).getBoolean(position), equalTo(present)); + } + + @Override + protected void assertOutputFromNullOnly(Block b, int position) { + assertThat(b.isNull(position), equalTo(false)); + assertThat(b.getValueCount(position), equalTo(1)); + assertThat(((BooleanBlock) b).getBoolean(b.getFirstValueIndex(position)), equalTo(false)); + } + + @Override + protected void assertOutputFromAllFiltered(Block b) { + assertThat(b.elementType(), equalTo(ElementType.BOOLEAN)); + BooleanVector v = (BooleanVector) b.asVector(); + for (int p = 0; p < v.getPositionCount(); p++) { + assertThat(v.getBoolean(p), equalTo(false)); + } + } +} diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/present.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/present.csv-spec new file mode 100644 index 0000000000000..42b206fdbcb66 --- /dev/null +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/present.csv-spec @@ -0,0 +1,112 @@ +// PRESENT-specific tests + +PRESENT with no filter +required_capability: fn_present + +// tag::present[] +FROM employees +| STATS is_present = PRESENT(languages) +// end::present[] +; + +// tag::present-result[] +is_present:boolean +true +// end::present-result[] +; + +PRESENT with grouping +required_capability: fn_present + +// tag::present-by[] +FROM employees +| STATS is_present = PRESENT(salary) BY languages +// end::present-by[] +; +ignoreOrder:true + +// tag::present-by-result[] +is_present:boolean | languages:integer +true | 1 +true | 2 +true | 3 +true | 4 +true | 5 +true | null +// end::present-by-result[] +; + +PRESENT with filter +required_capability: fn_present + +FROM employees +| WHERE emp_no IN (10019, 10020) +| STATS is_present = PRESENT(languages) +; + +is_present:boolean +true +; + +Not PRESENT with filter +required_capability: fn_present + +FROM employees +| WHERE emp_no == 10020 +| STATS is_present = PRESENT(languages) +; + +is_present:boolean +false +; + +Not PRESENT with null evaluated field +required_capability: fn_present + +FROM employees +| EVAL null_field = null +| STATS is_present = PRESENT(null_field) +; + +is_present:boolean +false +; + +PRESENT with an expression +required_capability: fn_present + +FROM employees +| STATS is_present = PRESENT(salary + 10) +; + +is_present:boolean +true +; + +Some fields are not PRESENT with grouping +required_capability: fn_present + +FROM employees +| WHERE gender IS NULL +| STATS is_present = PRESENT(gender) BY languages +; +ignoreOrder:true + +is_present:boolean | languages:integer +false | 4 +false | 5 +false | 1 +false | 2 +; + +PRESENT with per-agg filter +required_capability: fn_present + +FROM employees +| STATS p_true = PRESENT(salary) WHERE gender == "M", + p_false = PRESENT(salary) WHERE gender == "X" +; + +p_true:boolean | p_false:boolean +true | false +; diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/union_types.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/union_types.csv-spec index ddd57a30fc808..6802874d36ad8 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/union_types.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/union_types.csv-spec @@ -2518,3 +2518,34 @@ x:date_nanos | b1d:date_nanos 2023-10-24T13:00:00.000Z | 2023-10-23T13:00:00.000Z 2023-10-24T12:00:00.000Z | 2023-10-23T12:00:00.000Z ; + +multiIndexAggregationUsingPresent +required_capability: date_nanos_type +required_capability: fn_present + +FROM employees, employees_incompatible +| WHERE hire_date > DATE_PARSE("yyyy-MM-dd", "1989-01-01") and hire_date < DATE_PARSE("yyyy-MM-dd", "1991-01-01") +| STATS is_present = present(birth_date) BY b = BUCKET(hire_date, 1 month) +| SORT b DESC, is_present +; +ignoreOrder:true + +is_present:boolean | b:date_nanos +false | 1989-03-01T00:00:00.000Z +false | 1990-10-01T00:00:00.000Z +true | 1989-02-01T00:00:00.000Z +true | 1989-04-01T00:00:00.000Z +true | 1989-06-01T00:00:00.000Z +true | 1989-07-01T00:00:00.000Z +true | 1989-08-01T00:00:00.000Z +true | 1989-09-01T00:00:00.000Z +true | 1989-11-01T00:00:00.000Z +true | 1989-12-01T00:00:00.000Z +true | 1990-01-01T00:00:00.000Z +true | 1990-02-01T00:00:00.000Z +true | 1990-03-01T00:00:00.000Z +true | 1990-06-01T00:00:00.000Z +true | 1990-08-01T00:00:00.000Z +true | 1990-09-01T00:00:00.000Z +true | 1990-12-01T00:00:00.000Z +; diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java index 9132e59e2257a..d9c096ab8a8c7 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java @@ -1452,7 +1452,12 @@ public enum Cap { /** * Implicitly applies last_over_time in time-series aggregations when no specific over_time function is provided. */ - IMPLICIT_LAST_OVER_TIME(Build.current().isSnapshot()); + IMPLICIT_LAST_OVER_TIME(Build.current().isSnapshot()), + + /** + * Support for the Present function + */ + FN_PRESENT; private final boolean enabled; diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java index 08c17be59ba8c..c94d93c0e9be8 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java @@ -36,6 +36,7 @@ import org.elasticsearch.xpack.esql.expression.function.aggregate.Min; import org.elasticsearch.xpack.esql.expression.function.aggregate.MinOverTime; import org.elasticsearch.xpack.esql.expression.function.aggregate.Percentile; +import org.elasticsearch.xpack.esql.expression.function.aggregate.Present; import org.elasticsearch.xpack.esql.expression.function.aggregate.Rate; import org.elasticsearch.xpack.esql.expression.function.aggregate.Sample; import org.elasticsearch.xpack.esql.expression.function.aggregate.SpatialCentroid; @@ -341,7 +342,8 @@ private static FunctionDefinition[][] functions() { def(Sum.class, uni(Sum::new), "sum"), def(Top.class, tri(Top::new), "top"), def(Values.class, uni(Values::new), "values"), - def(WeightedAvg.class, bi(WeightedAvg::new), "weighted_avg") }, + def(WeightedAvg.class, bi(WeightedAvg::new), "weighted_avg"), + def(Present.class, uni(Present::new), "present") }, // math new FunctionDefinition[] { def(Abs.class, Abs::new, "abs"), diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/AggregateWritables.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/AggregateWritables.java index 20979f0748ac4..bc2ddc90591ef 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/AggregateWritables.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/AggregateWritables.java @@ -41,7 +41,8 @@ public static List getNamedWriteables() { SumOverTime.ENTRY, CountOverTime.ENTRY, CountDistinctOverTime.ENTRY, - WeightedAvg.ENTRY + WeightedAvg.ENTRY, + Present.ENTRY ); } } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Present.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Present.java new file mode 100644 index 0000000000000..7f3ef8f47b917 --- /dev/null +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Present.java @@ -0,0 +1,130 @@ +/* + * 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.aggregate; + +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.compute.aggregation.AggregatorFunctionSupplier; +import org.elasticsearch.compute.aggregation.PresentAggregatorFunction; +import org.elasticsearch.xpack.esql.core.expression.Expression; +import org.elasticsearch.xpack.esql.core.expression.Literal; +import org.elasticsearch.xpack.esql.core.expression.Nullability; +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.FunctionType; +import org.elasticsearch.xpack.esql.expression.function.Param; +import org.elasticsearch.xpack.esql.planner.ToAggregator; + +import java.io.IOException; +import java.util.List; + +import static java.util.Collections.emptyList; +import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.DEFAULT; +import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isType; + +/** + * The function that checks for the presence of a field in the output result. + * Presence means that the input expression yields any non-null value. + */ +public class Present extends AggregateFunction implements ToAggregator { + public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(Expression.class, "Present", Present::new); + + @FunctionInfo( + returnType = "boolean", + description = "Returns true if the input expression yields any non-null values within the current aggregation context. " + + "Otherwise it returns false.", + type = FunctionType.AGGREGATE, + examples = { + @Example(file = "present", tag = "present"), + @Example( + description = "To check for the presence inside a group use `PRESENT()` and `BY` clauses", + file = "present", + tag = "present-by" + ) } + ) + public Present( + Source source, + @Param( + name = "field", + type = { + "aggregate_metric_double", + "boolean", + "cartesian_point", + "cartesian_shape", + "date", + "date_nanos", + "double", + "geo_point", + "geo_shape", + "geohash", + "geotile", + "geohex", + "integer", + "ip", + "keyword", + "long", + "text", + "unsigned_long", + "version" }, + description = "Expression that outputs values to be checked for presence." + ) Expression field + ) { + this(source, field, Literal.TRUE); + } + + public Present(Source source, Expression field, Expression filter) { + super(source, field, filter, emptyList()); + } + + private Present(StreamInput in) throws IOException { + super(in); + } + + @Override + public String getWriteableName() { + return ENTRY.name; + } + + @Override + protected NodeInfo info() { + return NodeInfo.create(this, Present::new, field(), filter()); + } + + @Override + public AggregateFunction withFilter(Expression filter) { + return new Present(source(), field(), filter); + } + + @Override + public Present replaceChildren(List newChildren) { + return new Present(source(), newChildren.get(0), newChildren.get(1)); + } + + @Override + public DataType dataType() { + return DataType.BOOLEAN; + } + + @Override + public AggregatorFunctionSupplier supplier() { + return PresentAggregatorFunction.supplier(); + } + + @Override + public Nullability nullable() { + return Nullability.FALSE; + } + + @Override + protected TypeResolution resolveType() { + return isType(field(), dt -> dt.isCounter() == false, sourceText(), DEFAULT, "any type except counter types"); + } +} diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java index 6288cc9c23d87..1805388d49fd0 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java @@ -1135,8 +1135,13 @@ public void testAggregateOnCounter() { public void testGroupByCounter() { assertThat( - error("FROM tests | STATS count(*) BY network.bytes_in", tsdb), - equalTo("1:32: cannot group by on [counter_long] type for grouping [network.bytes_in]") + error("FROM test | STATS count(*) BY network.bytes_in", tsdb), + equalTo("1:31: cannot group by on [counter_long] type for grouping [network.bytes_in]") + ); + + assertThat( + error("FROM test | STATS present(name) BY network.bytes_in", tsdb), + equalTo("1:36: cannot group by on [counter_long] type for grouping [network.bytes_in]") ); } @@ -2129,6 +2134,10 @@ public void testSortByAggregate() { ); assertEquals("1:22: aggregate function [max(a)] not allowed outside STATS command", error("ROW a = 1 | SORT 1 + max(a)")); assertEquals("1:18: aggregate function [count(*)] not allowed outside STATS command", error("FROM test | SORT count(*)")); + assertEquals( + "1:18: aggregate function [present(gender)] not allowed outside STATS command", + error("FROM test | SORT present(gender)") + ); } public void testFilterByAggregate() { @@ -2142,6 +2151,10 @@ public void testFilterByAggregate() { "1:24: aggregate function [min(languages)] not allowed outside STATS command", error("FROM employees | WHERE min(languages) > 2") ); + assertEquals( + "1:19: aggregate function [present(gender)] not allowed outside STATS command", + error("FROM test | WHERE present(gender)") + ); } public void testDissectByAggregate() { @@ -2169,6 +2182,7 @@ public void testGrokByAggregate() { public void testAggregateInRow() { assertEquals("1:13: aggregate function [count(*)] not allowed outside STATS command", error("ROW a = 1 + count(*)")); assertEquals("1:9: aggregate function [avg(2)] not allowed outside STATS command", error("ROW a = avg(2)")); + assertEquals("1:9: aggregate function [present(123)] not allowed outside STATS command", error("ROW a = present(123)")); } public void testLookupJoinDataTypeMismatch() { diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/PresentErrorTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/PresentErrorTests.java new file mode 100644 index 0000000000000..b394fa803f7ce --- /dev/null +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/PresentErrorTests.java @@ -0,0 +1,43 @@ +/* + * 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.aggregate; + +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.ErrorsForCasesWithoutExamplesTestCase; +import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier; +import org.hamcrest.Matcher; + +import java.util.List; +import java.util.Set; + +import static org.hamcrest.Matchers.equalTo; + +public class PresentErrorTests extends ErrorsForCasesWithoutExamplesTestCase { + @Override + protected List cases() { + return paramsToSuppliers(PresentTests.parameters()); + } + + @Override + protected Expression build(Source source, List args) { + return new Present(source, args.get(0)); + } + + @Override + protected Matcher expectedTypeErrorMatcher(List> validPerPosition, List signature) { + assert false : "All checked types must work"; + return null; + } + + @Override + protected void assertNumberOfCheckedSignatures(int checked) { + assertThat(checked, equalTo(0)); + } +} diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/PresentSerializationTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/PresentSerializationTests.java new file mode 100644 index 0000000000000..ab221e43861f6 --- /dev/null +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/PresentSerializationTests.java @@ -0,0 +1,24 @@ +/* + * 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.aggregate; + +import org.elasticsearch.xpack.esql.expression.AbstractExpressionSerializationTests; + +import java.io.IOException; + +public class PresentSerializationTests extends AbstractExpressionSerializationTests { + @Override + protected Present createTestInstance() { + return new Present(randomSource(), randomChild()); + } + + @Override + protected Present mutateInstance(Present instance) throws IOException { + return new Present(instance.source(), randomValueOtherThan(instance.field(), AbstractExpressionSerializationTests::randomChild)); + } +} diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/PresentTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/PresentTests.java new file mode 100644 index 0000000000000..8e19759a1b790 --- /dev/null +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/PresentTests.java @@ -0,0 +1,111 @@ +/* + * 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.aggregate; + +import com.carrotsearch.randomizedtesting.annotations.Name; +import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; + +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.AbstractAggregationTestCase; +import org.elasticsearch.xpack.esql.expression.function.MultiRowTestCaseSupplier; +import org.elasticsearch.xpack.esql.expression.function.MultiRowTestCaseSupplier.IncludingAltitude; +import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.hamcrest.Matchers.equalTo; + +public class PresentTests extends AbstractAggregationTestCase { + public PresentTests(@Name("TestCase") Supplier testCaseSupplier) { + this.testCase = testCaseSupplier.get(); + } + + @ParametersFactory + public static Iterable parameters() { + ArrayList suppliers = new ArrayList<>(); + + Stream.of( + MultiRowTestCaseSupplier.nullCases(1, 1000), + MultiRowTestCaseSupplier.intCases(1, 1000, Integer.MIN_VALUE, Integer.MAX_VALUE, true), + MultiRowTestCaseSupplier.longCases(1, 1000, Long.MIN_VALUE, Long.MAX_VALUE, true), + MultiRowTestCaseSupplier.ulongCases(1, 1000, BigInteger.ZERO, UNSIGNED_LONG_MAX, true), + MultiRowTestCaseSupplier.doubleCases(1, 1000, -Double.MAX_VALUE, Double.MAX_VALUE, true), + MultiRowTestCaseSupplier.dateCases(1, 1000), + MultiRowTestCaseSupplier.dateNanosCases(1, 1000), + MultiRowTestCaseSupplier.booleanCases(1, 1000), + MultiRowTestCaseSupplier.ipCases(1, 1000), + MultiRowTestCaseSupplier.versionCases(1, 1000), + MultiRowTestCaseSupplier.geoPointCases(1, 1000, IncludingAltitude.YES), + MultiRowTestCaseSupplier.geoShapeCasesWithoutCircle(1, 1000, IncludingAltitude.YES), + MultiRowTestCaseSupplier.cartesianShapeCasesWithoutCircle(1, 1000, IncludingAltitude.YES), + MultiRowTestCaseSupplier.geohashCases(1, 1000), + MultiRowTestCaseSupplier.geotileCases(1, 1000), + MultiRowTestCaseSupplier.geohexCases(1, 1000), + MultiRowTestCaseSupplier.stringCases(1, 1000, DataType.KEYWORD), + MultiRowTestCaseSupplier.stringCases(1, 1000, DataType.TEXT) + ).flatMap(List::stream).map(PresentTests::makeSupplier).collect(Collectors.toCollection(() -> suppliers)); + + // No rows + for (var dataType : List.of( + DataType.BOOLEAN, + DataType.CARTESIAN_POINT, + DataType.CARTESIAN_SHAPE, + DataType.DATE_NANOS, + DataType.DATETIME, + DataType.DATE_NANOS, + DataType.DOUBLE, + DataType.GEO_POINT, + DataType.GEO_SHAPE, + DataType.INTEGER, + DataType.IP, + DataType.KEYWORD, + DataType.LONG, + DataType.TEXT, + DataType.UNSIGNED_LONG, + DataType.VERSION + )) { + suppliers.add( + new TestCaseSupplier( + "No rows (" + dataType + ")", + List.of(dataType), + () -> new TestCaseSupplier.TestCase( + List.of(TestCaseSupplier.TypedData.multiRow(List.of(), dataType, "field")), + "Present", + DataType.BOOLEAN, + equalTo(false) + ) + ) + ); + } + + // "No rows" expects 0 here instead of null + return parameterSuppliersFromTypedData(randomizeBytesRefsOffset(suppliers)); + } + + @Override + protected Expression build(Source source, List args) { + return new Present(source, args.getFirst()); + } + + private static TestCaseSupplier makeSupplier(TestCaseSupplier.TypedDataSupplier fieldSupplier) { + return new TestCaseSupplier(fieldSupplier.name(), List.of(fieldSupplier.type()), () -> { + TestCaseSupplier.TypedData fieldTypedData = fieldSupplier.get(); + boolean present = fieldTypedData.multiRowData().stream().anyMatch(Objects::nonNull); + + return new TestCaseSupplier.TestCase(List.of(fieldTypedData), "Present", DataType.BOOLEAN, equalTo(present)); + }); + } +} diff --git a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/60_usage.yml b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/60_usage.yml index a2840648b444e..5f2470675714d 100644 --- a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/60_usage.yml +++ b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/60_usage.yml @@ -129,7 +129,7 @@ setup: - match: {esql.functions.coalesce: $functions_coalesce} - gt: {esql.functions.categorize: $functions_categorize} # Testing for the entire function set isn't feasible, so we just check that we return the correct count as an approximation. - - length: {esql.functions: 172} # check the "sister" test below for a likely update to the same esql.functions length check + - length: {esql.functions: 173} # check the "sister" test below for a likely update to the same esql.functions length check --- "Basic ESQL usage output (telemetry) non-snapshot version":