One relation, CONTAINS, takes a shortcut that runs before the argument checks every other relation goes through.
LatLonPoint.newGeometryQuery(field, relation, geometries...)builds a query over indexed points. ForINTERSECTS,WITHINandDISJOINTit builds oneLatLonPointQuery(apart from twoINTERSECTSshortcuts for a single rectangle or circle), whose constructor checks the arguments ("geometries must not be empty", "geometries[i] must not be null").CONTAINSasks for documents whose points contain every given geometry. Points can only contain points, so the factory adds one required (MUST) clause perPoint, and returnsMatchNoDocsQueryfor a line or polygon: a choice made on purpose whenCONTAINSwas added in 8.8.0.
INTERSECTS and WITHIN reject an empty geometry array; CONTAINS takes its shortcut first and returns a query that matches nothing. Select the still to open it at full size. Generated with AI, checked against the code.- You build a CONTAINS query on a
LatLonPointfield from a list of points that turned out empty:LatLonPoint.newGeometryQuery("f", QueryRelation.CONTAINS)with 0 geometries. - You expect
IllegalArgumentException: the javadoc promises it when the geometries are "null, empty or contain a null", and the same call withINTERSECTSorWITHINthrows "geometries must not be empty". - Instead you get
ConstantScore(): aConstantScoreQueryover aBooleanQuerywith 0 clauses. On an index with one point at (0,0) it finds 0 hits, whereCONTAINS [Point(0,0)]finds 1. No error. - A null slot does the same:
CONTAINS [Point(0,0), null]returnsMatchNoDocsQuery("... with non-point geometries"), 0 hits, inLatLonPointand inLatLonDocValuesField.newSlowGeometryQuery;INTERSECTSthrows "geometries[1] must not be null". - Why: CONTAINS takes a shortcut before any argument check. Its loop never runs for 0 geometries, and
null instanceof Pointis false, so the null lands in the branch meant for lines and polygons.
Applications that build a CONTAINS query on a point field from a caller-built array that can be empty (no locations picked) or hold a null slot, relying on the documented check. They get zero hits and no error. No setting is involved: any index, assertions on or off. Valid input is not affected by this defect.
Mechanism, terms and numbers442 words
LatLonPoint.newGeometryQuery(field, queryRelation, latLonGeometries...) and LatLonDocValuesField.newSlowGeometryQuery document @throws IllegalArgumentException when the geometry array "is null, empty or contain a null". For INTERSECTS, WITHIN and DISJOINT the query constructors enforce it. For CONTAINS both factories take a shortcut first that checks only the geometry type (instanceof Point), not null or empty input: an empty array gives ConstantScore() over zero clauses, and a null element gives MatchNoDocsQuery("... with non-point geometries"). Both match no document, so a caller's mistake (an empty list of points, a null slot) becomes an empty result with no error. The doc-values factory still rejects the empty array; the null element is silent in both. Valid input is not affected by this defect. Introduced in 8.8.0 by LUCENE-9641; present in every release through 10.6.0 and on main.
- The contract.
document/LatLonPoint.java:314-326: "@param latLonGeometriesarray of LatLonGeometries. must not be null or empty." and "@throws IllegalArgumentExceptioniffieldis null,queryRelationis null,latLonGeometriesis null, empty or contain a null" (:323-324).document/LatLonDocValuesField.java:243-246has the same clause and adds "or line geometry"; that line part is not claimed here, because underCONTAINSa line gets theMatchNoDocsQuerythe introducing PR's review settled for non-point geometries (:255-261). - The checks live in the query constructors.
new LatLonPointQuery(...)(LatLonPoint.java:340) goes throughSpatialQuery(field and relation,document/SpatialQuery.java:74-85) andLatLonGeometry.create(geo/LatLonGeometry.java:24-45).new LatLonDocValuesQuery(...)(LatLonDocValuesField.java:263) does its own field and relation checks (document/LatLonDocValuesQuery.java:57-62) and callsLatLonGeometry.create(:87).LatLonGeometry.createrejects an empty array and a null element (:28-40); its null-array check (:25-26) is never reached fromLatLonPoint, where a null array fails earlier withNullPointerException(LatLonPoint.java:329,:345orSpatialQuery.java:83). - CONTAINS returns before them.
LatLonPoint.java:337-338sendsCONTAINStomakeContainsGeometryQuery(:343-355), which adds oneMUSTclause perPointand returnsMatchNoDocsQueryfor anything else (:346-348).- Empty array: the loop body never runs, and
:354returnsnew ConstantScoreQuery(builder.build())over zero clauses, which matches nothing (it prints asConstantScore(); at search time the emptyBooleanQueryrewrites toMatchNoDocsQuery("empty BooleanQuery"),search/BooleanQuery.java:268-271). - Null element:
(null instanceof Point) == falseis true at:346, so the null takes the non-point branch.
- Empty array: the loop body never runs, and
- The doc-values twin.
LatLonDocValuesField.java:255-261has the sameCONTAINSloop and the same null-element outcome; its empty array falls through toLatLonDocValuesQueryand is rejected there. - Not claimed.
MatchNoDocsQueryfor a valid line or polygon underCONTAINSis intended. The null-arrayNullPointerExceptionfails loudly (only the exception type differs), and a null field with an empty array needs a second invalid argument.
Exposure and likelihood128 words
- Who. Code that passes an empty or null-holding array to
LatLonPoint.newGeometryQuery(orLatLonDocValuesField.newSlowGeometryQuery, null element) withCONTAINS, for example a list of user-picked points that can be empty. Nothing tells the application its input was invalid. - Default vs opt-in. Query construction only: no configuration, any index and codec, assertions on and off.
CONTAINSon a point field is a niche relation, and no public report was found since 8.8.0 (2021), so the likelihood is rare. - Needs invalid input. This defect changes nothing for valid geometries (valid multi-point
CONTAINSonnewSlowGeometryQueryhas a separate defect, LFV-701). The documentedIndexOrDocValuesQuerypairing still throws for the empty array, because the doc-values half rejects it; the null element is silent in both halves. - Scale. One call; no index needed; deterministic.
One JUnit test method on the public API: expectThrows(IllegalArgumentException.class, () -> LatLonPoint.newGeometryQuery("f", QueryRelation.CONTAINS)). No index, no random input, one thread. It fails with assertions on and off and passes with the fix, so it is also the regression test.
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.lucene.document;
import org.apache.lucene.document.ShapeField.QueryRelation;
import org.apache.lucene.tests.util.LuceneTestCase;
public class TestLatLonPointContainsNoGeometries extends LuceneTestCase {
public void testContainsWithNoGeometriesThrows() {
// expected: the documented IllegalArgumentException; actual: a query that matches nothing
expectThrows(
IllegalArgumentException.class,
() -> LatLonPoint.newGeometryQuery("f", QueryRelation.CONTAINS));
}
}
cp $REPRO/TestLatLonPointContainsNoGeometries.java $LUCENE/lucene/core/src/test/org/apache/lucene/document/
cd $LUCENE && ./gradlew :lucene:core:test --tests org.apache.lucene.document.TestLatLonPointContainsNoGeometries -Ptests.seed=DEADBEEF
# add -Ptests.asserts=false for production mode: the same failure
- Expected
newGeometryQuery("f", CONTAINS)with no geometries throwsIllegalArgumentException.- Observed
- It returns
ConstantScore(), which matches nothing: "Expected exception IllegalArgumentException but no exception was thrown", assertions on and off.
More files, environment, full output268 words
TestLatLonPointContainsNoGeometries (one method, extends LuceneTestCase, package org.apache.lucene.document of the lucene/core tests, Apache license header). It is the hunt's minimal test with its comment shortened to pass checkGoogleJavaFormat. It covers the empty array, the simplest of the two silent cases; the null element follows from the same shortcut and is shown by the diagnostic below. Run with seed DEADBEEF; the hunt's equivalent tests also failed with seeds 4C0A0001, 4C0A0002, 12345678 and CAFEBABE.
| lucene | main 5adff78bb168 (12.0.0-SNAPSHOT): fails; 8.8.0-8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2, 10.6.0: fail; 8.7.0: no API |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux aarch64 (only this platform was run) |
cp $REPRO/TestLatLonPointContainsNoGeometries.java $LUCENE/lucene/core/src/test/org/apache/lucene/document/
cd $LUCENE && ./gradlew :lucene:core:test --tests org.apache.lucene.document.TestLatLonPointContainsNoGeometries -Ptests.seed=DEADBEEF
# gradle, scratch worktree at 5adff78bb168, -Ptests.seed=DEADBEEF (assertions on; -Ptests.asserts=false gives the same)
TestLatLonPointContainsNoGeometries > testContainsWithNoGeometriesThrows FAILED
junit.framework.AssertionFailedError: Expected exception IllegalArgumentException but no exception was thrown
at __randomizedtesting.SeedInfo.seed([DEADBEEF:D113C0FEC8040294]:0)
at app//org.apache.lucene.tests.util.LuceneTestCaseParent.expectThrows(LuceneTestCaseParent.java:1432)
at app//org.apache.lucene.tests.util.LuceneTestCaseParent.expectThrows(LuceneTestCaseParent.java:1418)
at app//org.apache.lucene.document.TestLatLonPointContainsNoGeometries.testContainsWithNoGeometriesThrows(TestLatLonPointContainsNoGeometries.java:26)
1 test completed, 1 failed
# with the validate-first fix: 1 test, 0 failures (assertions on and off)
# what the factories return (public-API diagnostic on main, assertions off, one document at (0,0))
P.newGeometryQuery(f, CONTAINS, []) -> ConstantScoreQuery [ConstantScore()] hits=0
P.newGeometryQuery(f, INTERSECTS, []) -> IllegalArgumentException: geometries must not be empty
P.newGeometryQuery(f, WITHIN, []) -> IllegalArgumentException: geometries must not be empty
DV.newSlowGeometryQuery(f, CONTAINS, []) -> IllegalArgumentException: geometries must not be empty
P.newGeometryQuery(f, CONTAINS, [Point(0,0), null]) -> MatchNoDocsQuery [MatchNoDocsQuery("Contains LatLonPoint.newGeometryQuery with non-point geometries")] hits=0
P.newGeometryQuery(f, INTERSECTS, [Point(0,0), null]) -> IllegalArgumentException: geometries[1] must not be null
DV.newSlowGeometryQuery(f, CONTAINS, [null]) -> MatchNoDocsQuery [MatchNoDocsQuery("Contains LatLonDocValuesField.newSlowGeometryQuery with non-point geometries")] hits=0
P.newGeometryQuery(f, CONTAINS, [Point(0,0)]) -> ConstantScoreQuery [ConstantScore(+LatLonPointQuery: field=f:[Point(0.0,0.0),])] hits=1- Both javadocs promise
IllegalArgumentExceptionfor an array that is "null, empty or contain a null"; the commit that addedCONTAINSrewrote and kept that clause. - For the same empty array,
INTERSECTS,WITHINand the doc-values twin'sCONTAINSthrow "geometries must not be empty". - The intended
MatchNoDocsQueryis for valid non-point geometries; a null reaches it only by failinginstanceof Point.
The full argument127 words
- The contract is explicit and current. The
@throwsclause names these inputs for every relation, in both factories. LUCENE-9641 (14009f4424d), which addedCONTAINSand the shortcut, rewrote the clause in the same diff and kept "null, empty or contain a null" (git blame). - The rest of the API enforces it.
INTERSECTS,WITHIN,newPolygonQueryandnewSlowGeometryQuery(CONTAINS)throw "geometries must not be empty" for the empty array;INTERSECTSthrows "geometries[i] must not be null" for a null element (executed). Before LUCENE-9641, on unreleased master, the constructors of the removedLatLonPointInGeometryQueryandLatLonDocValuesPointInGeometryQueryrejected all of these inputs. - Tests. No upstream test passes an empty or null-holding array with any relation;
BaseGeoPointTestCase.testPolygonNullField(lucene/test-framework/.../tests/geo/BaseGeoPointTestCase.java:472-483) treats these@throwsclauses as tested contract for the field argument.
The devil's-advocate check200 words
The case for intended (the devil's-advocate verifier and the cross-model refute lens): (a) @param says the array "must not be null or empty", so the caller misuses the API; (b) in the review of lucene-solr#2155 a reviewer (nknize) suggested throwing for non-point geometries under CONTAINS, and the author (iverase) answered "Still I return a MatchNoDocs query for the Query builders"; (c) LatLonShape.newGeometryQuery(f, CONTAINS) with an empty array also returns ConstantScore(); (d) an empty set matching nothing is common in Lucene.
Why it fails. (a) The javadoc does not stop at the precondition: it promises the exception for exactly these inputs. (b) The review covered valid non-point geometries, a relation points can never satisfy, not invalid arrays. (c) LatLonShape's empty case appears to be a historical consequence of LUCENE-9225 (286d22717b2, 8.5.0), which dropped a CONTAINS && length > 1 guard that XYShape.newGeometryQuery still has (XYShape throws), and for a null element LatLonShape throws "geometries[0] must not be null" while LatLonPoint returns MatchNoDocsQuery (executed). (d) Here the API documents the exception and enforces it on every other path. The cross-model reviewer rated the misuse defence weak against the explicit @throws; it does hold for impact, hence Low.
Recommended Validate every argument before any shortcut, in both factories (fix A)
Check every argument first: a small package-private helper, called as the first line of both LatLonPoint.newGeometryQuery and LatLonDocValuesField.newSlowGeometryQuery, throws IllegalArgumentException for a null field or relation, a null or empty array and a null element, before any shortcut. The repro passes, and the 236 TestLatLon* tests run with 0 failures (19 skipped).
All options and trade-offs3 options ยท 501 words
Fix A was built and checked through gradle on 5adff78bb168 in a scratch worktree: the repro with assertions on and off, then the repro with every org.apache.lucene.document.TestLatLon* and org.apache.lucene.search.TestLatLon* suite, seed DEADBEEF, and checkGoogleJavaFormat. Option B was not run. The regression test is the one repro test, unchanged; it covers only the empty LatLonPoint case, while the null element and the doc-values factory, which fix A also changes, are shown by the diagnostic in the repro section, not by a test.
Validate every argument before any shortcut, in both factories (fix A) (validated by the checks) (recommended)
Add LatLonPoint.checkGeometryQueryArguments(field, queryRelation, latLonGeometries) (package-private) and call it as the first statement of newGeometryQuery (LatLonPoint.java:327-328) and of newSlowGeometryQuery (LatLonDocValuesField.java:248-249); +27/-0, messages as in LatLonGeometry.create.
static void checkGeometryQueryArguments(
String field, ShapeField.QueryRelation queryRelation, LatLonGeometry... latLonGeometries) {
if (field == null) {
throw new IllegalArgumentException("field must not be null");
}
if (queryRelation == null) {
throw new IllegalArgumentException("queryRelation must not be null");
}
if (latLonGeometries == null) {
throw new IllegalArgumentException("geometries must not be null");
}
if (latLonGeometries.length == 0) {
throw new IllegalArgumentException("geometries must not be empty");
}
for (int i = 0; i < latLonGeometries.length; i++) {
if (latLonGeometries[i] == null) {
throw new IllegalArgumentException("geometries[" + i + "] must not be null");
}
}
}Trade-offs
Implements the null, empty and null-element part of both @throws clauses in one place, before every shortcut; the doc-values clause's "or line geometry" is left as it is. Side effects, all towards the documented behaviour: a null array gives IllegalArgumentException instead of NullPointerException (as newPolygonQuery did in 8.7.0), and so does a null field with an empty CONTAINS array. Callers that pass an empty CONTAINS array today get an exception instead of zero hits: a behaviour change, so consider a CHANGES entry. Cost: one linear null scan per query construction, run first on every path (the single-geometry INTERSECTS shortcuts included); the paths that reach LatLonGeometry.create scan the array again there.
What the checks validated
The repro passes with assertions on and off; the repro plus every document.TestLatLon* and search.TestLatLon* suite (22 classes): 236 tests, 0 failures, 19 skipped; :lucene:core:checkGoogleJavaFormat passes with the fix and the test in place. The hunt validated the LatLonPoint half alone the same way (repro passes; 41 TestLatLonPointQueries tests pass).
Throw for length == 0 before the loop in makeContainsGeometryQuery, and for geometry == null inside both CONTAINS loops (LatLonPoint.java:345-346, LatLonDocValuesField.java:256-257).
Trade-offs
Smallest change, leaves the other paths untouched. Keeps the undocumented NullPointerException for a null array and a second copy of the checks.
What the checks validated
Not run.
Change the javadoc so that an empty or null-holding array under CONTAINS is documented to match nothing.
Trade-offs
Keeps the empty array inconsistent with the other relations and with the doc-values twin, and keeps hiding the caller's mistake.
What the checks validated
Not run.
Downstream, a product that forwards a user's list of locations into a CONTAINS query gets zero hits for an empty list (and, under MUST_NOT, excludes nothing) instead of an error it could report.
A proof-obligation audit of the documented @throws contract of LatLonPoint.newGeometryQuery and LatLonDocValuesField.newSlowGeometryQuery: every path that returns a query must first reject a null, empty or null-holding array. The CONTAINS shortcut does not; a one-test public-API JUnit witnessed it.
The method in full107 words
Method: semi-formal (a proof-obligation audit of a class's contract, witnessed by an executable repro).
- The audit checked the paths of the two factories (
LatLonPoint.java:314-355,LatLonDocValuesField.java:232-264) against that@throwsclause. TheINTERSECTS,WITHINandDISJOINTpaths reach the checks in the query constructors andLatLonGeometry.create; theCONTAINSshortcut returns before them. - The finder then wrote a one-test public-API JUnit that fails on main with assertions on and off and on 10.6.0 and 9.12.3, and a probe for the null element in both factories.
- An independent reproducer, an executed cross-model review, a devil's-advocate verifier and an adjudication reproduced it ("Prior reports, history and checks").
No prior report found in apache/lucene issues, PRs and commits (with the JIRA archive and the archived lucene-solr PRs), the dev@, java-user@, issues@ and commits@ lists, downstream trackers or the web (searched 2026-11-03). Nearest: lucene-solr#2155, the introducing PR, whose review covered only valid non-point geometries.
Searches3 searches
No matching public report was found in the documented searches. Related, not reports: lucene-solr#2155 (LUCENE-9641, the introducing PR) and its issue #10680; lucene-solr#1940 / #10591 (LUCENE-9552, added newGeometryQuery); the open, stale #12162 (a LatLonField that delegates to the same factory and would inherit the gap); #16847 and #709 (other code in the same files). Limits: keyword search; GitHub search does not index PR diffs or every review comment; private reports are not visible.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues, PRs and commits (apache/lucene incl. the migrated JIRA archive; archived apache/lucene-solr PRs) | 2026-11-03, re-checked the same day | two scripted prior-report searches plus the finder's own run; 26 manual and 12 GraphQL searches (newGeometryQuery, makeContainsGeometryQuery, newSlowGeometryQuery, "geometries must not be empty", "non-point geometries", LUCENE-9641, LUCENE-9552); git log -S on the method and the message | no report; lucene-solr#2155, #10680, #10591 and #12162 are the nearest items |
| Open PRs | 2026-11-03 | the changed files of all 409 open apache/lucene PRs; every issue and PR created since 2026-11-01 | #16847, #12162 and #709 touch the files, not these code paths; nothing new is geo-related |
| Downstream code, mailing lists, web | 2026-11-03 | Elasticsearch, OpenSearch and Solr trackers; dev@, java-user@, issues@ and commits@; 13 web queries | no report |
Introduced in 8.8.0 by LUCENE-9641 (14009f4424d, 2021-01-08), which added the relation parameter and the CONTAINS shortcut before the checks, and removed the query classes that had rejected these inputs. Every release run from 8.8.0 to 10.6.0 fails; 8.7.0 has no such API. Not fixed upstream.
Versions tested4 rows
Introduced by 14009f4424d "LUCENE-9641: Support for spatial relationships in LatLonPoint (#2155)" (Ignacio Vera, 2021-01-08; apache/lucene-solr#2155, issue #10680; branch_8x copy f73f6b11f0e, lucene-solr#2188). It added the queryRelation parameter and the CONTAINS shortcuts in front of the validating constructors, removed LatLonPointInGeometryQuery and LatLonDocValuesPointInGeometryQuery, whose constructors had rejected these inputs, and kept "null, empty or contain a null" in the rewritten @throws (git blame, git log -S makeContainsGeometryQuery). First release 8.8.0, which is also the first release with newGeometryQuery at all.
All 34 main-line release tags 9.0.0-10.6.0 have the same unchecked bodies (read). The null-element case comes from the same commit and was executed on main only. Not new in 10.6. Upstream main 1c2072f0284 (2026-11-02) still dispatches CONTAINS before any check.
| Version or commit | Verdict | Result |
|---|---|---|
main 5adff78bb168 | affected | gradle and a standalone JUnit run: fails with assertions on and off; passes with the validate-first fix |
| 8.8.0, 8.8.1, 8.8.2, 8.9.0, 8.10.0, 8.10.1, 8.11.0-8.11.4 | affected | the hunt's one-test JUnit: "expected IllegalArgumentException, got ConstantScoreQuery ConstantScore()" |
| 9.0.0, 9.12.3, 10.0.0, 10.5.2, 10.6.0 | affected | the same failure (10.6.0 with assertions on and off; 10.5.2 from the independent reproducer's test) |
| 8.7.0 | not affected | the API does not exist (compile error) |
A finder, an independent reproducer, an executed cross-model review (OpenAI GPT-5.6, refute lens), a devil's-advocate verifier with an intended-lens cross-model review, an adjudication, a separate re-run and an audit re-run reproduced it and agree on the root cause; a final repro check confirmed it at Low. This doc's gradle runs reproduced it and validated fix A.
Checks and verdicts266 words
The cross-model refute review ran on main ea57360912ed, whose six relevant files are byte-identical to 5adff78bb168; its test was re-run on 5adff78bb168 with the same result. All other main runs used 5adff78bb168; the release runs used each release's own jars.
| Check | Verdict | What it found |
|---|---|---|
| finder | reproduced | Own one-test JUnit fails on main (assertions on and off), 10.6.0 and 9.12.3; passes with the LatLonPoint fix; a probe shows the null element in both factories. |
| independent reproducer | real, Low | Own expectThrows test fails on main, 10.6.0, 10.5.2 and 9.12.3; the diagnostic records the returned queries and hits; its own fix passes with 41 + 12 point-query tests. |
| cross-model review, refute lens (OpenAI GPT-5.6, executed) | real (high) | Own 3-method test fails 1 of 3, the empty CONTAINS (assertions on and off); that method alone also fails with two more seeds; its controls pass; the fix passes; the misuse defence rated weak. |
| devil's-advocate verifier | real, Low (medium risk that maintainers see it as noise) | Built the intended case (precondition, the LUCENE-9641 review, LatLonShape parity) and showed why it fails; an intended-lens cross-model review agreed and advised dropping the null-array and null-field sub-cases. |
| adjudication | confirmed (high), Low | Re-ran the repro on main with assertions off: fails; limited the claim to the empty array and the null element. |
| separate re-run, audit re-run and final repro check | reproduced; confirmed, Low | Fresh seeds on main (assertions on and off) and 10.6.0 fail; passes with the LatLonPoint fix shadowed in; the final check confirmed the minimal repro at Low. |
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 482 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,348 words
- Registry title
- LatLonPoint.newGeometryQuery with QueryRelation.CONTAINS returns a query that matches nothing instead of the documented IllegalArgumentException when the geometry array is empty or holds a null (LatLonDocValuesField.newSlowGeometryQuery does the same for a null element), because the CONTAINS shortcut runs before any argument check (since 8.8.0, LUCENE-9641)
- Area
- core / document: LatLon point geometry query factories (LatLonPoint.newGeometryQuery CONTAINS shortcut, LatLonDocValuesField.newSlowGeometryQuery)
- Priority
- Low, rank 418: A CONTAINS geometry query on a LatLonPoint field built from an empty array, or one holding a null, silently matches nothing instead of throwing the documented IllegalArgumentException, so the caller's mistake goes unnoticed, in every release since 8.8.0. Valid geometries get correct hits, and only callers that pass a bad array with this niche relation reach it.
- Impact class
- minor
- Impact note
- Applications that build a CONTAINS geometry query on a LatLonPoint field from a caller-built array that turns out empty, or that holds a null slot, get no IllegalArgumentException but a query that silently matches no document, so the caller's mistake goes unnoticed; query construction only, so any index configuration, -ea and -da, every release from 8.8.0 to 10.6.0 and main; valid geometry arrays get correct hits, the doc-values twin newSlowGeometryQuery still rejects the empty array (but not a null element), and nothing is persisted.
- Summary
- You call LatLonPoint.newGeometryQuery("f", QueryRelation.CONTAINS) with no geometries (an empty array), or with an array that holds a null, and expect the IllegalArgumentException its javadoc promises ('if latLonGeometries is null, empty or contain a null'), which INTERSECTS and WITHIN and the doc-values twin LatLonDocValuesField.newSlowGeometryQuery("f", CONTAINS) throw for the empty array ('geometries must not be empty'). Instead you get a query that matches no document: a ConstantScoreQuery over an empty BooleanQuery for the empty array, and MatchNoDocsQuery('Contains LatLonPoint.newGeometryQuery with non-point geometries') for a null element, which newSlowGeometryQuery also returns for a null element. The CONTAINS shortcut runs before any argument check: its loop never runs for an empty array, and a null element fails 'instanceof Point' and lands in the branch meant for valid non-point geometries.
- Introduced
- 14009f4424d847ce84fa23a71d8066dfe6c70da0 'LUCENE-9641: Support for spatial relationships in LatLonPoint (#2155)' by Ignacio Vera (GitHub iverase), apache/lucene-solr#2155, issue LUCENE-9641 = #10680; branch_8x copy f73f6b11f0ed4bcbfce445152d813adb3327d249 (lucene-solr#2188), same day [read: git blame -w -M -C -C on the CONTAINS dispatch, makeContainsGeometryQuery, the @throws text and the doc-values CONTAINS loop; git log -S 'makeContainsGeometryQuery' returns only this commit; exec: 8.7.0 lacks the API, 8.8.0 fails]. It added the queryRelation parameter and dispatched CONTAINS to new shortcuts before any validation, removed LatLonPointInGeometryQuery and LatLonDocValuesPointInGeometryQuery (whose constructors had rejected a null or empty array and null elements with IllegalArgumentException), and rewrote the @throws clause, keeping 'latLonGeometries is null, empty or contain a null' (2021-01-08 (author and commit date 2021-01-08T08:16:58+01:00; branch_8x copy the same day); first release 8.8.0 (tag 2021-01-29))
- First affected version
- 8.8.0 [exec] (8.7.0 has no relation-taking newGeometryQuery, so the repro does not compile there [exec]; LUCENE-9552, which added newGeometryQuery itself, also first shipped in 8.8.0, so no release has the relation-taking factories without the defect)
- Affected versions
- Every release from 8.8.0 through 10.6.0 (4ae946e5a9b) and main. Executed and reproduced (the empty-array repro): 8.8.0, 8.8.1, 8.8.2, 8.9.0, 8.10.0, 8.10.1, 8.11.0, 8.11.1, 8.11.2, 8.11.3, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2, 10.6.0 (-ea and -da) and main 5adff78bb168 (-ea and -da) [exec]; all 34 main-line release tags 9.0.0-10.6.0 and 8.8.0/8.8.1 have the same validation-free newGeometryQuery and makeContainsGeometryQuery bodies, and newSlowGeometryQuery keeps the unchecked CONTAINS loop in every tag from 8.8.0 [read]. The null-element sub-case comes from the same commit [read] and was executed on main only (both factories). Not affected: 8.7.0 and older (no such API; 8.7.0 executed: compile error). Not fixed upstream: upstream main 1c2072f0284 (author date 2026-11-02) still dispatches CONTAINS to makeContainsGeometryQuery before any check [read, 2026-11-03]. Not new in 10.6.
- Status
- documented (brief, checks and a per-finding doc exist)
- Verification
- Confirmed by AI-run checks: an independent repro, a cross-model review that agreed, an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
- Found by
- semi-formal: proof-obligation audit
- Upstream and prior reports
- none found (2026-11-03: two 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 (122 and 174 hits, the one high-rated hit #12162 by term overlap only), the finder's own 51-query run (287 hits, 0 high), 26 manual GitHub searches incl. Elasticsearch, OpenSearch and Solr and items created or updated since 2026-10-30, 12 GitHub GraphQL searches incl. the archived apache/lucene-solr PRs, the changed files of all 409 open apache/lucene PRs (#16847, #12162 and #709 touch the files, not the factory code paths), every issue and PR created since 2026-11-01 (#16959-#16966, none geo), commits by author date on main, branch_10x and branch_10_6 (newest on the files 1518164afa3, author date 2026-04-01, #15874, instanceof patterns, unrelated), git log -S on makeContainsGeometryQuery and the MatchNoDocsQuery message, and 13 web queries; the independent reproduction's and the intended-behaviour check's searches agree). Re-checked 2026-11-03: newGeometryQuery still matches only #12162; makeContainsGeometryQuery, newSlowGeometryQuery, 'geometries must not be empty' and 'non-point geometries' match nothing; issues and PRs created since 2026-11-02 are #16962-#16966, none geo (#16962, a MatchNoDocsQuery constructor cleanup, changes no file under document/ or geo/); upstream main 1c2072f0284 still dispatches CONTAINS at LatLonPoint.java:337-338. Nearest items, none of them this bug: lucene-solr#2155 (LUCENE-9641, the introducing PR; the review settled MatchNoDocsQuery for valid non-point CONTAINS geometries, nobody raised empty or null input), #10680 (its issue), lucene-solr#1940 / #10591 (LUCENE-9552, added newGeometryQuery and its @throws), open and stale #12162 (LatLonField: keeps the @throws and delegates to the same factory, so it would inherit the gap; no report of it), #16847 and #709 (other code paths in the same files).
- Lucene commit tested
- main
5adff78bb168(2026-11-02) through gradle on 2026-11-03 in a scratch worktree: the one-test repro with assertions on and off without a fix (fails) and with the validate-first fix (passes), plus everyorg.apache.lucene.document.TestLatLon*andorg.apache.lucene.search.TestLatLon*suite with the fix (236 tests, 0 failures, 19 skipped) andcheckGoogleJavaFormat. The hunt's equivalent one-test JUnit failed on 8.8.0 through 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2 and 10.6.0 (release jars) and on main with assertions on and off; 8.7.0 has no such API. A public-API diagnostic on main recorded the returned queries and hit counts. Only Linux aarch64 with OpenJDK 25.0.3 was run.
- Finding record
lucene/core/src/java/org/apache/lucene/document/LatLonPoint.java:302-312 (at 5adff78bb168) (newPolygonQuery delegates to newGeometryQuery), :314-326 (javadoc: '@param latLonGeometries ... must not be null or empty', '@throws IllegalArgumentException if field is null, queryRelation is null, latLonGeometries is null, empty or contain a null' at :323-324), :327-341 (newGeometryQuery: INTERSECTS single-geometry shortcut :329-336, CONTAINS dispatch to makeContainsGeometryQuery :337-338 before any check, LatLonPointQuery :340), :343-355 (makeContainsGeometryQuery: non-Point -> MatchNoDocsQuery :346-348, which a null element also takes; an empty array skips the loop and :354 returns ConstantScoreQuery over a zero-clause BooleanQuery)lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesField.java:232-264 (at 5adff78bb168) (newSlowGeometryQuery: the same @throws at :245-246; CONTAINS loop :255-261 returns MatchNoDocsQuery for a non-Point or null element; the empty array reaches LatLonDocValuesQuery and is rejected)lucene/core/src/java/org/apache/lucene/geo/LatLonGeometry.java:24-45 (at 5adff78bb168) (create: 'geometries must not be null', 'geometries must not be empty', 'geometries[i] must not be null'), lucene/core/src/java/org/apache/lucene/document/SpatialQuery.java:74-85 (field and queryRelation checks; geometries.clone() at :83), reached only on the non-shortcut pathstwo Lucene main checkouts: sha1 of LatLonPoint.java (d4a52ea99997), LatLonDocValuesField.java (4fd2180c0080), SpatialQuery.java (cabef6daeb93), LatLonPointQuery.java (b54a847ca846), LatLonDocValuesQuery.java (ad0c238b14c0) and LatLonGeometry.java (bcf8e96637eb) identical at ea57360912ed and 5adff78bb168 (checked 2026-11-03)
- The finding in 30 seconds
lucene/core/src/java/org/apache/lucene/document/LatLonPoint.java:314-355 (at 5adff78bb168)lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesField.java:232-264 (at 5adff78bb168)lucene/core/src/java/org/apache/lucene/document/SpatialQuery.java:74-85; (at 5adff78bb168) lucene/core/src/java/org/apache/lucene/document/LatLonPointQuery.java:43-50; lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesQuery.java:48, :55-87; lucene/core/src/java/org/apache/lucene/geo/LatLonGeometry.java:24-45; lucene/core/src/java/org/apache/lucene/search/BooleanQuery.java:268-271
- Who is affected
lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesField.java:232-238 (at 5adff78bb168) (IndexOrDocValuesQuery pairing)
- Why it looks unintended
lucene/core/src/java/org/apache/lucene/document/LatLonPoint.java:323-324; (at 5adff78bb168) LatLonDocValuesField.java:245-246lucene/test-framework/src/java/org/apache/lucene/tests/geo/BaseGeoPointTestCase.java:472-483 (at 5adff78bb168)lucene/core/src/java/org/apache/lucene/document/LatLonShape.java:325-342, (at 5adff78bb168) :354-370; XYShape.java:263
- Diagrams
lucene/core/src/java/org/apache/lucene/document/LatLonPoint.java:337-355; (at 5adff78bb168) lucene/core/src/java/org/apache/lucene/geo/LatLonGeometry.java:24-45
- TestLatLonPointContainsNoGeometries.java (lucene/core, package org.apache.lucene.document, 1 test):
repro/LFV-1203/TestLatLonPointContainsNoGeometries.java(30 lines)
The diagram follows the repro and the public-API diagnostic on main 5adff78bb168 (LatLonPoint.java:337-355, LatLonGeometry.java:24-45).
assets/LFV-1203/bug.png(482 KB)
![Diagram of the bug: LatLonPoint.newGeometryQuery called with an empty geometry array. With INTERSECTS or WITHIN the call reaches the query constructor, whose argument check throws IllegalArgumentException: geometries must not be empty. With CONTAINS the call takes the CONTAINS shortcut before any check; its loop over the geometries never runs and it returns ConstantScore() over a BooleanQuery with 0 clauses, which finds 0 hits on an index whose point (0,0) CONTAINS [Point(0,0)] finds. A null element under CONTAINS is not a Point and gets MatchNoDocsQuery.](../assets/LFV-1203/bug.png)