diff --git a/docs/reference/query-languages/esql/_snippets/functions/description/tbucket.md b/docs/reference/query-languages/esql/_snippets/functions/description/tbucket.md
new file mode 100644
index 0000000000000..dfe74f67beda8
--- /dev/null
+++ b/docs/reference/query-languages/esql/_snippets/functions/description/tbucket.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**
+
+Creates groups of values - buckets - out of a @timestamp attribute. The size of the buckets must be provided directly.
+
diff --git a/docs/reference/query-languages/esql/_snippets/functions/examples/tbucket.md b/docs/reference/query-languages/esql/_snippets/functions/examples/tbucket.md
new file mode 100644
index 0000000000000..0b4a724dc6493
--- /dev/null
+++ b/docs/reference/query-languages/esql/_snippets/functions/examples/tbucket.md
@@ -0,0 +1,43 @@
+% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it.
+
+**Examples**
+
+Provide a bucket size as an argument.
+
+```esql
+FROM sample_data
+| STATS min = MIN(@timestamp), max = MAX(@timestamp) BY bucket = TBUCKET(1 hour)
+| SORT min
+```
+
+| min:datetime | max:datetime | bucket:datetime |
+| --- | --- | --- |
+| 2023-10-23T12:15:03.360Z | 2023-10-23T12:27:28.948Z | 2023-10-23T12:00:00.000Z |
+| 2023-10-23T13:33:34.937Z | 2023-10-23T13:55:01.543Z | 2023-10-23T13:00:00.000Z |
+
+
+::::{note}
+When providing the bucket size, it must be a time duration or date period.
+Also the reference is epoch, which starts at `0001-01-01T00:00:00Z`.
+::::
+
+Provide a string representation of bucket size as an argument.
+
+```esql
+FROM sample_data
+| STATS min = MIN(@timestamp), max = MAX(@timestamp) BY bucket = TBUCKET("1 hour")
+| SORT min
+```
+
+| min:datetime | max:datetime | bucket:datetime |
+| --- | --- | --- |
+| 2023-10-23T12:15:03.360Z | 2023-10-23T12:27:28.948Z | 2023-10-23T12:00:00.000Z |
+| 2023-10-23T13:33:34.937Z | 2023-10-23T13:55:01.543Z | 2023-10-23T13:00:00.000Z |
+
+
+::::{note}
+When providing the bucket size, it can be a string representation of time duration or date period.
+For example, "1 hour". Also the reference is epoch, which starts at `0001-01-01T00:00:00Z`.
+::::
+
+
diff --git a/docs/reference/query-languages/esql/_snippets/functions/layout/tbucket.md b/docs/reference/query-languages/esql/_snippets/functions/layout/tbucket.md
new file mode 100644
index 0000000000000..9c7071a06d686
--- /dev/null
+++ b/docs/reference/query-languages/esql/_snippets/functions/layout/tbucket.md
@@ -0,0 +1,23 @@
+% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it.
+
+## `TBUCKET` [esql-tbucket]
+
+**Syntax**
+
+:::{image} ../../../images/functions/tbucket.svg
+:alt: Embedded
+:class: text-center
+:::
+
+
+:::{include} ../parameters/tbucket.md
+:::
+
+:::{include} ../description/tbucket.md
+:::
+
+:::{include} ../types/tbucket.md
+:::
+
+:::{include} ../examples/tbucket.md
+:::
diff --git a/docs/reference/query-languages/esql/_snippets/functions/parameters/tbucket.md b/docs/reference/query-languages/esql/_snippets/functions/parameters/tbucket.md
new file mode 100644
index 0000000000000..a811005a41bef
--- /dev/null
+++ b/docs/reference/query-languages/esql/_snippets/functions/parameters/tbucket.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**
+
+`buckets`
+: Desired bucket size.
+
diff --git a/docs/reference/query-languages/esql/_snippets/functions/types/tbucket.md b/docs/reference/query-languages/esql/_snippets/functions/types/tbucket.md
new file mode 100644
index 0000000000000..5c85899ebfb8a
--- /dev/null
+++ b/docs/reference/query-languages/esql/_snippets/functions/types/tbucket.md
@@ -0,0 +1,11 @@
+% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it.
+
+**Supported types**
+
+| buckets | result |
+| --- | --- |
+| date_period | date |
+| date_period | date_nanos |
+| time_duration | date |
+| time_duration | date_nanos |
+
diff --git a/docs/reference/query-languages/esql/_snippets/lists/grouping-functions.md b/docs/reference/query-languages/esql/_snippets/lists/grouping-functions.md
index bb0c752c63047..dbddb8e362266 100644
--- a/docs/reference/query-languages/esql/_snippets/lists/grouping-functions.md
+++ b/docs/reference/query-languages/esql/_snippets/lists/grouping-functions.md
@@ -1,2 +1,3 @@
* [`BUCKET`](../../functions-operators/grouping-functions.md#esql-bucket)
+* [`TBUCKET`](../../functions-operators/grouping-functions.md#esql-tbucket)
* [`CATEGORIZE`](../../functions-operators/grouping-functions.md#esql-categorize)
diff --git a/docs/reference/query-languages/esql/functions-operators/grouping-functions.md b/docs/reference/query-languages/esql/functions-operators/grouping-functions.md
index 7cd02febec968..e44617d5962d8 100644
--- a/docs/reference/query-languages/esql/functions-operators/grouping-functions.md
+++ b/docs/reference/query-languages/esql/functions-operators/grouping-functions.md
@@ -16,7 +16,10 @@ The [`STATS`](/reference/query-languages/esql/commands/stats-by.md) command supp
:::{include} ../_snippets/functions/layout/bucket.md
:::
-:::{note}
+:::{include} ../_snippets/functions/layout/tbucket.md
+:::
+
+:::{note}
The `CATEGORIZE` function requires a [platinum license](https://www.elastic.co/subscriptions).
:::
diff --git a/docs/reference/query-languages/esql/images/functions/tbucket.svg b/docs/reference/query-languages/esql/images/functions/tbucket.svg
new file mode 100644
index 0000000000000..095d6411c2c69
--- /dev/null
+++ b/docs/reference/query-languages/esql/images/functions/tbucket.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/reference/query-languages/esql/kibana/definition/functions/tbucket.json b/docs/reference/query-languages/esql/kibana/definition/functions/tbucket.json
new file mode 100644
index 0000000000000..c95f75bbfa2c6
--- /dev/null
+++ b/docs/reference/query-languages/esql/kibana/definition/functions/tbucket.json
@@ -0,0 +1,62 @@
+{
+ "comment" : "This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it.",
+ "type" : "grouping",
+ "name" : "tbucket",
+ "description" : "Creates groups of values - buckets - out of a @timestamp attribute. The size of the buckets must be provided directly.",
+ "signatures" : [
+ {
+ "params" : [
+ {
+ "name" : "buckets",
+ "type" : "date_period",
+ "optional" : false,
+ "description" : "Desired bucket size."
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "date"
+ },
+ {
+ "params" : [
+ {
+ "name" : "buckets",
+ "type" : "date_period",
+ "optional" : false,
+ "description" : "Desired bucket size."
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "date_nanos"
+ },
+ {
+ "params" : [
+ {
+ "name" : "buckets",
+ "type" : "time_duration",
+ "optional" : false,
+ "description" : "Desired bucket size."
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "date"
+ },
+ {
+ "params" : [
+ {
+ "name" : "buckets",
+ "type" : "time_duration",
+ "optional" : false,
+ "description" : "Desired bucket size."
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "date_nanos"
+ }
+ ],
+ "examples" : [
+ "FROM sample_data\n| STATS min = MIN(@timestamp), max = MAX(@timestamp) BY bucket = TBUCKET(1 hour)\n| SORT min",
+ "FROM sample_data\n| STATS min = MIN(@timestamp), max = MAX(@timestamp) BY bucket = TBUCKET(\"1 hour\")\n| SORT min"
+ ],
+ "preview" : false,
+ "snapshot_only" : false
+}
diff --git a/docs/reference/query-languages/esql/kibana/docs/functions/tbucket.md b/docs/reference/query-languages/esql/kibana/docs/functions/tbucket.md
new file mode 100644
index 0000000000000..bc0ad1ee27af9
--- /dev/null
+++ b/docs/reference/query-languages/esql/kibana/docs/functions/tbucket.md
@@ -0,0 +1,10 @@
+% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it.
+
+### TBUCKET
+Creates groups of values - buckets - out of a @timestamp attribute. The size of the buckets must be provided directly.
+
+```esql
+FROM sample_data
+| STATS min = MIN(@timestamp), max = MAX(@timestamp) BY bucket = TBUCKET(1 hour)
+| SORT min
+```
diff --git a/x-pack/plugin/esql-core/src/test/resources/mapping-odd-timestamp.json b/x-pack/plugin/esql-core/src/test/resources/mapping-odd-timestamp.json
new file mode 100644
index 0000000000000..0c3d34486bd11
--- /dev/null
+++ b/x-pack/plugin/esql-core/src/test/resources/mapping-odd-timestamp.json
@@ -0,0 +1,16 @@
+{
+ "properties": {
+ "@timestamp": {
+ "type": "boolean"
+ },
+ "client_ip": {
+ "type": "ip"
+ },
+ "event_duration": {
+ "type": "long"
+ },
+ "message": {
+ "type": "keyword"
+ }
+ }
+}
diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/k8s-timeseries.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/k8s-timeseries.csv-spec
index 5b226d647a606..a903503e147e0 100644
--- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/k8s-timeseries.csv-spec
+++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/k8s-timeseries.csv-spec
@@ -96,6 +96,18 @@ max(rate(network.total_bytes_in)): double | time_bucket:date
// end::rate-result[]
;
+oneRateWithTBucket
+required_capability: metrics_command
+required_capability: tbucket
+TS k8s
+| STATS max(rate(network.total_bytes_in)) BY time_bucket = tbucket(5minute)
+| SORT time_bucket DESC | LIMIT 2;
+
+max(rate(network.total_bytes_in)): double | time_bucket:date
+6.980660660660663 | 2024-05-10T00:20:00.000Z
+23.702205882352942 | 2024-05-10T00:15:00.000Z
+;
+
twoRatesWithBucket
required_capability: metrics_command
TS k8s | STATS max(rate(network.total_bytes_in)), sum(rate(network.total_bytes_in)) BY time_bucket = bucket(@timestamp,5minute) | SORT time_bucket DESC | LIMIT 3;
@@ -106,6 +118,16 @@ max(rate(network.total_bytes_in)):double | sum(rate(network.total_bytes_in)):dou
14.97039381153305 | 63.00652190262822 | 2024-05-10T00:10:00.000Z
;
+twoRatesWithTBucket
+required_capability: metrics_command
+required_capability: tbucket
+TS k8s | STATS max(rate(network.total_bytes_in)), sum(rate(network.total_bytes_in)) BY time_bucket = tbucket(5minute) | SORT time_bucket DESC | LIMIT 3;
+
+max(rate(network.total_bytes_in)):double | sum(rate(network.total_bytes_in)):double | time_bucket:datetime
+6.980660660660663 | 23.959973363184154 | 2024-05-10T00:20:00.000Z
+23.702205882352942 | 94.9517511187984 | 2024-05-10T00:15:00.000Z
+14.97039381153305 | 63.00652190262822 | 2024-05-10T00:10:00.000Z
+;
oneRateWithBucketAndCluster
required_capability: metrics_command
diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/tbucket.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/tbucket.csv-spec
new file mode 100644
index 0000000000000..cb0c699ae451e
--- /dev/null
+++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/tbucket.csv-spec
@@ -0,0 +1,347 @@
+// TBUCKET-specific tests
+
+tbucketByTenSecondsDuration
+required_capability: tbucket
+
+FROM sample_data
+| STATS min = MIN(@timestamp), max = MAX(@timestamp) BY bucket = TBUCKET(10 seconds)
+| SORT min
+;
+ignoreOrder:true
+
+min:datetime | max:datetime | bucket:datetime
+2023-10-23T12:15:03.360Z | 2023-10-23T12:15:03.360Z | 2023-10-23T12:15:00.000Z
+2023-10-23T12:27:28.948Z | 2023-10-23T12:27:28.948Z | 2023-10-23T12:27:20.000Z
+2023-10-23T13:33:34.937Z | 2023-10-23T13:33:34.937Z | 2023-10-23T13:33:30.000Z
+2023-10-23T13:51:54.732Z | 2023-10-23T13:51:54.732Z | 2023-10-23T13:51:50.000Z
+2023-10-23T13:52:55.015Z | 2023-10-23T13:52:55.015Z | 2023-10-23T13:52:50.000Z
+2023-10-23T13:53:55.832Z | 2023-10-23T13:53:55.832Z | 2023-10-23T13:53:50.000Z
+2023-10-23T13:55:01.543Z | 2023-10-23T13:55:01.543Z | 2023-10-23T13:55:00.000Z
+;
+
+tbucketByTenSecondsDurationAsString
+required_capability: implicit_casting_string_literal_to_temporal_amount
+required_capability: tbucket
+
+FROM sample_data
+| STATS min = MIN(@timestamp), max = MAX(@timestamp) BY bucket = TBUCKET("10 seconds")
+| SORT min
+;
+ignoreOrder:true
+
+min:datetime | max:datetime | bucket:datetime
+2023-10-23T12:15:03.360Z | 2023-10-23T12:15:03.360Z | 2023-10-23T12:15:00.000Z
+2023-10-23T12:27:28.948Z | 2023-10-23T12:27:28.948Z | 2023-10-23T12:27:20.000Z
+2023-10-23T13:33:34.937Z | 2023-10-23T13:33:34.937Z | 2023-10-23T13:33:30.000Z
+2023-10-23T13:51:54.732Z | 2023-10-23T13:51:54.732Z | 2023-10-23T13:51:50.000Z
+2023-10-23T13:52:55.015Z | 2023-10-23T13:52:55.015Z | 2023-10-23T13:52:50.000Z
+2023-10-23T13:53:55.832Z | 2023-10-23T13:53:55.832Z | 2023-10-23T13:53:50.000Z
+2023-10-23T13:55:01.543Z | 2023-10-23T13:55:01.543Z | 2023-10-23T13:55:00.000Z
+;
+
+tbucketByTenMinutesDuration
+required_capability: tbucket
+
+FROM sample_data
+| STATS min = MIN(@timestamp), max = MAX(@timestamp) BY bucket = TBUCKET(10 minutes)
+| SORT min
+;
+ignoreOrder:true
+
+min:datetime | max:datetime | bucket:datetime
+2023-10-23T12:15:03.360Z | 2023-10-23T12:15:03.360Z | 2023-10-23T12:10:00.000Z
+2023-10-23T12:27:28.948Z | 2023-10-23T12:27:28.948Z | 2023-10-23T12:20:00.000Z
+2023-10-23T13:33:34.937Z | 2023-10-23T13:33:34.937Z | 2023-10-23T13:30:00.000Z
+2023-10-23T13:51:54.732Z | 2023-10-23T13:55:01.543Z | 2023-10-23T13:50:00.000Z
+;
+
+tbucketByTenMinutesDurationAsString
+required_capability: implicit_casting_string_literal_to_temporal_amount
+required_capability: tbucket
+
+FROM sample_data
+| STATS min = MIN(@timestamp), max = MAX(@timestamp) BY bucket = TBUCKET("10 minutes")
+| SORT min
+;
+ignoreOrder:true
+
+min:datetime | max:datetime | bucket:datetime
+2023-10-23T12:15:03.360Z | 2023-10-23T12:15:03.360Z | 2023-10-23T12:10:00.000Z
+2023-10-23T12:27:28.948Z | 2023-10-23T12:27:28.948Z | 2023-10-23T12:20:00.000Z
+2023-10-23T13:33:34.937Z | 2023-10-23T13:33:34.937Z | 2023-10-23T13:30:00.000Z
+2023-10-23T13:51:54.732Z | 2023-10-23T13:55:01.543Z | 2023-10-23T13:50:00.000Z
+;
+
+docsTBucketByOneHourDuration
+required_capability: tbucket
+
+// tag::docsTBucketByOneHourDuration[]
+FROM sample_data
+| STATS min = MIN(@timestamp), max = MAX(@timestamp) BY bucket = TBUCKET(1 hour)
+| SORT min
+// end::docsTBucketByOneHourDuration[]
+;
+
+// tag::docsTBucketByOneHourDuration-result[]
+min:datetime | max:datetime | bucket:datetime
+2023-10-23T12:15:03.360Z | 2023-10-23T12:27:28.948Z | 2023-10-23T12:00:00.000Z
+2023-10-23T13:33:34.937Z | 2023-10-23T13:55:01.543Z | 2023-10-23T13:00:00.000Z
+// end::docsTBucketByOneHourDuration-result[]
+;
+
+docsTBucketByOneHourDurationAsString
+required_capability: implicit_casting_string_literal_to_temporal_amount
+required_capability: tbucket
+
+// tag::docsTBucketByOneHourDurationAsString[]
+FROM sample_data
+| STATS min = MIN(@timestamp), max = MAX(@timestamp) BY bucket = TBUCKET("1 hour")
+| SORT min
+// end::docsTBucketByOneHourDurationAsString[]
+;
+
+// tag::docsTBucketByOneHourDurationAsString-result[]
+min:datetime | max:datetime | bucket:datetime
+2023-10-23T12:15:03.360Z | 2023-10-23T12:27:28.948Z | 2023-10-23T12:00:00.000Z
+2023-10-23T13:33:34.937Z | 2023-10-23T13:55:01.543Z | 2023-10-23T13:00:00.000Z
+// end::docsTBucketByOneHourDurationAsString-result[]
+;
+
+tbucketByOneDayDuration
+required_capability: tbucket
+
+FROM sample_data
+| STATS min = MIN(@timestamp), max = MAX(@timestamp) BY bucket = TBUCKET(1 day)
+| SORT min
+;
+
+min:datetime | max:datetime | bucket:datetime
+2023-10-23T12:15:03.360Z | 2023-10-23T13:55:01.543Z | 2023-10-23T00:00:00.000Z
+;
+
+tbucketByOneDayDurationAsString
+required_capability: implicit_casting_string_literal_to_temporal_amount
+required_capability: tbucket
+
+FROM sample_data
+| STATS min = MIN(@timestamp), max = MAX(@timestamp) BY bucket = TBUCKET("1 day")
+| SORT min
+;
+
+min:datetime | max:datetime | bucket:datetime
+2023-10-23T12:15:03.360Z | 2023-10-23T13:55:01.543Z | 2023-10-23T00:00:00.000Z
+;
+
+tbucketByOneWeekDuration
+required_capability: tbucket
+
+FROM sample_data
+| STATS min = MIN(@timestamp), max = MAX(@timestamp) BY bucket = TBUCKET(1 week)
+| SORT min
+;
+
+min:datetime | max:datetime | bucket:datetime
+2023-10-23T12:15:03.360Z | 2023-10-23T13:55:01.543Z | 2023-10-23T00:00:00.000Z
+;
+
+tbucketByOneWeekDurationAsString
+required_capability: implicit_casting_string_literal_to_temporal_amount
+required_capability: tbucket
+
+FROM sample_data
+| STATS min = MIN(@timestamp), max = MAX(@timestamp) BY bucket = TBUCKET("1 week")
+| SORT min
+;
+
+min:datetime | max:datetime | bucket:datetime
+2023-10-23T12:15:03.360Z | 2023-10-23T13:55:01.543Z | 2023-10-23T00:00:00.000Z
+;
+
+tbucketByOneMonthDuration
+required_capability: tbucket
+
+FROM sample_data
+| STATS min = MIN(@timestamp), max = MAX(@timestamp) BY bucket = TBUCKET(1 month)
+| SORT min
+;
+
+min:datetime | max:datetime | bucket:datetime
+2023-10-23T12:15:03.360Z | 2023-10-23T13:55:01.543Z | 2023-10-01T00:00:00.000Z
+;
+
+tbucketByOneMonthDurationAsString
+required_capability: implicit_casting_string_literal_to_temporal_amount
+required_capability: tbucket
+
+FROM sample_data
+| STATS min = MIN(@timestamp), max = MAX(@timestamp) BY bucket = TBUCKET("1 month")
+| SORT min
+;
+
+min:datetime | max:datetime | bucket:datetime
+2023-10-23T12:15:03.360Z | 2023-10-23T13:55:01.543Z | 2023-10-01T00:00:00.000Z
+;
+
+tbucketByOneYearDuration
+required_capability: tbucket
+
+FROM sample_data
+| STATS min = MIN(@timestamp), max = MAX(@timestamp) BY bucket = TBUCKET(1 year)
+| SORT min
+;
+
+min:datetime | max:datetime | bucket:datetime
+2023-10-23T12:15:03.360Z | 2023-10-23T13:55:01.543Z | 2023-01-01T00:00:00.000Z
+;
+
+tbucketByOneYearDurationAsString
+required_capability: implicit_casting_string_literal_to_temporal_amount
+required_capability: tbucket
+
+FROM sample_data
+| STATS min = MIN(@timestamp), max = MAX(@timestamp) BY bucket = TBUCKET("1 year")
+| SORT min
+;
+
+min:datetime | max:datetime | bucket:datetime
+2023-10-23T12:15:03.360Z | 2023-10-23T13:55:01.543Z | 2023-01-01T00:00:00.000Z
+;
+
+reuseGroupingFunction
+required_capability: implicit_casting_string_literal_to_temporal_amount
+required_capability: tbucket
+
+from sample_data
+| stats x = 1 year + tbucket(1 day) by b1d = tbucket(1 day)
+;
+
+x:datetime | b1d:datetime
+2024-10-23T00:00:00.000Z | 2023-10-23T00:00:00.000Z
+;
+
+keepTimestampBeforeStats
+required_capability: implicit_casting_string_literal_to_temporal_amount
+required_capability: tbucket
+
+FROM sample_data
+| WHERE event_duration > 0
+| KEEP @timestamp, client_ip, event_duration
+| STATS count = COUNT(*), avg_dur = AVG(event_duration) BY hour = TBUCKET(1h), client_ip
+;
+ignoreOrder:true
+
+count:long | avg_dur:double | hour:datetime | client_ip:ip
+4 | 3945955.75 | 2023-10-23T13:00:00.000Z | 172.21.3.15
+1 | 3450233.0 | 2023-10-23T12:00:00.000Z | 172.21.2.162
+1 | 2764889.0 | 2023-10-23T12:00:00.000Z | 172.21.2.113
+1 | 1232382.0 | 2023-10-23T13:00:00.000Z | 172.21.0.5
+;
+
+keepAtWildcardBeforeStats
+required_capability: implicit_casting_string_literal_to_temporal_amount
+required_capability: tbucket
+
+FROM sample_data
+| WHERE message == "Connection error"
+| KEEP @*, message
+| STATS errors = COUNT() BY day = TBUCKET(1d), message
+;
+ignoreOrder:true
+
+errors:long | day:datetime | message:keyword
+3 | 2023-10-23T00:00:00.000Z | Connection error
+;
+
+keepWildcardBeforeStats
+required_capability: implicit_casting_string_literal_to_temporal_amount
+required_capability: tbucket
+
+FROM sample_data
+| WHERE client_ip IS NOT NULL
+| KEEP *stamp*, client_ip, event_duration
+| STATS p95 = PERCENTILE(event_duration, 95) BY ten_min = TBUCKET(10min), client_ip
+;
+ignoreOrder:true
+
+p95:double | ten_min:datetime | client_ip:ip
+3450233.0 | 2023-10-23T12:10:00.000Z | 172.21.2.162
+2764889.0 | 2023-10-23T12:20:00.000Z | 172.21.2.113
+1232382.0 | 2023-10-23T13:30:00.000Z | 172.21.0.5
+7782993.299999999 | 2023-10-23T13:50:00.000Z | 172.21.3.15
+;
+
+statsChainingWithTimestampCarriedForward
+required_capability: implicit_casting_string_literal_to_temporal_amount
+required_capability: tbucket
+
+FROM sample_data
+| KEEP @timestamp, event_duration
+| STATS day_count = COUNT(), day_p95 = PERCENTILE(event_duration, 95) BY day = TBUCKET(1d), @timestamp
+| WHERE day_count > 0
+| STATS hour_count = COUNT(), hour_p95 = PERCENTILE(day_p95, 95) BY hour = TBUCKET(1h), day
+;
+ignoreOrder:true
+
+hour_count:long | hour_p95:double | hour:datetime | day:datetime
+2 | 3415965.8 | 2023-10-23T12:00:00.000Z | 2023-10-23T00:00:00.000Z
+5 | 7621273.399999999 | 2023-10-23T13:00:00.000Z | 2023-10-23T00:00:00.000Z
+;
+
+statsChainingWithTimestampCarriedForwardAsByKey
+required_capability: implicit_casting_string_literal_to_temporal_amount
+required_capability: tbucket
+
+FROM sample_data
+| KEEP @timestamp, client_ip, event_duration
+| STATS reqs = COUNT(), max_dur = MAX(event_duration) BY day = TBUCKET(1d), client_ip, @timestamp
+| WHERE max_dur > 1000
+| STATS spikes = COUNT() BY hour = TBUCKET(1h), client_ip, day
+;
+ignoreOrder:true
+
+spikes:long | hour:datetime | client_ip:ip | day:datetime
+4 | 2023-10-23T13:00:00.000Z | 172.21.3.15 | 2023-10-23T00:00:00.000Z
+1 | 2023-10-23T12:00:00.000Z | 172.21.2.113 | 2023-10-23T00:00:00.000Z
+1 | 2023-10-23T12:00:00.000Z | 172.21.2.162 | 2023-10-23T00:00:00.000Z
+1 | 2023-10-23T13:00:00.000Z | 172.21.0.5 | 2023-10-23T00:00:00.000Z
+;
+
+statsWithTimestampEval
+required_capability: implicit_casting_string_literal_to_temporal_amount
+required_capability: tbucket
+
+FROM sample_data
+| KEEP @timestamp, event_duration, message
+| EVAL t = @timestamp
+| STATS total = COUNT(*), med = MEDIAN(event_duration) BY d = TBUCKET(1d), message
+;
+ignoreOrder:true
+
+total:long | med:double | d:datetime | message:keyword
+1 | 1232382.0 | 2023-10-23T00:00:00.000Z | Disconnected
+1 | 1756467.0 | 2023-10-23T00:00:00.000Z | Connected to 10.1.0.1
+1 | 2764889.0 | 2023-10-23T00:00:00.000Z | Connected to 10.1.0.2
+1 | 3450233.0 | 2023-10-23T00:00:00.000Z | Connected to 10.1.0.3
+3 | 5033755.0 | 2023-10-23T00:00:00.000Z | Connection error
+;
+
+statsChainingWithTimestampEval
+required_capability: implicit_casting_string_literal_to_temporal_amount
+required_capability: tbucket
+
+FROM sample_data
+| KEEP @timestamp, event_duration, message
+| EVAL t = @timestamp
+| STATS total = COUNT(*), med = MEDIAN(event_duration) BY d = TBUCKET(1d), message, @timestamp
+| WHERE total > 0
+| STATS day_total = SUM(total), hour_med = MEDIAN(med) BY h = TBUCKET(1h), message
+;
+ignoreOrder:true
+
+day_total:long | hour_med:double | h:datetime | message:keyword
+3 | 5033755.0 | 2023-10-23T13:00:00.000Z | Connection error
+1 | 3450233.0 | 2023-10-23T12:00:00.000Z | Connected to 10.1.0.3
+1 | 2764889.0 | 2023-10-23T12:00:00.000Z | Connected to 10.1.0.2
+1 | 1756467.0 | 2023-10-23T13:00:00.000Z | Connected to 10.1.0.1
+1 | 1232382.0 | 2023-10-23T13:00:00.000Z | Disconnected
+;
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 7c89864989b08..2788236bb4d9a 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
@@ -2524,3 +2524,18 @@ c:long | b:date_nanos | yr:date_nanos
22 | 1987-01-01T00:00:00.000Z | 1986-01-01T00:00:00.000Z
22 | 1986-01-01T00:00:00.000Z | 1985-01-01T00:00:00.000Z
;
+
+multiIndexTBucketGroupingAndAggregation
+required_capability: date_nanos_type
+required_capability: implicit_casting_date_and_date_nanos
+required_capability: implicit_casting_string_literal_to_temporal_amount
+required_capability: tbucket
+
+FROM sample_data_ts_nanos, sample_data
+| stats x = 1 day + tbucket(1 hour) by b1d = tbucket(1 hour)
+;
+
+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
+;
diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/LookupJoinTypesIT.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/LookupJoinTypesIT.java
index 06645ca3af517..bc4053584fc77 100644
--- a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/LookupJoinTypesIT.java
+++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/LookupJoinTypesIT.java
@@ -33,6 +33,7 @@
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -270,16 +271,21 @@ private static boolean existingIndex(Collection existing, DataType
/** This test generates documentation for the supported output types of the lookup join. */
public void testOutputSupportedTypes() throws Exception {
- Map, DataType> signatures = new LinkedHashMap<>();
+ Set signatures = new LinkedHashSet<>();
for (TestConfigs configs : testConfigurations.values()) {
if (configs.group.equals("unsupported") || configs.group.equals("union-types")) {
continue;
}
for (TestConfig config : configs.configs.values()) {
if (config instanceof TestConfigPasses) {
- signatures.put(
- List.of(new DocsV3Support.Param(config.mainType(), List.of()), new DocsV3Support.Param(config.lookupType(), null)),
- null
+ signatures.add(
+ new DocsV3Support.TypeSignature(
+ List.of(
+ new DocsV3Support.Param(config.mainType(), List.of()),
+ new DocsV3Support.Param(config.lookupType(), null)
+ ),
+ null
+ )
);
}
}
@@ -770,7 +776,7 @@ private boolean isValidDataType(DataType dataType) {
return UNDER_CONSTRUCTION.get(dataType) == null || UNDER_CONSTRUCTION.get(dataType).isEnabled();
}
- private static void saveJoinTypes(Supplier