Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
Expand Down Expand Up @@ -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<IndexRequestBuilder> 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<IndexRequestBuilder> 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<IndexRequestBuilder> 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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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());
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> 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);
};
}
}
Original file line number Diff line number Diff line change
@@ -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<DateHistogramAggregationBuilder> {

@Override
public Class<DateHistogramAggregationBuilder> 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<AggregationBuilder> getSubAggregations(DateHistogramAggregationBuilder agg) {
return agg.getSubAggregations();
}

@Override
public long getMinDocCount(DateHistogramAggregationBuilder agg) {
return agg.minDocCount();
}

@Override
public InternalAggregation toBucketAggregation(DateHistogramAggregationBuilder agg, List<BucketEntry> buckets) {
List<InternalDateHistogram.Bucket> 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()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,6 @@ private InternalAggregation buildBucket(
}
}

// Build parent key filter for sub-agg recursion
Map<String, Object> childKeyFilter = new HashMap<>(parentKeyFilter);
for (int i = 0; i < bucketFieldNames.size(); i++) {
String filterKey = (grouping instanceof ExpressionGrouping exprGrouping)
Expand Down
Loading
Loading