Skip to content

Commit cb0023a

Browse files
committed
Always lower a bucket function to a span
Review feedback: a histogram expression should end up as an OpenSearch histogram aggregate, so generating date_format or timestampadd was surprising. It is, and they are gone -- `format` and `time_zone` now defer to the legacy engine along with alias, min_doc_count and order. The legacy engine implements all five natively (AggMaker builds them straight onto the date_histogram aggregation), and for time_zone it does so better: `dateHistogram.timeZone(ZoneOffset.of(value))` shifts bucket boundaries properly, where this code was adding a fixed number of seconds and would have been wrong across a daylight-saving change. Handing those queries back means they are answered by the implementation that already had them right. What is left always produces a Span, which is what the earlier comment about lowering to existing AST primitives described. `missing` still wraps the field in `ifnull`, since substituting a value has to happen before bucketing. Verified on a live cluster: the plain and `missing` forms answer from V2 (12/24/17/19 and 19/20/20/13), while `format`, `time_zone` and `alias` reach legacy and answer correctly -- time_zone returning 5/18/30/19, the shifted boundaries. 983 default-route tests pass, and `:sql:build` is green including the coverage gate. Signed-off-by: Jialiang Liang <ryanleeang@gmail.com>
1 parent 981a438 commit cb0023a

3 files changed

Lines changed: 15 additions & 74 deletions

File tree

‎integ-test/src/test/java/org/opensearch/sql/sql/DateHistogramBucketFunctionIT.java‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,8 +186,9 @@ public void positionalCallReturnsHourlyBuckets() throws IOException {
186186
}
187187

188188
/**
189-
* `alias` has no lowering here but the legacy engine implements it, so the query still has to
190-
* answer. CsvFormatResponseIT.dateHistogramTest has asserted this shape for years.
189+
* The legacy engine implements alias, format, time_zone, min_doc_count and order through the
190+
* native date_histogram aggregation; this lowering has no equivalent, so those queries still have
191+
* to reach it. CsvFormatResponseIT.dateHistogramTest has asserted this shape for years.
191192
*/
192193
@Test
193194
@RequiresCapability(LEGACY_ENGINE_FALLBACK)

‎sql/src/main/java/org/opensearch/sql/sql/parser/AstExpressionBuilder.java‎

Lines changed: 6 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,6 @@
7272

7373
import com.google.common.collect.ImmutableList;
7474
import com.google.common.collect.ImmutableMap;
75-
import java.time.ZoneOffset;
7675
import java.util.Arrays;
7776
import java.util.Collections;
7877
import java.util.HashMap;
@@ -193,23 +192,18 @@ public UnresolvedExpression visitBucketFunctionCall(BucketFunctionCallContext ct
193192
UnresolvedExpression field = requireArg(args, "field", functionName);
194193
UnresolvedExpression missing = args.remove("missing");
195194
Literal interval = intervalOf(args, functionName);
196-
Literal format = stringArg(args, "format");
197-
Literal timeZone = stringArg(args, "time_zone");
198195

199-
// Anything left is a parameter with no lowering here. Some of them -- alias, min_doc_count,
200-
// order -- are implemented by the legacy engine, so decline in the one way RestSQLQueryAction
201-
// falls back on rather than failing the request outright.
196+
// Anything left is a parameter this lowering has no equivalent for. The legacy engine
197+
// implements all of them -- alias, format, time_zone, min_doc_count, order -- through the
198+
// native date_histogram aggregation, so decline in the one way RestSQLQueryAction falls back
199+
// on rather than failing a query it can answer.
202200
if (!args.isEmpty()) {
203201
throw new SyntaxCheckException(
204202
functionName + " does not accept parameter: " + String.join(", ", args.keySet()));
205203
}
206204

207-
UnresolvedExpression bucketed = substituteMissing(normalizeField(field), missing);
208-
if (timeZone != null) {
209-
bucketed = shiftByTimeZone(bucketed, timeZone);
210-
}
211-
Span span = AstDSL.spanFromSpanLengthLiteral(bucketed, interval);
212-
return format == null ? span : new Function("date_format", List.of(span, format));
205+
return AstDSL.spanFromSpanLengthLiteral(
206+
substituteMissing(normalizeField(field), missing), interval);
213207
}
214208

215209
private static UnresolvedExpression requireArg(
@@ -242,18 +236,6 @@ private static Literal intervalOf(Map<String, UnresolvedExpression> args, String
242236
return supplied.get(0);
243237
}
244238

245-
private static Literal stringArg(Map<String, UnresolvedExpression> args, String name) {
246-
UnresolvedExpression value = args.remove(name);
247-
if (value == null) {
248-
return null;
249-
}
250-
if (!(value instanceof Literal literal) || literal.getType() != DataType.STRING) {
251-
throw new SemanticCheckException(
252-
name + " must be a string literal (e.g. '1d', '15m'); got " + value);
253-
}
254-
return literal;
255-
}
256-
257239
private static Literal stringOrNumericArg(Map<String, UnresolvedExpression> args, String name) {
258240
UnresolvedExpression value = args.remove(name);
259241
if (value == null) {
@@ -283,24 +265,6 @@ private static UnresolvedExpression substituteMissing(
283265
return missing == null ? field : new Function("ifnull", List.of(field, missing));
284266
}
285267

286-
/**
287-
* Shifts the field by a {@link ZoneOffset} before bucketing. Validated here so an invalid offset
288-
* is reported rather than surfacing as an arithmetic failure at execution.
289-
*/
290-
private static UnresolvedExpression shiftByTimeZone(
291-
UnresolvedExpression field, Literal timeZone) {
292-
String offset = timeZone.getValue().toString();
293-
int seconds;
294-
try {
295-
seconds = ZoneOffset.of(offset).getTotalSeconds();
296-
} catch (RuntimeException e) {
297-
throw new SemanticCheckException(
298-
"time_zone must be a valid offset like '+05:30' or 'Z'; got '" + offset + "'");
299-
}
300-
return new Function(
301-
"timestampadd", List.of(AstDSL.stringLiteral("SECOND"), AstDSL.intLiteral(seconds), field));
302-
}
303-
304268
@Override
305269
public UnresolvedExpression visitGetFormatFunctionCall(GetFormatFunctionCallContext ctx) {
306270
return new Function(

‎sql/src/test/java/org/opensearch/sql/sql/parser/AstExpressionBuilderTest.java‎

Lines changed: 6 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -910,27 +910,6 @@ public void canBuildDateHistogramWithMissing() {
910910
buildExprAst("date_histogram('field'=ts, 'interval'='1h', 'missing'='1970-01-01')"));
911911
}
912912

913-
@Test
914-
public void canBuildDateHistogramWithTimeZoneShift() {
915-
assertEquals(
916-
new Span(
917-
function(
918-
"timestampadd", stringLiteral("SECOND"), intLiteral(19800), qualifiedName("ts")),
919-
intLiteral(1),
920-
SpanUnit.H),
921-
buildExprAst("date_histogram('field'=ts, 'interval'='1h', 'time_zone'='+05:30')"));
922-
}
923-
924-
@Test
925-
public void canBuildDateHistogramWithFormat() {
926-
assertEquals(
927-
function(
928-
"date_format",
929-
new Span(qualifiedName("ts"), intLiteral(1), SpanUnit.D),
930-
stringLiteral("yyyy-MM-dd")),
931-
buildExprAst("date_histogram('field'=ts, 'interval'='1d', 'format'='yyyy-MM-dd')"));
932-
}
933-
934913
/**
935914
* A parameter with no lowering here has to raise SyntaxCheckException -- the one type
936915
* RestSQLQueryAction falls back on -- because the legacy engine implements alias, min_doc_count
@@ -944,6 +923,12 @@ public void unsupportedBucketParameterDefersToLegacyEngine() {
944923
assertThrows(
945924
SyntaxCheckException.class,
946925
() -> buildExprAst("histogram('field'=age, 'interval'=10, 'min_doc_count'=1)"));
926+
assertThrows(
927+
SyntaxCheckException.class,
928+
() -> buildExprAst("date_histogram('field'=ts, 'interval'='1d', 'format'='yyyy-MM-dd')"));
929+
assertThrows(
930+
SyntaxCheckException.class,
931+
() -> buildExprAst("date_histogram('field'=ts, 'interval'='1h', 'time_zone'='+05:30')"));
947932
}
948933

949934
/** A bad argument inside a shape we own must not fall back, so the caller sees this message. */
@@ -955,15 +940,6 @@ public void badBucketArgumentIsReportedRatherThanDeferred() {
955940
assertThrows(
956941
SemanticCheckException.class,
957942
() -> buildExprAst("date_histogram('field'=ts, 'interval'='1d', 'fixed_interval'='2d')"));
958-
assertThrows(
959-
SemanticCheckException.class,
960-
() -> buildExprAst("date_histogram('field'=ts, 'interval'='1d', 'time_zone'='nope')"));
961-
assertThrows(
962-
SemanticCheckException.class,
963-
() -> buildExprAst("date_histogram('field'=ts, 'interval'='1d', 'format'=7)"));
964-
assertThrows(
965-
SemanticCheckException.class,
966-
() -> buildExprAst("date_histogram('field'=ts, 'interval'='1d', 'format'=other)"));
967943
assertThrows(
968944
SemanticCheckException.class,
969945
() -> buildExprAst("date_histogram('field'=ts, 'interval'=ts)"));

0 commit comments

Comments
 (0)