Skip to content

Commit 3c601d0

Browse files
committed
Handle long overflow in dates (#124048)
* Handle long overflow in dates (cherry picked from commit 07921a7) # Conflicts: # server/src/main/java/org/elasticsearch/index/mapper/DateFieldMapper.java # server/src/main/java/org/elasticsearch/index/mapper/MapperFeatures.java
1 parent 385db6a commit 3c601d0

File tree

10 files changed

+125
-8
lines changed

10 files changed

+125
-8
lines changed

docs/changelog/124048.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pr: 124048
2+
summary: Handle long overflow in dates
3+
area: Search
4+
type: bug
5+
issues:
6+
- 112483

rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search/500_date_range.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,39 @@ setup:
123123
- match: { hits.total: 1 }
124124
- length: { hits.hits: 1 }
125125
- match: { hits.hits.0._id: "4" }
126+
127+
---
128+
"test bad dates in range - past":
129+
- requires:
130+
cluster_features: [ "mapper.range.invalid_date_fix" ]
131+
reason: "Fix for invalid date required"
132+
- do:
133+
catch: /illegal_argument_exception/
134+
search:
135+
index: dates
136+
body:
137+
sort: field
138+
query:
139+
range:
140+
date:
141+
gte: -522000000
142+
lte: 2023
143+
format: date_optional_time
144+
145+
---
146+
"test bad dates in range - future":
147+
- requires:
148+
cluster_features: [ "mapper.range.invalid_date_fix" ]
149+
reason: "Fix for invalid date required"
150+
- do:
151+
catch: /illegal_argument_exception/
152+
search:
153+
index: dates
154+
body:
155+
sort: field
156+
query:
157+
range:
158+
date:
159+
gte: 2020
160+
lte: 522000000
161+
format: date_optional_time

server/src/main/java/org/elasticsearch/common/time/DateUtils.java

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ public static ZoneId of(String zoneId) {
186186

187187
/**
188188
* convert a java time instant to a long value which is stored in lucene
189-
* the long value resembles the nanoseconds since the epoch
189+
* the long value represents the nanoseconds since the epoch
190190
*
191191
* @param instant the instant to convert
192192
* @return the nano seconds and seconds as a single long
@@ -205,10 +205,35 @@ public static long toLong(Instant instant) {
205205
return instant.getEpochSecond() * 1_000_000_000 + instant.getNano();
206206
}
207207

208+
/**
209+
* Convert a java time instant to a long value which is stored in lucene,
210+
* the long value represents the milliseconds since epoch
211+
*
212+
* @param instant the instant to convert
213+
* @return the total milliseconds as a single long
214+
*/
215+
public static long toLongMillis(Instant instant) {
216+
try {
217+
return instant.toEpochMilli();
218+
} catch (ArithmeticException e) {
219+
if (instant.isAfter(Instant.now())) {
220+
throw new IllegalArgumentException(
221+
"date[" + instant + "] is too far in the future to be represented in a long milliseconds variable",
222+
e
223+
);
224+
} else {
225+
throw new IllegalArgumentException(
226+
"date[" + instant + "] is too far in the past to be represented in a long milliseconds variable",
227+
e
228+
);
229+
}
230+
}
231+
}
232+
208233
/**
209234
* Returns an instant that is with valid nanosecond resolution. If
210235
* the parameter is before the valid nanosecond range then this returns
211-
* the minimum {@linkplain Instant} valid for nanosecond resultion. If
236+
* the minimum {@linkplain Instant} valid for nanosecond resolution. If
212237
* the parameter is after the valid nanosecond range then this returns
213238
* the maximum {@linkplain Instant} valid for nanosecond resolution.
214239
* <p>

server/src/main/java/org/elasticsearch/index/mapper/DateFieldMapper.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
import java.util.function.LongSupplier;
7676

7777
import static org.elasticsearch.common.time.DateUtils.toLong;
78+
import static org.elasticsearch.common.time.DateUtils.toLongMillis;
7879

7980
/** A {@link FieldMapper} for dates. */
8081
public final class DateFieldMapper extends FieldMapper {
@@ -94,12 +95,13 @@ public final class DateFieldMapper extends FieldMapper {
9495
private static final DateMathParser EPOCH_MILLIS_PARSER = DateFormatter.forPattern("epoch_millis")
9596
.withLocale(DEFAULT_LOCALE)
9697
.toDateMathParser();
98+
public static final NodeFeature INVALID_DATE_FIX = new NodeFeature("mapper.range.invalid_date_fix");
9799

98100
public enum Resolution {
99101
MILLISECONDS(CONTENT_TYPE, NumericType.DATE, DateMillisDocValuesField::new) {
100102
@Override
101103
public long convert(Instant instant) {
102-
return instant.toEpochMilli();
104+
return toLongMillis(instant);
103105
}
104106

105107
@Override

server/src/main/java/org/elasticsearch/index/mapper/MapperFeatures.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ public Set<NodeFeature> getTestFeatures() {
5050
SORT_FIELDS_CHECK_FOR_NESTED_OBJECT_FIX,
5151
DYNAMIC_HANDLING_IN_COPY_TO,
5252
SourceFieldMapper.SYNTHETIC_RECOVERY_SOURCE,
53-
ObjectMapper.SUBOBJECTS_FALSE_MAPPING_UPDATE_FIX
53+
ObjectMapper.SUBOBJECTS_FALSE_MAPPING_UPDATE_FIX,
54+
DateFieldMapper.INVALID_DATE_FIX
5455
);
5556
}
5657
}

server/src/test/java/org/elasticsearch/common/time/DateUtilsTests.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import static org.elasticsearch.common.time.DateUtils.compareNanosToMillis;
2828
import static org.elasticsearch.common.time.DateUtils.toInstant;
2929
import static org.elasticsearch.common.time.DateUtils.toLong;
30+
import static org.elasticsearch.common.time.DateUtils.toLongMillis;
3031
import static org.elasticsearch.common.time.DateUtils.toMilliSeconds;
3132
import static org.elasticsearch.common.time.DateUtils.toNanoSeconds;
3233
import static org.hamcrest.Matchers.containsString;
@@ -93,6 +94,44 @@ public void testInstantToLongMax() {
9394
assertThat(e.getMessage(), containsString("is after"));
9495
}
9596

97+
public void testInstantToLongMillis() {
98+
assertThat(toLongMillis(Instant.EPOCH), is(0L));
99+
100+
Instant instant = createRandomInstant();
101+
long timeSinceEpochInMillis = instant.toEpochMilli();
102+
assertThat(toLongMillis(instant), is(timeSinceEpochInMillis));
103+
104+
Instant maxInstant = Instant.ofEpochSecond(Long.MAX_VALUE / 1000);
105+
long maxInstantMillis = maxInstant.toEpochMilli();
106+
assertThat(toLongMillis(maxInstant), is(maxInstantMillis));
107+
108+
Instant minInstant = Instant.ofEpochSecond(Long.MIN_VALUE / 1000);
109+
long minInstantMillis = minInstant.toEpochMilli();
110+
assertThat(toLongMillis(minInstant), is(minInstantMillis));
111+
}
112+
113+
public void testInstantToLongMillisMin() {
114+
/* negative millisecond value of this instant exceeds the maximum value a java long variable can store */
115+
Instant tooEarlyInstant = Instant.MIN;
116+
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> toLongMillis(tooEarlyInstant));
117+
assertThat(e.getMessage(), containsString("too far in the past"));
118+
119+
Instant tooEarlyInstant2 = Instant.ofEpochSecond(Long.MIN_VALUE / 1000 - 1);
120+
e = expectThrows(IllegalArgumentException.class, () -> toLongMillis(tooEarlyInstant2));
121+
assertThat(e.getMessage(), containsString("too far in the past"));
122+
}
123+
124+
public void testInstantToLongMillisMax() {
125+
/* millisecond value of this instant exceeds the maximum value a java long variable can store */
126+
Instant tooLateInstant = Instant.MAX;
127+
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> toLongMillis(tooLateInstant));
128+
assertThat(e.getMessage(), containsString("too far in the future"));
129+
130+
Instant tooLateInstant2 = Instant.ofEpochSecond(Long.MAX_VALUE / 1000 + 1);
131+
e = expectThrows(IllegalArgumentException.class, () -> toLongMillis(tooLateInstant2));
132+
assertThat(e.getMessage(), containsString("too far in the future"));
133+
}
134+
96135
public void testLongToInstant() {
97136
assertThat(toInstant(0), is(Instant.EPOCH));
98137
assertThat(toInstant(1), is(Instant.EPOCH.plusNanos(1)));

server/src/test/java/org/elasticsearch/index/mapper/DateFieldMapperTests.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,8 @@ protected List<ExampleMalformedValue> exampleMalformedValues() {
152152
return List.of(
153153
exampleMalformedValue("2016-03-99").mapping(mappingWithFormat("strict_date_optional_time||epoch_millis"))
154154
.errorMatches("failed to parse date field [2016-03-99] with format [strict_date_optional_time||epoch_millis]"),
155-
exampleMalformedValue("-522000000").mapping(mappingWithFormat("date_optional_time")).errorMatches("long overflow"),
155+
exampleMalformedValue("-522000000").mapping(mappingWithFormat("date_optional_time")).errorMatches("too far in the past"),
156+
exampleMalformedValue("522000000").mapping(mappingWithFormat("date_optional_time")).errorMatches("too far in the future"),
156157
exampleMalformedValue("2020").mapping(mappingWithFormat("strict_date"))
157158
.errorMatches("failed to parse date field [2020] with format [strict_date]"),
158159
exampleMalformedValue("hello world").mapping(mappingWithFormat("strict_date_optional_time"))

x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/ToDatetimeTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public static Iterable<Object[]> parameters() {
4242
read,
4343
TestCaseSupplier.dateCases(),
4444
DataType.DATETIME,
45-
v -> ((Instant) v).toEpochMilli(),
45+
v -> DateUtils.toLongMillis((Instant) v),
4646
emptyList()
4747
);
4848
TestCaseSupplier.unary(

x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/ToLongTests.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,14 @@ public static Iterable<Object[]> parameters() {
4343
TestCaseSupplier.forUnaryBoolean(suppliers, evaluatorName.apply("Boolean"), DataType.LONG, b -> b ? 1L : 0L, List.of());
4444

4545
// datetimes
46-
TestCaseSupplier.unary(suppliers, read, TestCaseSupplier.dateCases(), DataType.LONG, v -> ((Instant) v).toEpochMilli(), List.of());
46+
TestCaseSupplier.unary(
47+
suppliers,
48+
read,
49+
TestCaseSupplier.dateCases(),
50+
DataType.LONG,
51+
v -> DateUtils.toLongMillis((Instant) v),
52+
List.of()
53+
);
4754
TestCaseSupplier.unary(
4855
suppliers,
4956
read,

x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/ToStringTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ public static Iterable<Object[]> parameters() {
8787
"ToStringFromDatetimeEvaluator[field=" + read + "]",
8888
TestCaseSupplier.dateCases(),
8989
DataType.KEYWORD,
90-
i -> new BytesRef(DateFieldMapper.DEFAULT_DATE_TIME_FORMATTER.formatMillis(((Instant) i).toEpochMilli())),
90+
i -> new BytesRef(DateFieldMapper.DEFAULT_DATE_TIME_FORMATTER.formatMillis(DateUtils.toLongMillis((Instant) i))),
9191
List.of()
9292
);
9393
TestCaseSupplier.unary(

0 commit comments

Comments
 (0)