From 49469d5f3f15324abf9c52a7c56606d396fca303 Mon Sep 17 00:00:00 2001 From: Varun Date: Fri, 27 Mar 2026 04:29:25 -0700 Subject: [PATCH] Add date_histogram bucket aggregation support Supports calendar intervals (month/day/hour) and fixed intervals (milliseconds) for date bucketing. --- .../dsl/DslLogicalPlanIntegrationIT.java | 91 +++++++++++++ .../AggregationRegistryFactory.java | 2 + .../aggregation/DateHistogramGrouping.java | 103 ++++++++++++++ .../bucket/DateHistogramBucketShape.java | 74 ++++++++++ .../result/AggregationResponseBuilder.java | 1 - .../DateHistogramGroupingTest.java | 95 +++++++++++++ .../bucket/DateHistogramBucketShapeTest.java | 127 ++++++++++++++++++ .../histogram/InternalDateHistogram.java | 2 +- 8 files changed, 493 insertions(+), 2 deletions(-) create mode 100644 plugins/query-dsl-calcite/src/main/java/org/opensearch/dsl/aggregation/DateHistogramGrouping.java create mode 100644 plugins/query-dsl-calcite/src/main/java/org/opensearch/dsl/aggregation/bucket/DateHistogramBucketShape.java create mode 100644 plugins/query-dsl-calcite/src/test/java/org/opensearch/dsl/aggregation/DateHistogramGroupingTest.java create mode 100644 plugins/query-dsl-calcite/src/test/java/org/opensearch/dsl/aggregation/bucket/DateHistogramBucketShapeTest.java diff --git a/plugins/query-dsl-calcite/src/internalClusterTest/java/org/opensearch/dsl/DslLogicalPlanIntegrationIT.java b/plugins/query-dsl-calcite/src/internalClusterTest/java/org/opensearch/dsl/DslLogicalPlanIntegrationIT.java index 79e083e66f557..1657b64fd56eb 100644 --- a/plugins/query-dsl-calcite/src/internalClusterTest/java/org/opensearch/dsl/DslLogicalPlanIntegrationIT.java +++ b/plugins/query-dsl-calcite/src/internalClusterTest/java/org/opensearch/dsl/DslLogicalPlanIntegrationIT.java @@ -8,10 +8,13 @@ package org.opensearch.dsl; +import org.opensearch.action.index.IndexRequestBuilder; import org.opensearch.action.search.SearchResponse; import org.opensearch.index.query.QueryBuilders; import org.opensearch.search.aggregations.AggregationBuilders; import org.opensearch.search.aggregations.BucketOrder; +import org.opensearch.search.aggregations.bucket.histogram.DateHistogramInterval; +import org.opensearch.search.aggregations.bucket.histogram.Histogram; import org.opensearch.search.aggregations.bucket.terms.MultiTermsAggregationBuilder; import org.opensearch.search.aggregations.support.MultiTermsValuesSourceConfig; import org.opensearch.search.builder.SearchSourceBuilder; @@ -20,6 +23,9 @@ import java.util.List; import static org.hamcrest.Matchers.containsString; +import static org.opensearch.search.aggregations.AggregationBuilders.dateHistogram; +import static org.opensearch.test.hamcrest.OpenSearchAssertions.assertAcked; +import static org.opensearch.test.hamcrest.OpenSearchAssertions.assertSearchResponse; /** * Integration tests for DSL to Calcite conversion. @@ -1065,4 +1071,89 @@ public void testHistogramWithMinDocCount() throws Exception { // - Only buckets with >= 2 docs are returned (10 and 50 buckets) // - HistogramBucketShape handles minDocCount=0 workaround correctly } + + /** + * Tests date_histogram with calendar interval. + */ + public void testDateHistogramCalendarInterval() throws Exception { + String indexName = "test-date-histogram"; + assertAcked(prepareCreate(indexName).setMapping("timestamp", "type=date")); + + List builders = new ArrayList<>(); + builders.add(client().prepareIndex(indexName).setSource("timestamp", "2024-01-01T00:00:00Z")); + builders.add(client().prepareIndex(indexName).setSource("timestamp", "2024-01-15T00:00:00Z")); + builders.add(client().prepareIndex(indexName).setSource("timestamp", "2024-02-01T00:00:00Z")); + builders.add(client().prepareIndex(indexName).setSource("timestamp", "2024-02-20T00:00:00Z")); + builders.add(client().prepareIndex(indexName).setSource("timestamp", "2024-03-10T00:00:00Z")); + indexRandom(true, builders); + + SearchSourceBuilder searchSource = new SearchSourceBuilder(); + searchSource.aggregation(dateHistogram("date_hist").field("timestamp").calendarInterval(DateHistogramInterval.MONTH)); + searchSource.size(0); + + SearchResponse response = convertDsl(searchSource, indexName); + assertNotNull("SearchResponse should not be null", response); + + // TODO: Add deeper assertions to verify: + // - LogicalAggregate node with date_histogram grouping + // - DateHistogramGrouping extracts field, calendar interval correctly + // - Expression builds correct FLOOR(timestamp TO MONTH) + // - Response contains InternalDateHistogram with 3 buckets + // - Each bucket has correct doc count (2, 2, 1) + } + + /** + * Tests date_histogram with fixed interval. + */ + public void testDateHistogramFixedInterval() throws Exception { + String indexName = "test-date-histogram-fixed"; + assertAcked(prepareCreate(indexName).setMapping("timestamp", "type=date")); + + List builders = new ArrayList<>(); + long baseTime = 1704067200000L; // 2024-01-01T00:00:00Z + for (int i = 0; i < 10; i++) { + builders.add(client().prepareIndex(indexName).setSource("timestamp", baseTime + (i * 3600000L))); + } + indexRandom(true, builders); + + SearchSourceBuilder searchSource = new SearchSourceBuilder(); + searchSource.aggregation(dateHistogram("date_hist").field("timestamp").fixedInterval(DateHistogramInterval.hours(2))); + searchSource.size(0); + + SearchResponse response = convertDsl(searchSource, indexName); + assertNotNull("SearchResponse should not be null", response); + + // TODO: Add deeper assertions to verify: + // - LogicalAggregate node with date_histogram grouping + // - DateHistogramGrouping extracts field, fixed interval correctly + // - Expression builds correct FLOOR(epoch_ms / interval_ms) * interval_ms + // - Response contains InternalDateHistogram with 5 buckets + // - Each bucket has correct doc count (2) + } + + /** + * Tests date_histogram with min_doc_count. + */ + public void testDateHistogramWithMinDocCount() throws Exception { + String indexName = "test-date-histogram-mindoc"; + assertAcked(prepareCreate(indexName).setMapping("timestamp", "type=date")); + + List builders = new ArrayList<>(); + builders.add(client().prepareIndex(indexName).setSource("timestamp", "2024-01-01T00:00:00Z")); + builders.add(client().prepareIndex(indexName).setSource("timestamp", "2024-01-01T01:00:00Z")); + builders.add(client().prepareIndex(indexName).setSource("timestamp", "2024-01-01T05:00:00Z")); + indexRandom(true, builders); + + SearchSourceBuilder searchSource = new SearchSourceBuilder(); + searchSource.aggregation(dateHistogram("date_hist").field("timestamp").fixedInterval(DateHistogramInterval.hours(1)).minDocCount(2)); + searchSource.size(0); + + SearchResponse response = convertDsl(searchSource, indexName); + assertNotNull("SearchResponse should not be null", response); + + // TODO: Add deeper assertions to verify: + // - minDocCount filtering applied in AggregationResponseBuilder + // - Only buckets with >= 2 docs are returned + // - DateHistogramBucketShape handles minDocCount=0 workaround correctly + } } diff --git a/plugins/query-dsl-calcite/src/main/java/org/opensearch/dsl/aggregation/AggregationRegistryFactory.java b/plugins/query-dsl-calcite/src/main/java/org/opensearch/dsl/aggregation/AggregationRegistryFactory.java index 2dce8ca073d6b..b48f3bd1f57c3 100644 --- a/plugins/query-dsl-calcite/src/main/java/org/opensearch/dsl/aggregation/AggregationRegistryFactory.java +++ b/plugins/query-dsl-calcite/src/main/java/org/opensearch/dsl/aggregation/AggregationRegistryFactory.java @@ -8,6 +8,7 @@ package org.opensearch.dsl.aggregation; +import org.opensearch.dsl.aggregation.bucket.DateHistogramBucketShape; import org.opensearch.dsl.aggregation.bucket.HistogramBucketShape; import org.opensearch.dsl.aggregation.bucket.MultiTermsBucketShape; import org.opensearch.dsl.aggregation.bucket.TermsBucketShape; @@ -33,6 +34,7 @@ public static AggregationRegistry create() { registry.register(new TermsBucketShape()); registry.register(new MultiTermsBucketShape()); registry.register(new HistogramBucketShape()); + registry.register(new DateHistogramBucketShape()); registry.register(new AvgMetricTranslator()); registry.register(new SumMetricTranslator()); registry.register(new MinMetricTranslator()); diff --git a/plugins/query-dsl-calcite/src/main/java/org/opensearch/dsl/aggregation/DateHistogramGrouping.java b/plugins/query-dsl-calcite/src/main/java/org/opensearch/dsl/aggregation/DateHistogramGrouping.java new file mode 100644 index 0000000000000..911ec92e3443c --- /dev/null +++ b/plugins/query-dsl-calcite/src/main/java/org/opensearch/dsl/aggregation/DateHistogramGrouping.java @@ -0,0 +1,103 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.dsl.aggregation; + +import org.apache.calcite.avatica.util.TimeUnit; +import org.apache.calcite.rel.type.RelDataType; +import org.apache.calcite.rel.type.RelDataTypeField; +import org.apache.calcite.rex.RexBuilder; +import org.apache.calcite.rex.RexNode; +import org.apache.calcite.sql.fun.SqlStdOperatorTable; +import org.opensearch.dsl.exception.ConversionException; +import org.opensearch.search.aggregations.bucket.histogram.DateHistogramInterval; + +import java.util.List; + +public class DateHistogramGrouping implements ExpressionGrouping { + + private static final String BUCKET_SUFFIX = "_date_histogram_bucket"; + + private final String fieldName; + private final DateHistogramInterval calendarInterval; + private final DateHistogramInterval fixedInterval; + + public DateHistogramGrouping(String fieldName, DateHistogramInterval calendarInterval, + DateHistogramInterval fixedInterval) { + this.fieldName = fieldName; + this.calendarInterval = calendarInterval; + this.fixedInterval = fixedInterval; + } + + @Override + public List getFieldNames() { + return List.of(fieldName); + } + + @Override + public String getProjectedColumnName() { + return fieldName + BUCKET_SUFFIX; + } + + @Override + public RexNode buildExpression(RelDataType inputRowType, RexBuilder builder) throws ConversionException { + RelDataTypeField field = inputRowType.getField(fieldName, true, false); + if (field == null) { + throw ConversionException.invalidField(fieldName); + } + + RexNode fieldRef = builder.makeInputRef(field.getType(), field.getIndex()); + + if (calendarInterval != null) { + return buildCalendarExpression(fieldRef, builder); + } else { + return buildFixedExpression(fieldRef, builder); + } + } + + private RexNode buildCalendarExpression(RexNode fieldRef, RexBuilder builder) { + TimeUnit unit = mapToTimeUnit(calendarInterval.toString()); + return builder.makeCall(SqlStdOperatorTable.FLOOR, fieldRef, builder.makeFlag(unit)); + } + + private RexNode buildFixedExpression(RexNode fieldRef, RexBuilder builder) { + long intervalMs = parseFixedInterval(fixedInterval.toString()); + RexNode intervalLiteral = builder.makeBigintLiteral(java.math.BigDecimal.valueOf(intervalMs)); + + RexNode divided = builder.makeCall(SqlStdOperatorTable.DIVIDE, fieldRef, intervalLiteral); + RexNode floored = builder.makeCall(SqlStdOperatorTable.FLOOR, divided); + return builder.makeCall(SqlStdOperatorTable.MULTIPLY, floored, intervalLiteral); + } + + private TimeUnit mapToTimeUnit(String interval) { + return switch (interval) { + case "1y" -> TimeUnit.YEAR; + case "1M" -> TimeUnit.MONTH; + case "1w" -> TimeUnit.WEEK; + case "1d" -> TimeUnit.DAY; + case "1h" -> TimeUnit.HOUR; + case "1m" -> TimeUnit.MINUTE; + case "1s" -> TimeUnit.SECOND; + default -> throw new IllegalArgumentException("Unsupported calendar interval: " + interval); + }; + } + + private long parseFixedInterval(String interval) { + String unit = interval.substring(interval.length() - 1); + long value = Long.parseLong(interval.substring(0, interval.length() - 1)); + + return switch (unit) { + case "ms" -> value; + case "s" -> value * 1000; + case "m" -> value * 60 * 1000; + case "h" -> value * 60 * 60 * 1000; + case "d" -> value * 24 * 60 * 60 * 1000; + default -> throw new IllegalArgumentException("Unsupported fixed interval: " + interval); + }; + } +} diff --git a/plugins/query-dsl-calcite/src/main/java/org/opensearch/dsl/aggregation/bucket/DateHistogramBucketShape.java b/plugins/query-dsl-calcite/src/main/java/org/opensearch/dsl/aggregation/bucket/DateHistogramBucketShape.java new file mode 100644 index 0000000000000..d2e4f6650527c --- /dev/null +++ b/plugins/query-dsl-calcite/src/main/java/org/opensearch/dsl/aggregation/bucket/DateHistogramBucketShape.java @@ -0,0 +1,74 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.dsl.aggregation.bucket; + +import org.opensearch.dsl.aggregation.DateHistogramGrouping; +import org.opensearch.dsl.aggregation.GroupingInfo; +import org.opensearch.dsl.result.BucketEntry; +import org.opensearch.search.DocValueFormat; +import org.opensearch.search.aggregations.AggregationBuilder; +import org.opensearch.search.aggregations.BucketOrder; +import org.opensearch.search.aggregations.InternalAggregation; +import org.opensearch.search.aggregations.InternalAggregations; +import org.opensearch.search.aggregations.bucket.histogram.DateHistogramAggregationBuilder; +import org.opensearch.search.aggregations.bucket.histogram.InternalDateHistogram; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +public class DateHistogramBucketShape implements BucketShape { + + @Override + public Class getAggregationType() { + return DateHistogramAggregationBuilder.class; + } + + @Override + public GroupingInfo getGrouping(DateHistogramAggregationBuilder agg) { + return new DateHistogramGrouping(agg.field(), agg.getCalendarInterval(), agg.getFixedInterval()); + } + + @Override + public BucketOrder getOrder(DateHistogramAggregationBuilder agg) { + return agg.order(); + } + + @Override + public Collection getSubAggregations(DateHistogramAggregationBuilder agg) { + return agg.getSubAggregations(); + } + + @Override + public long getMinDocCount(DateHistogramAggregationBuilder agg) { + return agg.minDocCount(); + } + + @Override + public InternalAggregation toBucketAggregation(DateHistogramAggregationBuilder agg, List buckets) { + List dateHistogramBuckets = new ArrayList<>(buckets.size()); + for (BucketEntry entry : buckets) { + long key = ((Number) entry.keys().get(0)).longValue(); + dateHistogramBuckets.add(new InternalDateHistogram.Bucket( + key, entry.docCount(), agg.keyed(), DocValueFormat.RAW, (InternalAggregations) entry.subAggs() + )); + } + + // InternalDateHistogram requires EmptyBucketInfo when minDocCount is 0, but DSL doesn't support + // extended bounds. Use minDocCount of 1 to avoid this requirement. Actual filtering is done + // in AggregationResponseBuilder before calling this method. + long minDocCount = agg.minDocCount() == 0 ? 1 : agg.minDocCount(); + + return new InternalDateHistogram( + agg.getName(), dateHistogramBuckets, agg.order(), minDocCount, + 0L, null, DocValueFormat.RAW, agg.keyed(), Map.of() + ); + } +} diff --git a/plugins/query-dsl-calcite/src/main/java/org/opensearch/dsl/result/AggregationResponseBuilder.java b/plugins/query-dsl-calcite/src/main/java/org/opensearch/dsl/result/AggregationResponseBuilder.java index 3f55f0f1c776a..47c69ef20fa72 100644 --- a/plugins/query-dsl-calcite/src/main/java/org/opensearch/dsl/result/AggregationResponseBuilder.java +++ b/plugins/query-dsl-calcite/src/main/java/org/opensearch/dsl/result/AggregationResponseBuilder.java @@ -250,7 +250,6 @@ private InternalAggregation buildBucket( } } - // Build parent key filter for sub-agg recursion Map childKeyFilter = new HashMap<>(parentKeyFilter); for (int i = 0; i < bucketFieldNames.size(); i++) { String filterKey = (grouping instanceof ExpressionGrouping exprGrouping) diff --git a/plugins/query-dsl-calcite/src/test/java/org/opensearch/dsl/aggregation/DateHistogramGroupingTest.java b/plugins/query-dsl-calcite/src/test/java/org/opensearch/dsl/aggregation/DateHistogramGroupingTest.java new file mode 100644 index 0000000000000..d6e19b4f3a8fd --- /dev/null +++ b/plugins/query-dsl-calcite/src/test/java/org/opensearch/dsl/aggregation/DateHistogramGroupingTest.java @@ -0,0 +1,95 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.dsl.aggregation; + +import org.apache.calcite.avatica.util.TimeUnit; +import org.apache.calcite.jdbc.JavaTypeFactoryImpl; +import org.apache.calcite.rel.type.RelDataType; +import org.apache.calcite.rel.type.RelDataTypeFactory; +import org.apache.calcite.rel.type.RelDataTypeFieldImpl; +import org.apache.calcite.rel.type.RelRecordType; +import org.apache.calcite.rex.RexBuilder; +import org.apache.calcite.rex.RexCall; +import org.apache.calcite.rex.RexNode; +import org.apache.calcite.sql.fun.SqlStdOperatorTable; +import org.apache.calcite.sql.type.SqlTypeName; +import org.junit.Before; +import org.junit.Test; +import org.opensearch.dsl.exception.ConversionException; +import org.opensearch.search.aggregations.bucket.histogram.DateHistogramInterval; + +import java.util.List; + +import static org.junit.Assert.*; + +public class DateHistogramGroupingTest { + + private RexBuilder rexBuilder; + private RelDataType inputRowType; + + @Before + public void setUp() { + RelDataTypeFactory typeFactory = new JavaTypeFactoryImpl(); + rexBuilder = new RexBuilder(typeFactory); + RelDataType timestampType = typeFactory.createSqlType(SqlTypeName.TIMESTAMP); + inputRowType = new RelRecordType(List.of( + new RelDataTypeFieldImpl("timestamp", 0, timestampType) + )); + } + + @Test + public void testGetFieldNames() { + DateHistogramGrouping grouping = new DateHistogramGrouping("timestamp", DateHistogramInterval.MONTH, null); + assertEquals(List.of("timestamp"), grouping.getFieldNames()); + } + + @Test + public void testGetProjectedColumnName() { + DateHistogramGrouping grouping = new DateHistogramGrouping("timestamp", DateHistogramInterval.MONTH, null); + assertEquals("timestamp_date_histogram_bucket", grouping.getProjectedColumnName()); + } + + @Test + public void testBuildExpressionWithCalendarInterval() throws ConversionException { + DateHistogramGrouping grouping = new DateHistogramGrouping("timestamp", DateHistogramInterval.MONTH, null); + RexNode expression = grouping.buildExpression(inputRowType, rexBuilder); + + assertNotNull(expression); + assertTrue(expression instanceof RexCall); + RexCall call = (RexCall) expression; + assertEquals(SqlStdOperatorTable.FLOOR, call.getOperator()); + assertEquals(2, call.getOperands().size()); + } + + @Test + public void testBuildExpressionWithFixedInterval() throws ConversionException { + DateHistogramGrouping grouping = new DateHistogramGrouping("timestamp", null, DateHistogramInterval.hours(2)); + RexNode expression = grouping.buildExpression(inputRowType, rexBuilder); + + assertNotNull(expression); + assertTrue(expression instanceof RexCall); + RexCall outerCall = (RexCall) expression; + assertEquals(SqlStdOperatorTable.MULTIPLY, outerCall.getOperator()); + assertEquals(2, outerCall.getOperands().size()); + + RexNode floorNode = outerCall.getOperands().get(0); + assertTrue(floorNode instanceof RexCall); + assertEquals(SqlStdOperatorTable.FLOOR, ((RexCall) floorNode).getOperator()); + + RexNode divideNode = ((RexCall) floorNode).getOperands().get(0); + assertTrue(divideNode instanceof RexCall); + assertEquals(SqlStdOperatorTable.DIVIDE, ((RexCall) divideNode).getOperator()); + } + + @Test(expected = ConversionException.class) + public void testBuildExpressionWithInvalidField() throws ConversionException { + DateHistogramGrouping grouping = new DateHistogramGrouping("invalid_field", DateHistogramInterval.MONTH, null); + grouping.buildExpression(inputRowType, rexBuilder); + } +} diff --git a/plugins/query-dsl-calcite/src/test/java/org/opensearch/dsl/aggregation/bucket/DateHistogramBucketShapeTest.java b/plugins/query-dsl-calcite/src/test/java/org/opensearch/dsl/aggregation/bucket/DateHistogramBucketShapeTest.java new file mode 100644 index 0000000000000..e59ecf5ff3b09 --- /dev/null +++ b/plugins/query-dsl-calcite/src/test/java/org/opensearch/dsl/aggregation/bucket/DateHistogramBucketShapeTest.java @@ -0,0 +1,127 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.dsl.aggregation.bucket; + +import org.junit.Before; +import org.junit.Test; +import org.opensearch.dsl.aggregation.DateHistogramGrouping; +import org.opensearch.dsl.aggregation.GroupingInfo; +import org.opensearch.dsl.result.BucketEntry; +import org.opensearch.search.aggregations.AggregationBuilder; +import org.opensearch.search.aggregations.BucketOrder; +import org.opensearch.search.aggregations.InternalAggregation; +import org.opensearch.search.aggregations.InternalAggregations; +import org.opensearch.search.aggregations.bucket.histogram.DateHistogramAggregationBuilder; +import org.opensearch.search.aggregations.bucket.histogram.DateHistogramInterval; +import org.opensearch.search.aggregations.bucket.histogram.InternalDateHistogram; +import org.opensearch.search.aggregations.metrics.AvgAggregationBuilder; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import static org.junit.Assert.*; + +public class DateHistogramBucketShapeTest { + + private DateHistogramBucketShape shape; + private DateHistogramAggregationBuilder agg; + + @Before + public void setUp() { + shape = new DateHistogramBucketShape(); + agg = new DateHistogramAggregationBuilder("test_date_histogram") + .field("timestamp") + .calendarInterval(DateHistogramInterval.MONTH); + } + + @Test + public void testGetAggregationType() { + assertEquals(DateHistogramAggregationBuilder.class, shape.getAggregationType()); + } + + @Test + public void testGetGrouping() { + GroupingInfo grouping = shape.getGrouping(agg); + + assertNotNull(grouping); + assertTrue(grouping instanceof DateHistogramGrouping); + assertEquals(List.of("timestamp"), grouping.getFieldNames()); + } + + @Test + public void testGetOrder() { + BucketOrder order = BucketOrder.key(true); + agg.order(order); + + assertEquals(order, shape.getOrder(agg)); + } + + @Test + public void testGetSubAggregations() { + agg.subAggregation(new AvgAggregationBuilder("avg_value").field("value")); + + Collection subAggs = shape.getSubAggregations(agg); + + assertNotNull(subAggs); + assertEquals(1, subAggs.size()); + assertEquals("avg_value", subAggs.iterator().next().getName()); + } + + @Test + public void testGetMinDocCount() { + assertEquals(0L, shape.getMinDocCount(agg)); + + agg.minDocCount(5); + assertEquals(5L, shape.getMinDocCount(agg)); + } + + @Test + public void testToBucketAggregationWithEmptyBuckets() { + List buckets = new ArrayList<>(); + + InternalAggregation result = shape.toBucketAggregation(agg, buckets); + + assertNotNull(result); + assertTrue(result instanceof InternalDateHistogram); + InternalDateHistogram histogram = (InternalDateHistogram) result; + assertEquals("test_date_histogram", histogram.getName()); + assertEquals(0, histogram.getBuckets().size()); + } + + @Test + public void testToBucketAggregation() { + long timestamp = 1704067200000L; + BucketEntry entry = new BucketEntry(List.of(timestamp), 10L, InternalAggregations.EMPTY); + List buckets = List.of(entry); + + InternalAggregation result = shape.toBucketAggregation(agg, buckets); + + assertNotNull(result); + assertTrue(result instanceof InternalDateHistogram); + InternalDateHistogram histogram = (InternalDateHistogram) result; + assertEquals(1, histogram.getBuckets().size()); + assertEquals(timestamp, ((InternalDateHistogram.Bucket) histogram.getBuckets().get(0)).getKey()); + assertEquals(10L, histogram.getBuckets().get(0).getDocCount()); + } + + @Test + public void testToBucketAggregationWithMinDocCountZero() { + agg.minDocCount(0); + + BucketEntry entry = new BucketEntry(List.of(1704067200000L), 10L, InternalAggregations.EMPTY); + + // This test verifies the minDocCount=0 workaround. Without it, InternalDateHistogram + // constructor would throw exception requiring EmptyBucketInfo. + InternalAggregation result = shape.toBucketAggregation(agg, List.of(entry)); + + assertNotNull(result); + assertTrue(result instanceof InternalDateHistogram); + } +} diff --git a/server/src/main/java/org/opensearch/search/aggregations/bucket/histogram/InternalDateHistogram.java b/server/src/main/java/org/opensearch/search/aggregations/bucket/histogram/InternalDateHistogram.java index e0b6010c6c3e8..917b8da943551 100644 --- a/server/src/main/java/org/opensearch/search/aggregations/bucket/histogram/InternalDateHistogram.java +++ b/server/src/main/java/org/opensearch/search/aggregations/bucket/histogram/InternalDateHistogram.java @@ -233,7 +233,7 @@ public int hashCode() { private final long offset; final EmptyBucketInfo emptyBucketInfo; - InternalDateHistogram( + public InternalDateHistogram( String name, List buckets, BucketOrder order,