diff --git a/lucene/core/src/java/org/apache/lucene/search/BooleanQuery.java b/lucene/core/src/java/org/apache/lucene/search/BooleanQuery.java index 20919dc1695..119d3ed13f7 100644 --- a/lucene/core/src/java/org/apache/lucene/search/BooleanQuery.java +++ b/lucene/core/src/java/org/apache/lucene/search/BooleanQuery.java @@ -124,7 +124,8 @@ public class BooleanQuery extends Query implements Iterable { private final int minimumNumberShouldMatch; private final List clauses; // used for toString() and getClauses() - private final Map> clauseSets; // used for equals/hashcode + // WARNING: Do not let clauseSets escape from this class as it breaks immutability: + private final Map> clauseSets; // used for equals/hashCode private BooleanQuery(int minimumNumberShouldMatch, BooleanClause[] clauses) { this.minimumNumberShouldMatch = minimumNumberShouldMatch; @@ -153,7 +154,9 @@ public class BooleanQuery extends Query implements Iterable { /** Return the collection of queries for the given {@link Occur}. */ public Collection getClauses(Occur occur) { - return clauseSets.get(occur); + // turn this immutable here, because we need to preserve the correct collection types for + // equals/hashCode! + return Collections.unmodifiableCollection(clauseSets.get(occur)); } /** diff --git a/lucene/core/src/test/org/apache/lucene/search/TestBooleanQuery.java b/lucene/core/src/test/org/apache/lucene/search/TestBooleanQuery.java index 3c9fa764ba4..9e014dfb875 100644 --- a/lucene/core/src/test/org/apache/lucene/search/TestBooleanQuery.java +++ b/lucene/core/src/test/org/apache/lucene/search/TestBooleanQuery.java @@ -1383,4 +1383,36 @@ public class TestBooleanQuery extends LuceneTestCase { } }); } + + public void testClauseSetsImmutability() throws Exception { + Term a = new Term("f", "a"); + Term b = new Term("f", "b"); + Term c = new Term("f", "c"); + Term d = new Term("f", "d"); + BooleanQuery.Builder bqBuilder = new BooleanQuery.Builder(); + bqBuilder.add(new TermQuery(a), Occur.SHOULD); + bqBuilder.add(new TermQuery(a), Occur.SHOULD); + bqBuilder.add(new TermQuery(b), Occur.MUST); + bqBuilder.add(new TermQuery(b), Occur.MUST); + bqBuilder.add(new TermQuery(c), Occur.FILTER); + bqBuilder.add(new TermQuery(c), Occur.FILTER); + bqBuilder.add(new TermQuery(d), Occur.MUST_NOT); + bqBuilder.add(new TermQuery(d), Occur.MUST_NOT); + BooleanQuery bq = bqBuilder.build(); + // should and must are not dedupliacated + assertEquals(2, bq.getClauses(Occur.SHOULD).size()); + assertEquals(2, bq.getClauses(Occur.MUST).size()); + // filter and must not are deduplicated + assertEquals(1, bq.getClauses(Occur.FILTER).size()); + assertEquals(1, bq.getClauses(Occur.MUST_NOT).size()); + // check immutability + for (var occur : Occur.values()) { + assertThrows( + UnsupportedOperationException.class, + () -> bq.getClauses(occur).add(new MatchNoDocsQuery())); + } + assertThrows( + UnsupportedOperationException.class, + () -> bq.clauses().add(new BooleanClause(new MatchNoDocsQuery(), Occur.SHOULD))); + } }