Skip to content

Commit ef4c51e

Browse files
authored
Enhance doc and error message handling for bins on time-related fields (#4713)
1 parent 5bb2747 commit ef4c51e

5 files changed

Lines changed: 54 additions & 2 deletions

File tree

core/src/main/java/org/opensearch/sql/calcite/utils/CalciteToolsHelper.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,17 @@ public RelNode visit(TableScan scan) {
360360
final RelRunner runner = connection.unwrap(RelRunner.class);
361361
return runner.prepareStatement(rel);
362362
} catch (SQLException e) {
363+
// Detect if error is due to window functions in unsupported context (bins on time fields)
364+
String errorMsg = e.getMessage();
365+
if (errorMsg != null
366+
&& errorMsg.contains("Error while preparing plan")
367+
&& errorMsg.contains("WIDTH_BUCKET")) {
368+
throw new UnsupportedOperationException(
369+
"The 'bins' parameter on timestamp fields requires: (1) pushdown to be enabled"
370+
+ " (controlled by plugins.calcite.pushdown.enabled, enabled by default), and"
371+
+ " (2) the timestamp field to be used as an aggregation bucket (e.g., 'stats"
372+
+ " count() by @timestamp').");
373+
}
363374
throw Util.throwAsRuntime(e);
364375
}
365376
}

core/src/main/java/org/opensearch/sql/calcite/utils/binning/handlers/CountBinHandler.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@ public RexNode createExpression(
2828
CountBin countBin = (CountBin) node;
2929

3030
// Create validated binnable field (validates that field is numeric or time-based)
31-
// Note: bins parameter works with both numeric and time-based fields
31+
// Note: bins parameter on time-based fields requires:
32+
// 1. Pushdown to be enabled (controlled by plugins.calcite.pushdown.enabled, enabled by
33+
// default)
34+
// 2. The time-based field to be used as an aggregation bucket
35+
// (e.g., stats count() by @timestamp)
3236
String fieldName = BinFieldValidator.extractFieldName(node);
3337
BinnableField field = new BinnableField(fieldExpr, fieldExpr.getType(), fieldName);
3438

docs/user/ppl/cmd/bin.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,12 @@ bin \<field\> [span=\<interval\>] [minspan=\<interval\>] [bins=\<count\>] [align
2020
* day (d, day, days)
2121
* month (M, mon, month, months)
2222
* minspan: optional. The minimum interval size for automatic span calculation. Cannot be used with span or bins parameters.
23-
* bins: optional. The maximum number of equal-width bins to create. Cannot be used with span or minspan parameters. The bins parameter must be between 2 and 50000 (inclusive).
23+
* bins: optional. The maximum number of equal-width bins to create. Cannot be used with span or minspan parameters. The bins parameter must be between 2 and 50000 (inclusive).
24+
25+
**Limitation**: The bins parameter on timestamp fields has the following requirements:
26+
27+
1. **Pushdown must be enabled**: Controlled by ``plugins.calcite.pushdown.enabled`` (enabled by default). When pushdown is disabled, use the ``span`` parameter instead (e.g., ``bin @timestamp span=5m``).
28+
2. **Timestamp field must be used as an aggregation bucket**: The binned timestamp field must be used in a ``stats`` aggregation (e.g., ``source=events | bin @timestamp bins=3 | stats count() by @timestamp``). Using bins on timestamp fields outside of aggregation buckets is not supported.
2429
* aligntime: optional. Align the bin times for time-based fields. Valid only for time-based discretization. Options:
2530
* earliest: Align bins to the earliest timestamp in the data
2631
* latest: Align bins to the latest timestamp in the data

integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalciteBinCommandIT.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
package org.opensearch.sql.calcite.remote;
77

8+
import static org.junit.Assert.assertThrows;
89
import static org.junit.Assert.assertTrue;
910
import static org.junit.jupiter.api.Assertions.assertThrows;
1011
import static org.opensearch.sql.legacy.TestsConstants.*;
@@ -988,6 +989,33 @@ public void testStatsWithBinsOnTimeAndTermField_Avg() throws IOException {
988989
rows(40.25, "us-west", "2024-07-01 00:01:00"));
989990
}
990991

992+
@Test
993+
public void testBinsOnTimeFieldWithPushdownDisabled_ShouldFail() throws IOException {
994+
// Verify that bins parameter on timestamp fields fails with clear error when pushdown disabled
995+
enabledOnlyWhenPushdownIsDisabled();
996+
997+
ResponseException exception =
998+
assertThrows(
999+
ResponseException.class,
1000+
() ->
1001+
executeQuery(
1002+
"source=events_null | bin @timestamp bins=3 | stats count() by @timestamp"));
1003+
1004+
// Verify the error message clearly explains the limitation and suggests solutions
1005+
// Note: bins parameter on timestamp fields requires BOTH:
1006+
// 1. Pushdown to be enabled (plugins.calcite.pushdown.enabled=true, enabled by default)
1007+
// 2. The timestamp field to be used as an aggregation bucket (e.g., stats count() by
1008+
// @timestamp)
1009+
String errorMessage = exception.getMessage();
1010+
assertTrue(
1011+
"Expected clear error message about bins parameter requirements on timestamp fields, but"
1012+
+ " got: "
1013+
+ errorMessage,
1014+
errorMessage.contains("bins' parameter on timestamp fields requires")
1015+
&& errorMessage.contains("pushdown to be enabled")
1016+
&& errorMessage.contains("aggregation bucket"));
1017+
}
1018+
9911019
@Test
9921020
public void testBinWithNestedFieldWithoutExplicitProjection() throws IOException {
9931021
// Test bin command on nested field without explicit fields projection

integ-test/src/test/java/org/opensearch/sql/ppl/PPLIntegTestCase.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,10 @@ protected void enabledOnlyWhenPushdownIsEnabled() throws IOException {
323323
Assume.assumeTrue("This test is only for when push down is enabled", !isPushdownDisabled());
324324
}
325325

326+
protected void enabledOnlyWhenPushdownIsDisabled() throws IOException {
327+
Assume.assumeTrue("This test is only for when push down is disabled", isPushdownDisabled());
328+
}
329+
326330
public void updatePushdownSettings() throws IOException {
327331
String pushdownEnabled = String.valueOf(GlobalPushdownConfig.enabled);
328332
assert !pushdownEnabled.isBlank() : "Pushdown enabled setting cannot be empty";

0 commit comments

Comments
 (0)