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