To make a bound exclusive, the code moves it one step and then tests inclusively. At the type limit it skips the step to avoid an overflow, so the bound silently stays inclusive.
FunctionRangeQuery(source, lower, upper, includeLower, includeUpper)matches documents whose value from aValueSourcelies in the range;includeLower=falsemeansvalue > lower,includeUpper=falsemeansvalue < upper.- For int and long sources, Lucene rewrites a strict bound into an inclusive one on the next integer,
n > 5inton >= 6, so one inclusive testlower <= n <= uppercovers all four cases.
n > 2147483647 should become n >= 2147483648, which no int can hold, so nothing should match; the code skips the step at the limit and tests n >= 2147483647 instead, so the document matches. Select the still to open it at full size. Generated with AI, checked against the code.- Index one document with
NumericDocValuesField("n", Integer.MAX_VALUE), so n = 2147483647, and searchnew FunctionRangeQuery(new IntFieldSource("n"), "2147483647", null, false, true): n > 2147483647, printed{2147483647 TO *]. - You expect 0 hits: no int is greater than 2147483647. The float and double sources do give 0 hits for x > MAX_VALUE.
- Instead you get 1 hit. The code turns
n > 5inton >= 6by adding 1, but skips the+ 1at 2147483647 to avoid an overflow, son > 2147483647staysn >= 2147483647and the document matches. - The same holds for
n < Integer.MIN_VALUE,m > Long.MAX_VALUE,m < Long.MIN_VALUEand enum sources: 1 hit instead of 0 each. One step inside the limit,n > 2147483646, correctly gives 1. No error, with assertions on or off.
Users of the lucene/queries function-range API over int, long or enum sources, when a bound is generated exactly at MIN_VALUE or MAX_VALUE and a document holds exactly that value, for example a MAX_VALUE sentinel. Only that value is over-matched, silently. Opt-in module, rare.
Mechanism, terms and numbers572 words
FunctionRangeQuery with includeLower=false or includeUpper=false means a strict bound (> or <). For int, long and enum value sources, getRangeScorer implements it by moving the endpoint one step (lower++, upper--) and then testing inclusively, but it skips the step when the bound is already MAX_VALUE (or MIN_VALUE) to avoid an overflow. The bound then stays inclusive, so n > Integer.MAX_VALUE matches the document whose value is exactly Integer.MAX_VALUE: 1 hit instead of 0, silently, with assertions on and off.
It affects IntDocValues, LongDocValues and EnumFieldSource (six lines), every checked release with FunctionRangeQuery (5.4.0 to 10.6.0) and main, and the public FunctionValues.getRangeScorer since 4.0.0-ALPHA. Only a document whose value is exactly the limit is over-matched, so it is a rare, low-priority bug. Making the range empty when an exclusive bound sits at the limit fixes it; that fix passes the repro, the module's tests and check.
The bug, step by step (main 645d4fe14a20, under lucene/queries/src/java/org/apache/lucene/queries/function/)
- Entry.
FunctionRangeQuerykeeps the bounds as strings plus the two flags (FunctionRangeQuery.java:55-80; theNumberconstructor acceptsInteger.MAX_VALUEdirectly), printsfalseas{/}(:102-112) and, per segment, callsfunctionValues.getRangeScorer(context, lowerVal, upperVal, includeLower, includeUpper)(:172).IntFieldSourcereturns anIntDocValues,LongFieldSourceaLongDocValues. - The endpoint adjustment.
IntDocValues.getRangeScorer(docvalues/IntDocValues.java:81-117) says "instead of using separate comparison functions, adjust the endpoints" (:90), then runsif (!includeLower && lower < Integer.MAX_VALUE) lower++;(:96) andif (!includeUpper && upper > Integer.MIN_VALUE) upper--;(:103), and matches withval >= ll && val <= uu(:114). - At the limit. The condition
lower < Integer.MAX_VALUEstopslower++from wrapping toInteger.MIN_VALUE(which would match everything). But whenlowerisMAX_VALUEnothing happens at all: the bound stays 2147483647, andval >= 2147483647admits theMAX_VALUEdocument.n > 2147483647becomes the same test asn >= 2147483647. The upper bound atMIN_VALUEis the mirror image. - Same lines elsewhere.
docvalues/LongDocValues.java:99(comment),:105,:112(guards withLong.MAX_VALUE/Long.MIN_VALUE),:123(inclusive test);valuesource/EnumFieldSource.java:143,:148,:154,:165(int guards on the enum's int value). By inheritance (read, not executed):MultiValuedIntFieldSourceandMultiValuedLongFieldSourceextendIntFieldSource/LongFieldSourceand read real numeric doc values, so they are affected the same way. The count sources (DocFreqValueSource,JoinDocFreqValueSource,MaxDocValueSource,NumDocsValueSource,TermFreqValueSource,TotalTermFreqValueSource,SumTotalTermFreqValueSource) also return anIntDocValuesorLongDocValues, but are not practical triggers: their values are never negative and cannot realistically reachMAX_VALUE(document counts are at mostIndexWriter.MAX_DOCS = Integer.MAX_VALUE - 128). - The contrast. The default
FunctionValues.getRangeScorer(FunctionValues.java:224-292) andDoubleDocValues(docvalues/DoubleDocValues.java:86-147) honour a strict bound with strict comparisons (docVal > l,docVal < u), so the float and double sources are right at the limit.
Executed on main (one document per value, assertions off; the same with assertions on and on 10.5.2)
| range | expected | actual |
|---|---|---|
int n > 2147483647 (MAX) | 0 | 1 |
int n < -2147483648 (MIN) | 0 | 1 |
int n > 2147483646, n < -2147483647 (controls) | 1 | 1 |
long m > 9223372036854775807 (MAX) | 0 | 1 |
long m < -9223372036854775808 (MIN) | 0 | 1 |
| long one step inside the limits (controls) | 1 | 1 |
enum e > top (int value MAX), e < bottom (int value MIN) | 0 | 1 |
float x > 3.4028235E38, double x > 1.7976931348623157E308 | 0 | 0 |
Exposure and likelihood180 words
- Who is hit. Users of
FunctionRangeQuery(lucene/queries,@lucene.experimental) or ofFunctionValues.getRangeScorerdirectly, over anIntFieldSource,LongFieldSource(or their multi-valued subclasses) orEnumFieldSource. Not default and not inlucene/core. - What it takes. Two things at once: an exclusive bound exactly at
Integer/LongMIN_VALUEorMAX_VALUE, and a document whose value is exactly that limit. Nobody typesn > 2147483647by hand; such bounds come from generated queries, for example "greater than the last value seen" over a field that usesMAX_VALUEas a sentinel (an example route, not executed). One document is enough; no scale is needed. - What goes wrong. One extra value matches, silently: no exception and the same result with assertions on and off. Where the range is used as a filter, the limit-valued documents slip through it.
- Enum. The
EnumFieldSourcecase needs an enum whose int value is exactlyInteger.MIN_VALUEorInteger.MAX_VALUE, which is contrived; the int and long cases are the ones that matter. - Likelihood: rare. No public report found since the guard entered Lucene in 2011.
One JUnit test in lucene/queries, public API only: one document with n = Integer.MAX_VALUE in a ByteBuffersDirectory, one FunctionRangeQuery for n > Integer.MAX_VALUE, and a count. Fixed inputs, single thread; fails under the recorded seed with assertions on and off, and under a random seed.
package org.apache.lucene.queries.function;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.NumericDocValuesField;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.queries.function.valuesource.IntFieldSource;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;
public class TestFunctionRangeQueryExclusiveLimit extends LuceneTestCase {
public void testExclusiveLowerBoundAtIntMax() throws Exception {
try (Directory dir = new ByteBuffersDirectory()) {
try (IndexWriter w = new IndexWriter(dir, new IndexWriterConfig())) {
Document doc = new Document();
doc.add(new NumericDocValuesField("n", Integer.MAX_VALUE));
w.addDocument(doc);
}
try (DirectoryReader reader = DirectoryReader.open(dir)) {
Query q =
new FunctionRangeQuery(
new IntFieldSource("n"), String.valueOf(Integer.MAX_VALUE), null, false, true);
// n > Integer.MAX_VALUE matches nothing: expected 0 hits, actual 1
assertEquals(q.toString(), 0, new IndexSearcher(reader).count(q));
}
}
}
}
cd $LUCENE # an apache/lucene checkout at 645d4fe14a20 (or 10.6.0, branch_10_6, upstream main)
cp $REPRO/TestFunctionRangeQueryExclusiveLimit.java lucene/queries/src/test/org/apache/lucene/queries/function/
./gradlew :lucene:queries:test --tests org.apache.lucene.queries.function.TestFunctionRangeQueryExclusiveLimit -Ptests.seed=DEADBEEF
# assertions off fails the same way: add -Ptests.asserts=false
- Expected
- 0 hits: no int is greater than
Integer.MAX_VALUE. - Observed
- 1 hit, the document holding 2147483647:
frange(int(n)):{2147483647 TO *] expected:<0> but was:<1>, with assertions on and off.
More files, environment, full output149 words
Plain IndexWriterConfig and IndexSearcher. The test's own inputs are fixed (it calls no random()); the test framework still picks a seeded random codec, similarity, locale and time zone, and the result is the same under any seed tried. The package is org.apache.lucene.queries.function only for locality.
As the regression test it needs only the ASF license header; with it, :lucene:queries:check -x test passes, and it fails on stock and passes with the recommended fix.
| lucene | main 645d4fe14a20 (11.0.0-SNAPSHOT): affected; the same test also fails on the 10.6.0, 10.5.2, 10.0.0, 9.12.3 and 9.1.0 jars |
|---|---|
| jdk | OpenJDK 25.0.3 |
| os | Linux, aarch64 |
TestFunctionRangeQueryExclusiveLimit > testExclusiveLowerBoundAtIntMax FAILED
java.lang.AssertionError: frange(int(n)):{2147483647 TO *] expected:<0> but was:<1>
at __randomizedtesting.SeedInfo.seed([DEADBEEF:25D5DFBD05250EC1]:0)
at junit@4.13.2/org.junit.Assert.fail(Assert.java:89)
at junit@4.13.2/org.junit.Assert.failNotEquals(Assert.java:835)
at junit@4.13.2/org.junit.Assert.assertEquals(Assert.java:647)
at org.apache.lucene.queries.function.TestFunctionRangeQueryExclusiveLimit.testExclusiveLowerBoundAtIntMax(TestFunctionRangeQueryExclusiveLimit.java:29)
:lucene:queries:test (FAILURE): 1 test, 1 failure
# the same failure with -Ptests.asserts=false, and with the standalone runner on main and the 10.5.2 and 9.12.3 jars- The code comment says the step replaces strict comparisons, and the float, double and default implementations compare strictly: 0 hits.
- The test that came with the guard in 2009 asserts that exclusive
frangematches the same documents as a standard exclusive range query. - Lucene made the same corner case an empty result in
NumericRangeQuery(2009) andDocValuesRangeQuery(2016).
The full argument430 words
- The comment. "instead of using separate comparison functions, adjust the endpoints" (
IntDocValues.java:90,LongDocValues.java:99,EnumFieldSource.java:143): the adjustment must give the same answer asdocVal > lower, which is no match forlower = MAX_VALUE. The defaultFunctionValues.getRangeScorerandDoubleDocValuesdo exactly that. - The author's own test. The guard was written in SOLR-939 (
6bd49091023, 2009-03-23, Solr'sfrange). The same commit'sTestRangeQuery.testRandomRangeQueriesbuilds, for random bounds withincl=false incu=false, bothfield:{lower TO upper}and thefrangequery and asserts they match the same documents (sameDocs). It indexes values 5 to 24 and draws bounds from 0 to 29, so it never reaches the type limit. The same commit'stestRangeQueriesalso asserts that an exclusivefrangelower bound at the largest indexed value matches 0 documents. - The same corner case, fixed as a bug. Commit
d820e34a260(2009-06-26,NumericRangeQuery): "Fix the special corner case, that an exclusive range starting/ending with MAX_VALUE/MIN_VALUE overflows the bounds. Return a empty TermEnum in this case", with tests assertingEMPTY_DOCIDSETfor an exclusive range starting atInteger.MAX_VALUEor ending atInteger.MIN_VALUE. That class was then one week old and unreleased, and thereminBound++wrapped around rather than saturating; the fix made the range empty. LUCENE-7111 (#8166, 2016) fixed the same corner case in the sandboxDocValuesRangeQuery(there the+1overflowed and the query matched every document) with a patch that returns no documents, which Yonik Seeley, the author of this guard, +1'd.FunctionRangeQuery.java:38still namesDocValuesRangeQueryas similar; that javadoc reference is stale, as the class no longer exists on main. - The other range code we checked on main. Facet
LongRangerejects such a range as matching nothing (IllegalArgumentException,LongRange.java:58-76,Range.java:37-40); the float and double function sources return 0 hits (executed). - Not documented or tested where we looked. No javadoc,
CHANGES.txtorMIGRATE.mdentry says an exclusive bound at the limit is inclusive;TestFunctionRangeQueryuses bounds between 2 and 10 andTestValueSourcesusesMIN..MAXinclusive. No javadoc sentence defines the limit case the other way either; the case for "empty" rests on the comment, the default implementation and the tests above. - Legal input. Public constructors, a valid int or long value, no documented precondition. Out-of-range literals are rejected (
Integer.parseInt/Long.parseLongthrowNumberFormatException), so the exact limit values are the valid boundary inputs.EnumFieldSourceresolves a bound through its label map (unknown labels fall back to its default value), so the enum case needs an enum that maps a label toInteger.MIN_VALUEorInteger.MAX_VALUE, as the probe's enum does.
The devil's-advocate check152 words
The strongest case: the guard is deliberate overflow protection and saturating at the limit is a conscious trade-off; n > Integer.MAX_VALUE is a meaningless query, so garbage in, garbage out; the class is @lucene.experimental, and open issue #16820 lists ValueSource among deprecation candidates for Lucene 11. The cross-model review rated this argument weak.
Why it fails: preventing the overflow is right, but keeping the bound inclusive contradicts the comment above it, the author's tests, the 2009 NumericRangeQuery and 2016 LUCENE-7111 fixes, and the comparators we checked on main (the default FunctionValues scorer and DoubleDocValues match nothing; facet LongRange rejects the range). No javadoc sentence defines the limit case, so this rests on that code and test evidence. A generated "after the last value" bound is a valid question whose answer is empty. @lucene.experimental allows API changes, not wrong results. It is a reason for low priority, not for keeping the behaviour.
Recommended An exclusive bound at the limit makes the range empty (+63/-6 in three files)
When an exclusive bound sits exactly at the limit, use an empty range instead of keeping the bound, in IntDocValues, LongDocValues and EnumFieldSource; the overflow protection stays. Validated on main: the repro passes with assertions on and off, the closest suites (62 tests) and the whole lucene/queries test task (512 tests) pass, and check passes.
if (!includeLower) {
if (lower == Integer.MAX_VALUE) {
matchesNothing = true; // nothing is greater than Integer.MAX_VALUE
} else {
lower++;
}
}
// ... the same for the upper bound at Integer.MIN_VALUE, then:
if (matchesNothing) {
lower = 1; // an empty range (lower > upper)
upper = 0;
}All options and trade-offs5 options · 644 words
The recommended fix was run in a scratch worktree created at 645d4fe14a20 (OpenJDK 25.0.3, seed DEADBEEF) and removed afterwards; the script does not record the worktree's revision, which is corroborated by the diff applying cleanly to the 645d4fe14a20 files. An independent fact check re-ran the key steps in its own worktree with the revision recorded (645d4fe14a20): the repro fails on stock (assertions on and off, and a random seed) and passes with the fix (on and off); TestFunctionRangeQuery and TestValueSources (50 tests) and checkGoogleJavaFormat pass with the fix. "Closest suites" means TestFunctionRangeQuery, TestValueSources, TestDocValuesFieldSources, TestSortedSetFieldSource, TestFunctionQuerySort and TestFieldScoreQuery in lucene/queries.
An exclusive bound at the limit makes the range empty (+63/-6 in three files) (validated by the checks) (recommended)
In each of the three getRangeScorer overrides, replace the saturating guard with an explicit check; when an exclusive bound is at the limit, set an empty range (lower > upper), for which the existing inclusive test matches nothing. The upper bound at MIN_VALUE, LongDocValues and EnumFieldSource get the same change. It is the outcome Lucene already chose for the same corner case in NumericRangeQuery (d820e34a260, 2009) and in Ishan Chattopadhyaya's LUCENE-7111 patch (#8166, 2016).
if (!includeLower) {
if (lower == Integer.MAX_VALUE) {
matchesNothing = true; // nothing is greater than Integer.MAX_VALUE
} else {
lower++;
}
}
// ... the same for the upper bound at Integer.MIN_VALUE, then:
if (matchesNothing) {
lower = 1; // an empty range (lower > upper)
upper = 0;
}Trade-offs
- Setup-time only, no per-document cost; inclusive bounds and bounds inside the range behave as before.
- The sentinel range
1..0needs its comment to read clearly; returning a scorer that matches nothing would state the intent more directly but repeats code in three places (not run). - Open ends are deliberately left alone: a
nullbound still becomesMIN_VALUE/MAX_VALUE, inclusive, whatever its include flag (an open end has no value to exclude); only explicit bounds change.
What the checks validated
- Stock
645d4fe14a20: the repro fails (1 of 1) with assertions on and off; the closest suites pass (62 tests). - With the fix: the repro passes with assertions on and off; the closest suites pass (62 tests); the whole
:lucene:queries:testtask passes (512 tests, 5 skipped, 0 failures);:lucene:queries:check -x test(google-java-format, ecj lint, forbidden APIs, license checks) passes with the license-headed regression test in place, which passes with the fix and fails on stock. Targeted runs, not the full Lucene test suite. - The all-sites probe with the three fixed files shadowed gives all 13 rows correct (int, long, enum at MIN and MAX: 0 hits; controls 1; float and double 0).
The tie-breaker's verification sketch for IntDocValues and LongDocValues: the same check, then if (matchNone) return false; before the value test. With it shadowed, the tie-breaker's own int/long/double probe passes all its 13 rows, the minimal test passes, and 54 upstream tests pass.
Trade-offs
Same result, one extra branch per document; EnumFieldSource needs the same change.
On LUCENE-7111 (#8166), Robert Muir suggested Math.addExact / Math.subtractExact for the old DocValuesRangeQuery, so that an overflow throws instead of passing silently. Catching the exception to return an empty range is our own variation; the patch that landed there checked the limit explicitly and returned no documents.
Trade-offs
Same semantics, but uses an exception for control flow; not run.
Four scorer variants per class.
Trade-offs
A larger change in three classes for the same result; not run.
"matches nothing".
Trade-offs
Not recommended: turns a valid empty question into an error, unlike the float and double sources, and breaks callers that generate bounds.
Where an application uses such a range as a restriction filter, documents holding exactly MIN_VALUE or MAX_VALUE slip through it; Solr's {!frange ... incl=false} reaches the same getRangeScorer code (SOLR-8867 fixed another over-match there; read, not tested).
Not a formal finding. While verifying a related bug in the flexible query parser (an exclusive point bound at the limit wraps around), an AI verifier read this endpoint adjustment, noticed the opposite failure and ran a small test. A follow-up AI tool confirmed it with its own test; later AI tools reproduced it on main and old releases.
The method in full130 words
Method: non-formal (code reading, then executed tests).
- During the 2026-10-22 study, an AI verifier checking the flexible
StandardQueryParserbug where an exclusive int/long point bound at the limit wraps around and matches everything (LFV-050) readIntDocValues.getRangeScorerand saw that its guard avoids the wrap but keeps the bound inclusive. Its sibling test gave 1 hit forn > MAX32,n < MIN32andm > MAX64. - A follow-up of the study executed it with its own test (
testFunctionRangeAtLimits, with the controln > MAX32 - 1) and rated it new and low, with a separate root cause. - The hunt then cut the repro to one test, re-ran it on each new main, and an independent tie-breaker wrote its own probe and checked the intent evidence.
No prior public report found: 105 tool queries (6 of them web searches run by hand) and 89 further manual ones over GitHub (incl. all 410 open PRs' files), the JIRA archive, the mailing lists, downstream projects and the web (2026-10-31). Not fixed upstream. The closest item, LUCENE-7111 (2016), fixed the same corner case in another, since-removed query.
Searches4 searches
Verdict new. LUCENE-7111 (#8166) is the same class of mistake in the sandbox DocValuesRangeQuery (there the +1 overflowed and the query matched everything), fixed there only. SOLR-8867 fixed a different over-match (missing values) in the same getRangeScorer methods. Context only: #7778 (LUCENE-6720, added FunctionRangeQuery), #5321 (LUCENE-4251), #7785 (LUCENE-6727, open), #13383 (facet LongRange excludes the limit correctly), #16820 (open, possible ValueSource deprecation). Gaps: GitHub search reads at most 50 results per query; 3 queries hit the rate limit and were re-run; private reports cannot be searched.
| Where | When | How | Found |
|---|---|---|---|
| a scripted prior-report search: GitHub (apache/lucene issues, PRs, commits), downstream repos, mailing lists, JIRA, web | 2026-10-31 | 105 queries (40 GitHub issue/PR, 6 commit, 5 downstream, 40 list on dev@, java-user@, issues@, commits@, 8 LUCENE JIRA, 6 web) | 669 distinct hits, 0 high; no report |
| GitHub (manual): apache/lucene, apache/solr, Elasticsearch, OpenSearch, all of GitHub; open PRs | 2026-10-31 | 30 searches (FunctionRangeQuery, getRangeScorer, frange, IntDocValues, LongDocValues, EnumFieldSource, MAX_VALUE exclusive, ...); the changed files of all 410 open apache/lucene PRs | no report; no open PR touches the guards |
| JIRA archive (SOLR, LUCENE) and mailing lists (manual) | 2026-10-31 | 22 JIRA queries with descriptions and comments read; 36 list searches on 6 Lucene and Solr lists | SOLR-8867 (different over-match), SOLR-5084 (no limit discussion); no report |
| Earlier searches | 2026-10-22, 2026-10-31 | the study follow-up's read-only GitHub searches; an earlier scripted prior-report search (62 hits, 0 high) | no report |
The guard was written for Solr in 2009 (SOLR-939) and entered Lucene's queries module in 2011 (LUCENE-3249, first in 4.0.0-ALPHA); FunctionRangeQuery arrived in 5.4.0 (LUCENE-6720). Executed on 19 releases from 4.0.0-ALPHA to 10.6.0 and on main; not fixed upstream.
Versions tested6 rows
Introduced into Lucene by a993ae90323 LUCENE-3249 (#4322, "Moved FunctionQuery impls to new Queries module", 2011-06-28), which moved Solr's IntFieldSource and LongFieldSource with the guard into the new queries module; first release 4.0.0-ALPHA. The guard itself was written for Solr's frange in 6bd49091023 (SOLR-939, 2009-03-23), not shipped by Lucene before 4.0. FunctionRangeQuery came with LUCENE-6720 (#7778, 13f8ab93a97, 2015-08-06), first released in 5.4.0, and did not touch the guard.
Later moves kept the text: LUCENE-5247 (#6311, 9db7442d648, 2013-10-01) moved the int and long getRangeScorer into IntDocValues / LongDocValues (where git blame stops), and SOLR-5084 copied the int guard into EnumFieldSource (2013-11-05: a9f8bf654c2 on trunk, first released in 6.0.0, and the backport 49ed368ae72, first released in 4.6.0). The int and long guards are in all 124 release tags of the history clone from 4.0.0-alpha to 10.6.0, and FunctionRangeQuery in all 92 from 5.4.0; 8.9.0 to 8.11.x are not tagged in the clone, and of those only 8.11.4 was checked (executed). No commit in the clone changed the guards' logic. Upstream main 2f424fd428da (2026-10-30) still has all six lines.
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20 | affected | the minimal test fails (gradle and the standalone runner, assertions on and off); all six sites wrong in the probe |
| 10.6.0, 10.5.2, 10.0.0, 9.12.3, 9.1.0 | affected | the minimal test, unchanged, fails (10.5.2 also with assertions on; all six sites wrong on 10.5.2) |
| 5.4.0, 5.5.5, 6.0.0, 6.6.6, 7.0.0, 7.7.3, 8.0.0, 8.11.4, 9.0.0 | 5.4.0: first with FunctionRangeQuery | FunctionRangeQuery probe: n > MAX and m > MAX give 1 hit, the control n > MAX-1 is correct |
| 4.0.0-ALPHA, 4.0.0, 4.10.4, 5.0.0, 5.3.2 | through getRangeScorer | no FunctionRangeQuery yet; through the public FunctionValues.getRangeScorer, n > MAX and m > MAX give 1 hit, the control is correct |
| 3.6.2 and earlier | not affected | no org.apache.lucene.queries.function package (the guard lived only in Solr) |
fix on 645d4fe14a20 | not affected | the repro, the closest suites, the lucene/queries tests and check pass; all 13 probe rows correct |
Five AI tools reproduced or confirmed it (the study verifier and follow-up, a candidate reproduction, the current-main reproducer and a tie-breaker with its own probe); a reading-only reviewer from another model family (OpenAI GPT-5.6) gave a procedural "not real" but agreed on the root cause. It still reproduces on main and 10.5.2.
Checks and verdicts222 words
There is no executing cross-model review: the review by the other model family could read the code only; a separate run re-ran the repro on 10.5.2 (fails).
| Check | Verdict | What it found |
|---|---|---|
| Study verifier, side observation | lead, executed | Sibling test: n > MAX32, n < MIN32, m > MAX64 give 1 hit each. |
| Study follow-up | new, low | Own test with a control one step inside the limit. |
| Candidate reproduction | reproduced | Minimal test on main, 10.5.2 and 9.12.3; all-sites probe; 8.11.4 and 5.4.0 probes; fix sketch. |
| Cross-model review, refute lens (OpenAI GPT-5.6, reading only) | not real (low), procedural | Could not run code; agreed on the root cause and that the inputs are in contract; rated the intended argument weak. |
| Current-main reproducer | real | Main 645d4fe14a20 with assertions on and off; 10.5.2. |
| Tie-breaker | real, high | Own probe on main, 10.5.2 and 9.12.3; pristine sources from git; found the SOLR-939 equivalence test and the 2009 NumericRangeQuery fix. |
| This doc | still reproduces; fix validated | Gradle on a scratch worktree of 645d4fe14a20. |
| Fact checks of this doc | real; wording fixed | An independent re-run (repro, all-sites probe and fix through gradle, revision recorded) and a reviewer from another model family (OpenAI GPT-5.6, reading only) checked every claim; their wording fixes are applied. |
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 371 KB
Finding record, Lucene and upstream sources, repro files and diagram notes864 words
- Registry title
- FunctionRangeQuery over Int/Long/EnumFieldSource turns an exclusive bound at the type limit into an inclusive one
- Area
- queries / function
- Priority
- Low, rank 960: FunctionRangeQuery over Int/Long/Enum sources turns an exclusive bound at the type limit into an inclusive one, letting through only a document whose value is exactly MIN or MAX, a module API and int-limit edge case.
- Impact class
- silent wrong results
- Impact note
- Users of lucene/queries FunctionRangeQuery over an IntFieldSource, LongFieldSource or EnumFieldSource with an exclusive bound at MIN or MAX; only documents whose value is exactly that limit are wrongly included; ordinary scale; every release from 5.4.0 to 10.6.0 and main (executed; the underlying public getRangeScorer since 4.0.0-ALPHA).
- Summary
- You run a FunctionRangeQuery over an IntFieldSource, LongFieldSource or EnumFieldSource with an exclusive bound exactly at the type limit, such as n > Integer.MAX_VALUE. You expect no document to match. Instead getRangeScorer adjusts an exclusive bound only when it is not already at the limit (if (!includeLower && lower < Integer.MAX_VALUE) lower++), so at the limit the bound stays inclusive and the document whose value is exactly MAX (or MIN) matches: 1 hit instead of 0.
- Introduced
- a993ae9032337ca8d5b2c7a4d58bf41a6f5053f5 (LUCENE-3249 'Moved FunctionQuery impls to new Queries module', #4322, svn trunk r1140906, 2011-06-28), which moved Solr's IntFieldSource and LongFieldSource with the saturating guard 'if (!includeLower && lower < Integer.MAX_VALUE) lower++' (and its upper-bound and Long twins) into Lucene's new queries module; the guard was written for Solr's frange by 6bd49091023 (SOLR-939, 2009-03-23), and FunctionRangeQuery came with LUCENE-6720 (#7778; 13f8ab93a97 on trunk, 88d137632d38 on branch_5x, 2015-08-06) without touching it; later moves kept the text: LUCENE-5247 (#6311, 9db7442d648, 2013-10-01, into IntDocValues/LongDocValues, where git blame stops) and SOLR-5084 (a9f8bf654c2 / 49ed368ae72, 2013-11-05, the int guard copied into EnumFieldSource) [read: git log -S, git blame -C -C -M and git show in the history clone; exec: 3.6.2 has no org.apache.lucene.queries.function package, 4.0.0-ALPHA fails] (2011-06-28 (a993ae90323, the guard enters Lucene's queries module; first released in 4.0.0-ALPHA); the guard itself dates from 2009-03-23 (SOLR-939, Solr only) and FunctionRangeQuery from 2015-08-06 (LUCENE-6720, first released in 5.4.0))
- First affected version
- 5.4.0 [exec, -ea and -da] for FunctionRangeQuery, the first release with the class (5.3.2 has no FunctionRangeQuery [exec]); the same wrong range scorer is reachable through the public FunctionValues.getRangeScorer of IntFieldSource and LongFieldSource since 4.0.0-ALPHA [exec, -ea and -da]; 3.6.2 has no org.apache.lucene.queries.function package [exec: compile error]; the EnumFieldSource site from 4.6.0 [read]
- Affected versions
- FunctionRangeQuery: every release from 5.4.0 to 10.6.0 and main 645d4fe14a20 [exec: the FunctionRangeQuery probe fails on 5.4.0 (also -ea), 5.5.5, 6.0.0, 6.6.6, 7.0.0, 7.7.3, 8.0.0, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2, 10.6.0 and main (also -ea), with -da; the minimal test, unchanged, fails on 9.1.0, 9.12.3, 10.0.0, 10.5.2, 10.6.0 and main (10.5.2, 10.6.0 and main also with -ea)]; getRangeScorer level, before FunctionRangeQuery existed: 4.0.0-ALPHA, 4.0.0, 4.10.4, 5.0.0 and 5.3.2 [exec]; [read] the int and long guards are in all 124 release tags of the history clone from 4.0.0-alpha to 10.6.0 (8.9.0-8.11.x are not tagged there; 8.11.4 was executed) and FunctionRangeQuery.java in all 92 tags from 5.4.0; not fixed: upstream main 2f424fd428da (2026-10-30) still has all six guard lines [read]
- Status
- documented (brief, checks and a per-finding doc exist)
- Verification
- Confirmed by AI-run checks: a cross-model review that could only read the code (its low-confidence “not real” is procedural), an adjudication that ruled it real and a prior-report search; not yet triaged by a person.
- Found by
- non-formal: code reading
- Upstream and prior reports
- none found (the study follow-up's read-only GitHub searches, 2026-10-22; a prior-report search on 2026-10-31 found none either)
- Lucene commit tested
- main
645d4fe14a20(gradle in a scratch worktree and the standalone runner, assertions on and off); the 10.5.2 and 9.12.3 release jars with the minimal test (9.1.0, 10.0.0 and 10.6.0 in the history sweep);FunctionRangeQueryprobes on 5.4.0 to 10.6.0 andgetRangeScorerprobes on 4.0.0-ALPHA to 5.3.2; the recommended fix in a scratch worktree of645d4fe14a20. Upstream main2f424fd428da(2026-10-30) has the same six guard lines.
- Finding record
a Lucene clone tag releases/lucene/10.5.2: lucene/queries/src/java/org/apache/lucene/queries/function/docvalues/IntDocValues.java:95-104SECURITY.md:38, 50 (threat model)
- The finding in 30 seconds
lucene/queries/src/java/org/apache/lucene/queries/function/FunctionRangeQuery.java:31-43, (at 645d4fe14a20) 55-80, 102-112, 172lucene/queries/src/java/org/apache/lucene/queries/function/docvalues/IntDocValues.java:81-117 (at 645d4fe14a20) (90, 96, 103, 114)lucene/queries/src/java/org/apache/lucene/queries/function/docvalues/LongDocValues.java:99, (at 645d4fe14a20) 105, 112, 123lucene/queries/src/java/org/apache/lucene/queries/function/valuesource/EnumFieldSource.java:143, (at 645d4fe14a20) 148, 154, 165lucene/queries/src/java/org/apache/lucene/queries/function/FunctionValues.java:224-292; (at 645d4fe14a20) docvalues/DoubleDocValues.java:86-147lucene/queries/src/java/org/apache/lucene/queries/function/valuesource/ (at 645d4fe14a20) (MultiValuedIntFieldSource.java:37 and MultiValuedLongFieldSource.java:37 extend IntFieldSource/LongFieldSource; DocFreqValueSource, JoinDocFreqValueSource, MaxDocValueSource, NumDocsValueSource, TermFreqValueSource, TotalTermFreqValueSource, SumTotalTermFreqValueSource return IntDocValues/LongDocValues; grep, read); lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:208 (MAX_DOCS)
- Who is affected
lucene/queries/src/java/org/apache/lucene/queries/function/FunctionRangeQuery.java:42 (at 645d4fe14a20) (@lucene.experimental)
- Why it looks unintended
lucene/queries/src/java/org/apache/lucene/queries/function/docvalues/IntDocValues.java:90; (at 645d4fe14a20) docvalues/LongDocValues.java:99; valuesource/EnumFieldSource.java:143; FunctionValues.java:224-292; docvalues/DoubleDocValues.java:86-147; FunctionRangeQuery.java:38lucene/facet/src/java/org/apache/lucene/facet/range/LongRange.java:58-76; (at 645d4fe14a20) Range.java:37-40lucene/queries/src/test/org/apache/lucene/queries/function/TestFunctionRangeQuery.java:75-140; (at 645d4fe14a20) TestValueSources.java:497-500lucene/queries/src/java/org/apache/lucene/queries/function/valuesource/EnumFieldSource.java:70-88 (at 645d4fe14a20) (label resolution, default value); docvalues/IntDocValues.java:95, 102 (Integer.parseInt); docvalues/LongDocValues.java:87 (Long.parseLong)
- Suggested fix
lucene/facet/src/java/org/apache/lucene/facet/range/LongRange.java:58-76 (at 645d4fe14a20)
- TestFunctionRangeQueryExclusiveLimit.java (one test):
repro/LFV-083/src/org/apache/lucene/queries/function/TestFunctionRangeQueryExclusiveLimit.java(33 lines)
Made separately from this text, from the code at 645d4fe14a20 and runs executed for the diagrams; the PNG is the summary still.
assets/LFV-083/bug.png(371 KB)
![Diagram of the bug, from the code at 645d4fe14a20 and runs on main. One document with n = 2147483647 (Integer.MAX_VALUE). The query FunctionRangeQuery(IntFieldSource n, lower 2147483647, exclusive), printed frange(int(n)):{2147483647 TO *], means n > 2147483647: expected 0 hits. IntDocValues.getRangeScorer turns an exclusive bound into an inclusive one with lower++ only when lower < Integer.MAX_VALUE; at 2147483647 the bound is left as is, the inclusive test val >= 2147483647 is true, and the query returns 1 hit. The same at Integer.MIN_VALUE, for Long and for EnumFieldSource; one step inside the limit is correct. Fix: an exclusive bound at the limit makes the range empty.](../assets/LFV-083/bug.png)