-
Notifications
You must be signed in to change notification settings - Fork 222
Add SQL histogram and date_histogram bucket functions #5700
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
RyanL1997
merged 18 commits into
opensearch-project:main
from
RyanL1997:sql-explore/sql-histogram
Aug 20, 2026
Merged
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
6573786
Add SQL histogram and date_histogram bucket functions
RyanL1997 cc420ab
Defer every unlowerable bucket call to the legacy engine
RyanL1997 7072d03
Make the bucket-function ITs behave the same with and without analyti…
RyanL1997 510eaaf
Gate the legacy-only bucket tests behind a capability instead of drop…
RyanL1997 b2cdea1
Report a bad argument instead of deferring to the legacy engine
RyanL1997 b954e10
Handle bucket functions the way this parser handles its other functions
RyanL1997 981a438
Make the missing parameter substitute a value the engine can evaluate
RyanL1997 cb0023a
Always lower a bucket function to a span
RyanL1997 bb83e90
Let V2 answer bucket calls written with bare argument names
RyanL1997 3a2b1e6
Report a misspelled bucket parameter instead of deferring it
RyanL1997 3f6b51c
Fold the bucket argument checks into the visitor
RyanL1997 7950169
Drop an unreachable bucket argument name
RyanL1997 448b3ad
Keep deferring the bucket parameters legacy implements
RyanL1997 f3aaf4f
Follow the conventions this repo already has for the bucket tests
RyanL1997 9d798fc
Let the grammar decide which bucket parameters we answer
RyanL1997 ed7284e
Build the bucket span the way PPL builds its own
RyanL1997 7f6f3a3
Document the bucket functions where GROUP BY is documented
RyanL1997 724b1c4
Point at the bucket functions from the function list
RyanL1997 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
199 changes: 199 additions & 0 deletions
199
integ-test/src/test/java/org/opensearch/sql/sql/DateHistogramBucketFunctionIT.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,199 @@ | ||
| /* | ||
| * Copyright OpenSearch Contributors | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| package org.opensearch.sql.sql; | ||
|
|
||
| import static org.junit.Assert.assertThrows; | ||
| import static org.opensearch.sql.util.Capability.LEGACY_ENGINE_FALLBACK; | ||
| import static org.opensearch.sql.util.MatcherUtils.rows; | ||
| import static org.opensearch.sql.util.MatcherUtils.verifyDataRows; | ||
| import static org.opensearch.sql.util.MatcherUtils.verifyDataRowsInOrder; | ||
|
|
||
| import java.io.IOException; | ||
| import org.json.JSONObject; | ||
| import org.junit.Test; | ||
| import org.opensearch.client.ResponseException; | ||
| import org.opensearch.sql.legacy.SQLIntegTestCase; | ||
| import org.opensearch.sql.util.RequiresCapability; | ||
|
|
||
| /** | ||
| * Execution coverage for {@code date_histogram} and {@code histogram}. The expander unit tests | ||
| * assert the AST that gets built; these assert what comes back after analysis, planning and | ||
| * pushdown, against 72 documents on fixed timestamps: | ||
| * | ||
| * <pre> | ||
| * 00:00 x5 alpha 00:30 x7 beta 01:00 x11 alpha | ||
| * 01:45 x13 gamma 02:00 x17 beta 03:00 x19 alpha | ||
| * </pre> | ||
| * | ||
| * so hourly grouping must yield 12/24/17/19 and half-hourly 5/7/11/13/17/19. | ||
| */ | ||
| public class DateHistogramBucketFunctionIT extends SQLIntegTestCase { | ||
|
|
||
| private static final String IDX = "date_histogram_test"; | ||
|
|
||
| @Override | ||
| protected void init() throws Exception { | ||
| super.init(); | ||
| loadIndex(Index.DATE_HISTOGRAM_TEST); | ||
| } | ||
|
|
||
| /** | ||
| * The bucket has to be projected in a derived table before it can be grouped on; see {@link | ||
| * #groupingOnTheBucketWithoutADerivedTableIsRejected}. This is also the shape Dashboards emits. | ||
| */ | ||
| private static String bucketed(String bucketExpr) { | ||
| return "SELECT b, COUNT(*) FROM (SELECT " | ||
| + bucketExpr | ||
| + " AS b FROM " | ||
| + IDX | ||
| + ") sub GROUP BY b ORDER BY b"; | ||
| } | ||
|
|
||
| @Test | ||
| public void hourlyBucketsCarryKeysAndCounts() throws IOException { | ||
| JSONObject response = executeQuery(bucketed("date_histogram(field=ts, interval='1h')")); | ||
|
|
||
| verifyDataRowsInOrder( | ||
| response, | ||
| rows("2026-01-01 00:00:00", 12), | ||
| rows("2026-01-01 01:00:00", 24), | ||
| rows("2026-01-01 02:00:00", 17), | ||
| rows("2026-01-01 03:00:00", 19)); | ||
| } | ||
|
|
||
| /** A sub-hour interval must split 00:00/00:30 and 01:00/01:45 rather than merge them. */ | ||
| @Test | ||
| public void halfHourlyBucketsSplitWithinTheHour() throws IOException { | ||
| JSONObject response = executeQuery(bucketed("date_histogram(field=ts, interval='30m')")); | ||
|
|
||
| verifyDataRowsInOrder( | ||
| response, | ||
| rows("2026-01-01 00:00:00", 5), | ||
| rows("2026-01-01 00:30:00", 7), | ||
| rows("2026-01-01 01:00:00", 11), | ||
| rows("2026-01-01 01:30:00", 13), | ||
| rows("2026-01-01 02:00:00", 17), | ||
| rows("2026-01-01 03:00:00", 19)); | ||
| } | ||
|
|
||
| @Test | ||
| public void dailyIntervalCollapsesEverythingIntoOneBucket() throws IOException { | ||
| JSONObject response = executeQuery(bucketed("date_histogram(field=ts, interval='1d')")); | ||
|
|
||
| verifyDataRows(response, rows("2026-01-01 00:00:00", 72)); | ||
| } | ||
|
|
||
| /** {@code fixed_interval} and {@code calendar_interval} are accepted as synonyms of interval. */ | ||
| @Test | ||
| public void intervalSynonymsProduceTheSameBuckets() throws IOException { | ||
| JSONObject viaFixed = executeQuery(bucketed("date_histogram(field=ts, fixed_interval='1h')")); | ||
| JSONObject viaCalendar = | ||
| executeQuery(bucketed("date_histogram(field=ts, calendar_interval='1h')")); | ||
|
|
||
| for (JSONObject response : new JSONObject[] {viaFixed, viaCalendar}) { | ||
| verifyDataRowsInOrder( | ||
| response, | ||
| rows("2026-01-01 00:00:00", 12), | ||
| rows("2026-01-01 01:00:00", 24), | ||
| rows("2026-01-01 02:00:00", 17), | ||
| rows("2026-01-01 03:00:00", 19)); | ||
| } | ||
| } | ||
|
|
||
| /** A second grouping key needs the scan in a derived table of its own as well. */ | ||
| @Test | ||
| public void bucketsCombineWithAnAdditionalGroupingKey() throws IOException { | ||
| JSONObject response = | ||
| executeQuery( | ||
| "SELECT b, c, COUNT(*) FROM (SELECT date_histogram(field=ts, interval='1h') AS b," | ||
| + " category AS c FROM (SELECT * FROM " | ||
| + IDX | ||
| + ") inner_scan) sub GROUP BY b, c ORDER BY b, c"); | ||
|
|
||
| verifyDataRowsInOrder( | ||
| response, | ||
| rows("2026-01-01 00:00:00", "alpha", 5), | ||
| rows("2026-01-01 00:00:00", "beta", 7), | ||
| rows("2026-01-01 01:00:00", "alpha", 11), | ||
| rows("2026-01-01 01:00:00", "gamma", 13), | ||
| rows("2026-01-01 02:00:00", "beta", 17), | ||
| rows("2026-01-01 03:00:00", "alpha", 19)); | ||
| } | ||
|
|
||
| @Test | ||
| public void bucketsRespectAWhereClause() throws IOException { | ||
| JSONObject response = | ||
| executeQuery( | ||
| "SELECT b, COUNT(*) FROM (SELECT date_histogram(field=ts, interval='1h') AS b FROM " | ||
| + IDX | ||
| + " WHERE category = 'alpha') sub GROUP BY b ORDER BY b"); | ||
|
|
||
| verifyDataRowsInOrder( | ||
| response, | ||
| rows("2026-01-01 00:00:00", 5), | ||
| rows("2026-01-01 01:00:00", 11), | ||
| rows("2026-01-01 03:00:00", 19)); | ||
| } | ||
|
|
||
| @Test | ||
| public void numericHistogramBucketsByInterval() throws IOException { | ||
| JSONObject response = executeQuery(bucketed("histogram(field=value, interval=20)")); | ||
|
|
||
| // value runs 1..72, so the 20-wide buckets hold 19, 20, 20 and 13 documents. | ||
| verifyDataRowsInOrder(response, rows(0, 19), rows(20, 20), rows(40, 20), rows(60, 13)); | ||
| } | ||
|
|
||
| /** | ||
| * The legacy engine implements alias, format, time_zone, min_doc_count and order through the | ||
| * native date_histogram aggregation; this lowering has no equivalent, so those queries still have | ||
| * to reach it. CsvFormatResponseIT.dateHistogramTest has asserted this shape for years. | ||
| */ | ||
| @Test | ||
| @RequiresCapability(LEGACY_ENGINE_FALLBACK) | ||
| public void callWithAliasParameterReturnsHourlyBuckets() throws IOException { | ||
| JSONObject response = | ||
| executeQuery( | ||
| "SELECT COUNT(*) FROM " | ||
| + IDX | ||
| + " GROUP BY date_histogram(field='ts',fixed_interval='1h','alias'='hours')"); | ||
|
|
||
| verifyDataRows(response, rows(12), rows(24), rows(17), rows(19)); | ||
| } | ||
|
|
||
| @Test | ||
| public void unquotedArgumentNamesReturnNumericBuckets() throws IOException { | ||
| JSONObject response = executeQuery(bucketed("histogram(field=value, interval=20)")); | ||
|
|
||
| verifyDataRowsInOrder(response, rows(0, 19), rows(20, 20), rows(40, 20), rows(60, 13)); | ||
| } | ||
|
|
||
| /** | ||
| * A span over a bare table scan cannot resolve its field, with or without a select alias, so the | ||
| * bucket always has to be projected in a derived table first. Both routes reject this; only the | ||
| * message differs, so the assertion is on the rejection alone. | ||
| */ | ||
| @Test | ||
| public void groupingOnTheBucketWithoutADerivedTableIsRejected() { | ||
| assertThrows( | ||
| ResponseException.class, | ||
| () -> | ||
| executeQuery( | ||
| "SELECT date_histogram(field=ts, interval='1h') AS b, COUNT(*) FROM " | ||
| + IDX | ||
| + " GROUP BY date_histogram(field=ts, interval='1h')")); | ||
| } | ||
|
|
||
| /** Calendar units: Dashboards emits 1M and 1y at the wider zoom levels. */ | ||
| @Test | ||
| public void calendarIntervalsBucketByMonthAndYear() throws IOException { | ||
| verifyDataRows( | ||
| executeQuery(bucketed("date_histogram(field=ts, interval='1M')")), | ||
| rows("2026-01-01 00:00:00", 72)); | ||
| verifyDataRows( | ||
| executeQuery(bucketed("date_histogram(field=ts, interval='1y')")), | ||
| rows("2026-01-01 00:00:00", 72)); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dai-chen I added under
aggregations.rst, but do we need to mention it here?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think no need to mention it here. It should be clear since we've called both bucket or windowing function like other database.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
will remove this as a follow up.