A range end that is the last year of a thousand-year block is printed as that block's first year.
DateRangePrefixTreecuts time into nested cells: million-year blocks, thousand-year blocks (years 1000-1999, 2000-2999, ...), single years, then months, days and so on down to milliseconds.- A range is a start cell and an end cell, each first replaced by the coarsest cell that means the same thing. An end of 1999-12-31 is the last day of 1999, and 1999 is the last year of 1000-1999, so the end becomes that thousand-year block.
- ISO dates have no way to write a thousand-year block, so the printer writes a block as a single year.
[1990 TO 1999]: the end 1999 is stored as the thousand-year block 1000-1999 and printed as the block's first year, 1000, instead of its last, 1999; parseShape then rejects "[1990 TO 1000]". Select the still to open it at full size. Generated with AI, checked against the code.- Create
new DateRangePrefixTree(DateRangePrefixTree.DEFAULT_CAL)and parse a decade:parseShape("[1990 TO 1999]"). The shape is right: it contains 1999-06-15, and searches with it match correctly. - You expect
shape.toString()to give a string thatparseShapereads back as the same range, such as"[1990 TO 1999]": its javadoc says the result "should be parseable by parseShape(String)". - Instead it prints
"[1990 TO 1000]". The tree stores the end 1999 as the whole block 1000-1999, because 1999 is that block's last year, and the printer writes any block as its first year: right for a start, wrong for an end. - Parsing that string throws
IllegalArgumentException: Wrong order: 1990 TO 1000. When the start lies at or before the block's first year, it parses without error to a shorter range:[* TO 1999-12-31]prints"[* TO 1000]", which no longer contains 1500-06-15. - Query
toStringshows the same text: queries built from[* TO 1999-12-31]and from its printed text both printqueryShape=[* TO 1000]but are not equal. Matching, query equality and the index use the shape object and are unaffected.
Users of the opt-in, experimental DateRangePrefixTree (spatial-extras; Solr's DateRangeField is built on it) with ranges ending on the last year of a thousand-year block: 1999, 2999, 9999, 2999-12-31T23:59:59.999. Query toString and logs show the wrong end; code that stores the text and parses it again gets an exception or a shorter range.
Mechanism, terms and numbers767 words
DateRangePrefixTree (spatial-extras, @lucene.experimental) indexes date ranges. When a range's end is the last year of a thousand-year block (1999, 2999, ..., 9999, at any precision down to 1999-12-31T23:59:59.999) or of a million-year block (2 BC), the range normalization stores that end as the whole block, and toString prints the block as its first year. So [1990 TO 1999] prints as "[1990 TO 1000]", which parseShape rejects with Wrong order, and [* TO 1999-12-31] prints as "[* TO 1000]", which parses to a range that ends 999 years early. The shape object is right, so matching, equality and the index are unaffected; the wrong text shows in the prefix-tree queries' toString, in logs, and in any code that stores or forwards a range as text and parses it again. In every release since 5.0.0.
- The tree. Level 1 has 586 million-year cells, level 2 has 1,000 thousand-year cells in each, level 3 has 1,000 years in each (only the cells at the tree's maximum date are partial,
getNumSubCells,DateRangePrefixTree.java:172-177), then months, days, hours, minutes, seconds and milliseconds (DateRangePrefixTree.java:128-140,YEAR_LEVEL = 3at:102). The year count is offset by a whole number of million years (:153-154), so the thousand-year cells hold the years 0-999, 1000-1999, 2000-2999, ... (toShape,:315-322). 1999 is year 999, the last, of the cell 1000-1999. - Parsing.
parseShape("[1990 TO 1999]")parses both ends as cells and callstoRangeShape(NumberRangePrefixTree.java:205-215). - Normalization.
toRangeShapereplaces each end with the coarsest equivalent cell: "this normalization/optimization process is actually REQUIRED based on assumptions elsewhere." (:118-119).truncateEndVals(:124, "chops off trailing max-vals";:160-166) walks up while a value is the last one of its parent, so the end 1999 becomes the level-2 cell 1000-1999. The same holds for1999-12,1999-12-31and so on down to1999-12-31T23:59:59.999. An end of ISO year -1 (2 BC) or 999999 is the last year of a million-year cell and becomes that level-1 cell ([1 TO 999999]prints"[0001 TO 0000]", executed by the hunt's probe). The shape[1990, 1000-1999]covers exactly 1990-01-01 to 1999-12-31. - Printing.
SpanUnitsNRShape.toStringprints both ends with the same per-cell printer (:399-406):"[" + toString(minLV) + " TO " + toString(maxLV) + "]". - The per-cell printer.
DateRangePrefixTree.toString(UnitNRShape)istoString(toCalendar(lv))(DateRangePrefixTree.java:380-383).toCalendaradds up only the values the cell has (:357-363), so a level-2 cell gives the block's first year, at year precision (:364-370). The end 1000-1999 prints as1000. For a range start that is right:[1000 TO 1998]round-trips. - Re-parsing.
parseShape("[1990 TO 1000]")throwsIllegalArgumentException("Wrong order: ...")(NumberRangePrefixTree.java:126-129, documented at:114-115). If the start lies at or before the block's first year (*,0500,1000-03), there is no error and the range now ends on the last day of the block's first year: 999 years early, or about a million years for a million-year end cell.
input to parseShape | toString() | parseShape of that string |
|---|---|---|
[1990 TO 1999] | [1990 TO 1000] | throws Wrong order: 1990 TO 1000 |
[2014 TO 2999] | [2014 TO 2000] | throws Wrong order: 2014 TO 2000 |
[2014 TO 2999-12-31T23:59:59.999] | [2014 TO 2000] | throws Wrong order: 2014 TO 2000 |
[-9999 TO -1] (ends in 2 BC) | [-9999 TO -1000000] | throws Wrong order: -9999 TO -1000000 |
[* TO 1999-12-31] | [* TO 1000] | a different shape: contains 1500-06-15 before, disjoint from it after |
[1990 TO 2999] | [1990 TO 2000] | a shorter shape |
[* TO 9999], [2020 TO 9999] | [* TO 9000], [2020 TO 9000] | shorter shapes |
controls: [1990 TO 1998], [1000 TO 1998], [* TO 2014-09-15] | the same strings | the same shapes |
IntersectsPrefixTreeQuery built from [* TO 1999-12-31] and from its printed text both print queryShape=[* TO 1000], and equals is false. With the fix every row round-trips ([-9999 TO -1] prints "[-9999 TO -0001]", [* TO 1999-12-31] prints "[* TO 1999]"), and the two queries print queryShape=[* TO 1999] and are equal.
Not part of this bug: a range that is exactly one block ([2000 TO 2999]) normalizes to a single cell and prints as "2000", which parses back as the single year 2000. The date syntax has no token for such a cell and an upstream test accepts a date-like string for a bare coarse cell, so that may be intended; the fix leaves it unchanged.
Exposure and likelihood328 words
- Who. Applications that use
DateRangePrefixTreefrom the opt-inspatial-extrasmodule (@lucene.experimental), for example throughNumberRangePrefixTreeStrategy. Downstream, Apache Solr'sDateRangeFieldis built on it (read from Solr's code, not run). - Trigger. Either built-in calendar (
DEFAULT_CALorJAVA_UTIL_TIME_COMPAT_CAL) and ordinary input: decades or centuries ending in a year X999,[* TO 1999-12-31], and far-future open-end values such as2999-12-31T23:59:59.999or9999. Deterministic, assertions on or off. - Misleading text. A query over
[* TO 1999-12-31]printsqueryShape=[* TO 1000]inIntersectsPrefixTreeQuery,ContainsPrefixTreeQueryandWithinPrefixTreeQuerytoString; logs and debug output show a range that is not the one searched, and two different queries print identically. - Broken round trip. Code that keeps a range as
toString()text and parses it later getsIllegalArgumentExceptionwhen the start lies inside the end's block after its first year, and otherwise a range that silently ends 999 years early (about a million years for a million-year end cell:[* TO -1]prints"[* TO -1000000]", which parses without error). Over 452,984 legal ranges in two trees (the default and the java.time-compatible calendar; grids of 434 and 839 edge values), the hunt's exhaustive check found 10,430 silent interval mismatches and 4,655 parse exceptions, all with a block end (default tree: 2,464 and 1,477), and none with the fix (the grids are not a usage frequency). - Downstream reach (read, not run). Solr stores the client's input string for a
DateRangeFieldvalue and usesshape.toString()only for values that arrive as a Shape object through the Java API; an atomic update of such a document re-parses that stored text. - Not affected. Matching, query
equals/hashCode, caching and the index (CheckIndex passes on an index with the affected ranges). No extra cost: a parse-print-re-parse cycle takes 5.8 to 11.1 microseconds, flat. - How likely: plausible. Such range ends are ordinary, but the harmful half needs code that re-parses the text, and nothing was reported in the 11 years since 5.0.0.
One JUnit class, one test method, three lines of public API, no index: parse [1990 TO 1999], print it, parse the printed string. Deterministic: the default calendar is UTC with the ROOT locale.
/*
* 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.spatial.prefix.tree;
import org.apache.lucene.spatial.prefix.tree.NumberRangePrefixTree.NRShape;
import org.apache.lucene.tests.util.LuceneTestCase;
public class TestDateRangeBlockEndToString extends LuceneTestCase {
public void testRangeEndingOnLastYearOfMillennium() throws Exception {
DateRangePrefixTree tree = new DateRangePrefixTree(DateRangePrefixTree.DEFAULT_CAL);
NRShape shape = tree.parseShape("[1990 TO 1999]");
// expected toString() to parse back to the same shape; actual "[1990 TO 1000]" -> "Wrong order"
assertEquals(shape, tree.parseShape(shape.toString()));
}
}
cd $LUCENE && ./gradlew :lucene:spatial-extras:test --tests TestDateRangeBlockEndToString -Ptests.seed=DEADBEEF -Ptests.asserts=true
- Expected
parseShape(shape.toString())returns the same shape; the printed string would be"[1990 TO 1999]".- Observed
java.lang.IllegalArgumentException: Wrong order: 1990 TO 1000fromparseShape, assertions on and off; the same on 10.6.0 and 9.1.0.
More files, environment, full output199 words
The test asserts that parseShape(shape.toString()) equals the shape. On a stock build it fails with the IllegalArgumentException that parseShape throws on the printed string, so the message Wrong order: 1990 TO 1000 shows the printed end (1000, expected 1999). With the fix the assertEquals runs and passes. It failed with both seeds run (DEADBEEF, 5EED1234); all lines are within 100 columns.
Regression test: this same class; it passes with the recommended fix.
| lucene | main 645d4fe14a20 (11.0.0-SNAPSHOT): fails; releases 10.6.0 and 9.1.0: fail; with the fix: passes |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux, aarch64 (the only platform run; plain Java calendar and string logic) |
cp $REPRO/TestDateRangeBlockEndToString.java $LUCENE/lucene/spatial-extras/src/test/org/apache/lucene/spatial/prefix/tree/
cd $LUCENE && ./gradlew :lucene:spatial-extras:test --tests TestDateRangeBlockEndToString -Ptests.seed=DEADBEEF -Ptests.asserts=true
# assertions off fails the same way: -Ptests.asserts=false
# gradle, stock 645d4fe14a20, -Ptests.seed=DEADBEEF -Ptests.asserts=true (suite output trimmed; -Ptests.asserts=false identical)
TestDateRangeBlockEndToString > testRangeEndingOnLastYearOfMillennium FAILED
java.lang.IllegalArgumentException: Wrong order: 1990 TO 1000
at __randomizedtesting.SeedInfo.seed([DEADBEEF:ACD30FF6D2086A48]:0)
at org.apache.lucene.spatial.prefix.tree.NumberRangePrefixTree.toRangeShape(NumberRangePrefixTree.java:128)
at org.apache.lucene.spatial.prefix.tree.NumberRangePrefixTree.parseShape(NumberRangePrefixTree.java:215)
at org.apache.lucene.spatial.prefix.tree.TestDateRangeBlockEndToString.testRangeEndingOnLastYearOfMillennium(TestDateRangeBlockEndToString.java:28)
1 test completed, 1 failed
# standalone runner, seed DEADBEEF (summary of the runs)
main 645d4fe14a20 -ea: FAIL 1/1 -da: FAIL 1/1
10.6.0 -ea: FAIL 1/1 -da: FAIL 1/1
9.1.0 -ea: FAIL 1/1
message: java.lang.IllegalArgumentException: Wrong order: 1990 TO 1000NRShape#toString: "The result should be parseable by parseShape(String)"."[1990 TO 1000]"is not parseable at all.- The class's author stated the intent on LUCENE-5648 (#6710): "you can round-trip toString() and parseShape() ... or a range".
- Range optimization only swaps a range for an equivalent one (the upstream optimizer test's examples), and an exact string exists: the block's last year,
1999.
The full argument210 words
- Contract.
NRShape#toString: "The result should be parseable by {@link #parseShape(String)}." (NumberRangePrefixTree.java:84).parseShape: the range syntax "is the same thing as the toString() of the range shape, notwithstanding range optimization." (:199). The per-cell printer: "A string representation of the UnitNRShape that is parse-able by {@link #parseUnitShape(String)}." (:174-178). - Author's intent. On LUCENE-5648 (now #6710), David Smiley, who wrote the class, described a patch on 2014-05-21: "Parses calendar ranges. This means you can round-trip toString() and parseShape() wether it's a single Calendar value "2014-05" or a range "[- TO 2013]"." The
NRShape#toStringjavadoc came with their refactoring of 2014-12-14. - The optimizer test's examples keep the meaning.
TestDateRangePrefixTree.testShapeRangeOptimizer(:262-280) asserts exact strings of optimized ranges (strings only, not containment), each denoting the input range ([2014-01-01 TO 2014-12-31]prints"2014"); none has a block as its end. - An exact string exists. A thousand-year end block means "up to the end of its last year", which the syntax writes as
1999; the printer already gets the start case right ([1000 TO 1998]round-trips). - No acceptance found. No upstream test,
CHANGES.txtorMIGRATE.mdentry accepts a lossy or unparseable range string, and the prior-art searches found no issue or list thread that does.
The devil's-advocate check173 words
The strongest case for "intended": the thousand- and million-year levels are internal tree levels that ISO dates cannot express; "should be parseable" does not say "parses back to an equal shape"; "notwithstanding range optimization" admits a different string; the classes are @lucene.experimental; Lucene does not promise that Query#toString parses back; and TestDateRangePrefixTree.java:60 accepts a date-like string for a bare level-1 cell.
Why it fails: "[1990 TO 1000]" is not parseable at all, so even the weakest reading breaks; the optimization only replaces a range by an equivalent one, so it cannot license a string that means a shorter range; the author stated round-trip intent for ranges; and the line-60 test is a "test no underflow" check: level-1 cell 0 prints the full-precision minimum date -292269054-12-02T16:47:04.192 and only its BC sign is asserted, so it says nothing about range ends. Verifiers, the adjudication and the cross-model reviewer of the intended lens rejected the intended reading. Expect a maintainer to ask about the bare-block case; it is left out of this report on purpose.
Recommended Print a block end cell as the block's last year (validated)
Print a thousand- or million-year cell that ends a range as the block's last year: a protected toStringAsRangeEnd hook in NumberRangePrefixTree, used for the end in SpanUnitsNRShape.toString, overridden in DateRangePrefixTree (+26/-1). Bare cells and starts print as before. It passes the repro and the whole spatial-extras module.
// NumberRangePrefixTree: new hook; SpanUnitsNRShape.toString uses it for maxLV
protected String toStringAsRangeEnd(UnitNRShape lv) { return toString(lv); }
// DateRangePrefixTree (condensed)
@Override
protected String toStringAsRangeEnd(UnitNRShape lv) {
if (lv.getLevel() > 0 && lv.getLevel() < YEAR_LEVEL) { // a block prints as its LAST year
int[] vals = new int[YEAR_LEVEL];
for (int level = 1; level <= lv.getLevel(); level++) vals[level - 1] = lv.getValAtLevel(level);
for (int level = lv.getLevel(); level < YEAR_LEVEL; level++)
vals[level] = getNumSubCells(toShape(vals.clone(), level)) - 1;
return toString(toCalendar(toShape(vals, YEAR_LEVEL)));
}
return toString(lv);
}All options and trade-offs3 options · 533 words
One fix was written by the finder and validated again in a scratch worktree of 645d4fe14a20; another fix site is described but not written, and two variants are rejected.
The change is +26/-1 lines in NumberRangePrefixTree.java and DateRangePrefixTree.java. SpanUnitsNRShape.toString prints its end with a new protected toStringAsRangeEnd(UnitNRShape), which defaults to toString(lv). DateRangePrefixTree overrides it: a million- or thousand-year cell is filled down to the year level with each level's last child (getNumSubCells(...) - 1, which also handles the partial last million-year block) and printed as that year. [1990 TO 1999] then prints as itself, [-9999 TO -1] as "[-9999 TO -0001]", [* TO 1999-12-31] as the equivalent "[* TO 1999]".
// NumberRangePrefixTree: new hook; SpanUnitsNRShape.toString uses it for maxLV
protected String toStringAsRangeEnd(UnitNRShape lv) { return toString(lv); }
// DateRangePrefixTree (condensed)
@Override
protected String toStringAsRangeEnd(UnitNRShape lv) {
if (lv.getLevel() > 0 && lv.getLevel() < YEAR_LEVEL) { // a block prints as its LAST year
int[] vals = new int[YEAR_LEVEL];
for (int level = 1; level <= lv.getLevel(); level++) vals[level - 1] = lv.getValAtLevel(level);
for (int level = lv.getLevel(); level < YEAR_LEVEL; level++)
vals[level] = getNumSubCells(toShape(vals.clone(), level)) - 1;
return toString(toCalendar(toShape(vals, YEAR_LEVEL)));
}
return toString(lv);
}Trade-offs
- Changes only the printed end of a
SpanUnitsNRShapewhose end is a million- or thousand-year cell; bare cells and range starts are unchanged, soTestDateRangePrefixTree.java:60and the optimizer test still hold. OtherNumberRangePrefixTreesubclasses keep the default. - Affects
toStringonly; nothing on the indexing or search path. - Adds one protected method to an
@lucene.experimentalclass.
What the checks validated
Gradle in a scratch worktree of 645d4fe14a20, OpenJDK 25.0.3, seed DEADBEEF (2026-10-31). Stock: the minimal test fails with assertions on and off. With the fix:
- the minimal test passes with assertions on and off;
TestDateRangePrefixTree,TestDateNRStrategyandTestNumberRangeFacetspass: 3 classes, 20 tests, 0 failures;- the whole
:lucene:spatial-extras:testpasses: 26 classes, 117 tests, 0 failures, 5 skipped; checkGoogleJavaFormat,ecjLintMainandforbiddenApisMainfor spatial-extras pass (the finder's diff as is failed onlycheckGoogleJavaFormaton one 101-column comment line, rewrapped and lightly reworded in this diff; the code is identical).
Earlier: the upstream date-tree suites passed 100 of 100 (5 iterations), a verifier's 7 tests pass 7 of 7 including the 2 BC end, and both mismatch kinds of the exhaustive check drop to 0 with it.
A maintainer may prefer a range-aware printer inside DateRangePrefixTree, or a SpanUnitsNRShape.toString that asks the tree for the last year of a cell (suggested by the adjudication and a verifier). That moves the code, not the behaviour. Any variant must keep toString(UnitNRShape) printing a bare cell as a unit, as its javadoc requires ("parse-able by parseUnitShape").
Printing a bare block as "[first TO last]" (the finder's first variant) breaks the upstream assertion at TestDateRangePrefixTree.java:60 and the unit-string contract of toString(UnitNRShape). Not truncating the end changes the stored shape, its equality and its indexed cells, and the normalization is "REQUIRED based on assumptions elsewhere" (NumberRangePrefixTree.java:118-119); not tried.
An application that keeps an exclusion rule as this text and rebuilds it with parseShape would hide fewer documents (a deny rule [* TO 1999-12-31] rebuilt from "[* TO 1000]" stops hiding 1500-06-15); no product is known to do this.
A hunt on spatial-extras' range trees printed every legal range over edge-date grids of two trees (both built-in calendars; BC, leap and far-future years, every precision), parsed the text again and compared it with a java.time reference. All 15,085 failures of this kind had a thousand- or million-year end; the finder then reduced them to a JUnit.
The method in full140 words
Bounded-exhaustive translation validation of the stock jar: for every legal [a TO b] range over 839 edge units of the java.time-compatible tree (31 years including BC, leap and >9999 years, at every precision; 356,472 ranges) and 434 edge units of the default tree (16 years from 1583; 96,512 ranges), 452,984 ranges in all, the harness re-parses toString() and compares the re-parsed interval with a proleptic-Gregorian java.time reference. It found 10,430 interval mismatches and 4,655 parse exceptions (default tree: 2,464 and 1,477), all with a thousand- or million-year END cell, with assertions on and off; both kinds disappear with the fix. The finder then wrote a three-line public-API JUnit, and the hunt later cut it to the one-input test shown here.
The harness's other mismatch kinds belong to other leads of the same hunt and are not part of this bug.
No prior report found (213 logged queries on 2026-10-26 over GitHub, JIRA, the Lucene and Solr lists and the web; rechecked 2026-10-31). The design issue #6710 states the round-trip intent; nothing reports this bug.
Searches4 searches
Verdict new. Closest items, none this bug: #6710 (LUCENE-5648, the design issue; states the round-trip intent), #7154 (LUCENE-6092, made the normalization unconditional; does not touch toString), #8865 (LUCENE-7814, years >= 292,000,000), #9686 (LUCENE-8640), #8333 (LUCENE-7278), #9831 (LUCENE-8787), SOLR-6103 (invalid month-00 input), SOLR-16306 (faceting on data with 2999-12-31T23:59:59.999Z ends; about faceting) and two "Wrong order" reports caused by inverted user input. No open PR changes the printing (#12295 only replaces casts). Limits: GitHub search returns at most 50 hits per query; the open web is covered with medium confidence; private reports cannot be searched.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues, PRs and commits (apache/lucene, apache/solr, apache/lucene-solr, apache/lucenenet, two other search engines, all of GitHub) | 2026-10-26 | a scripted prior-report search (two runs plus the finder's) and 23 manual searches: class names, "Wrong order", toRangeShape, 1999, 2999, millennium, decade, round trip, BC; 7 issues read with comments; changed files of all 407 open PRs | only the context items listed above |
| JIRA (LUCENE, SOLR) and mailing lists | 2026-10-26 | 9 JIRA searches with 23 issues read in full; dev@, java-user@, issues@, commits@ and the Solr lists | two "Wrong order" threads from inverted user input |
| Web | 2026-10-26 | 10 queries; one Q&A page read through an archive copy | usage questions only |
| GitHub (recheck) | 2026-10-31 | 5 apache/lucene issue/PR searches (DateRangePrefixTree, NumberRangePrefixTree, SpanUnitsNRShape, parseShape toString, "Wrong order"); the 50 issues and PRs created since 2026-10-26 by title | nothing new |
In the first version of the code: LUCENE-5648 (bb267a143e0, 2014-06-05), first released in 5.0.0. Executed failing on 5.0.0, in every major line through 10.6.0 and on main; never fixed.
Versions tested6 rows
- Introduced by
bb267a143e009f7b1375f7fa9755b76fcc82ec4f, "LUCENE-5648: DateRangePrefixTree and NumberRangePrefixTreeStrategy" (David Smiley, 2014-06-05; svn era, no PR; the JIRA issue is now #6710);branch_5xgot it through the backport55594bad9b07(2014-09-18). The end truncation, the range printer and the first-year cell printer were all in that commit. - Kept by
29c90c668cb(LUCENE-6092, #7154: normalization made unconditional, the "REQUIRED" comment),0ddb7567f1c(2014-12-14: types renamed,NRShape#toStringjavadoc added) and2cbf261032d(2021 reformat);git blameshows no behaviour change since. - Upstream now: main
2f424fd428daandbranch_11x603af8703753have the two files byte-identical to645d4fe14a20;branch_10_6has 10.6.0's copies (mechanical differences only);branch_10xwas decommissioned (#16911). - Releases between the executed points rest on
git blame.
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20; 10.6.0 | affected | minimal repro fails, assertions on and off |
9.1.0, 9.12.3, 10.0.0, 10.5.2, main a9779ef3c193 | affected | the finder's JUnit fails (9.1.0 also with the minimal repro) |
| 5.0.0, 5.5.5, 6.0.0, 6.6.6, 7.7.3, 8.11.4 | affected | version-neutral probe: 4 of 4 cases broken, 0 of 3 controls |
bb267a143e0 (introducing), af748a9254e (the parent of 29c90c668cb), 29c90c668cb | shadow | the commit's tree package over the 5.0.0 jars: probe 4 of 4 broken |
| 4.10.4 and earlier | not affected | no DateRangePrefixTree (the API did not exist) |
| main with the fix | not affected | minimal repro passes; probe 0 of 4 broken |
An independent reproducer, a devil's-advocate verifier and a read-only review by OpenAI GPT-5.6 found it real; a second read-only reviewer said "not real" on procedure only (it could not run Java), and its objection narrowed the claim to range ends. The adjudication confirmed it (high confidence, impact minor).
Checks and verdicts294 words
Every executed check ran on stock builds; the cross-model reviewers could only read code, and separate runs re-ran the repro.
| Check | Verdict | What it found |
|---|---|---|
| Finder (exhaustive print-and-re-parse check, then a JUnit) | candidate | 10,430 silent mismatches and 4,655 exceptions over 452,984 ranges in two trees, all with a block end, none with the fix; a 3-case JUnit fails on main (-ea, -da), 10.5.2 and 9.12.3. |
| Refute lens (OpenAI GPT-5.6, read-only) | not real (procedural, low) | Could not run Java; its static evidence supported the root cause, and its one objection (bare-block printing) led to narrowing the claim and the fix to range ends. A separate run re-ran the repro: fails. |
| Independent reproducer | real | Own 7 tests: 5 fail with -ea and -da on main, 10.5.2 and 9.12.3, the 2 controls pass; adds the 2 BC end and a containment check; its probe fails on 8.11.4, 6.0.0 and 5.0.0; 7/7 with the fix. |
| Intended lens (OpenAI GPT-5.6, read-only) | real (medium) | Intended argument weak, root cause agreed, inputs in contract; a separate check fails on main -da and 9.12.3. |
| Intended-behaviour devil's advocate | real (high, range ends only) | Re-ran the repro on main and 10.5.2; showed that the upstream test at line 60 checks only the BC sign of a full-precision date. |
| Adjudication | confirmed (high), impact minor | Re-ran the independent reproduction's tests on main: 5 of 7 fail with -ea and -da, controls pass. |
Minimal repro on 645d4fe14a20 | still reproduces | Fails with -ea and -da, two seeds; passes with the fix. |
| This doc's runs | real | Gradle in a scratch worktree of 645d4fe14a20: stock fails, the fix passes the repro and the module; standalone runner: 10.6.0 and 9.1.0 fail. |
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 510 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,498 words
- Registry title
- DateRangePrefixTree prints a date range whose end is the last year of a thousand- or million-year block (a year X999, or 2 BC) with that end shown as the block's FIRST year, e.g. [1990 TO 1999] prints as "[1990 TO 1000]", so parseShape rejects the printed string ('Wrong order') or reads it as a much shorter range ([* TO 1999-12-31] prints as "[* TO 1000]")
- Area
- spatial-extras / prefix trees (DateRangePrefixTree / NumberRangePrefixTree range-shape string form: SpanUnitsNRShape.toString of a range whose end normalizes to a thousand- or million-year cell; also shown in the 'queryShape=' part of the prefix-tree queries' toString)
- Priority
- Low, rank 775: DateRangePrefixTree prints ranges that end on a block's last year with the wrong end, which breaks the toString round trip while queries stay correct, so it sits with the niche toString round-trip row LFV-356.
- Impact class
- minor
- Impact note
- Hits applications that use the opt-in, @lucene.experimental DateRangePrefixTree of the spatial-extras module (in practice also Solr's DateRangeField, which is built on it) and print, log, store or forward a range shape whose end is the last instant of a year X999 or of 2 BC, with ordinary input and no scale ([1990 TO 1999], [* TO 1999-12-31], [2014 TO 2999], [2020 TO 9999]): matching, query equality and the index are correct because they use the shape object, but the shape's toString and the 'queryShape=' part of IntersectsPrefixTreeQuery / ContainsPrefixTreeQuery / WithinPrefixTreeQuery toString show the wrong end, and code that parses the string again gets IllegalArgumentException 'Wrong order' or, more often, a range that silently ends up to 999 years early (about a million years for an end of 2 BC); deterministic, -ea and -da, every release since 5.0.0.
- Summary
- You parse an ordinary date range such as [1990 TO 1999] with DateRangePrefixTree.parseShape and print it, expecting a string that parseShape reads back as the same range, as NRShape#toString promises ('The result should be parseable by parseShape(String)'). Instead it prints "[1990 TO 1000]", which parseShape rejects with IllegalArgumentException 'Wrong order', and [* TO 1999-12-31] prints as "[* TO 1000]" ([1990 TO 2999] as "[1990 TO 2000]"), which parses without error to a range that ends up to 999 years early. Range normalization turns an end of 1999 into the thousand-year cell 1000-1999 (an end of 2 BC into a million-year cell), and the per-cell printer writes a cell as its FIRST year, which is right for a range start but wrong for a range end; searches are unaffected, only the text is wrong.
- Introduced
- bb267a143e009f7b1375f7fa9755b76fcc82ec4f (trunk, svn r1600555, 'LUCENE-5648: DateRangePrefixTree and NumberRangePrefixTreeStrategy', author GitHub dsmiley); branch_5x got it through the bulk backport 55594bad9b07 (LUCENE-5944, 2014-09-18). No PR (svn era; JIRA LUCENE-5648, now GitHub issue #6710). The defect is in the first version of the code, not a regression: toRangeShape already truncated trailing max values of the end, the range toString already printed both ends with the per-cell printer, and that printer already wrote a level-1/2 cell as its block's first year [read: git blame on main, git log --follow and -S on both files; exec: the commit's prefix/tree package (12 of 13 files, QuadPrefixTree.java left out, it does not compile against 5.0.0 core and is not loaded) shadowed over the 5.0.0 jars fails the probe the same way]. Later commits kept the behaviour: 29c90c668cb (LUCENE-6092, #7154) restructured the normalization, 0ddb7567f1c (2014-12-14) renamed the types and added the NRShape#toString javadoc, 2cbf261032d (2021) reformatted (2014-06-05 (trunk commit); 2014-09-18 (branch_5x backport))
- First affected version
- 5.0.0 [exec] (first release with the API); 4.10.4 and earlier have no DateRangePrefixTree or NumberRangePrefixTree [exec: the probe does not compile on 4.10.4; read: absent in 4.9.0, 4.9.1, 4.10.0]
- Affected versions
- Every release from 5.0.0 through 10.6.0 (10.6.0 by reading) and main a9779ef3c193, never fixed. [exec] the follow-up's version-neutral main() probe breaks 4 of 4 defect cases ([1990 TO 1999], [* TO 1999-12-31], [2014 TO 2999], [-9999 TO -1]) and 0 of 3 controls on 5.0.0, 5.5.5, 6.0.0, 6.6.6, 7.7.3, 8.11.4, 9.12.3, 10.5.2 and main, and on commits bb267a143e0, af748a9254e and 29c90c668cb (shadowed over the 5.0.0 jars); the finder's JUnit fails on 9.1.0, 9.12.3, 10.0.0, 10.5.2 and main (-ea and -da); the independent reproduction's JUnit fails 5 of 7 on main (-ea and -da), 10.5.2 (-ea and -da) and 9.12.3, and its probe fails on 8.11.4, 6.0.0 and 5.0.0. [read] releases between the executed points by git blame (no behaviour change to NumberRangePrefixTree.java:117-166, 399-406 or DateRangePrefixTree.java:349-383); upstream main a95c754ee003 (2026-10-26) has only mechanical commits on both files since then (latest 2026-06-14, typo checks), and branch_10x, branch_10_6 and branch_9_12 last changed NumberRangePrefixTree.java in 2023-12 (#12880, redundant modifiers), so 10.6.0 (released 2026-10-29) and the unreleased 11.0 line are affected (not executed). Not executed: x86 (pure Java string logic). JDK 25.0.3, aarch64
- Status
- documented (brief, checks and a per-finding doc exist)
- Verification
- Confirmed by AI-run checks: an independent repro, a cross-model review that could only read the code (its low-confidence “not real” is procedural), an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
- Found by
- semi-formal: exhaustive check
- Upstream and prior reports
- none found (2026-10-26, read-only; the prior-report record verdict new, 213 logged queries: scripted prior-report searches over apache/lucene issues, PRs and commits, downstream repositories, the dev@, java-user@, issues@ and commits@ lists, the JIRA archive and the web, including the finder's earlier run; manual GitHub searches in apache/lucene, apache/solr, apache/lucene-solr, apache/lucenenet, elastic/elasticsearch, opensearch-project/OpenSearch and all of GitHub; ASF JIRA searches over LUCENE and SOLR with full reads of 23 JIRA issues and 7 GitHub issues; Solr mailing-list searches; the changed-file lists of all 407 open apache/lucene PRs). Closest items, none the same bug: #6710 (LUCENE-5648, the design issue, whose author states the toString/parseShape round-trip intent), #7154 (LUCENE-6092, made the normalization that creates the end block mandatory, does not touch toString), #8865 (LUCENE-7814), #9686 (LUCENE-8640), #8333 (LUCENE-7278), #9831 (LUCENE-8787), SOLR-6103 (invalid month-00 input), SOLR-16306 (faceting on data with 2999-12-31 ends, about faceting), and two 'Wrong order' reports caused by inverted user input (a users@solr thread of 2022-06 and Stratio/cassandra-lucene-index#36). Not fixed upstream: upstream main a95c754ee003 has only mechanical commits on both files, branch_10x, branch_10_6 and branch_9_12 last changed NumberRangePrefixTree.java in 2023-12 (#12880), and the only open PR that touches the files (#12295, instanceof pattern matching) only replaces casts. Confidence high for GitHub, JIRA and the ASF lists, medium for the open web (GitHub search returns at most 50 hits per query; one Q&A site was read only through an archive copy)
- Lucene commit tested
- main
645d4fe14a20(2026-10-28): the minimal test fails with assertions on and off through gradle in a scratch worktree and through a standalone runner; with the fix it passes, as do the date-tree suites and the whole spatial-extras module. Releases 10.6.0 (assertions on and off) and 9.1.0 fail with the standalone runner. Earlier, the hunt ran its tests and a version-neutral probe on 5.0.0 to 10.5.2, on the introducing commit and on maina9779ef3c193(spatial-extras identical to645d4fe14a20). Upstream main2f424fd428da(2026-10-30) andbranch_11x603af8703753carry the same two files [read].
- Finding record
lucene/spatial-extras/src/java/org/apache/lucene/spatial/prefix/tree/NumberRangePrefixTree.java:58, (at a9779ef3c193) :81 (@lucene.experimental), :84 (NRShape#toString 'The result should be parseable by {@link #parseShape(String)}'), :114-115 (toRangeShape throws IllegalArgumentException on wrong order), :117-147 (toRangeShape; :118-119 'this normalization/optimization process is actually REQUIRED'; :124 end truncation 'chops off trailing max-vals'), :160-166 (truncateEndVals), :174-178 (toString(UnitNRShape) javadoc 'parse-able by parseUnitShape'), :190-199 (parseShape javadoc: the range syntax 'is the same thing as the toString() of the range shape, notwithstanding range optimization'), :399-406 (SpanUnitsNRShape.toString prints minLV and maxLV with the same toString(UnitNRShape))lucene/spatial-extras/src/java/org/apache/lucene/spatial/prefix/tree/DateRangePrefixTree.java:39 (at a9779ef3c193) (@lucene.experimental), :345-378 (toCalendar: yearAdj from the block's start values, year precision), :380-383 (toString(UnitNRShape) = toString(toCalendar(lv)))lucene/spatial-extras/src/java/org/apache/lucene/spatial/prefix/IntersectsPrefixTreeQuery.java:90, (at a9779ef3c193) ContainsPrefixTreeQuery.java:81, WithinPrefixTreeQuery.java:106 ('queryShape=' + queryShape in toString)lucene/spatial-extras/src/test/org/apache/lucene/spatial/prefix/tree/TestDateRangePrefixTree.java:59-60 (at a9779ef3c193) ('test no underflow', startsWith("-")), :262-280 (testShapeRangeOptimizer: exact toString of optimized ranges, none with a block END)a Lucene main checkout: git log ea57360912ed..a9779ef3c193 (7 commits, none touching lucene/spatial-extras)
- The finding in 30 seconds
a Lucene main checkout (645d4fe14a20) lucene/spatial-extras/src/java/org/apache/lucene/spatial/prefix/tree/NumberRangePrefixTree.java:84, :114-129, :160-166, :174-178, :199, :205-215, :399-406a Lucene main checkout (645d4fe14a20) lucene/spatial-extras/src/java/org/apache/lucene/spatial/prefix/tree/DateRangePrefixTree.java:102, :128-140, :153-154, :315-322, :349-383
- Who is affected
a Lucene main checkout (645d4fe14a20) lucene/spatial-extras/src/java/org/apache/lucene/spatial/prefix/tree/DateRangePrefixTree.java:39 (@lucene.experimental); a Lucene main checkout (645d4fe14a20) lucene/spatial-extras/src/java/org/apache/lucene/spatial/prefix/tree/NumberRangePrefixTree.java:58a Lucene main checkout (645d4fe14a20) lucene/spatial-extras/src/java/org/apache/lucene/spatial/prefix/IntersectsPrefixTreeQuery.java:90; ContainsPrefixTreeQuery.java:81; WithinPrefixTreeQuery.java:106
- Minimal repro
a Lucene main checkout (645d4fe14a20) lucene/spatial-extras/src/java/org/apache/lucene/spatial/prefix/tree/DateRangePrefixTree.java:62-71 (DEFAULT_CAL: UTC, Locale.ROOT)
- Why it looks unintended
a Lucene main checkout (645d4fe14a20) lucene/spatial-extras/src/java/org/apache/lucene/spatial/prefix/tree/NumberRangePrefixTree.java:84, :174-178, :199a Lucene main checkout (645d4fe14a20) lucene/spatial-extras/src/test/org/apache/lucene/spatial/prefix/tree/TestDateRangePrefixTree.java:59-60, :262-280
- Suggested fix
a Lucene main checkout (645d4fe14a20) lucene/spatial-extras/src/java/org/apache/lucene/spatial/prefix/tree/NumberRangePrefixTree.java:118-119, :174-178
- History
a Lucene clone: git diff 645d4fe14a20 origin/main, origin/branch_11x, releases/lucene/10.6.0 -- the two files; git log -1 origin/branch_10x
- TestDateRangeBlockEndToString.java (one test class):
repro/LFV-610/src/org/apache/lucene/spatial/prefix/tree/TestDateRangeBlockEndToString.java(30 lines)
Made in parallel with this doc from the code at 645d4fe14a20 and the minimal repro.
assets/LFV-610/bug.png(510 KB)
![Diagram of the bug: a time line with the thousand-year block 1000-1999. The range [1990 TO 1999] starts at the year 1990 and ends with the whole block 1000-1999, because 1999 is the block's last year. The printer writes the start as 1990 and the end block as its first year, 1000, so toString gives "[1990 TO 1000]", and parseShape rejects that string with 'Wrong order'. The correct end would be printed as 1999.](../assets/LFV-610/bug.png)