round(Long.MIN_VALUE, DAY): the start of that day lies 16.8 hours below the smallest long, and the calendar's arithmetic wraps it to 16.8 hours before the largest one, in AD 292278994. Select the still to open it at full size. Generated with AI, checked against the code.- You round the open lower bound of a date range:
DateTools.round(Long.MIN_VALUE, Resolution.DAY).Long.MIN_VALUEis the valueLongPoint's javadoc suggests for a half-open range; as a date it is 292269055 BC, Dec 2, at 16:47:04.192 in GMT. - You expect a result no later than the input. The start of that day is 16.8 hours (60,424,192 ms) earlier, below the
longrange, soLong.MIN_VALUEitself is the closest possible answer. roundsets hour, minute, second and millisecond to 0 on a lenientGregorianCalendarand reads the time back withgetTimeInMillis(). The value belowLong.MIN_VALUEwraps around by 2^64, unchecked.- You get
9223372036794351616(AD 292278994-08-16 14:25:51.616), 16.8 hours beforeLong.MAX_VALUE, with no error. ALongPointrange that found 1 document withLong.MIN_VALUEas its lower bound finds 0 with the rounded one. - YEAR to SECOND do the same for any time before the first year, month, day, hour, minute or second boundary above
Long.MIN_VALUE(up to 29.3 days for YEAR and MONTH, 7.2 h for DAY, down to 808 ms for SECOND); MILLISECOND never does. Real dates are never that close.
Only code that calls DateTools.round(long|Date, Resolution) directly with Long.MIN_VALUE (or new Date(Long.MIN_VALUE)) as a "no lower bound" sentinel, or a time within one unit above it. No setting is involved, and every release since 1.9 has it. Lucene's own string methods (timeToString, and dateToString through it) give wrong strings for such dates anyway (LFV-645).
Mechanism, terms and numbers377 words
DateTools.round(long, Resolution) and round(Date, Resolution) promise the input with every field finer than the resolution set to 0 or 1, so the result is never later than the input. For Long.MIN_VALUE (292269055 BC, Dec 2, at 16:47:04.192 in GMT) the start of its day lies 60,424,192 ms below the long range. round sets the fields on a lenient GregorianCalendar and returns getTimeInMillis() (DateTools.java:146-171 at 5adff78bb168), whose long arithmetic wraps by 2^64: round(Long.MIN_VALUE, DAY) is 9223372036794351616, AD 292278994-08-16 14:25:51.616, with no error and the same result with assertions on and off. YEAR to SECOND wrap the same way for every input before the first year, month, day, hour, minute or second boundary above Long.MIN_VALUE (up to 29.3 days for YEAR and MONTH, 7.2 h for DAY, down to 808 ms for SECOND). A LongPoint range whose open lower bound is rounded this way matches nothing. Every release since 1.9 (2006) has it.
- A lenient calendar.
rounduses a per-threadCalendar.getInstance(GMT, Locale.ROOT)(:46-52), aGregorianCalendar, lenient by default, and loads the input withsetTimeInMillis(time)(:148). The fields come out correct: BC 292269055, Dec 2, 16:47:04.192. - Fields cleared. A deliberate fall-through
switchsets every field finer than the resolution to 0 or 1 (:150-168); for DAY: hour, minute, second and millisecond. - The read-back wraps.
return calInstance.getTimeInMillis();(:170, the fix site) recomputes the time from the fields. The true value, -9223372036915200000, is belowLong.MIN_VALUE; the calendar's long arithmetic wraps by 2^64 and nothing checks it.Math.floorDiv(Long.MIN_VALUE, 86400000) * 86400000gives the same value. - Both overloads.
round(Date, Resolution)isnew Date(round(date.getTime(), resolution))(:132-134).
| resolution | round(Long.MIN_VALUE, r) | wraps for inputs up to Long.MIN_VALUE + |
|---|---|---|
| YEAR | 9223372007850351616 | 2,531,575,807 ms (about 29.3 days) |
| MONTH | 9223372036707951616 | 2,531,575,807 ms |
| DAY | 9223372036794351616 | 25,975,807 ms (about 7.2 h) |
| HOUR | 9223372036851951616 | 775,807 ms |
| MINUTE | 9223372036854771616 | 55,807 ms |
| SECOND | 9223372036854775616 | 807 ms |
| MILLISECOND | -9223372036854775808 (correct) | never |
The one-test repro checks only DAY at Long.MIN_VALUE; the other rows, the windows and the facts below come from separate probes (sources). The first input after each window rounds correctly. The wrapped value is not even rounded (DAY gives 14:25:51.616), and rounding it again gives yet another value (9223372036742400000).
Exposure and likelihood188 words
- Who. Code that rounds the
Long.MIN_VALUEsentinel, for example a helper that aligns both ends of aLongPointdate range to a resolution after filling an open start withLong.MIN_VALUE, the formLongPoint.newRangeQuery's javadoc suggests (LongPoint.java:202-203). The same sentinel-into-date-rounding slip happened in Elasticsearch's own code (elastic/elasticsearch #138757, #142725), so the pattern is real, though not in Lucene. - Effect. A silently wrong value: the lower bound becomes later than every real date. With one document dated 2004-09-21,
LongPoint.newRangeQuery("date", Long.MIN_VALUE, day)counts 1 hit and the same query with the rounded bound counts 0. - Default vs opt-in. A plain public static utility; no setting.
- Not hit. Real dates: the windows lie within 29.3 days of 292 million BC. Lucene's only production caller of
roundistimeToString(DateTools.java:91;dateToStringreaches it throughtimeToString,:79). There the wrap shows as a far-future string,timeToString(Long.MIN_VALUE, DAY)="2922789940816"(year 292278994); without the wrap the string would still be wrong for LFV-645's reason (years outside AD 1..9999), so the effect that matters is for code that callsrounddirectly. - Scale. One call; no data, no concurrency.
One JUnit test method on the public API: one call, DateTools.round(Long.MIN_VALUE, DAY), and one assertion that the result is not later than the input. Deterministic, single thread; it fails with assertions on and off.
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.lucene.document;
import org.apache.lucene.tests.util.LuceneTestCase;
public class TestDateToolsRoundNearLongMin extends LuceneTestCase {
public void testRoundNeverMovesTimeForward() {
// expected: a result no later than the input; actual: 9223372036794351616 (AD 292278994)
long rounded = DateTools.round(Long.MIN_VALUE, DateTools.Resolution.DAY);
assertTrue("round(Long.MIN_VALUE, DAY) = " + rounded, rounded <= Long.MIN_VALUE);
}
}
cp $REPRO/TestDateToolsRoundNearLongMin.java $LUCENE/lucene/core/src/test/org/apache/lucene/document/
cd $LUCENE && ./gradlew :lucene:core:test --tests org.apache.lucene.document.TestDateToolsRoundNearLongMin -Ptests.seed=DEADBEEF
# same failure with -Ptests.asserts=false
- Expected
round(Long.MIN_VALUE, DAY)is no later than the input (<= Long.MIN_VALUE).- Observed
AssertionError: round(Long.MIN_VALUE, DAY) = 9223372036794351616, with assertions on and off.
More files, environment, full output151 words
TestDateToolsRoundNearLongMin (one method, extends LuceneTestCase, package org.apache.lucene.document of the lucene/core tests). It passes checkGoogleJavaFormat and carries the Apache license header; as the regression test it also fits as one method in TestDateTools. The test uses no randomness, so the seed does not matter. With the saturating fix it passes unchanged; if a fix throws instead, the assertion has to expect the exception.
| lucene | main 5adff78bb168 (12.0.0-SNAPSHOT): fails; 1.9.1 to 10.6.0 (13 releases run): fail; 1.4.3: no DateTools |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux aarch64 (pure Java arithmetic; only this platform was run) |
cp $REPRO/TestDateToolsRoundNearLongMin.java $LUCENE/lucene/core/src/test/org/apache/lucene/document/
cd $LUCENE && ./gradlew :lucene:core:test --tests org.apache.lucene.document.TestDateToolsRoundNearLongMin -Ptests.seed=DEADBEEF
# gradle, scratch worktree at 5adff78bb168, -Ptests.seed=DEADBEEF -Ptests.asserts=false
TestDateToolsRoundNearLongMin > testRoundNeverMovesTimeForward FAILED
java.lang.AssertionError: round(Long.MIN_VALUE, DAY) = 9223372036794351616
at org.apache.lucene.document.TestDateToolsRoundNearLongMin.testRoundNeverMovesTimeForward(TestDateToolsRoundNearLongMin.java:26)
# gradle, same, assertions on: the same failure and value
# with the fix: PASS with assertions on and off- Both
roundoverloads promise the input with finer fields "set to 0 or 1" and state no range or exception; the wrapped result reads 14:25:51.616 and is later than the input. - Lucene's
DateRangePrefixTreealready warns about this very Calendar underflow. - A non-lenient
Calendarandjava.timeboth throw instead of wrapping.
The full argument134 words
- Contract.
round(Date, Resolution)andround(long, Resolution)promise "the date with all values more precise thanresolutionset to 0 or 1" (DateTools.java:130,:142-143), with no range, precondition or@throws; the class javadoc states no range either (:28-41). - Tests.
TestDateToolsuses ordinary dates only (years 1961 to 2005, and 1970); nothing nearLong.MIN_VALUE. - History. No commit on
DateTools.javaorTestDateTools.javamentionsMIN_VALUE(git log -S'MIN_VALUE': 0 commits). - The project knows the pitfall.
DateRangePrefixTree(spatial-extras,DateRangePrefixTree.java:47-48, from #6710 / LUCENE-5648): "Beware Calendar underflow of the underlying long. If you create a Calendar from LONG.MIN_VALUE, and clear a field, it will underflow and appear close to LONG.MAX_VALUE (BC to AD)." - Loud alternatives. A non-lenient
CalendarthrowsIllegalArgumentException("HOUR_OF_DAY: 0 -> 14");Instant.ofEpochMilli(Long.MIN_VALUE).truncatedTo(DAYS).toEpochMilli()throwsArithmeticException: long overflow.
The devil's-advocate check130 words
The strongest case for "not a bug": no long can hold the floor; the JDK's Calendar.setTime javadoc warns that Date(Long.MIN_VALUE) may give incorrect field values; the class author wrote on java-user@ in 2006 "There is no limit on the given dates in DateTools (within the limits of what Java's Calendar/Date class can do)"; and a sentinel is not a date.
Why it fails: Lucene documents none of this to its callers. The JDK caveat is about the fields read with get(), which are correct here; the wrap happens later, in getTimeInMillis. Jumping silently to the far end of the range is worse than either loud alternative. The argument does bear on likelihood, hence priority Low; a credibility audit also rated it real but minor, with a small fix.
Recommended Saturate to Long.MIN_VALUE when the result would be later than the input (fix A)
Return Long.MIN_VALUE when the rounded time comes out later than the input, which in the scope probe happened only when the floor lies below the long range (+6/-1 in DateTools.java, with one javadoc sentence). The repro passes with assertions on and off; the repro, TestDateTools, two date-sort and four query-parser suites: 182 tests, 0 failures, 1 skipped by its suite.
final long rounded = calInstance.getTimeInMillis();
// a start below Long.MIN_VALUE wraps around to a time near Long.MAX_VALUE
return rounded > time ? Long.MIN_VALUE : rounded;All options and trade-offs3 options ยท 451 words
Option A was built and checked through gradle on 5adff78bb168; B and C were not run. By construction the fix changes only results later than their input. In a scope probe on unpatched main the check fired only at the wrap: of 35,377,755 sampled inputs (uniform over the whole long range, years about 1653 to 2286, the 1582 calendar switch at about 1-minute steps, and both ends), round(t, r) > t held for 6, all Long.MIN_VALUE at YEAR to SECOND (a sample, not a proof over every long).
Saturate to Long.MIN_VALUE when the result would be later than the input (fix A) (validated by the checks) (recommended)
A result later than the input is exactly the bad outcome, and in the scope probe it came only from a floor below the long range, so check exactly that after getTimeInMillis(), and say so in the javadoc of round(long, Resolution). Saturating is a policy choice, not the only correct answer: at Long.MIN_VALUE no long can meet the literal @return, and throwing is the other reasonable choice.
final long rounded = calInstance.getTimeInMillis();
// a start below Long.MIN_VALUE wraps around to a time near Long.MAX_VALUE
return rounded > time ? Long.MIN_VALUE : rounded;Trade-offs
At Long.MIN_VALUE the result is not at midnight, so the literal @return still cannot hold there (no long can); the added javadoc sentence says so. As a range lower bound it is exactly right, since no long lies below it, so a rounded open bound stays open, and the repro passes unchanged as the regression test. Lucene saturates range bounds the same way in Lucene90DocValuesProducer.saturatingShiftLower (Lucene90DocValuesProducer.java:585-600).
What the checks validated
- Gradle in a scratch worktree at
5adff78bb168, seedDEADBEEF: the repro fails without the fix (assertions on and off) and passes with it (on and off). - With the fix: the repro,
TestDateTools(5),TestDateSort(1),TestCustomSearcherSort(2),TestQueryParser(73),TestQPHelper(38, 1 skipped by the suite),TestStandardQP(46) andTestPrecedenceQueryParser(16): 182 tests, 0 failures.:lucene:core:checkGoogleJavaFormatpasses.
The same check with throw new IllegalArgumentException(...) in place of the clamp.
Trade-offs
Fails loudly, like java.time and a non-lenient Calendar, but turns today's silent value into an exception for timeToString/dateToString callers too, and the regression test has to expect the exception (expectThrows) instead.
State in the javadoc of both round overloads that inputs less than one unit above Long.MIN_VALUE are not supported (a simple bound that covers every input that wraps).
Trade-offs
Smallest change, and it could go with LFV-645's documentation of years AD 1..9999, but callers who do not read it still get the far-future value.
Downstream, a date restriction written as a range whose open lower bound Long.MIN_VALUE is first passed through DateTools.round matches nothing, so a rule built that way silently covers no documents.
A proof-obligation audit of DateTools.round's contract: the result must be the input with its finer fields set to 0 or 1, so never later than the input, for every long. At Long.MIN_VALUE that floor is not representable; a one-call public-API JUnit test then witnessed the wrapped value.
The method in full123 words
Method: semi-formal (a proof-obligation audit of a class's contract, witnessed by an executable repro).
- The finder read both
roundoverloads against their@returnobligation and the domain they accept (anylong, anyDate; no range, precondition or@throws). The obligation "not later than the input" needs a representable floor; for times before the first unit boundary aboveLong.MIN_VALUEthere is none, and the code reads the time back without a check. - The finder then wrote a public-API JUnit test that fails on main with assertions on and off and on 10.6.0.
- An independent reproducer, two executed cross-model reviews and a devil's-advocate verifier reproduced it with their own tests, and an adjudication re-ran the reproducer's test ("Prior reports, history and checks").
No prior report found in apache/lucene issues, PRs and commits (with the JIRA archive), the dev@, java-user@, issues@ and commits@ lists, downstream trackers or the web (searched 2026-11-02). The nearest item, #6710, documents the same Calendar underflow for spatial-extras' DateRangePrefixTree, not DateTools.
Searches3 searches
No matching public report was found in the documented searches. The nearest items are related, not reports: #6710 (LUCENE-5648, the DateRangePrefixTree warning), #2147 (LUCENE-1070, the only issue that names DateTools.round, about time zones), the 2006 java-user@ thread "DateField vs DateTools" (the class author's remark on date limits), and elastic/elasticsearch #138757 and #142725 (the same sentinel slip in Elasticsearch's own rounding code). Limits: keyword search; GitHub search does not index review comments; private reports are not visible.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues, PRs and commits (apache/lucene incl. the migrated JIRA archive) | 2026-11-02, re-checked | three scripted prior-report searches (51 queries each: identifiers; trigger and symptom; root cause in general terms); all 29 DateTools issues read and grepped for MIN_VALUE, overflow, wrap, lenient, getTimeInMillis and 292; everything created or updated since 2026-10-31 | no report; #6710 and #2147 are the nearest items |
| Open PRs | 2026-11-02 | the changed files of all 408 open apache/lucene PRs | none touches DateTools.java |
| Mailing lists, JIRA, downstream, web | 2026-11-02 | dev@, java-user@, issues@ and commits@ (4 threads read in full), 5 JIRA full-text queries, Solr, Elasticsearch and Lucene.NET trackers, 21 web queries; scripted searches with 2,066, 2,054 and 573 hits | nothing about this bug; the 2006 java-user@ remark and elastic/elasticsearch #138757, #142725 |
Present since DateTools was added (c7fbb991c3e, Daniel Naber, 2004; first release 1.9, 2006). Every release run, 1.9.1 to 10.6.0, and main wrap (the DAY value, or on 10.5.2 the YEAR value); later rewrites kept the mechanism. Not fixed upstream.
Versions tested5 rows
Introduced by c7fbb991c3e "Add a new class that supports converting dates to readable strings, with limited resolution..." (Daniel Naber, 2004-09-05, SVN r150483; no PR or JIRA id): round set the finer fields on a lenient Calendar and read the time back from the start, with today's @return text. First release 1.9 (2006-02-27); 1.4.3 has no DateTools.
Later rewrites kept the mechanism: LUCENE-491 (GMT), LUCENE-1653 (cached calendar, setTimeInMillis / getTimeInMillis), LUCENE-1472 (a ThreadLocal instead of synchronization, Resolution enum), LUCENE-1846 (locale), LUCENE-1736 (today's per-thread TL_CAL and fall-through switch). Not fixed upstream: DateTools.java has the same blob on upstream main 1c2072f02843 (2026-11-02) and on 10.6.0; its last commit is dbaf97f5dcc (#14965, 2025-07-18, a lint cleanup). All runs used JDK 25, so old releases ran against today's GregorianCalendar.
| Version or commit | Verdict | Result |
|---|---|---|
main 5adff78bb168 | affected | gradle: fails with assertions on and off; passes with fix A |
| 9.12.3, 10.0.0, 10.6.0 | affected | the finder's JUnit test (the same call and assertion as the repro): round(Long.MIN_VALUE, DAY) = 9223372036794351616 |
| 10.5.2 | YEAR variant | the independent reproducer's YEAR test: round(Long.MIN_VALUE, YEAR) = 9223372007850351616 |
| 1.9.1, 2.4.1, 2.9.0, 3.0.3, 3.1.0, 3.3.0, 4.0.0, 4.10.4, 8.11.4 | main() form | the same call and check: the same value |
| 1.4.3 | no DateTools | the class does not exist |
A finder, an independent reproducer, two executed cross-model reviews (OpenAI GPT-5.6, refute and intended lenses), a devil's-advocate verifier, an adjudication, an audit and a final repro check reproduced it and agree on the root cause. This doc's gradle runs reproduced it on main and validated the fix.
Checks and verdicts263 words
The cross-model reviews ran on main ea57360912ed, where DateTools.java is unchanged; the intended-lens review's test was re-run on 5adff78bb168 with the same result. The other checks on main ran on 5adff78bb168; the release runs used each release's own jars.
| Check | Verdict | What it found |
|---|---|---|
| finder | reproduced | Its own version of the one-call test (the same call and assertion) fails on main (assertions on and off) and on 10.6.0. |
| independent reproducer | real | Own YEAR test fails on main, 10.6.0, 10.5.2 and 9.12.3; measured the wrap windows; result minus 2^64 equals the exact floor; the Date overload wraps too. |
| cross-model review, refute lens (OpenAI GPT-5.6, executed) | real (high) | Own test fails (hour 14 instead of 0); controls (MILLISECOND, one day above the minimum) pass. |
| cross-model review, intended lens (OpenAI GPT-5.6, executed) | real (high) | YEAR to SECOND all wrap; a non-lenient Calendar and java.time fail loudly; intended argument moderate. |
| devil's-advocate verifier | real (medium) | The "garbage in" case does not hold; Long.MAX_VALUE and Long.MIN_VALUE + 1 day round correctly. |
| adjudication | confirmed, Low | Re-ran the reproducer's test on main with assertions on and off: fail. |
| separate re-run, audit and final repro check | confirmed; impact minor | The one-test repro fails on main with assertions on and off; the final check set the impact class to minor. |
| this doc | reproduced; fix A validated | Gradle: fails without the fix; with it 182 tests run, 0 failures (181 pass, 1 @Ignored by its suite); in the scope probe the check fired only at the wrap. |
The diagram as a still image, generated with AI and checked against the code. It is under 10 MB, the limit for a GitHub attachment.
- PNG 570 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,495 words
- Registry title
- DateTools.round returns a time near Long.MAX_VALUE (AD 292278994) for Long.MIN_VALUE, and for times before the first resolution-unit boundary above it, instead of rounding down: the rounded-down time lies below the long range and the lenient Calendar arithmetic silently wraps
- Area
- core / document (DateTools resolution limiting: round(long, Resolution) and round(Date, Resolution))
- Priority
- Low, rank 538: DateTools.round returns a time near Long.MAX_VALUE, with no error, for Long.MIN_VALUE and for times before the first resolution-unit boundary above it, so a range lower bound built from it matches nothing, in every release since 1.9. Real dates never get that close, so only code that rounds the Long.MIN_VALUE open-bound sentinel hits it, a rare boundary case.
- Impact class
- minor
- Impact note
- Hits only code that calls the public static DateTools.round(long|Date, Resolution) with Long.MIN_VALUE (for example a half-open lower bound, the form LongPoint's javadoc suggests) or with a time before the first resolution-unit boundary above it (YEAR and MONTH about 29.3 days, DAY about 7.2 h, HOUR 776 s, MINUTE 56 s, SECOND 807 ms; MILLISECOND never): the result is a value above every real date, with no error, identically with -ea and -da [exec], so a range lower bound built from it matches nothing [read]; no setting and no scale are needed, every release from 1.9 (2006) through 10.6.0 and main is affected, and inside Lucene only timeToString/dateToString call round, whose output for such dates is already wrong for the separate reason recorded in LFV-645. Minor rather than silent wrong results, because the wrong value is a public utility return value with no in-tree effect of its own (the only in-tree callers, timeToString/dateToString, are already wrong there for LFV-645's reason) and the trigger is limited to the Long.MIN_VALUE sentinel or times within one unit above it.
- Summary
- You call DateTools.round(Long.MIN_VALUE, Resolution.DAY), for example to align the open lower bound of a date range with day-rounded indexed values, and expect a time no later than the input, as the javadoc promises ('the date with all values more precise than resolution set to 0 or 1'). Instead you get 9223372036794351616, a time in AD 292278994 just below Long.MAX_VALUE, with no error: the start of that day lies below Long.MIN_VALUE, and the lenient GregorianCalendar that round() uses wraps around in its long arithmetic. YEAR, MONTH, HOUR, MINUTE and SECOND wrap the same way for inputs before the first unit boundary above Long.MIN_VALUE, and round(Date, Resolution) delegates, so it wraps too.
- Introduced
- c7fbb991c3e (trunk, svn r150483, Daniel Naber, 'Add a new class that supports converting dates to readable strings, with limited resolution...'; SVN era, no PR and no JIRA id): DateTools was born with round(long, Resolution) setting the finer fields on a lenient java.util.Calendar and reading the time back, with today's @return wording and no range check [read: git show in the history clone]. Not a regression: LUCENE-491 (646d9646cbb, GMT calendar, before 1.9), LUCENE-1653 (3c3b283e3c0, cached Calendar and getTimeInMillis), LUCENE-1472 (fcf6e305b48 / b640e28ad38, ThreadLocal calendar, Resolution enum, switch), LUCENE-1846 (bb391fd3edc / 3c3e77e08c8, locale) and LUCENE-1736 (31479ab48ce / b49d9c897df, today's fall-through switch over TL_CAL) all kept the mechanism [read]; every executed release from 1.9.1 on fails [exec]. (2004-09-05 (author and committer date of c7fbb991c3e))
- First affected version
- 1.9 [read] (release tag 2006-02-27, the first release with DateTools; 1.9 is not on Maven Central and DateTools.java is identical at the 1.9 and 1.9.1 tags); 1.9.1 [exec] is the earliest executed release and fails; 1.4.3 and earlier have no DateTools [exec: no DateTools class in the 1.4.3 jar]
- Affected versions
- Every release from 1.9 (2006) through 10.6.0 (released 2026-10-29, 4ae946e5a9b) and main 5adff78bb168 (12.0.0-SNAPSHOT); never fixed. [exec] round(Long.MIN_VALUE, DAY) = 9223372036794351616 on 1.9.1, 2.4.1, 2.9.0, 3.0.3, 3.1.0, 3.3.0, 4.0.0, 4.10.4 and 8.11.4 (a main() variant of the same call and check, -da, because releases before 9.x lack today's LuceneTestCase), and with the one-test JUnit unchanged on 9.12.3 and 10.0.0 (-da), 10.6.0 (-ea and -da) and main 5adff78bb168 (-ea and -da, also in the re-run); the independent reproduction's YEAR test fails on main (-ea and -da), 10.6.0, 10.5.2 and 9.12.3. Not new in 10.6 (10.5.2 and every earlier executed release fail). Not fixed upstream: DateTools.java has the same blob dda55bf2fb13 on 5adff78bb168, on 10.6.0 and on upstream main 1c2072f02843 (2026-11-02); its last commit is dbaf97f5dcc (#14965, author date 2025-07-18, a lint cleanup that does not touch round) [read through the GitHub API, 2026-11-02]. All runs used JDK 25.0.3 on Linux aarch64, so old releases ran against today's GregorianCalendar; how the JDKs of 2006-2011 behaved was not executed.
- Status
- documented (brief, checks and a per-finding doc exist)
- Verification
- Confirmed by AI-run checks: an independent repro, a cross-model review that agreed, an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
- Found by
- semi-formal: proof-obligation audit
- Upstream and prior reports
- none found (2026-11-02, on top of the finder's, the independent reproduction's, the intended-behaviour check's and the adjudication's GitHub searches; rechecked the same day): three scripted prior-report searches (identifiers; trigger and symptom; root cause in general terms) of 51 queries each over apache/lucene issues, PRs and commits, downstream repositories, the dev@, java-user@, issues@ and commits@ lists, the JIRA archive and the web (2066, 2054 and 573 distinct hits, none the same bug), plus manual GitHub searches (all 29 apache/lucene DateTools issues, bodies and comments grepped for MIN_VALUE, overflow, wrap, negative, lenient, getTimeInMillis and 292; apache/solr, apache/lucene-solr, apache/lucenenet, Elasticsearch, OpenSearch and all of GitHub), the changed-file lists of all 408 open apache/lucene PRs (none touches DateTools.java), everything created or updated since 2026-10-31, 5 JIRA full-text queries, direct list searches with 4 threads read in full, and 21 web queries: no report of DateTools.round wrapping for Long.MIN_VALUE or for times just above it, by anyone. Same-day recheck: GitHub search on apache/lucene 'DateTools round' (only #2147, LUCENE-1070, time zones), 'DateTools MIN_VALUE' (0), 'DateTools overflow' (0), open PRs 'DateTools' (0); none of the 38 issues and PRs updated since 2026-10-31 mentions DateTools (#16896 mentions Long.MIN_VALUE but is the doc-values GCD range-transform fix); upstream main 1c2072f02843 has the same DateTools.java blob. Related, not reports: #6710 (LUCENE-5648, 2014), whose DateRangePrefixTree in spatial-extras documents and assert-guards the same JDK Calendar underflow ('If you create a Calendar from LONG.MIN_VALUE, and clear a field, it will underflow and appear close to LONG.MAX_VALUE'), without mentioning DateTools; a 2006 java-user@ reply by the class author ('There is no limit on the given dates in DateTools (within the limits of what Java's Calendar/Date class can do)'); Elasticsearch #138757 and #142725, where Long.MIN_VALUE open lower bounds reached date rounding in Elasticsearch's own code.
- Lucene commit tested
- main
5adff78bb168(2026-11-02) through gradle in a scratch worktree with assertions on and off, without and with the saturating fix (the repro,TestDateTools,TestDateSort,TestCustomSearcherSort, four query-parser suites,checkGoogleJavaFormat), and through a standalone JUnit runner on the main jars. The hunt ran its own version of the test (the same call and assertion) on 9.12.3, 10.0.0, 10.6.0 and main, amain()with the same call and check on nine releases from 1.9.1 to 8.11.4, and the independent reproducer's YEAR variant on 10.5.2 (13 releases, all fail); 1.4.3 has noDateTools. Only Linux aarch64 with OpenJDK 25.0.3 was run, so old releases ran against today'sGregorianCalendar.
- Finding record
lucene/core/src/java/org/apache/lucene/document/DateTools.java:28-41 (at 5adff78bb168) (class javadoc: lexicographic order of the strings, 'helps you to limit the resolution of your dates', LongPoint offered as the other approach for dates; no range stated), :46-52 (TL_CAL: Calendar.getInstance(GMT, Locale.ROOT), lenient by default), :90-93 (timeToString: round, then format), :125-134 (round(Date, Resolution): '@return the date with all values more precise than resolution set to 0 or 1'; delegates to round(long)), :136-171 (round(long, Resolution): javadoc :136-144 with the same @return plus 'expressed as milliseconds since January 1, 1970, 00:00:00 GMT', no range, precondition or @throws; :148 setTimeInMillis(time); :150-168 fall-through switch that sets MONTH, DAY_OF_MONTH, HOUR_OF_DAY, MINUTE, SECOND and MILLISECOND; :170 return calInstance.getTimeInMillis(), fix site)lucene/core/src/java/org/apache/lucene/document/LongPoint.java:202-203 (at 5adff78bb168) ('You can have half-open ranges ... by setting lowerValue = Long.MIN_VALUE'); lucene/core/src/test/org/apache/lucene/document/TestDateTools.java (no test near Long.MIN_VALUE)https://github.com/apache/lucene/issues/6710 (LUCENE-5648), https://github.com/apache/lucene/issues/2147 (LUCENE-1070), https://lists.apache.org/thread/czzb8pn36p9g5x81d0oo1c29fnd6w1w0 (java-user@ 2006 'DateField vs DateTools'), https://github.com/elastic/elasticsearch/issues/138757, https://github.com/elastic/elasticsearch/issues/142725, read 2026-11-02
- The finding in 30 seconds
lucene/core/src/java/org/apache/lucene/document/DateTools.java:46-52, (at 5adff78bb168) :132-134, :146-171
- Who is affected
lucene/core/src/java/org/apache/lucene/document/LongPoint.java:202-203 (at 5adff78bb168) (half-open ranges with Long.MIN_VALUE)lucene/core/src/java/org/apache/lucene/document/DateTools.java:78-93 (at 5adff78bb168) (timeToString/dateToString call round)https://github.com/elastic/elasticsearch/issues/138757, https://github.com/elastic/elasticsearch/issues/142725 (read 2026-11-02)
- Why it looks unintended
lucene/core/src/java/org/apache/lucene/document/DateTools.java:28-41, (at 5adff78bb168) :125-144 (javadoc: no range, precondition or @throws)lucene/core/src/test/org/apache/lucene/document/TestDateTools.java (at 5adff78bb168) (ordinary dates only)lucene/spatial-extras/src/java/org/apache/lucene/spatial/prefix/tree/DateRangePrefixTree.java:47-48 (at 5adff78bb168)https://lists.apache.org/thread/czzb8pn36p9g5x81d0oo1c29fnd6w1w0 (java-user@ 2006, read 2026-11-02)https://github.com/apache/lucene/issues/6710
- Suggested fix
lucene/core/src/java/org/apache/lucene/codecs/lucene90/Lucene90DocValuesProducer.java:585-600 (at 5adff78bb168) (saturatingShiftLower)
- Diagrams
lucene/core/src/java/org/apache/lucene/document/DateTools.java:146-171 (at 5adff78bb168)
- TestDateToolsRoundNearLongMin.java (lucene/core, package org.apache.lucene.document, 1 test):
repro/LFV-1123/TestDateToolsRoundNearLongMin.java(28 lines)
The diagram illustrates the repro's call (main 5adff78bb168, DateTools.java:146-171).
assets/LFV-1123/bug.png(570 KB)
