diff --git a/docs/changelog/124048.yaml b/docs/changelog/124048.yaml new file mode 100644 index 0000000000000..c08fd6f9722ab --- /dev/null +++ b/docs/changelog/124048.yaml @@ -0,0 +1,6 @@ +pr: 124048 +summary: Handle long overflow in dates +area: Search +type: bug +issues: + - 112483 diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search/500_date_range.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search/500_date_range.yml index 76057b5a364fb..97174f1aa01cb 100644 --- a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search/500_date_range.yml +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search/500_date_range.yml @@ -123,3 +123,39 @@ setup: - match: { hits.total: 1 } - length: { hits.hits: 1 } - match: { hits.hits.0._id: "4" } + +--- +"test bad dates in range - past": + - requires: + cluster_features: [ "mapper.range.invalid_date_fix" ] + reason: "Fix for invalid date required" + - do: + catch: /illegal_argument_exception/ + search: + index: dates + body: + sort: field + query: + range: + date: + gte: -522000000 + lte: 2023 + format: date_optional_time + +--- +"test bad dates in range - future": + - requires: + cluster_features: [ "mapper.range.invalid_date_fix" ] + reason: "Fix for invalid date required" + - do: + catch: /illegal_argument_exception/ + search: + index: dates + body: + sort: field + query: + range: + date: + gte: 2020 + lte: 522000000 + format: date_optional_time diff --git a/server/src/main/java/org/elasticsearch/common/time/DateUtils.java b/server/src/main/java/org/elasticsearch/common/time/DateUtils.java index 72306b6ed675e..0d7592c9c0865 100644 --- a/server/src/main/java/org/elasticsearch/common/time/DateUtils.java +++ b/server/src/main/java/org/elasticsearch/common/time/DateUtils.java @@ -186,7 +186,7 @@ public static ZoneId of(String zoneId) { /** * convert a java time instant to a long value which is stored in lucene - * the long value resembles the nanoseconds since the epoch + * the long value represents the nanoseconds since the epoch * * @param instant the instant to convert * @return the nano seconds and seconds as a single long @@ -205,10 +205,35 @@ public static long toLong(Instant instant) { return instant.getEpochSecond() * 1_000_000_000 + instant.getNano(); } + /** + * Convert a java time instant to a long value which is stored in lucene, + * the long value represents the milliseconds since epoch + * + * @param instant the instant to convert + * @return the total milliseconds as a single long + */ + public static long toLongMillis(Instant instant) { + try { + return instant.toEpochMilli(); + } catch (ArithmeticException e) { + if (instant.isAfter(Instant.now())) { + throw new IllegalArgumentException( + "date[" + instant + "] is too far in the future to be represented in a long milliseconds variable", + e + ); + } else { + throw new IllegalArgumentException( + "date[" + instant + "] is too far in the past to be represented in a long milliseconds variable", + e + ); + } + } + } + /** * Returns an instant that is with valid nanosecond resolution. If * the parameter is before the valid nanosecond range then this returns - * the minimum {@linkplain Instant} valid for nanosecond resultion. If + * the minimum {@linkplain Instant} valid for nanosecond resolution. If * the parameter is after the valid nanosecond range then this returns * the maximum {@linkplain Instant} valid for nanosecond resolution. *

diff --git a/server/src/main/java/org/elasticsearch/index/mapper/DateFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/DateFieldMapper.java index d8019c058b509..7a1fbd5383476 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/DateFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/DateFieldMapper.java @@ -36,6 +36,7 @@ import org.elasticsearch.common.util.LocaleUtils; import org.elasticsearch.core.Nullable; import org.elasticsearch.core.TimeValue; +import org.elasticsearch.features.NodeFeature; import org.elasticsearch.index.IndexMode; import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.IndexSortConfig; @@ -81,6 +82,7 @@ import java.util.function.LongSupplier; import static org.elasticsearch.common.time.DateUtils.toLong; +import static org.elasticsearch.common.time.DateUtils.toLongMillis; /** A {@link FieldMapper} for dates. */ public final class DateFieldMapper extends FieldMapper { @@ -100,12 +102,13 @@ public final class DateFieldMapper extends FieldMapper { private static final DateMathParser EPOCH_MILLIS_PARSER = DateFormatter.forPattern("epoch_millis") .withLocale(DEFAULT_LOCALE) .toDateMathParser(); + public static final NodeFeature INVALID_DATE_FIX = new NodeFeature("mapper.range.invalid_date_fix"); public enum Resolution { MILLISECONDS(CONTENT_TYPE, NumericType.DATE, DateMillisDocValuesField::new) { @Override public long convert(Instant instant) { - return instant.toEpochMilli(); + return toLongMillis(instant); } @Override diff --git a/server/src/main/java/org/elasticsearch/index/mapper/MapperFeatures.java b/server/src/main/java/org/elasticsearch/index/mapper/MapperFeatures.java index fcd54590bac42..69477c272cab6 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/MapperFeatures.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/MapperFeatures.java @@ -61,7 +61,8 @@ public Set getTestFeatures() { ObjectMapper.SUBOBJECTS_FALSE_MAPPING_UPDATE_FIX, UKNOWN_FIELD_MAPPING_UPDATE_ERROR_MESSAGE, DOC_VALUES_SKIPPER, - RESCORE_VECTOR_QUANTIZED_VECTOR_MAPPING + RESCORE_VECTOR_QUANTIZED_VECTOR_MAPPING, + DateFieldMapper.INVALID_DATE_FIX ); } } diff --git a/server/src/test/java/org/elasticsearch/common/time/DateUtilsTests.java b/server/src/test/java/org/elasticsearch/common/time/DateUtilsTests.java index e15bbbf75a529..08da6f0dfc957 100644 --- a/server/src/test/java/org/elasticsearch/common/time/DateUtilsTests.java +++ b/server/src/test/java/org/elasticsearch/common/time/DateUtilsTests.java @@ -27,6 +27,7 @@ import static org.elasticsearch.common.time.DateUtils.compareNanosToMillis; import static org.elasticsearch.common.time.DateUtils.toInstant; import static org.elasticsearch.common.time.DateUtils.toLong; +import static org.elasticsearch.common.time.DateUtils.toLongMillis; import static org.elasticsearch.common.time.DateUtils.toMilliSeconds; import static org.elasticsearch.common.time.DateUtils.toNanoSeconds; import static org.hamcrest.Matchers.containsString; @@ -93,6 +94,44 @@ public void testInstantToLongMax() { assertThat(e.getMessage(), containsString("is after")); } + public void testInstantToLongMillis() { + assertThat(toLongMillis(Instant.EPOCH), is(0L)); + + Instant instant = createRandomInstant(); + long timeSinceEpochInMillis = instant.toEpochMilli(); + assertThat(toLongMillis(instant), is(timeSinceEpochInMillis)); + + Instant maxInstant = Instant.ofEpochSecond(Long.MAX_VALUE / 1000); + long maxInstantMillis = maxInstant.toEpochMilli(); + assertThat(toLongMillis(maxInstant), is(maxInstantMillis)); + + Instant minInstant = Instant.ofEpochSecond(Long.MIN_VALUE / 1000); + long minInstantMillis = minInstant.toEpochMilli(); + assertThat(toLongMillis(minInstant), is(minInstantMillis)); + } + + public void testInstantToLongMillisMin() { + /* negative millisecond value of this instant exceeds the maximum value a java long variable can store */ + Instant tooEarlyInstant = Instant.MIN; + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> toLongMillis(tooEarlyInstant)); + assertThat(e.getMessage(), containsString("too far in the past")); + + Instant tooEarlyInstant2 = Instant.ofEpochSecond(Long.MIN_VALUE / 1000 - 1); + e = expectThrows(IllegalArgumentException.class, () -> toLongMillis(tooEarlyInstant2)); + assertThat(e.getMessage(), containsString("too far in the past")); + } + + public void testInstantToLongMillisMax() { + /* millisecond value of this instant exceeds the maximum value a java long variable can store */ + Instant tooLateInstant = Instant.MAX; + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> toLongMillis(tooLateInstant)); + assertThat(e.getMessage(), containsString("too far in the future")); + + Instant tooLateInstant2 = Instant.ofEpochSecond(Long.MAX_VALUE / 1000 + 1); + e = expectThrows(IllegalArgumentException.class, () -> toLongMillis(tooLateInstant2)); + assertThat(e.getMessage(), containsString("too far in the future")); + } + public void testLongToInstant() { assertThat(toInstant(0), is(Instant.EPOCH)); assertThat(toInstant(1), is(Instant.EPOCH.plusNanos(1))); diff --git a/server/src/test/java/org/elasticsearch/index/mapper/DateFieldMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/DateFieldMapperTests.java index 5a03034663c26..1d49d2cc93cc6 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/DateFieldMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/DateFieldMapperTests.java @@ -152,7 +152,8 @@ protected List exampleMalformedValues() { return List.of( exampleMalformedValue("2016-03-99").mapping(mappingWithFormat("strict_date_optional_time||epoch_millis")) .errorMatches("failed to parse date field [2016-03-99] with format [strict_date_optional_time||epoch_millis]"), - exampleMalformedValue("-522000000").mapping(mappingWithFormat("date_optional_time")).errorMatches("long overflow"), + exampleMalformedValue("-522000000").mapping(mappingWithFormat("date_optional_time")).errorMatches("too far in the past"), + exampleMalformedValue("522000000").mapping(mappingWithFormat("date_optional_time")).errorMatches("too far in the future"), exampleMalformedValue("2020").mapping(mappingWithFormat("strict_date")) .errorMatches("failed to parse date field [2020] with format [strict_date]"), exampleMalformedValue("hello world").mapping(mappingWithFormat("strict_date_optional_time")) diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/ToDatetimeTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/ToDatetimeTests.java index d897b187b1b62..44ebd6c251f99 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/ToDatetimeTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/ToDatetimeTests.java @@ -42,7 +42,7 @@ public static Iterable parameters() { read, TestCaseSupplier.dateCases(), DataType.DATETIME, - v -> ((Instant) v).toEpochMilli(), + v -> DateUtils.toLongMillis((Instant) v), emptyList() ); TestCaseSupplier.unary( diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/ToLongTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/ToLongTests.java index a5ff0113f5d34..50748d582ed1f 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/ToLongTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/ToLongTests.java @@ -41,7 +41,14 @@ public static Iterable parameters() { TestCaseSupplier.forUnaryBoolean(suppliers, evaluatorName("Boolean", "bool"), DataType.LONG, b -> b ? 1L : 0L, List.of()); // datetimes - TestCaseSupplier.unary(suppliers, read, TestCaseSupplier.dateCases(), DataType.LONG, v -> ((Instant) v).toEpochMilli(), List.of()); + TestCaseSupplier.unary( + suppliers, + read, + TestCaseSupplier.dateCases(), + DataType.LONG, + v -> DateUtils.toLongMillis((Instant) v), + List.of() + ); TestCaseSupplier.unary( suppliers, read, diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/ToStringTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/ToStringTests.java index 046a116ac551c..f766deb2e1686 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/ToStringTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/ToStringTests.java @@ -87,7 +87,7 @@ public static Iterable parameters() { "ToStringFromDatetimeEvaluator[datetime=" + read + "]", TestCaseSupplier.dateCases(), DataType.KEYWORD, - i -> new BytesRef(DateFieldMapper.DEFAULT_DATE_TIME_FORMATTER.formatMillis(((Instant) i).toEpochMilli())), + i -> new BytesRef(DateFieldMapper.DEFAULT_DATE_TIME_FORMATTER.formatMillis(DateUtils.toLongMillis((Instant) i))), List.of() ); TestCaseSupplier.unary(