Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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 @@ -123,3 +123,47 @@ setup:
- match: { hits.total: 1 }
- length: { hits.hits: 1 }
- match: { hits.hits.0._id: "4" }

---
"test bad dates in range - past":
- requires:
test_runner_features: [ capabilities ]
capabilities:
- method: GET
path: /_search
capabilities: [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:
test_runner_features: [ capabilities ]
capabilities:
- method: GET
path: /_search
capabilities: [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
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,31 @@ 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 resembles the milliseconds since the 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,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 @@ -104,7 +105,7 @@ 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 @@ -45,6 +45,7 @@ private SearchCapabilities() {}
private static final String KNN_QUANTIZED_VECTOR_RESCORE_OVERSAMPLE = "knn_quantized_vector_rescore_oversample";

private static final String HIGHLIGHT_MAX_ANALYZED_OFFSET_DEFAULT = "highlight_max_analyzed_offset_default";
private static final String INVALID_DATE_FIX = "invalid_date_fix";

public static final Set<String> CAPABILITIES;
static {
Expand All @@ -63,6 +64,7 @@ private SearchCapabilities() {}
capabilities.add(K_DEFAULT_TO_SIZE);
capabilities.add(KQL_QUERY_SUPPORTED);
capabilities.add(HIGHLIGHT_MAX_ANALYZED_OFFSET_DEFAULT);
capabilities.add(INVALID_DATE_FIX);
CAPABILITIES = Set.copyOf(capabilities);
}
}
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