Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/changelog/124048.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pr: 124048
summary: Handle long overflow in dates
area: Search
type: bug
issues:
- 112483
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,39 @@ setup:
to: 2023
include_lower: false
include_upper: false

---
"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
29 changes: 27 additions & 2 deletions server/src/main/java/org/elasticsearch/common/time/DateUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,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
Expand All @@ -209,10 +209,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.
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,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.IndexVersion;
import org.elasticsearch.index.IndexVersions;
import org.elasticsearch.index.fielddata.FieldDataContext;
Expand Down Expand Up @@ -76,6 +77,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 {
Expand All @@ -95,12 +97,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ public Set<NodeFeature> getTestFeatures() {
TSDB_NESTED_FIELD_SUPPORT,
SourceFieldMapper.SYNTHETIC_RECOVERY_SOURCE,
ObjectMapper.SUBOBJECTS_FALSE_MAPPING_UPDATE_FIX,
UKNOWN_FIELD_MAPPING_UPDATE_ERROR_MESSAGE
UKNOWN_FIELD_MAPPING_UPDATE_ERROR_MESSAGE,
DateFieldMapper.INVALID_DATE_FIX
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,8 @@ protected List<ExampleMalformedValue> 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"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public static Iterable<Object[]> parameters() {
read,
TestCaseSupplier.dateCases(),
DataType.DATETIME,
v -> ((Instant) v).toEpochMilli(),
v -> DateUtils.toLongMillis((Instant) v),
emptyList()
);
TestCaseSupplier.unary(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,14 @@ public static Iterable<Object[]> parameters() {
TestCaseSupplier.forUnaryBoolean(suppliers, evaluatorName.apply("Boolean"), 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ public static Iterable<Object[]> parameters() {
"ToStringFromDatetimeEvaluator[field=" + 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(
Expand Down