Lucene's classic and flexible query parsers separate clauses at a tab, a line break or a full-width space (U+3000) just as at a space, but not next to a range bound. The token for a bound stops only at a plain space, ] or }, so such a character becomes part of the bound: the parse fails, or the bounds change silently.
- The query parser's lexer cuts the text into tokens. After
[or{it switches to a Range state with its own token rules, until]or}. - In the Range state a bound is one
RANGE_GOOPtoken: a run of any characters except a plain space,]and}.TOand quoted bounds are their own tokens. - Whitespace is a SKIP rule: matched and thrown away. When two rules match, the lexer keeps the longest match; on a tie, the rule declared first.
- Index five dates as
StringFields: 20191231, 20200101, 20200615, 20201231, 20210101. Type a range over three lines,date:[/20200101 TO 20201231/], and parse it with the classicQueryParserorStandardQueryParserandStandardAnalyzer. - You expect the 3 hits of
date:[20200101 TO 20201231]: both grammars declare tab, LF, CR and U+3000 as whitespace, and skip whitespace inside ranges too. - You get 4 hits, 20191231 included, and no error. The bound token stops only at a plain space,
]or}, so it swallows each line break: the bounds are<LF>20200101and20201231<LF>, and LF sorts before2. - The same rule breaks other shapes:
date:[20200101<TAB>TO<TAB>20201231]throws a ParseException (one bound, noTO);date:{20200101 TO 20201231<TAB>}gives 2 hits instead of 1 (its own excluded endpoint); a leading U+3000, as CJK input methods type it, gives 0 instead of 3.
Anyone who parses user-typed query text with the classic QueryParser (or a subclass), StandardQueryParser or PrecedenceQueryParser with default parser settings, on every release since 1.3-rc1. Multi-line or tab-separated ranges and full-width spaces from CJK input methods reach it. A character that touches TO gives a ParseException; one glued to a bound but not to TO silently changes that bound. Rare.
Mechanism, terms and numbers752 words
Both query grammars, the classic QueryParser.jj and the flexible StandardSyntaxParser.jj, declare U+0020, TAB, LF, CR and U+3000 as whitespace and skip it in the Range lexical state as well. But the token for a range bound, RANGE_GOOP, is any run of characters other than U+0020, ] and }. The lexer keeps the longest match, so a TAB, LF, CR or U+3000 that touches a bound is swallowed into it.
Two outcomes follow. When the character separates a bound from TO, the bound and TO become one token and the parse fails: date:[20200101<TAB>TO<TAB>20201231] throws in both parsers. When it is glued to a bound but not to TO (right after [/{, right before ]/}, or between a bound and the space next to TO), the query parses without error and the bound carries the character: over five dates date:[<LF>20200101 TO 20201231<LF>] matches 4 instead of 3, date:{20200101 TO 20201231<TAB>} admits its own exclusive endpoint, date:[<U+3000>20200101 TO 20201231] matches nothing, and date:[20200101<LF> TO 20201231], a value pasted with a trailing newline, matches 2 instead of 3. It affects the classic parser and its subclasses, StandardQueryParser and PrecedenceQueryParser with default parser settings, and every release since the TO syntax arrived in 1.3-rc1. The fix adds the four characters to the exclusion list in both grammars and regenerates the token managers.
- Grammar whitespace:
<#_WHITESPACE: ( " " | "\t" | "\n" | "\r" | "\u3000") >(QueryParser.jj:187; the same atStandardSyntaxParser.jj:135). Outside ranges these five characters are also excluded from terms (_TERM_START_CHAR,QueryParser.jj:183-185), so there they always separate clauses: upstreamtestTabNewlineCarriageReturnandtestCJKassert it. - Range state: after
[/{the lexer uses the rulesRANGE_TO(TO),RANGEIN_END(]),RANGEEX_END(}),RANGE_QUOTEDandRANGE_GOOP(QueryParser.jj:221-227;StandardSyntaxParser.jj:195-201), and the whitespace SKIP rule<DEFAULT, Range> SKIP : { < <_WHITESPACE>> }(QueryParser.jj:191-193;<DEFAULT, Range, Function>atStandardSyntaxParser.jj:139-141). - Range rule: bound,
TO, bound, closing bracket (QueryParser.jj:343-347;StandardSyntaxParser.jj:855-859).
RANGE_GOOPis(~[ " ", "]", "}" ])+(QueryParser.jj:226,StandardSyntaxParser.jj:200): it excludes only U+0020 of the five whitespace characters.- At a TAB that starts a token and is followed by a letter or digit, SKIP matches 1 character and
RANGE_GOOPmatches more, so the TAB joins the bound. SKIP wins only a tie, when the character starts a token and is followed by U+0020,],}or the end of the input. Inside a bound there is no token start:RANGE_GOOPruns on through the character, so one glued to the end of a bound is swallowed whatever follows it (20200101<LF> TOgives the bound20200101<LF>). - Loud: in
[20200101<TAB>TO<TAB>20201231]the whole inside is oneRANGE_GOOP, the parser meets]where it needsTO, and throws (Was expecting: "TO"in the classic parser,INVALID_SYNTAX_CANNOT_PARSEin the flexible one). - Silent: a character glued to a bound but not to
TO(right after[/{, right before]/}, or between a bound and the space next toTO) stays in the bound text. The classic parser passes it tonewRangeQuery, whereAnalyzer.normalize(QueryParserBase.java:550,:556) only lowercases forStandardAnalyzer; the flexible parser builds its bound nodes from the token text the same way (StandardSyntaxParser.jj:870-873). The result is aTermRangeQuerywhose bound contains the character. The open-bound check"*".equals(goop1.image)(QueryParser.jj:354) also fails, so[<LF>* TO b]searches from the literal term<LF>*.
Five StringField dates 20191231, 20200101, 20200615, 20201231, 20210101; both parsers give the same result on every line (the fact check ran four more parsers built on the two grammars, with the same result).
| query | bounds | hits | with spaces |
|---|---|---|---|
date:[20200101<TAB>TO<TAB>20201231] | ParseException | - | 3 |
date:[<LF>20200101 TO 20201231<LF>] | <LF>20200101 to 20201231<LF> | 4 (20191231 too) | 3 |
date:{20200101 TO 20201231<TAB>} | upper 20201231<TAB> | 2 (20201231 too) | 1 |
date:[<U+3000>20200101 TO 20201231] | lower <U+3000>20200101 | 0 | 3 |
date:[20200101<LF> TO 20201231] | lower 20200101<LF> | 2 (20200101 itself excluded) | 3 |
date:[20200101 TO <TAB>20201231] | upper <TAB>20201231 (TAB sorts before 2) | 0 | 3 |
date:[<TAB> 20200101 TO 20201231 <TAB>] | right (each TAB starts a token and a space or ] follows it) | 3 | 3 |
date:[<CR><LF> 20200101 TO 20201231 <CR><LF>] | ParseException (<CR><LF> is a 2-character bound) | - | 3 |
date:["20200101"<TAB>TO<TAB>"20201231"] | ParseException (quoting does not help) | - | 3 |
date:[<LF>* TO 20201231] | lower is the literal <LF>*, not open | - | - |
Exposure and likelihood462 words
- Who is exposed. Applications that parse query text with the classic
QueryParseror its subclassesMultiFieldQueryParser,ComplexPhraseQueryParserandExtendableQueryParser, withStandardQueryParser, or withPrecedenceQueryParser(which extends it), with default parser settings (the caller supplies the analyzer; the repro passesStandardAnalyzer, and the defect is in the lexer, before any analysis). Executed forQueryParser,MultiFieldQueryParser,StandardQueryParserandPrecedenceQueryParser; the other two inherit the classic grammar. Only these two grammars have a range syntax: the surround parser (its grammar has no Range state) andSimpleQueryParser(no range operator) are not affected. - Inputs that reach it. A range typed over several lines or with tabs in a query text area; a value with a trailing newline or tab pasted between the brackets; Windows line endings (
<CR><LF>breaks even indented input); the full-width space U+3000 that CJK input methods type, the case LUCENE-1528 added U+3000 for; generated query strings built from templates. - Loud vs silent. A character that touches
TOmakesTOpart of a bound token and the parse fails: a ParseException for a range whose parts are separated by one of these characters, and quoting the bounds does not help. A character glued to a bound but not toTOsilently changes that bound (extra hits, an exclusive endpoint admitted, or no hits at all): right after[/{, right before]/}, a value pasted with a trailing newline (date:[20200101<LF> TO 20201231]: 2 hits instead of 3) or a tab typed afterTO(date:[20200101 TO <TAB>20201231]: 0 instead of 3). A character with a space or bracket on both sides is skipped. - Scale. One query string reproduces it; no setting, index size or concurrency is involved, and assertions on or off behave the same.
- Setup after lexing. The hit counts are for
StringFieldterms andStandardAnalyzer, whosenormalizeonly lowercases; an analyzer whose normalization strips or maps these characters would hide part of the silent outcome (not run). Both parsers first try to read each bound as a date in their locale, but with no date resolution configured (the default, as in the repro)DateTools.dateToStringthrows on thenullresolution and both keep the bound text, so the random test locale cannot change the results; with a date resolution set, a bound the locale's date format reads as a date is rewritten first (not run). - How likely. Rare: it needs one of these characters inside the brackets of a range, the parse fails whenever one touches
TO, and the searches (see Prior reports) found no public report in over 20 years of releases. - Downstream. Solr's fork of the classic grammar has the same
RANGE_GOOPline and Range-state SKIP (read on apache/solr main by the finder).
The test shows that the classic QueryParser keeps a line break that touches a range bracket as part of the bound, although its grammar skips LF as whitespace inside ranges (RANGE_GOOP excludes only U+0020).
/*
* 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.queryparser.classic;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.tests.util.LuceneTestCase;
public class TestRangeBoundWhitespace extends LuceneTestCase {
public void testLineBreakInsideRangeBrackets() throws Exception {
QueryParser qp = new QueryParser("body", new StandardAnalyzer());
// expected the LFs skipped like spaces; actual bounds "\n20200101" and "20201231\n"
assertEquals(
qp.parse("date:[20200101 TO 20201231]"), qp.parse("date:[\n20200101 TO 20201231\n]"));
}
}
# in a copy of this finding's repro folder (repro/LFV-845/, which holds src/)
# Lucene checkout at the tested commit, built with: ./gradlew --no-daemon assemble (JDK 25)
LUCENE=/path/to/lucene; V=12.0.0-SNAPSHOT
CP=$LUCENE/lucene/core/build/libs/lucene-core-$V.jar
CP=$CP:$LUCENE/lucene/codecs/build/libs/lucene-codecs-$V.jar
CP=$CP:$LUCENE/lucene/test-framework/build/libs/lucene-test-framework-$V.jar
CP=$CP:$LUCENE/lucene/facet/build/libs/lucene-facet-$V.jar
CP=$CP:$LUCENE/lucene/queries/build/libs/lucene-queries-$V.jar
CP=$CP:$LUCENE/lucene/queryparser/build/libs/lucene-queryparser-$V.jar
CP=$CP:$LUCENE/lucene/sandbox/build/libs/lucene-sandbox-$V.jar
CP=$CP:randomizedtesting-runner-2.9.2.jar:junit-4.13.2.jar:hamcrest-3.0.jar # test-framework dependencies (Maven Central)
CP=$CP:junit-platform-console-standalone-6.1.3.jar # JUnit Platform console launcher (Maven Central)
javac -d classes -cp "$CP" $(find src -name '*.java')
# run 1: assertions on, seed DEADBEEF
java -ea -esa --add-modules jdk.incubator.vector --enable-native-access=ALL-UNNAMED -Dtests.seed=DEADBEEF \
-cp "classes:$CP" org.junit.platform.console.ConsoleLauncher \
execute --select-class org.apache.lucene.queryparser.classic.TestRangeBoundWhitespace
# the module(s) facet also need their third-party runtime jars
- Expected vs actual
- Expected
date:[<LF>20200101 TO 20201231<LF>]to parse likedate:[20200101 TO 20201231]; actual: the bounds are<LF>20200101and20201231<LF>(expected:<date:[20200101 TO 20201231]> but was:<date:[<LF>20200101 TO 20201231<LF>]>), with-eaand-da.
More files, environment, full output40 words
| lucene | Lucene main e1faeba08bde (2026-11-02) |
|---|---|
| jdk | 25.0.3+9-LTS |
# Lucene main 645d4fe14a20, assertions on (-ea), seed DEADBEEF: 1 test(s) run, 1 failed
=> java.lang.AssertionError: expected:<date:[20200101 TO 20201231]> but was:<date:[
20200101 TO 20201231
]>
org.junit.Assert.fail(Assert.java:89)
org.junit.Assert.failNotEquals(Assert.java:835)
org.junit.Assert.assertEquals(Assert.java:120)
org.junit.Assert.assertEquals(Assert.java:146)
org.apache.lucene.queryparser.classic.TestRangeBoundWhitespace.testLineBreakInsideRangeBrackets(TestRangeBoundWhitespace.java:27)- The grammar declares tab, LF, CR and U+3000 as whitespace and skips it inside ranges (
QueryParser.jj:187-193); the bound token lets that rule fire only at a token start before a space or bracket. - Upstream tests treat all five characters as one class (
testTabNewlineCarriageReturn,testCJK); no test pins the range behaviour. - The 2002 author described ranges as "using whitespace as delimiters".
The full argument313 words
- The grammar's own rules.
_WHITESPACElists U+0020, TAB, LF, CR and U+3000 and is skipped in the Range state (QueryParser.jj:187,:191-193;StandardSyntaxParser.jj:135,:139-141). BecauseRANGE_GOOPswallows the other four, that SKIP fires for them only in a tie, so the same character is skipped or kept depending on its neighbours:date:[<TAB> 20200101 ...gives the lower bound20200101,date:[<TAB>20200101 ...gives<TAB>20200101. No design note, test or discussion found explains this distinction. - Tests.
testTabNewlineCarriageReturn(QueryParserTestBase.java:896,TestQPHelper.java:937) asserts that"+weltbank\n+worlbank","+weltbank\r\n+worlbank"and similar equal"+weltbank +worlbank";testCJK(QueryParserTestBase.java:250-255,TestQPHelper.java:324-329) that"term\u3000term\u3000term"equals the space form. No test has a range with a raw TAB, LF, CR or U+3000; with the fix all 30 upstream queryparser suites pass. - History. The 2002 commit that added the
TOsyntax also extended the whitespace SKIP, then" "and TAB, to the new range states, and defined a bound token that excludes only" ". Later the same day (about 17 hours after the commit), its author wrote on dev@: "right now, we're using whitespace as delimiters in the range -- which means that dates like 2002-01-01 1:35 PM are no good unless enclosed in quotes". The 2004 change (LUCENE-93) "treats \n and \r characters in query strings the same way that \t and " " are treated"; LUCENE-1528 (2009) added U+3000 for CJK text. Neither touched the bound token. The LUCENE-1528 review asked how to avoid "defining the whitespace chars in two places" and missed this third copy. - Committer remark. In the merged test PR #13323, dweiss wrote that range terms "are parsed differently compared to normal tokens. This is strange but I bet there was a reason for it - now long forgotten. I think, logically, it should accept the same term notation that is used elsewhere" (said about escapes).
The devil's-advocate check239 words
The strongest case for "intended" (built by the devil's-advocate verifier; the cross-model reviews rated it weak, the cross-model fact check of this doc moderate): the delimiter has always been written " TO " with ASCII spaces (the 2002 commit message, the LUCENE-7821 CHANGES entry, the test comment // " TO " is required between range endpoints at QueryParserTestBase.java:637); the user docs show only space-separated ranges; the package docs call the parser "designed for human-entered text, not for program-generated text" and say date ranges "are better added directly through the query API"; the behaviour is over 20 years old with no report found; and today a raw TAB inside an unquoted bound (k:[a<TAB>b TO c]) parses, which the fix would break.
Why it fails: " TO " is about the keyword being required and delimited (LUCENE-7821 was about ranges without TO and never discusses the kind of whitespace). The docs' silence is filled by the grammar's own declarations and the tests. "Human-entered text" argues for the fix: people press Enter in a text area, and CJK input methods type U+3000. Age shows the case was never exercised, not that it was chosen. The raw TAB inside an unquoted bound is the real compatibility question: no doc, test or design note mentions it, the 2002 author named quoting for a date containing spaces, and quoting keeps working with the fix; maintainers should weigh it against consistent Range-state whitespace.
Recommended Add TAB, LF, CR and U+3000 to the RANGE_GOOP exclusions and regenerate
Exclude the grammar's other whitespace from the bound token in both grammars, (~[ " ", "\t", "\n", "\r", "\u3000", "]", "}" ])+, and regenerate the token managers. The repro and all 539 queryparser tests pass. An unquoted bound with a raw tab inside (k:[a<TAB>b TO c]) then needs quotes, so target main with a CHANGES entry.
-| <RANGE_GOOP: (~[ " ", "]", "}" ])+ >
+| <RANGE_GOOP: (~[ " ", "\t", "\n", "\r", "\u3000", "]", "}" ])+ >All options and trade-offs4 options · 710 words
Option A is the fix the finder proposed and this doc validated with gradle. B merges it with the escape fix of #13234; C and D are listed because a reviewer may ask about them. Add the new test (or its cases in QueryParserTestBase) with whichever is chosen.
Add TAB, LF, CR and U+3000 to the RANGE_GOOP exclusions and regenerate (validated by the checks) (recommended)
One line in each grammar (QueryParser.jj:226, StandardSyntaxParser.jj:200), then ./gradlew :lucene:queryparser:regenerateClassicQueryParser :lucene:queryparser:regenerateFlexibleStandardSyntaxParser, which changes 7 lines in each generated token manager and updates the two checksum files.
-| <RANGE_GOOP: (~[ " ", "]", "}" ])+ >
+| <RANGE_GOOP: (~[ " ", "\t", "\n", "\r", "\u3000", "]", "}" ])+ >Trade-offs
- An unquoted bound that contains one of the four characters stops parsing:
k:[a<TAB>b TO c]gives the boundsa<TAB>bandctoday and a ParseException with the fix, whilek:["a<TAB>b" TO c]keeps working. The 2002 author described whitespace as the range delimiter and said a date containing spaces needed quotes; propose it for main / 11.0 with a CHANGES entry ("the query parser syntax may change from release to release", classicpackage-info.java:74-75). - A JavaCC character list holds characters only and cannot name
_WHITESPACE, so the whitespace list is now written three times per grammar. Add a comment that ties_WHITESPACE,_TERM_START_CHARandRANGE_GOOPtogether (the LUCENE-1528 review already worried about two copies), and keep them in step if_WHITESPACEever grows. - Nothing else changes: the rule differs only on the four characters and only in the Range state, so only ranges that contain one of them parse differently; with the fix quoted bounds next to a TAB parse too.
What the checks validated
- Scratch worktree at
a9779ef3c193, gradle, JDK 25: the new test fails 5 of 6 on stock (assertions on and off) and passes 6 of 6 with the fix; the whole:lucene:queryparser:testpasses with assertions on (seed 5EED0845) and off (seed 5EED0846): 31 suites, 539 tests, 0 failures, 1 skipped (the 30 upstream suites includeTestQueryParser,TestStandardQP,TestQPHelper,TestMultiFieldQueryParser,TestPrecedenceQueryParser,TestComplexPhraseQueryandTestExtendableQueryParser).checkGoogleJavaFormat, both regenerate checksum checks andforbiddenApisMain/forbiddenApisTestpass. - After the fact checks, the test file exactly as printed (the formatter's one-line
assertEquals) was run again in a fresh scratch worktree:checkGoogleJavaFormatandforbiddenApisTestpass on stock and with the fix (with both regenerate checksum checks); the test fails 5 of 6 on stock and passes 6 of 6 with the fix, assertions on and off. - A probe with the regenerated token managers gives every input the bounds of its space form in both parsers;
<CR><LF>input and quoted bounds next to a TAB parse;[<LF>* TO b]is open-ended again. - Earlier, with the finder's regenerated token managers, the finder's 4 tests and the independent reproducer's 7 tests passed, the bounded lexer check held in all four lexer/state pairs, and 14 upstream queryparser test classes (323 tests) passed.
For example (~["\\", " ", "\t", "\n", "\r", "\u3000", "]", "}"] | "\\" ~[])+: the rule proposed in closed PR #13887 plus the four characters. It would close #13234 and this bug in one change.
Trade-offs
It also changes how escapes inside bounds behave. #13887 proposed exactly that and was closed by its author (2026-03-04) without a review or a maintainer comment; dweiss noted on #13323 that such a change "will break backward compatibility but maybe consistency is worth this cost", so it needs that discussion first. #13887's rule alone leaves this bug in place.
Strip the four characters from both ends of each bound before the * check and getRangeQuery.
Trade-offs
Fixes the silent cases (a character at either end of a bound) if it runs before the * check. 20200101<TAB>TO<TAB>20201231 is still one token, so the ParseException stays; and the lexer and the grammar's SKIP rule keep disagreeing.
Say so in the classic package docs and the StandardQueryParser javadoc. Not run.
Trade-offs
Changes no output, leaves the silent bound changes, and contradicts the grammar's own Range-state SKIP.
Search applications and servers that hand user-typed query text to these parsers inherit the bug (Solr's fork of the classic grammar has the same RANGE_GOOP line); building range clauses through the query API avoids it.
A program compared the character lists of both grammars and found that the bound token's exclusions miss four of the five declared whitespace characters. A bounded-exhaustive check of the real generated lexers then showed whitespace inside a token only there, and a public-API JUnit reproduced it.
The method in full206 words
Method: static comparison of the grammars' lexer tables, then bounded-exhaustive checking of the generated token managers (the study's "semi-formal" tier; the finder tagged it formal). A hunt unit, query-parser whitespace tables versus Unicode White_Space, 2026-10-28.
- Table diff. A custom program (not a solver) extracted the character classes from
QueryParser.jjandStandardSyntaxParser.jjand checked set obligations between them. Obligation O3, "the grammar's_WHITESPACEis a subset of theRANGE_GOOPexclusions", was violated for U+0009, U+000A, U+000D and U+3000 in both grammars. The signal came from this leg. - Lexer check. All 111,150 inputs of length 1 to 4 over an 18-symbol alphabet, run through the real generated token managers per lexer and lexical state: the grammar's own whitespace ends up inside a token only in the Range state and only in
RANGE_GOOP; the DEFAULT state is clean. With the regenerated (fixed) token managers the property holds in all four lexer/state pairs. - Parser sweep. A metamorphic sweep (replace a space by each whitespace character and compare the parsed query) found 720 mismatches in 2,358 hard-oracle checks, all in the 10 range templates.
- Then a public-API JUnit on the stock main jars, followed by independent reproduction, cross-model reviews, a devil's advocate, an adjudication and an audit (see the checks).
No prior report found (search of 2026-10-28): a scripted prior-report search (51 queries, 1,789 hits), about 20 manual searches in apache/lucene plus downstream repositories, all 411 open PRs, JIRA, the mailing lists and the web. Open #13234 and closed PR #13887 change the same line for escapes, a different defect.
Searches5 searches
Nothing reports that a TAB, LF, CR or U+3000 inside a [..] / {..} range ends up in a bound or breaks the parse. The RANGE_GOOP line is unchanged on upstream main 20ced71c1e6e, branch_10x and branch_10_6, and no open PR touches either grammar.
Related, not the same bug: open #13234 (escaped brackets inside an unquoted range bound fail with the same Was expecting: "TO" message) and closed-unmerged PR #13887 (its proposed RANGE_GOOP adds escapes and still swallows these characters); merged test PR #13323 (the remark that range terms are lexed differently); #1171 / LUCENE-93 (newlines between clauses, 2004); #2602 / LUCENE-1528 (U+3000); #8872 / LUCENE-7821 (TO made mandatory); open #4765 / LUCENE-3691 (a query of only U+3000, no range). You may prefer to post this as a comment on #13234 that proposes one combined fix.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues, PRs and commits (apache/lucene), downstream repositories, mailing lists, JIRA archive | 2026-10-28 | two runs of a scripted prior-report search, 51 queries each (for example RANGE_GOOP, range query newline parse, range tab ParseException, range query ideographic space, multiline range query), comments read for the top hits | no report; the follow-up's run: 1,789 distinct hits, 0 high overlap |
| GitHub, manual (apache/lucene, apache/solr, elastic/elasticsearch, opensearch-project/OpenSearch, apache/lucenenet) | 2026-10-28 | about 20 queries in apache/lucene plus searches in the four downstream repositories, for example RANGE_GOOP, _WHITESPACE, RANGEIN_GOOP, range tab / newline queryparser, ideographic space, "Was expecting" "TO" range | only the related items above |
| Open PRs and recent work | 2026-10-28 | the changed files of all 411 open apache/lucene PRs; issues and PRs updated since 2026-10-25; re-checked | none touches QueryParser.jj, StandardSyntaxParser.jj or the token managers |
| JIRA (LUCENE, SOLR) and lists.apache.org | 2026-10-28 | JIRA REST queries; dev@ and solr-user@ mails that mention RANGE_GOOP scanned for tab, newline, CR and U+3000; java-user@ and users@solr | no report; the 2002 dev@ thread with the design statement |
| Web | 2026-10-28 | 12 queries in English (one CJK-language query was throttled) | no report |
Introduced with the TO range syntax in 2002 (8ab0396cfcc, first released in 1.3-rc1) for TAB; LF and CR joined in 2004 (LUCENE-93), U+3000 and the flexible parser in 2.9.0. A history probe found all 160 releases it ran, 1.3-rc1 to 10.5.2, affected (the 9 without a published jar have the same rules); 1.2 is not.
Versions tested10 rows
Introduced by 8ab0396cfcc (svn r149785, 2002-06-25, "Support for new range query syntax. The delimiter is " TO ", but is optional for backward compatibility with previous syntax."; no issue; CHANGES 1.3 RC1 item 5). It replaced the one-token range, whose inside the analyzer split, with range lexical states, extended the whitespace SKIP to them (whitespace was then " " and TAB) and defined bound tokens that exclude only " ". Widened without touching the bound token by 9ada9671259 (2004, LUCENE-93 / #1171: LF and CR), 13d5245314e (2009, LUCENE-1528 / #2602: U+3000) and 343992fcbb4 (2009, LUCENE-1567 / #2641: the flexible parser, with a copy of the rules). LUCENE-996 (c8c9ee2cb20, 2010) and the 2011 flexible-grammar commit cffd01664bc merged the two bound tokens into today's RANGE_GOOP with the same exclusions. Not fixed upstream.
| Version or commit | Verdict | Result |
|---|---|---|
main a9779ef3c193 | affected | this doc's test 5 of 6 fail (gradle and standalone, assertions on and off); the finder's 4 of 4 |
branch_10_6 4ae946e5a9b9 | affected | the finder's test 4 of 4 and the independent reproducer's 6 of 7 fail |
| 10.5.2, 9.12.3 | affected | this doc's test 5 of 6 fail (release jars, SHA-1 checked) |
| 2.9.0 to 10.5.2 (147 releases) | affected | history probe: TAB, LF, CR and U+3000, classic and flexible parser |
| 1.4.1, 1.4.3, 1.9.1 to 2.4.1 | TAB, LF, CR | history probe: classic parser (U+3000 not yet whitespace, no flexible parser) |
| 1.3-rc1, 1.3 | TAB only | history probe on the release jars (LF, CR, U+3000 not yet whitespace) |
| 1.2 | no TO syntax | the old one-token range; not affected |
8ab0396cfcc vs its parent b1b3d33cf77 | bisect | the commit is affected for TAB; its parent is not (both grammars regenerated and run) |
9ada9671259 vs its parent cf06b516bc0 | bisect | TAB before; TAB, LF and CR after |
| main + fix | fixed | this doc's test 6 of 6; 539 queryparser tests pass |
An independent reproducer, a devil's advocate and an adjudication confirmed it with their own runs, the adjudication with high confidence; reviews by another model family could not start any command and rated the intended-behaviour case weak. A registry re-run, an audit, this doc's own gradle runs and its fact check reproduced it.
Checks and verdicts569 words
Every check below ran its own test or the repro against the stock jars unless noted. Still open: a cross-model review that executes code.
| Check | Verdict | What it found |
|---|---|---|
| finder | candidate | Public-API JUnit fails 4 of 4 on main (assertions on and off), branch_10_6, 10.5.2 and 9.12.3; passes with the regenerated token managers; 14 upstream queryparser classes (323 tests) still pass. |
| cross-model review, refute lens | procedural "not real" (low), not a refutation | An independent reviewer from another model family (OpenAI GPT-5.6) could not run any command (two attempts), so it ran and read nothing; it says the verdict is procedural. Its one counter-argument (the ASCII-space ' TO ') is weak. A separate run re-ran the repro: 4 of 4 fail. |
| Independent reproducer | real | Own 7-test JUnit, written from the claim alone: 6 of 7 fail on main (assertions on and off), branch_10_6, 10.5.2 and 9.12.3, the control passes; 7 of 7 with the fix. Checked that #13887's proposed rule would not fix it. |
| Cross-model review, intended lens | real (low, reading only) | OpenAI GPT-5.6: intended-behaviour argument weak, root cause agrees, inputs in contract; it could not run any command, so it judged only from the material in its prompt; a separate run re-ran the repro (4 of 4 fail) and a probe on main. |
| Devil's advocate | real (high) | Re-ran the repro (4 of 4, assertions on and off); its probe on main and 10.5.2 covers four parsers, quoted bounds, CRLF and the open bound; found the 2002 design statement on dev@. |
| adjudication | confirmed (high) | Re-ran the independent reproduction's tests with a new seed: 6 of 7 fail with assertions on and off; security none. |
| security assessment | not security-sensitive | A parse-correctness bug in the author's own range clause; a probe shows each widened range is a subset of a legal range and a query-API filter still applies. |
| registry re-run and audit | keep | The finder's test fails 4 of 4 again with new seeds (assertions off, then on); the audit's own probe confirms the bounds in both parsers. |
| this doc's runs | reproduced; fix validated | A new 6-test JUnit through gradle and the standalone runner on main, 10.5.2 and 9.12.3 (5 of 6 fail); the fix validated against the whole queryparser module with assertions on and off. |
| independent fact check of this doc | bug holds; 3 errors, fixed | Re-ran the gradle repro and the standalone runner on main, 10.5.2 and 9.12.3 (5 of 6 fail, assertions on and off), regenerated the fix (a byte-identical diff; 539 tests pass) and probed six parsers. It found three errors in the doc: the printed test failed the format check, #13887's closing reason was wrong, and the silent trigger was stated too narrowly. All three are fixed. |
| cross-model fact check (OpenAI GPT-5.6, reading only) | substantially sound; 2 errors, 4 overclaims, 4 missing caveats | Worked from the saved material only. Fixed: StandardAnalyzer is the caller's choice, not a default; the refute review could not read source either; four overclaims; caveats on the analyzer and date handling, the license header and the parser scope. The seed caveat is answered from the code. It rated the intended-behaviour case moderate. |
| diagram check | 1 layout error, fixed | Read every frame against the code and the diagram's executed probe (no run of its own): every value matched. One clipped footnote in section 5 was wrapped. |
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 463 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,667 words
- Registry title
- Classic QueryParser and flexible StandardQueryParser: a TAB, LF, CR or U+3000 next to a range bound inside [..] or {..} becomes part of the bound, because RANGE_GOOP excludes only U+0020 although both grammars declare these characters as whitespace and skip them in the Range state; such ranges fail to parse or, when the character touches a bracket, silently change their bounds
- Area
- queryparser / classic and flexible standard grammars (Range lexical state, RANGE_GOOP token)
- Priority
- Low, rank 532: It hits the default classic and flexible parsers in every release, but the input is rare (a TAB, line break or U+3000 inside a range), the usual result is a loud ParseException and bounds change silently only when the character touches a bracket.
- Impact class
- silent wrong results
- Impact note
- Applications that parse query text with the classic QueryParser (or MultiFieldQueryParser, ComplexPhraseQueryParser, ExtendableQueryParser), StandardQueryParser or PrecedenceQueryParser in their default configuration are exposed whenever a range is written with a TAB, a line break or a full-width space (U+3000, as typed by CJK input methods) next to a bound: the common outcome is a ParseException for a tab-separated or multi-line range, and the silent outcome (changed bounds: extra hits, an exclusive endpoint admitted, or no hits at all) needs the character directly after '[' / '{' or directly before ']' / '}'; no scale is needed, and every release since 1.3-rc1 is affected [exec].
- Summary
- You parse 'date:[20200101<TAB>TO<TAB>20201231]' or the multi-line 'date:[<LF>20200101 TO 20201231<LF>]' with the classic or the flexible standard query parser; both grammars declare TAB, LF, CR and U+3000 as whitespace and skip it inside ranges too, so you expect the same query as with spaces. Instead the range-bound token RANGE_GOOP, which excludes only U+0020 and the closing brackets, swallows any of these characters that touches a bound: the first query throws ParseException ('Was expecting: "TO"'), and the second parses without error into a TermRangeQuery whose bounds are '<LF>20200101' and '20201231<LF>', so it also matches 20191231 (4 hits instead of 3). In the same way 'date:{20200101 TO 20201231<TAB>}' admits its own exclusive endpoint, and a leading U+3000 makes the range match nothing.
- Introduced
- 8ab0396cfccdafe51e6b741ea8e35a6491e1aa09 (svn r149785, author Brian Goetz, 'Support for new range query syntax. The delimiter is " TO ", but is optional for backward compatibility with previous syntax.'; no issue or PR; CHANGES.txt 1.3 RC1 item 5): it replaced the one-token range, whose inside the analyzer split, with the RangeIn/RangeEx lexical states, extended the whitespace SKIP rule to them ('<DEFAULT, RangeIn, RangeEx> SKIP', _WHITESPACE then ' ' | TAB) and defined RANGEIN_GOOP (~[' ', ']'])+ and RANGEEX_GOOP (~[' ', '}'])+, which exclude only U+0020, so a TAB next to a bound has been part of the bound since then [exec: commit-level, the grammars of the parent b1b3d33cf77 and of 8ab0396cfcc regenerated with JavaCC 7 in front of the 1.2 jar: parent not affected, 8ab0396cfcc affected for TAB]. Widened without touching the goop rule by 9ada9671259 (2004, LUCENE-93 / #1171, Bugzilla 18847: LF and CR added to _WHITESPACE) [exec: commit-level against its parent cf06b516bc0], 13d5245314e (2009, LUCENE-1528 / #2602: U+3000 added) [read; bracketed by the releases 2.4.1 and 2.9.0, exec] and 343992fcbb4 (2009, LUCENE-1567 / #2641: the flexible parser created with a copy of the same token rules) [read]; c8c9ee2cb20 (LUCENE-996, 2010) and cffd01664bc (2011, flexible grammar) merged the two goop rules into RANGE_GOOP (~[' ', ']', '}'])+ with the same exclusions [read] (2002-06-25 (author date of 8ab0396cfcc, TAB); widened 2004-03-03 (LF, CR), 2009-01-28 (U+3000) and 2009-08-03 (flexible parser))
- First affected version
- 1.3-rc1 [exec] (TAB; LF and CR from 1.4-rc1 [read], first on Maven Central 1.4.1 [exec]; U+3000 and the flexible parser from 2.9.0 [exec]); 1.2 [exec] is not affected because the TO range syntax did not exist yet
- Affected versions
- Every release from 1.3-rc1 to 10.5.2 (latest 9.x: 9.12.3; latest 10.x before 10.6.0: 10.5.2), the branch_10_6 head 4ae946e5a9b9 (10.6.0, released 2026-10-29) and main a9779ef3c193; not fixed upstream (the RANGE_GOOP line is unchanged on upstream main 20ced71c1e6e, branch_10x and branch_10_6, 2026-10-28 [read]). [exec] history probe (one class, public parse API through reflection): 1.3-rc1, 1.3, 1.4.1 and 1.4.3 (plain JDK runs on SHA-1-checked Maven Central jars) and all 156 releases from 1.9.1 to 10.5.2 through repro.sh are affected, 1.2 is not; the finder's JUnit fails 4/4 on main (-ea and -da), the branch_10_6 head, 10.5.2 and 9.12.3 (it does not compile on 9.0.0, older test-framework package); the independent reproduction's JUnit fails 6/7 on main (-ea and -da), the branch_10_6 head, 10.5.2 and 9.12.3. Characters affected: TAB in 1.3-rc1 and 1.3; TAB, LF and CR in 1.4-rc1 to 2.4.1 (classic only); all four in 2.9.0 to main, classic and flexible. [read] 1.4-rc1 to 1.4 final and 1.9 (not on Maven Central; same token rules in their trees). All runs JDK 25 on aarch64; the defect is in the generated lexer tables, so no platform dependence is expected (not executed on x86)
- 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; also reached by exhaustive check
- Upstream and prior reports
- none found (2026-10-28, read-only; the prior-report record verdict new; re-checked 2026-10-28): no issue, PR, commit, JIRA entry, mailing-list thread or downstream report says that a TAB, LF, CR or U+3000 inside a [..] / {..} range is swallowed into a bound or breaks the parse; the RANGE_GOOP line is unchanged on upstream main 20ced71c1e6e, branch_10x and branch_10_6, and no open PR touches either grammar or the generated token managers. Related, a different defect on the same token rule: open #13234 (2024-03-28, escaped brackets inside an unquoted range bound fail to parse with the same 'Was expecting: "TO"' message) and closed-unmerged PR #13887 (GitHub benchaplin, closed 2026-03-04), whose proposed RANGE_GOOP adds escapes and would not fix this bug; neither mentions TAB, LF, CR, U+3000 or line breaks. Context: merged test-only PR #13323, where GitHub dweiss remarked that range terms are lexed differently from normal tokens and 'logically' should accept the same term notation (said about escapes); LUCENE-93 (#1171, LF/CR to be treated like TAB and space, clause separation only), LUCENE-1528 (#2602, U+3000; its review discussed keeping two whitespace lists in sync and missed this third one) and LUCENE-7821 (#8872, ' TO ' made mandatory, no whitespace discussion); open LUCENE-3691 (#4765, a query consisting only of U+3000, no range) is unrelated. Searched: a scripted prior-report search (51 queries over GitHub issues, PRs and commits, downstream repositories, the dev@, java-user@, issues@ and commits@ lists and the JIRA archive: 1,789 distinct hits, 0 high overlap), the finder's 51-query run, about 30 manual GitHub searches (apache/lucene, apache/solr, elastic/elasticsearch, opensearch-project/OpenSearch, apache/lucenenet), the changed files of all 411 open apache/lucene PRs, JIRA REST (LUCENE, SOLR), lists.apache.org and 12 web queries. Gaps: one rate-limited GitHub query (its terms covered by others), one throttled CJK-language web query, and private reports to security@lucene.apache.org are not visible. Downstream: Solr's fork of the classic grammar has the same RANGE_GOOP and Range-state SKIP [read].
- Lucene commit tested
- main
a9779ef3c193(commit of 2026-10-26; tested 2026-10-28) through gradle in a scratch worktree and through the standalone JUnit runner, with assertions on and off; the 10.5.2 and 9.12.3 release jars; the branch_10_6 head4ae946e5a9b9(the finder's and the independent reproducer's tests); a reflection-only history probe on 1.2 (not affected), 1.3-rc1, 1.3, 1.4.1, 1.4.3, all 156 releases from 1.9.1 to 10.5.2, plus commit-level runs of the introducing commit and its parent (follow-up). The fix was validated with gradle ona9779ef3c193. After the fact checks the test file as printed was run again (gradle with assertions on and off, stock and with the fix; the standalone runner on main, 10.5.2 and 9.12.3). Upstream main20ced71c1e6e, branch_10x and branch_10_6 have the sameRANGE_GOOPline (read, not executed).
- Finding record
lucene/queryparser/src/java/org/apache/lucene/queryparser/classic/QueryParser.jj:183-185 (at a9779ef3c193) (_TERM_START_CHAR excludes ' ', TAB, LF, CR, U+3000), :187 (_WHITESPACE = ' ' | TAB | LF | CR | U+3000), :191-193 ('<DEFAULT, Range> SKIP : { <_WHITESPACE> }'), :226 (RANGE_GOOP: (~[ " ", "]", "}" ])+), :343-347 (range production goop1 RANGE_TO goop2)lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/parser/StandardSyntaxParser.jj:135 (at a9779ef3c193) (_WHITESPACE), :139-141 ('<DEFAULT, Range, Function> SKIP'), :200 (RANGE_GOOP, same definition), :855-859 (TermRangeExpr)lucene/queryparser/src/test/org/apache/lucene/queryparser/util/QueryParserTestBase.java:250 (at a9779ef3c193) (testCJK, U+3000 separates clauses), :896 (testTabNewlineCarriageReturn); lucene/queryparser/src/test/org/apache/lucene/queryparser/flexible/standard/TestQPHelper.java:324 (testCJK), :937 (testTabNewlineCarriageReturn)lucene/queryparser/src/java/org/apache/lucene/queryparser/classic/package-info.java:74-75 (at a9779ef3c193) ('the query parser syntax may change from release to release'), :82-95 (parser designed for human-entered text; range and other program-generated clauses better built through the query API)
- The finding in 30 seconds
lucene/queryparser/src/java/org/apache/lucene/queryparser/classic/QueryParser.jj:183-185, (at a9779ef3c193) :187, :191-193, :221-227 (:226 RANGE_GOOP), :343-347, :354, :359lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/parser/StandardSyntaxParser.jj:131-135, (at a9779ef3c193) :139-141, :195-201 (:200 RANGE_GOOP), :855-859, :870-873lucene/queryparser/src/java/org/apache/lucene/queryparser/classic/QueryParserBase.java:542-561; (at a9779ef3c193) lucene/core/src/java/org/apache/lucene/analysis/standard/StandardAnalyzer.java:99-101
- Who is affected
lucene/queryparser/src/java/org/apache/lucene/queryparser/classic/MultiFieldQueryParser.java:31; (at a9779ef3c193) complexPhrase/ComplexPhraseQueryParser.java:61; ext/ExtendableQueryParser.java:66; flexible/precedence/PrecedenceQueryParser.java:35lucene/queryparser/src/java/org/apache/lucene/queryparser/surround/parser/QueryParser.jj:192-237 (at a9779ef3c193) (lexical states DEFAULT and Boost only); queryparser/src/java/org/apache/lucene/queryparser/simple/SimpleQueryParser.java:47-58 (operators, no range)lucene/queryparser/src/java/org/apache/lucene/queryparser/classic/QueryParser.java:91 (at a9779ef3c193) (the constructor takes the analyzer); queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/StandardQueryParser.java:235, :255-258lucene/queryparser/src/java/org/apache/lucene/queryparser/classic/QueryParserBase.java:447-481 (at a9779ef3c193) (getRangeQuery: date parse, catch at :456-458); queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/processors/TermRangeQueryNodeProcessor.java:101-141; core/src/java/org/apache/lucene/document/DateTools.java:90-92, :150 (switch on the resolution)
- Why it looks unintended
lucene/queryparser/src/java/org/apache/lucene/queryparser/classic/QueryParser.jj:187, (at a9779ef3c193) :191-193, :226lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/parser/StandardSyntaxParser.jj:135, (at a9779ef3c193) :139-141, :200lucene/queryparser/src/test/org/apache/lucene/queryparser/util/QueryParserTestBase.java:250-255 (at a9779ef3c193) (testCJK), :637 (" TO " comment), :896-915 (testTabNewlineCarriageReturn)lucene/queryparser/src/test/org/apache/lucene/queryparser/flexible/standard/TestQPHelper.java:324-329 (at a9779ef3c193) (testCJK), :937-956 (testTabNewlineCarriageReturn)lucene/queryparser/src/java/org/apache/lucene/queryparser/classic/package-info.java:74-75, (at a9779ef3c193) :82-95, :225-239 (Range Searches)lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/StandardQueryParser.java:145-158 (at a9779ef3c193) (Range operators)GitHub, read-only: apache/lucene PR #13323 (dweiss's comment), issue #8872 (LUCENE-7821), issue #2602 (LUCENE-1528)
- Suggested fix
lucene/queryparser/src/java/org/apache/lucene/queryparser/classic/package-info.java:74-75 (at a9779ef3c193)
- TestRangeBoundWhitespace.java (JUnit test):
repro/LFV-845/src/org/apache/lucene/queryparser/classic/TestRangeBoundWhitespace.java(30 lines)
A light, step-by-step picture of the repro, made in parallel with this doc from executed probe runs on main a9779ef3c193 (stock and with the fix) and the code; it ends on a summary card, which is the PNG.
assets/LFV-845/bug.png(463 KB)
![Diagram of the example: five indexed dates (20191231, 20200101, 20200615, 20201231, 20210101) and the query date:[ LF 20200101 TO 20201231 LF ] typed over three lines. Both query grammars skip whitespace inside ranges, but the bound token RANGE_GOOP stops only at a space, ] or }, so it takes each line break into the bounds; the range then matches 4 dates instead of 3, 20191231 included. With the fix, RANGE_GOOP stops at every whitespace character of the grammar and the range matches 3.](../assets/LFV-845/bug.png)