From 5303123b47b278b236e95ae1e9f0dfb5e99e9fdf Mon Sep 17 00:00:00 2001 From: Mouhcine Aitounejjar Date: Tue, 25 Nov 2025 14:50:36 -0500 Subject: [PATCH] [ES|QL] Support for First/Last - Unit Testing - Add ALL_FIRST/ALL_LAST unit tests, serialization tests, and documentation material (csv-test doc sample, md, json, etc.). --- .../functions/description/all_first.md | 6 + .../functions/description/all_last.md | 6 + .../_snippets/functions/examples/all_first.md | 29 +++ .../_snippets/functions/examples/all_last.md | 32 +++ .../_snippets/functions/layout/all_first.md | 27 +++ .../_snippets/functions/layout/all_last.md | 27 +++ .../functions/parameters/all_first.md | 10 + .../functions/parameters/all_last.md | 10 + .../_snippets/functions/types/all_first.md | 17 ++ .../_snippets/functions/types/all_last.md | 17 ++ .../esql/images/functions/all_first.svg | 1 + .../esql/images/functions/all_last.svg | 1 + .../definition/functions/all_first.json | 193 ++++++++++++++++++ .../kibana/definition/functions/all_last.json | 193 ++++++++++++++++++ .../esql/kibana/docs/functions/all_first.md | 24 +++ .../esql/kibana/docs/functions/all_last.md | 24 +++ .../stats_all_first_all_last.csv-spec | 95 +++++++-- .../aggregate/AbstractFirstLastTestCase.java | 80 ++++++++ .../aggregate/AllFirstSerializationTests.java | 32 +++ .../function/aggregate/AllFirstTests.java | 34 +++ .../aggregate/AllLastSerializationTests.java | 32 +++ .../function/aggregate/AllLastTests.java | 34 +++ .../function/aggregate/FirstTests.java | 61 +----- .../function/aggregate/LastTests.java | 22 +- 24 files changed, 914 insertions(+), 93 deletions(-) create mode 100644 docs/reference/query-languages/esql/_snippets/functions/description/all_first.md create mode 100644 docs/reference/query-languages/esql/_snippets/functions/description/all_last.md create mode 100644 docs/reference/query-languages/esql/_snippets/functions/examples/all_first.md create mode 100644 docs/reference/query-languages/esql/_snippets/functions/examples/all_last.md create mode 100644 docs/reference/query-languages/esql/_snippets/functions/layout/all_first.md create mode 100644 docs/reference/query-languages/esql/_snippets/functions/layout/all_last.md create mode 100644 docs/reference/query-languages/esql/_snippets/functions/parameters/all_first.md create mode 100644 docs/reference/query-languages/esql/_snippets/functions/parameters/all_last.md create mode 100644 docs/reference/query-languages/esql/_snippets/functions/types/all_first.md create mode 100644 docs/reference/query-languages/esql/_snippets/functions/types/all_last.md create mode 100644 docs/reference/query-languages/esql/images/functions/all_first.svg create mode 100644 docs/reference/query-languages/esql/images/functions/all_last.svg create mode 100644 docs/reference/query-languages/esql/kibana/definition/functions/all_first.json create mode 100644 docs/reference/query-languages/esql/kibana/definition/functions/all_last.json create mode 100644 docs/reference/query-languages/esql/kibana/docs/functions/all_first.md create mode 100644 docs/reference/query-languages/esql/kibana/docs/functions/all_last.md create mode 100644 x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/AbstractFirstLastTestCase.java create mode 100644 x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/AllFirstSerializationTests.java create mode 100644 x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/AllFirstTests.java create mode 100644 x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/AllLastSerializationTests.java create mode 100644 x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/AllLastTests.java diff --git a/docs/reference/query-languages/esql/_snippets/functions/description/all_first.md b/docs/reference/query-languages/esql/_snippets/functions/description/all_first.md new file mode 100644 index 0000000000000..010c06c5a9787 --- /dev/null +++ b/docs/reference/query-languages/esql/_snippets/functions/description/all_first.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** + +Calculates the earliest value of a field, and can operate on null values. + diff --git a/docs/reference/query-languages/esql/_snippets/functions/description/all_last.md b/docs/reference/query-languages/esql/_snippets/functions/description/all_last.md new file mode 100644 index 0000000000000..3a755cd859dba --- /dev/null +++ b/docs/reference/query-languages/esql/_snippets/functions/description/all_last.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** + +Calculates the latest value of a field, and can operate on null values. + diff --git a/docs/reference/query-languages/esql/_snippets/functions/examples/all_first.md b/docs/reference/query-languages/esql/_snippets/functions/examples/all_first.md new file mode 100644 index 0000000000000..7139e13472834 --- /dev/null +++ b/docs/reference/query-languages/esql/_snippets/functions/examples/all_first.md @@ -0,0 +1,29 @@ +% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it. + +**Example** + +```esql +ROW row = [ + # @timestamp | name | number + "2025-11-25T00:00:00.000Z | alpha | ", + "2025-11-25T00:00:01.000Z | alpha | 2", + "2025-11-25T00:00:02.000Z | bravo | ", + "2025-11-25T00:00:03.000Z | alpha | 4", + "2025-11-25T00:00:04.000Z | bravo | 5", + "2025-11-25T00:00:05.000Z | charlie | 6", + "2025-11-25T00:00:06.000Z | delta | " +] +| MV_EXPAND row +| DISSECT row """%{@timestamp} | %{name} | %{number}""" +| KEEP @timestamp, name, number +| EVAL @timestamp = TO_DATETIME(@timestamp), + name = TRIM(name), + number = TO_LONG(number) +| STATS first_val = ALL_FIRST(number, @timestamp) +``` + +| first_val:long | +| --- | +| null | + + diff --git a/docs/reference/query-languages/esql/_snippets/functions/examples/all_last.md b/docs/reference/query-languages/esql/_snippets/functions/examples/all_last.md new file mode 100644 index 0000000000000..037bd4d206669 --- /dev/null +++ b/docs/reference/query-languages/esql/_snippets/functions/examples/all_last.md @@ -0,0 +1,32 @@ +% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it. + +**Example** + +```esql +ROW row = [ + # @timestamp | name | number + "2025-11-25T00:00:00.000Z | alpha | ", + "2025-11-25T00:00:01.000Z | alpha | 2", + "2025-11-25T00:00:02.000Z | bravo | ", + "2025-11-25T00:00:03.000Z | alpha | 4", + "2025-11-25T00:00:04.000Z | bravo | 5", + "2025-11-25T00:00:05.000Z | charlie | 6", + "2025-11-25T00:00:06.000Z | delta | " +] +| MV_EXPAND row +| DISSECT row """%{@timestamp} | %{name} | %{number}""" +| KEEP @timestamp, name, number +| EVAL @timestamp = TO_DATETIME(@timestamp), + name = TRIM(name), + number = TO_LONG(number) +| STATS last_val = ALL_LAST(number, @timestamp) BY name +``` + +| last_val:long | name:keyword | +| --- | --- | +| 4 | alpha | +| 5 | bravo | +| 6 | charlie | +| null | delta | + + diff --git a/docs/reference/query-languages/esql/_snippets/functions/layout/all_first.md b/docs/reference/query-languages/esql/_snippets/functions/layout/all_first.md new file mode 100644 index 0000000000000..014e7c851f0f3 --- /dev/null +++ b/docs/reference/query-languages/esql/_snippets/functions/layout/all_first.md @@ -0,0 +1,27 @@ +% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it. + +## `ALL_FIRST` [esql-all_first] +```{applies_to} +stack: development +serverless: preview +``` + +**Syntax** + +:::{image} ../../../images/functions/all_first.svg +:alt: Embedded +:class: text-center +::: + + +:::{include} ../parameters/all_first.md +::: + +:::{include} ../description/all_first.md +::: + +:::{include} ../types/all_first.md +::: + +:::{include} ../examples/all_first.md +::: diff --git a/docs/reference/query-languages/esql/_snippets/functions/layout/all_last.md b/docs/reference/query-languages/esql/_snippets/functions/layout/all_last.md new file mode 100644 index 0000000000000..29a3ac27992e8 --- /dev/null +++ b/docs/reference/query-languages/esql/_snippets/functions/layout/all_last.md @@ -0,0 +1,27 @@ +% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it. + +## `ALL_LAST` [esql-all_last] +```{applies_to} +stack: development +serverless: preview +``` + +**Syntax** + +:::{image} ../../../images/functions/all_last.svg +:alt: Embedded +:class: text-center +::: + + +:::{include} ../parameters/all_last.md +::: + +:::{include} ../description/all_last.md +::: + +:::{include} ../types/all_last.md +::: + +:::{include} ../examples/all_last.md +::: diff --git a/docs/reference/query-languages/esql/_snippets/functions/parameters/all_first.md b/docs/reference/query-languages/esql/_snippets/functions/parameters/all_first.md new file mode 100644 index 0000000000000..78c98d3923a2e --- /dev/null +++ b/docs/reference/query-languages/esql/_snippets/functions/parameters/all_first.md @@ -0,0 +1,10 @@ +% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it. + +**Parameters** + +`value` +: Values to return + +`sort` +: Sort key + diff --git a/docs/reference/query-languages/esql/_snippets/functions/parameters/all_last.md b/docs/reference/query-languages/esql/_snippets/functions/parameters/all_last.md new file mode 100644 index 0000000000000..78c98d3923a2e --- /dev/null +++ b/docs/reference/query-languages/esql/_snippets/functions/parameters/all_last.md @@ -0,0 +1,10 @@ +% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it. + +**Parameters** + +`value` +: Values to return + +`sort` +: Sort key + diff --git a/docs/reference/query-languages/esql/_snippets/functions/types/all_first.md b/docs/reference/query-languages/esql/_snippets/functions/types/all_first.md new file mode 100644 index 0000000000000..ca982dfb580bc --- /dev/null +++ b/docs/reference/query-languages/esql/_snippets/functions/types/all_first.md @@ -0,0 +1,17 @@ +% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it. + +**Supported types** + +| value | sort | result | +| --- | --- | --- | +| double | date | double | +| double | date_nanos | double | +| integer | date | integer | +| integer | date_nanos | integer | +| keyword | date | keyword | +| keyword | date_nanos | keyword | +| long | date | long | +| long | date_nanos | long | +| text | date | keyword | +| text | date_nanos | keyword | + diff --git a/docs/reference/query-languages/esql/_snippets/functions/types/all_last.md b/docs/reference/query-languages/esql/_snippets/functions/types/all_last.md new file mode 100644 index 0000000000000..ca982dfb580bc --- /dev/null +++ b/docs/reference/query-languages/esql/_snippets/functions/types/all_last.md @@ -0,0 +1,17 @@ +% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it. + +**Supported types** + +| value | sort | result | +| --- | --- | --- | +| double | date | double | +| double | date_nanos | double | +| integer | date | integer | +| integer | date_nanos | integer | +| keyword | date | keyword | +| keyword | date_nanos | keyword | +| long | date | long | +| long | date_nanos | long | +| text | date | keyword | +| text | date_nanos | keyword | + diff --git a/docs/reference/query-languages/esql/images/functions/all_first.svg b/docs/reference/query-languages/esql/images/functions/all_first.svg new file mode 100644 index 0000000000000..e0b1e19f92cb5 --- /dev/null +++ b/docs/reference/query-languages/esql/images/functions/all_first.svg @@ -0,0 +1 @@ +ALL_FIRST(value,sort) \ No newline at end of file diff --git a/docs/reference/query-languages/esql/images/functions/all_last.svg b/docs/reference/query-languages/esql/images/functions/all_last.svg new file mode 100644 index 0000000000000..0f08160c4fae6 --- /dev/null +++ b/docs/reference/query-languages/esql/images/functions/all_last.svg @@ -0,0 +1 @@ +ALL_LAST(value,sort) \ No newline at end of file diff --git a/docs/reference/query-languages/esql/kibana/definition/functions/all_first.json b/docs/reference/query-languages/esql/kibana/definition/functions/all_first.json new file mode 100644 index 0000000000000..75bd76e078a1f --- /dev/null +++ b/docs/reference/query-languages/esql/kibana/definition/functions/all_first.json @@ -0,0 +1,193 @@ +{ + "comment" : "This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it.", + "type" : "agg", + "name" : "all_first", + "description" : "Calculates the earliest value of a field, and can operate on null values.", + "signatures" : [ + { + "params" : [ + { + "name" : "value", + "type" : "double", + "optional" : false, + "description" : "Values to return" + }, + { + "name" : "sort", + "type" : "date", + "optional" : false, + "description" : "Sort key" + } + ], + "variadic" : false, + "returnType" : "double" + }, + { + "params" : [ + { + "name" : "value", + "type" : "double", + "optional" : false, + "description" : "Values to return" + }, + { + "name" : "sort", + "type" : "date_nanos", + "optional" : false, + "description" : "Sort key" + } + ], + "variadic" : false, + "returnType" : "double" + }, + { + "params" : [ + { + "name" : "value", + "type" : "integer", + "optional" : false, + "description" : "Values to return" + }, + { + "name" : "sort", + "type" : "date", + "optional" : false, + "description" : "Sort key" + } + ], + "variadic" : false, + "returnType" : "integer" + }, + { + "params" : [ + { + "name" : "value", + "type" : "integer", + "optional" : false, + "description" : "Values to return" + }, + { + "name" : "sort", + "type" : "date_nanos", + "optional" : false, + "description" : "Sort key" + } + ], + "variadic" : false, + "returnType" : "integer" + }, + { + "params" : [ + { + "name" : "value", + "type" : "keyword", + "optional" : false, + "description" : "Values to return" + }, + { + "name" : "sort", + "type" : "date", + "optional" : false, + "description" : "Sort key" + } + ], + "variadic" : false, + "returnType" : "keyword" + }, + { + "params" : [ + { + "name" : "value", + "type" : "keyword", + "optional" : false, + "description" : "Values to return" + }, + { + "name" : "sort", + "type" : "date_nanos", + "optional" : false, + "description" : "Sort key" + } + ], + "variadic" : false, + "returnType" : "keyword" + }, + { + "params" : [ + { + "name" : "value", + "type" : "long", + "optional" : false, + "description" : "Values to return" + }, + { + "name" : "sort", + "type" : "date", + "optional" : false, + "description" : "Sort key" + } + ], + "variadic" : false, + "returnType" : "long" + }, + { + "params" : [ + { + "name" : "value", + "type" : "long", + "optional" : false, + "description" : "Values to return" + }, + { + "name" : "sort", + "type" : "date_nanos", + "optional" : false, + "description" : "Sort key" + } + ], + "variadic" : false, + "returnType" : "long" + }, + { + "params" : [ + { + "name" : "value", + "type" : "text", + "optional" : false, + "description" : "Values to return" + }, + { + "name" : "sort", + "type" : "date", + "optional" : false, + "description" : "Sort key" + } + ], + "variadic" : false, + "returnType" : "keyword" + }, + { + "params" : [ + { + "name" : "value", + "type" : "text", + "optional" : false, + "description" : "Values to return" + }, + { + "name" : "sort", + "type" : "date_nanos", + "optional" : false, + "description" : "Sort key" + } + ], + "variadic" : false, + "returnType" : "keyword" + } + ], + "examples" : [ + "ROW row = [\n # @timestamp | name | number\n \"2025-11-25T00:00:00.000Z | alpha | \",\n \"2025-11-25T00:00:01.000Z | alpha | 2\",\n \"2025-11-25T00:00:02.000Z | bravo | \",\n \"2025-11-25T00:00:03.000Z | alpha | 4\",\n \"2025-11-25T00:00:04.000Z | bravo | 5\",\n \"2025-11-25T00:00:05.000Z | charlie | 6\",\n \"2025-11-25T00:00:06.000Z | delta | \"\n]\n| MV_EXPAND row\n| DISSECT row \"\"\"%{@timestamp} | %{name} | %{number}\"\"\"\n| KEEP @timestamp, name, number\n| EVAL @timestamp = TO_DATETIME(@timestamp),\n name = TRIM(name),\n number = TO_LONG(number)\n| STATS first_val = ALL_FIRST(number, @timestamp)" + ], + "preview" : true, + "snapshot_only" : true +} diff --git a/docs/reference/query-languages/esql/kibana/definition/functions/all_last.json b/docs/reference/query-languages/esql/kibana/definition/functions/all_last.json new file mode 100644 index 0000000000000..5286def5c3626 --- /dev/null +++ b/docs/reference/query-languages/esql/kibana/definition/functions/all_last.json @@ -0,0 +1,193 @@ +{ + "comment" : "This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it.", + "type" : "agg", + "name" : "all_last", + "description" : "Calculates the latest value of a field, and can operate on null values.", + "signatures" : [ + { + "params" : [ + { + "name" : "value", + "type" : "double", + "optional" : false, + "description" : "Values to return" + }, + { + "name" : "sort", + "type" : "date", + "optional" : false, + "description" : "Sort key" + } + ], + "variadic" : false, + "returnType" : "double" + }, + { + "params" : [ + { + "name" : "value", + "type" : "double", + "optional" : false, + "description" : "Values to return" + }, + { + "name" : "sort", + "type" : "date_nanos", + "optional" : false, + "description" : "Sort key" + } + ], + "variadic" : false, + "returnType" : "double" + }, + { + "params" : [ + { + "name" : "value", + "type" : "integer", + "optional" : false, + "description" : "Values to return" + }, + { + "name" : "sort", + "type" : "date", + "optional" : false, + "description" : "Sort key" + } + ], + "variadic" : false, + "returnType" : "integer" + }, + { + "params" : [ + { + "name" : "value", + "type" : "integer", + "optional" : false, + "description" : "Values to return" + }, + { + "name" : "sort", + "type" : "date_nanos", + "optional" : false, + "description" : "Sort key" + } + ], + "variadic" : false, + "returnType" : "integer" + }, + { + "params" : [ + { + "name" : "value", + "type" : "keyword", + "optional" : false, + "description" : "Values to return" + }, + { + "name" : "sort", + "type" : "date", + "optional" : false, + "description" : "Sort key" + } + ], + "variadic" : false, + "returnType" : "keyword" + }, + { + "params" : [ + { + "name" : "value", + "type" : "keyword", + "optional" : false, + "description" : "Values to return" + }, + { + "name" : "sort", + "type" : "date_nanos", + "optional" : false, + "description" : "Sort key" + } + ], + "variadic" : false, + "returnType" : "keyword" + }, + { + "params" : [ + { + "name" : "value", + "type" : "long", + "optional" : false, + "description" : "Values to return" + }, + { + "name" : "sort", + "type" : "date", + "optional" : false, + "description" : "Sort key" + } + ], + "variadic" : false, + "returnType" : "long" + }, + { + "params" : [ + { + "name" : "value", + "type" : "long", + "optional" : false, + "description" : "Values to return" + }, + { + "name" : "sort", + "type" : "date_nanos", + "optional" : false, + "description" : "Sort key" + } + ], + "variadic" : false, + "returnType" : "long" + }, + { + "params" : [ + { + "name" : "value", + "type" : "text", + "optional" : false, + "description" : "Values to return" + }, + { + "name" : "sort", + "type" : "date", + "optional" : false, + "description" : "Sort key" + } + ], + "variadic" : false, + "returnType" : "keyword" + }, + { + "params" : [ + { + "name" : "value", + "type" : "text", + "optional" : false, + "description" : "Values to return" + }, + { + "name" : "sort", + "type" : "date_nanos", + "optional" : false, + "description" : "Sort key" + } + ], + "variadic" : false, + "returnType" : "keyword" + } + ], + "examples" : [ + "ROW row = [\n # @timestamp | name | number\n \"2025-11-25T00:00:00.000Z | alpha | \",\n \"2025-11-25T00:00:01.000Z | alpha | 2\",\n \"2025-11-25T00:00:02.000Z | bravo | \",\n \"2025-11-25T00:00:03.000Z | alpha | 4\",\n \"2025-11-25T00:00:04.000Z | bravo | 5\",\n \"2025-11-25T00:00:05.000Z | charlie | 6\",\n \"2025-11-25T00:00:06.000Z | delta | \"\n]\n| MV_EXPAND row\n| DISSECT row \"\"\"%{@timestamp} | %{name} | %{number}\"\"\"\n| KEEP @timestamp, name, number\n| EVAL @timestamp = TO_DATETIME(@timestamp),\n name = TRIM(name),\n number = TO_LONG(number)\n| STATS last_val = ALL_LAST(number, @timestamp) BY name" + ], + "preview" : true, + "snapshot_only" : true +} diff --git a/docs/reference/query-languages/esql/kibana/docs/functions/all_first.md b/docs/reference/query-languages/esql/kibana/docs/functions/all_first.md new file mode 100644 index 0000000000000..7918982bea93e --- /dev/null +++ b/docs/reference/query-languages/esql/kibana/docs/functions/all_first.md @@ -0,0 +1,24 @@ +% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it. + +### ALL FIRST +Calculates the earliest value of a field, and can operate on null values. + +```esql +ROW row = [ + # @timestamp | name | number + "2025-11-25T00:00:00.000Z | alpha | ", + "2025-11-25T00:00:01.000Z | alpha | 2", + "2025-11-25T00:00:02.000Z | bravo | ", + "2025-11-25T00:00:03.000Z | alpha | 4", + "2025-11-25T00:00:04.000Z | bravo | 5", + "2025-11-25T00:00:05.000Z | charlie | 6", + "2025-11-25T00:00:06.000Z | delta | " +] +| MV_EXPAND row +| DISSECT row """%{@timestamp} | %{name} | %{number}""" +| KEEP @timestamp, name, number +| EVAL @timestamp = TO_DATETIME(@timestamp), + name = TRIM(name), + number = TO_LONG(number) +| STATS first_val = ALL_FIRST(number, @timestamp) +``` diff --git a/docs/reference/query-languages/esql/kibana/docs/functions/all_last.md b/docs/reference/query-languages/esql/kibana/docs/functions/all_last.md new file mode 100644 index 0000000000000..a2c6f7983a2db --- /dev/null +++ b/docs/reference/query-languages/esql/kibana/docs/functions/all_last.md @@ -0,0 +1,24 @@ +% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it. + +### ALL LAST +Calculates the latest value of a field, and can operate on null values. + +```esql +ROW row = [ + # @timestamp | name | number + "2025-11-25T00:00:00.000Z | alpha | ", + "2025-11-25T00:00:01.000Z | alpha | 2", + "2025-11-25T00:00:02.000Z | bravo | ", + "2025-11-25T00:00:03.000Z | alpha | 4", + "2025-11-25T00:00:04.000Z | bravo | 5", + "2025-11-25T00:00:05.000Z | charlie | 6", + "2025-11-25T00:00:06.000Z | delta | " +] +| MV_EXPAND row +| DISSECT row """%{@timestamp} | %{name} | %{number}""" +| KEEP @timestamp, name, number +| EVAL @timestamp = TO_DATETIME(@timestamp), + name = TRIM(name), + number = TO_LONG(number) +| STATS last_val = ALL_LAST(number, @timestamp) BY name +``` diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/stats_all_first_all_last.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/stats_all_first_all_last.csv-spec index 5ee6458680315..8ae74ae581174 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/stats_all_first_all_last.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/stats_all_first_all_last.csv-spec @@ -4,13 +4,13 @@ required_capability: all_last ROW row = [ # @timestamp | name | number - "2023-01-23T00:00:00.000Z | alpha | ", - "2023-01-23T00:00:01.000Z | alpha | 2", - "2023-01-23T00:00:02.000Z | bravo | ", - "2023-01-23T00:00:03.000Z | alpha | 4", - "2023-01-23T00:00:04.000Z | bravo | 5", - "2023-01-23T00:00:05.000Z | charlie | 6", - "2023-01-23T00:00:06.000Z | delta | " + "2025-11-25T00:00:00.000Z | alpha | ", + "2025-11-25T00:00:01.000Z | alpha | 2", + "2025-11-25T00:00:02.000Z | bravo | ", + "2025-11-25T00:00:03.000Z | alpha | 4", + "2025-11-25T00:00:04.000Z | bravo | 5", + "2025-11-25T00:00:05.000Z | charlie | 6", + "2025-11-25T00:00:06.000Z | delta | " ] | MV_EXPAND row | DISSECT row """%{@timestamp} | %{name} | %{number}""" @@ -45,13 +45,13 @@ required_capability: all_last ROW row = [ # @timestamp | name | number - "2023-01-23T00:00:00.000Z | alpha | ", - "2023-01-23T00:00:01.000Z | alpha | 2", - "2023-01-23T00:00:02.000Z | bravo | ", - "2023-01-23T00:00:03.000Z | alpha | 4", - "2023-01-23T00:00:04.000Z | bravo | 5", - "2023-01-23T00:00:05.000Z | charlie | 6", - "2023-01-23T00:00:06.000Z | delta | " + "2025-11-25T00:00:00.000Z | alpha | ", + "2025-11-25T00:00:01.000Z | alpha | 2", + "2025-11-25T00:00:02.000Z | bravo | ", + "2025-11-25T00:00:03.000Z | alpha | 4", + "2025-11-25T00:00:04.000Z | bravo | 5", + "2025-11-25T00:00:05.000Z | charlie | 6", + "2025-11-25T00:00:06.000Z | delta | " ] | MV_EXPAND row | DISSECT row """%{@timestamp} | %{name} | %{number}""" @@ -83,3 +83,70 @@ null | null | null | null | 5 6 | 6 | 6.0 | 6 | 6 | 6 | 6.0 | 6 | charlie null | null | null | null | null | null | null | null | delta ; + +all_first_long_by_date +required_capability: all_first +// tag::all_first[] +ROW row = [ + # @timestamp | name | number + "2025-11-25T00:00:00.000Z | alpha | ", + "2025-11-25T00:00:01.000Z | alpha | 2", + "2025-11-25T00:00:02.000Z | bravo | ", + "2025-11-25T00:00:03.000Z | alpha | 4", + "2025-11-25T00:00:04.000Z | bravo | 5", + "2025-11-25T00:00:05.000Z | charlie | 6", + "2025-11-25T00:00:06.000Z | delta | " +] +| MV_EXPAND row +| DISSECT row """%{@timestamp} | %{name} | %{number}""" +| KEEP @timestamp, name, number +| EVAL @timestamp = TO_DATETIME(@timestamp), + name = TRIM(name), + number = TO_LONG(number) +| STATS first_val = ALL_FIRST(number, @timestamp) +// end::all_first[] +; + +warning:Line 15:10: evaluation of [TO_LONG(number)] failed, treating result as null. Only first 20 failures recorded. +warning:Line 15:10: org.elasticsearch.xpack.esql.core.InvalidArgumentException: Cannot parse number [] + +// tag::all_first-result[] +first_val:long +null +// end::all_first-result[] +; + +all_last_long_by_date +required_capability: all_last +// tag::all_last[] +ROW row = [ + # @timestamp | name | number + "2025-11-25T00:00:00.000Z | alpha | ", + "2025-11-25T00:00:01.000Z | alpha | 2", + "2025-11-25T00:00:02.000Z | bravo | ", + "2025-11-25T00:00:03.000Z | alpha | 4", + "2025-11-25T00:00:04.000Z | bravo | 5", + "2025-11-25T00:00:05.000Z | charlie | 6", + "2025-11-25T00:00:06.000Z | delta | " +] +| MV_EXPAND row +| DISSECT row """%{@timestamp} | %{name} | %{number}""" +| KEEP @timestamp, name, number +| EVAL @timestamp = TO_DATETIME(@timestamp), + name = TRIM(name), + number = TO_LONG(number) +| STATS last_val = ALL_LAST(number, @timestamp) BY name +// end::all_last[] +; + +warning:Line 15:10: evaluation of [TO_LONG(number)] failed, treating result as null. Only first 20 failures recorded. +warning:Line 15:10: org.elasticsearch.xpack.esql.core.InvalidArgumentException: Cannot parse number [] + +// tag::all_last-result[] +last_val:long | name:keyword +4 | alpha +5 | bravo +6 | charlie +null | delta +// end::all_last-result[] +; diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/AbstractFirstLastTestCase.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/AbstractFirstLastTestCase.java new file mode 100644 index 0000000000000..394d772de2962 --- /dev/null +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/AbstractFirstLastTestCase.java @@ -0,0 +1,80 @@ +/* + * 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.collect.Iterators; +import org.elasticsearch.xpack.esql.core.type.DataType; +import org.elasticsearch.xpack.esql.expression.function.AbstractAggregationTestCase; +import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier; +import org.hamcrest.Matchers; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.elasticsearch.xpack.esql.expression.function.MultiRowTestCaseSupplier.unlimitedSuppliers; +import static org.hamcrest.Matchers.anyOf; + +public abstract class AbstractFirstLastTestCase extends AbstractAggregationTestCase { + + public static Iterable parameters(boolean isFirst, boolean isNullable) { + int rows = 1000; + List suppliers = new ArrayList<>(); + + for (DataType valueType : List.of(DataType.INTEGER, DataType.LONG, DataType.DOUBLE, DataType.KEYWORD, DataType.TEXT)) { + for (TestCaseSupplier.TypedDataSupplier valueSupplier : unlimitedSuppliers(valueType, rows, rows)) { + for (DataType sortType : List.of(DataType.DATETIME, DataType.DATE_NANOS)) { + for (TestCaseSupplier.TypedDataSupplier sortSupplier : unlimitedSuppliers(sortType, rows, rows)) { + suppliers.add(makeSupplier(valueSupplier, sortSupplier, isFirst, isNullable)); + } + } + } + } + + return parameterSuppliersFromTypedData(randomizeBytesRefsOffset(suppliers)); + } + + private static TestCaseSupplier makeSupplier( + TestCaseSupplier.TypedDataSupplier valueSupplier, + TestCaseSupplier.TypedDataSupplier sortSupplier, + boolean first, + boolean isNullable + ) { + return new TestCaseSupplier( + valueSupplier.name() + ", " + sortSupplier.name(), + List.of(valueSupplier.type(), sortSupplier.type()), + () -> { + Long firstSort = null; + Set expected = new HashSet<>(); + TestCaseSupplier.TypedData values = valueSupplier.get(); + TestCaseSupplier.TypedData sorts = sortSupplier.get(); + List valuesList = (List) values.data(); + List sortsList = (List) sorts.data(); + + for (int p = 0; p < valuesList.size(); p++) { + Long s = (Long) sortsList.get(p); + if (firstSort == null || (first ? s < firstSort : s > firstSort)) { + firstSort = s; + expected.clear(); + expected.add(valuesList.get(p)); + } else if (firstSort.equals(s)) { + expected.add(valuesList.get(p)); + } + } + + return new TestCaseSupplier.TestCase( + List.of(values, sorts), + (isNullable ? "All" : "") + standardAggregatorName(first ? "First" : "Last", values.type()) + "ByTimestamp", + values.type(), + anyOf(() -> Iterators.map(expected.iterator(), Matchers::equalTo)) + ); + } + ); + } +} diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/AllFirstSerializationTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/AllFirstSerializationTests.java new file mode 100644 index 0000000000000..574013017c06b --- /dev/null +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/AllFirstSerializationTests.java @@ -0,0 +1,32 @@ +/* + * 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.expression.AbstractExpressionSerializationTests; + +import java.io.IOException; + +public class AllFirstSerializationTests extends AbstractExpressionSerializationTests { + @Override + protected AllFirst createTestInstance() { + return new AllFirst(randomSource(), randomChild(), randomChild()); + } + + @Override + protected AllFirst mutateInstance(AllFirst instance) throws IOException { + Expression field = instance.field(); + Expression sort = instance.sort(); + if (randomBoolean()) { + field = randomValueOtherThan(field, AbstractExpressionSerializationTests::randomChild); + } else { + sort = randomValueOtherThan(sort, AbstractExpressionSerializationTests::randomChild); + } + return new AllFirst(instance.source(), field, sort); + } +} diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/AllFirstTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/AllFirstTests.java new file mode 100644 index 0000000000000..693f2e47caf26 --- /dev/null +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/AllFirstTests.java @@ -0,0 +1,34 @@ +/* + * 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.expression.function.TestCaseSupplier; + +import java.util.List; +import java.util.function.Supplier; + +public class AllFirstTests extends AbstractFirstLastTestCase { + public AllFirstTests(@Name("TestCase") Supplier testCaseSupplier) { + this.testCase = testCaseSupplier.get(); + } + + @ParametersFactory + public static Iterable parameters() { + return parameters(true, true); + } + + @Override + protected Expression build(Source source, List args) { + return new AllFirst(source, args.get(0), args.get(1)); + } +} diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/AllLastSerializationTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/AllLastSerializationTests.java new file mode 100644 index 0000000000000..9ec337929ee95 --- /dev/null +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/AllLastSerializationTests.java @@ -0,0 +1,32 @@ +/* + * 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.expression.AbstractExpressionSerializationTests; + +import java.io.IOException; + +public class AllLastSerializationTests extends AbstractExpressionSerializationTests { + @Override + protected AllLast createTestInstance() { + return new AllLast(randomSource(), randomChild(), randomChild()); + } + + @Override + protected AllLast mutateInstance(AllLast instance) throws IOException { + Expression field = instance.field(); + Expression sort = instance.sort(); + if (randomBoolean()) { + field = randomValueOtherThan(field, AbstractExpressionSerializationTests::randomChild); + } else { + sort = randomValueOtherThan(sort, AbstractExpressionSerializationTests::randomChild); + } + return new AllLast(instance.source(), field, sort); + } +} diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/AllLastTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/AllLastTests.java new file mode 100644 index 0000000000000..c3d57f74a6593 --- /dev/null +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/AllLastTests.java @@ -0,0 +1,34 @@ +/* + * 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.expression.function.TestCaseSupplier; + +import java.util.List; +import java.util.function.Supplier; + +public class AllLastTests extends AbstractFirstLastTestCase { + public AllLastTests(@Name("TestCase") Supplier testCaseSupplier) { + this.testCase = testCaseSupplier.get(); + } + + @ParametersFactory + public static Iterable parameters() { + return parameters(false, true); + } + + @Override + protected Expression build(Source source, List args) { + return new AllLast(source, args.get(0), args.get(1)); + } +} diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/FirstTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/FirstTests.java index f6fbde2aa126b..b3a6604d2669f 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/FirstTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/FirstTests.java @@ -10,82 +10,25 @@ import com.carrotsearch.randomizedtesting.annotations.Name; import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; -import org.elasticsearch.common.collect.Iterators; 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.TestCaseSupplier; -import org.hamcrest.Matchers; -import java.util.ArrayList; -import java.util.HashSet; import java.util.List; -import java.util.Set; import java.util.function.Supplier; -import static org.elasticsearch.xpack.esql.expression.function.MultiRowTestCaseSupplier.unlimitedSuppliers; -import static org.hamcrest.Matchers.anyOf; - -public class FirstTests extends AbstractAggregationTestCase { +public class FirstTests extends AbstractFirstLastTestCase { public FirstTests(@Name("TestCase") Supplier testCaseSupplier) { this.testCase = testCaseSupplier.get(); } @ParametersFactory public static Iterable parameters() { - int rows = 1000; - List suppliers = new ArrayList<>(); - - for (DataType valueType : List.of(DataType.INTEGER, DataType.LONG, DataType.DOUBLE, DataType.KEYWORD, DataType.TEXT)) { - for (TestCaseSupplier.TypedDataSupplier valueSupplier : unlimitedSuppliers(valueType, rows, rows)) { - for (DataType sortType : List.of(DataType.DATETIME, DataType.DATE_NANOS)) { - for (TestCaseSupplier.TypedDataSupplier sortSupplier : unlimitedSuppliers(sortType, rows, rows)) { - suppliers.add(makeSupplier(valueSupplier, sortSupplier, true)); - } - } - } - } - return parameterSuppliersFromTypedData(randomizeBytesRefsOffset(suppliers)); + return parameters(true, false); } @Override protected Expression build(Source source, List args) { return new First(source, args.get(0), args.get(1)); } - - static TestCaseSupplier makeSupplier( - TestCaseSupplier.TypedDataSupplier valueSupplier, - TestCaseSupplier.TypedDataSupplier sortSupplier, - boolean first - ) { - return new TestCaseSupplier( - valueSupplier.name() + ", " + sortSupplier.name(), - List.of(valueSupplier.type(), sortSupplier.type()), - () -> { - Long firstSort = null; - Set expected = new HashSet<>(); - TestCaseSupplier.TypedData values = valueSupplier.get(); - TestCaseSupplier.TypedData sorts = sortSupplier.get(); - List valuesList = (List) values.data(); - List sortsList = (List) sorts.data(); - for (int p = 0; p < valuesList.size(); p++) { - Long s = (Long) sortsList.get(p); - if (firstSort == null || (first ? s < firstSort : s > firstSort)) { - firstSort = s; - expected.clear(); - expected.add(valuesList.get(p)); - } else if (firstSort.equals(s)) { - expected.add(valuesList.get(p)); - } - } - return new TestCaseSupplier.TestCase( - List.of(values, sorts), - standardAggregatorName(first ? "First" : "Last", values.type()) + "ByTimestamp", - values.type(), - anyOf(() -> Iterators.map(expected.iterator(), Matchers::equalTo)) - ); - } - ); - } } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/LastTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/LastTests.java index abba5631373e1..6311be0cdf681 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/LastTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/LastTests.java @@ -12,37 +12,19 @@ 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.TestCaseSupplier; -import java.util.ArrayList; import java.util.List; import java.util.function.Supplier; -import static org.elasticsearch.xpack.esql.expression.function.MultiRowTestCaseSupplier.unlimitedSuppliers; -import static org.elasticsearch.xpack.esql.expression.function.aggregate.FirstTests.makeSupplier; - -public class LastTests extends AbstractAggregationTestCase { +public class LastTests extends AbstractFirstLastTestCase { public LastTests(@Name("TestCase") Supplier testCaseSupplier) { this.testCase = testCaseSupplier.get(); } @ParametersFactory public static Iterable parameters() { - int rows = 1000; - List suppliers = new ArrayList<>(); - - for (DataType valueType : List.of(DataType.INTEGER, DataType.LONG, DataType.DOUBLE, DataType.KEYWORD, DataType.TEXT)) { - for (TestCaseSupplier.TypedDataSupplier valueSupplier : unlimitedSuppliers(valueType, rows, rows)) { - for (DataType sortType : List.of(DataType.DATETIME, DataType.DATE_NANOS)) { - for (TestCaseSupplier.TypedDataSupplier sortSupplier : unlimitedSuppliers(sortType, rows, rows)) { - suppliers.add(makeSupplier(valueSupplier, sortSupplier, false)); - } - } - } - } - return parameterSuppliersFromTypedData(randomizeBytesRefsOffset(suppliers)); + return parameters(false, false); } @Override