diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.aggregation/49_range_timezone_bug.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.aggregation/49_range_timezone_bug.yml new file mode 100644 index 0000000000000..9e17425a4766d --- /dev/null +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.aggregation/49_range_timezone_bug.yml @@ -0,0 +1,95 @@ +setup: + - do: + indices.create: + index: test + body: + settings: + number_of_replicas: 0 + mappings: + properties: + mydate: + type: date + format: "uuuu-MM-dd'T'HH:mm:ss.SSSSSSSSSZZZZZ" + + - do: + cluster.health: + wait_for_status: green + + - do: + index: + index: test + id: 1 + body: { "mydate": "2021-08-12T01:00:00.000000000+02:00" } + + - do: + indices.refresh: {} + +--- +"respect offsets in range bounds": + - skip: + version: " - 7.99.99" + reason: "Fixed in 7.16 (backport pending)" + - do: + search: + rest_total_hits_as_int: true + body: { + "query": { + "match_all": {} + }, + "aggregations": { + "myagg": { + "date_range": { + "field": "mydate", + "ranges": [ + { + "from": "2021-08-12T00:00:00.000000000+02:00", + "to": "2021-08-12T02:00:00.000000000+02:00" + } + ] + } + } + } + } + - match: { hits.total: 1 } + - length: { aggregations.myagg.buckets: 1 } + - match: { aggregations.myagg.buckets.0.from_as_string: "2021-08-11T22:00:00.000000000Z" } + - match: { aggregations.myagg.buckets.0.from: 1628719200000 } + - match: { aggregations.myagg.buckets.0.to_as_string: "2021-08-12T00:00:00.000000000Z" } + - match: { aggregations.myagg.buckets.0.to: 1628726400000 } + - match: { aggregations.myagg.buckets.0.doc_count: 1 } + +--- +"offsets and timezones play nicely together": + - skip: + version: " - 7.99.99" + reason: "Fixed in 7.16 (backport pending)" + - do: + search: + rest_total_hits_as_int: true + body: { + "query": { + "match_all": {} + }, + "aggregations": { + "myagg": { + "date_range": { + "time_zone": "America/New_York", + "field": "mydate", + "ranges": [ + { + "from": "2021-08-12T00:00:00.000000000+02:00", + "to": "2021-08-12T02:00:00.000000000+02:00" + } + ] + } + } + } + } + - match: { hits.total: 1 } + - length: { aggregations.myagg.buckets: 1 } + - match: { aggregations.myagg.buckets.0.from_as_string: "2021-08-11T18:00:00.000000000-04:00" } + - match: { aggregations.myagg.buckets.0.from: 1628719200000 } + - match: { aggregations.myagg.buckets.0.to_as_string: "2021-08-11T20:00:00.000000000-04:00" } + - match: { aggregations.myagg.buckets.0.to: 1628726400000 } + - match: { aggregations.myagg.buckets.0.doc_count: 1 } + diff --git a/server/src/main/java/org/elasticsearch/common/time/JavaDateMathParser.java b/server/src/main/java/org/elasticsearch/common/time/JavaDateMathParser.java index 1b3b80491017d..5bce40479801d 100644 --- a/server/src/main/java/org/elasticsearch/common/time/JavaDateMathParser.java +++ b/server/src/main/java/org/elasticsearch/common/time/JavaDateMathParser.java @@ -210,7 +210,9 @@ private Instant parseDateTime(String value, ZoneId timeZone, boolean roundUpIfNo return DateFormatters.from(formatter.parse(value)).toInstant(); } else { TemporalAccessor accessor = formatter.parse(value); - ZoneId zoneId = TemporalQueries.zone().queryFrom(accessor); + // Use the offset if provided, otherwise fall back to the zone, or null. + ZoneOffset offset = TemporalQueries.offset().queryFrom(accessor); + ZoneId zoneId = offset == null ? TemporalQueries.zoneId().queryFrom(accessor) : ZoneId.ofOffset("", offset); if (zoneId != null) { timeZone = zoneId; } diff --git a/server/src/test/java/org/elasticsearch/search/DocValueFormatTests.java b/server/src/test/java/org/elasticsearch/search/DocValueFormatTests.java index a38989eed881c..c02a7e68e8d3c 100644 --- a/server/src/test/java/org/elasticsearch/search/DocValueFormatTests.java +++ b/server/src/test/java/org/elasticsearch/search/DocValueFormatTests.java @@ -22,6 +22,7 @@ import java.time.ZoneId; import java.time.ZoneOffset; +import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.List; @@ -345,4 +346,45 @@ public void testDateTimeWithTimezone() { tokyo.parseLong(tokyo.format(millis), false, () -> { throw new UnsupportedOperationException("don't use now"); }) ); } + + /** + * This is a regression test for https://github.com/elastic/elasticsearch/issues/76415 + */ + public void testParseOffset() { + DocValueFormat.DateTime parsesZone = new DocValueFormat.DateTime( + DateFormatter.forPattern("uuuu-MM-dd'T'HH:mm:ss.SSSSSSSSSZZZZZ"), + ZoneOffset.UTC, + Resolution.MILLISECONDS + ); + long expected = 1628719200000L; + ZonedDateTime sample = ZonedDateTime.of(2021, 8, 12, 0, 0, 0, 0, ZoneId.ofOffset("", ZoneOffset.ofHours(2))); + assertEquals("GUARD: wrong initial millis", expected, sample.toEpochSecond() * 1000); + long actualMillis = parsesZone.parseLong( + "2021-08-12T00:00:00.000000000+02:00", + false, + () -> { throw new UnsupportedOperationException("don't use now"); } + ); + assertEquals(expected, actualMillis); + } + + /** + * Make sure fixing 76415 doesn't break parsing zone strings + */ + public void testParseZone() { + DocValueFormat.DateTime parsesZone = new DocValueFormat.DateTime( + DateFormatter.forPattern("uuuu-MM-dd'T'HH:mm:ss.SSSSSSSSSVV"), + ZoneOffset.UTC, + Resolution.MILLISECONDS + ); + long expected = 1628719200000L; + ZonedDateTime sample = ZonedDateTime.of(2021, 8, 12, 0, 0, 0, 0, ZoneId.ofOffset("", ZoneOffset.ofHours(2))); + assertEquals("GUARD: wrong initial millis", expected, sample.toEpochSecond() * 1000); + //assertEquals("GUARD: wrong initial string", "2021-08-12T00:00:00.000000000+02:00", parsesZone.format(expected)); + long actualMillis = parsesZone.parseLong( + "2021-08-12T00:00:00.000000000CET", + false, + () -> { throw new UnsupportedOperationException("don't use now"); } + ); + assertEquals(expected, actualMillis); + } }