From 3982b7028532e42bc4216b5ba800de0f2f093b84 Mon Sep 17 00:00:00 2001 From: Dave Borowitz Date: Thu, 23 May 2013 17:01:34 -0700 Subject: [PATCH] Teach secondary index to handle boolean queries Modify ChangeIndex.getSource() to allow And/Or/NotPredicates in addition to IndexPredicates, and convert these to the appropriate index-specific query types. Add a preliminary rewrite step before the manual rewrite steps in ChangeQueryRewriter. This step attempts to find maximal subtrees that contain only boolean operations on IndexPredicates, i.e. exactly those subtrees that can now be queried in the index. The rewrite step is smart enough to partition children of a single boolean node into index/non-index predicates, but not smart enough to rearrange the tree structure beyond that. For each IndexPredicate type, determine whether it is only satisfiable by looking at the index; when partitioning children, a non-index-only predicates may be duplicated to remain as a hint to the database-based rewrite step. Change-Id: Ifc9e94f87388764781fdc3f50aba0286829c33c9 --- .../gerrit/lucene/LuceneChangeIndex.java | 63 ++++-- .../gerrit/lucene/LuceneIndexModule.java | 3 + .../gerrit/server/index/ChangeIndex.java | 9 +- .../gerrit/server/index/IndexPredicate.java | 8 + .../gerrit/server/index/NoIndexModule.java | 2 + .../gerrit/server/index/PredicateWrapper.java | 9 +- .../query/change/ChangeQueryBuilder.java | 6 +- .../query/change/ChangeQueryRewriter.java | 10 +- .../query/change/EqualsFilePredicate.java | 5 + .../server/query/change/IndexRewrite.java | 37 ++++ .../server/query/change/IndexRewriteImpl.java | 171 +++++++++++++++++ .../server/query/change/IndexRewriteTest.java | 181 ++++++++++++++++++ 12 files changed, 480 insertions(+), 24 deletions(-) create mode 100644 gerrit-server/src/main/java/com/google/gerrit/server/query/change/IndexRewrite.java create mode 100644 gerrit-server/src/main/java/com/google/gerrit/server/query/change/IndexRewriteImpl.java create mode 100644 gerrit-server/src/test/java/com/google/gerrit/server/query/change/IndexRewriteTest.java diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java index aa76355daf..b49d035e93 100644 --- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java +++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java @@ -16,6 +16,10 @@ package com.google.gerrit.lucene; import static com.google.gerrit.server.query.change.ChangeQueryBuilder.FIELD_CHANGE; +import static org.apache.lucene.search.BooleanClause.Occur.MUST; +import static org.apache.lucene.search.BooleanClause.Occur.MUST_NOT; +import static org.apache.lucene.search.BooleanClause.Occur.SHOULD; + import com.google.common.collect.Lists; import com.google.gerrit.extensions.events.LifecycleListener; import com.google.gerrit.reviewdb.client.Change; @@ -26,6 +30,10 @@ import com.google.gerrit.server.index.FieldDef; import com.google.gerrit.server.index.FieldDef.FillArgs; import com.google.gerrit.server.index.FieldType; import com.google.gerrit.server.index.IndexPredicate; +import com.google.gerrit.server.query.AndPredicate; +import com.google.gerrit.server.query.NotPredicate; +import com.google.gerrit.server.query.OrPredicate; +import com.google.gerrit.server.query.Predicate; import com.google.gerrit.server.query.QueryParseException; import com.google.gerrit.server.query.change.ChangeData; import com.google.gerrit.server.query.change.ChangeDataSource; @@ -43,6 +51,8 @@ import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.IndexWriterConfig; import org.apache.lucene.index.IndexWriterConfig.OpenMode; import org.apache.lucene.index.Term; +import org.apache.lucene.search.BooleanClause; +import org.apache.lucene.search.BooleanQuery; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.Query; import org.apache.lucene.search.ScoreDoc; @@ -132,15 +142,9 @@ public class LuceneChangeIndex implements ChangeIndex, LifecycleListener { } @Override - public ChangeDataSource getSource(IndexPredicate p) + public ChangeDataSource getSource(Predicate p) throws QueryParseException { - if (p.getType() == FieldType.INTEGER) { - return intQuery(p); - } else if (p.getType() == FieldType.EXACT) { - return exactQuery(p); - } else { - throw badFieldType(p.getType()); - } + return new QuerySource(toQuery(p)); } public Directory getDirectory() { @@ -156,13 +160,47 @@ public class LuceneChangeIndex implements ChangeIndex, LifecycleListener { searcherManager.maybeRefresh(); } + private Query toQuery(Predicate p) throws QueryParseException { + if (p.getClass() == AndPredicate.class) { + return booleanQuery(p, MUST); + } else if (p.getClass() == OrPredicate.class) { + return booleanQuery(p, SHOULD); + } else if (p.getClass() == NotPredicate.class) { + return booleanQuery(p, MUST_NOT); + } else if (p instanceof IndexPredicate) { + return fieldQuery((IndexPredicate) p); + } else { + throw new QueryParseException("Cannot convert to index predicate: " + p); + } + } + + private Query booleanQuery(Predicate p, BooleanClause.Occur o) + throws QueryParseException { + BooleanQuery q = new BooleanQuery(); + for (int i = 0; i < p.getChildCount(); i++) { + q.add(toQuery(p.getChild(i)), o); + } + return q; + } + + private Query fieldQuery(IndexPredicate p) + throws QueryParseException { + if (p.getType() == FieldType.INTEGER) { + return intQuery(p); + } else if (p.getType() == FieldType.EXACT) { + return exactQuery(p); + } else { + throw badFieldType(p.getType()); + } + } + private Term intTerm(String name, int value) { BytesRef bytes = new BytesRef(NumericUtils.BUF_SIZE_INT); NumericUtils.intToPrefixCodedBytes(value, 0, bytes); return new Term(name, bytes); } - private QuerySource intQuery(IndexPredicate p) + private Query intQuery(IndexPredicate p) throws QueryParseException { int value; try { @@ -172,12 +210,11 @@ public class LuceneChangeIndex implements ChangeIndex, LifecycleListener { } catch (IllegalArgumentException e) { throw new QueryParseException("not an integer: " + p.getValue()); } - return new QuerySource(new TermQuery(intTerm(p.getOperator(), value))); + return new TermQuery(intTerm(p.getOperator(), value)); } - private QuerySource exactQuery(IndexPredicate p) { - return new QuerySource(new TermQuery( - new Term(p.getOperator(), p.getValue()))); + private Query exactQuery(IndexPredicate p) { + return new TermQuery(new Term(p.getOperator(), p.getValue())); } private class QuerySource implements ChangeDataSource { diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java index 9c344bb0e6..e951ea65d9 100644 --- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java +++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java @@ -19,6 +19,8 @@ import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.index.ChangeIndex; import com.google.gerrit.server.index.ChangeIndexer; import com.google.gerrit.server.index.ChangeIndexerImpl; +import com.google.gerrit.server.query.change.IndexRewrite; +import com.google.gerrit.server.query.change.IndexRewriteImpl; import com.google.inject.Injector; import com.google.inject.Key; @@ -44,6 +46,7 @@ public class LuceneIndexModule extends LifecycleModule { protected void configure() { bind(ChangeIndex.class).to(LuceneChangeIndex.class); bind(ChangeIndexer.class).to(ChangeIndexerImpl.class); + bind(IndexRewrite.class).to(IndexRewriteImpl.class); listener().to(LuceneChangeIndex.class); if (checkVersion) { listener().to(IndexVersionCheck.class); diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndex.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndex.java index def42d1b8d..0a651420ec 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndex.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndex.java @@ -14,6 +14,7 @@ package com.google.gerrit.server.index; +import com.google.gerrit.server.query.Predicate; import com.google.gerrit.server.query.QueryParseException; import com.google.gerrit.server.query.change.ChangeData; import com.google.gerrit.server.query.change.ChangeDataSource; @@ -44,7 +45,7 @@ public interface ChangeIndex { } @Override - public ChangeDataSource getSource(IndexPredicate p) + public ChangeDataSource getSource(Predicate p) throws QueryParseException { throw new UnsupportedOperationException(); } @@ -81,12 +82,14 @@ public interface ChangeIndex { * Convert the given operator predicate into a source searching the index and * returning only the documents matching that predicate. * - * @param p the predicate to match. + * @param p the predicate to match. Must be a tree containing only AND, OR, + * or NOT predicates as internal nodes, and {@link IndexPredicate}s as + * leaves. * @return a source of documents matching the predicate. * * @throws QueryParseException if the predicate could not be converted to an * indexed data source. */ - public ChangeDataSource getSource(IndexPredicate p) + public ChangeDataSource getSource(Predicate p) throws QueryParseException; } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexPredicate.java index dbbd46799f..605582a1ad 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexPredicate.java @@ -28,4 +28,12 @@ public abstract class IndexPredicate extends OperatorPredicate { public FieldType getType() { return def.getType(); } + + /** + * @return whether this predicate can only be satisfied by looking at the + * secondary index, i.e. it cannot be expressed as a query over the DB. + */ + public boolean isIndexOnly() { + return false; + } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/NoIndexModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/NoIndexModule.java index 96f37cdcc0..1c4ffa82c8 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/index/NoIndexModule.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/NoIndexModule.java @@ -14,6 +14,7 @@ package com.google.gerrit.server.index; +import com.google.gerrit.server.query.change.IndexRewrite; import com.google.inject.AbstractModule; public class NoIndexModule extends AbstractModule { @@ -25,5 +26,6 @@ public class NoIndexModule extends AbstractModule { protected void configure() { bind(ChangeIndex.class).toInstance(ChangeIndex.DISABLED); bind(ChangeIndexer.class).toInstance(ChangeIndexer.DISABLED); + bind(IndexRewrite.class).toInstance(IndexRewrite.DISABLED); } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/PredicateWrapper.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/PredicateWrapper.java index c740b41f7d..1cddf82ed5 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/index/PredicateWrapper.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/PredicateWrapper.java @@ -33,10 +33,10 @@ import java.util.Collection; */ public class PredicateWrapper extends Predicate implements ChangeDataSource { - private final IndexPredicate pred; + private final Predicate pred; private final ChangeDataSource source; - public PredicateWrapper(ChangeIndex index, IndexPredicate pred) + public PredicateWrapper(ChangeIndex index, Predicate pred) throws QueryParseException { this.pred = pred; this.source = index.getSource(pred); @@ -84,4 +84,9 @@ public class PredicateWrapper extends Predicate implements && getClass() == other.getClass() && pred.equals(((PredicateWrapper) other).pred); } + + @Override + public String toString() { + return "index(" + pred + ")"; + } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java index cbf8599aa1..42074f92d2 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java @@ -31,7 +31,6 @@ import com.google.gerrit.server.account.GroupBackends; import com.google.gerrit.server.config.AllProjectsName; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.index.ChangeIndex; -import com.google.gerrit.server.index.PredicateWrapper; import com.google.gerrit.server.patch.PatchListCache; import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.ProjectCache; @@ -297,10 +296,7 @@ public class ChangeQueryBuilder extends QueryBuilder { } } else { if (!file.startsWith("^") && args.index != ChangeIndex.DISABLED) { - // TODO(dborowitz): Wrap predicates in query rewriter, not here. - return new PredicateWrapper( - args.index, - new EqualsFilePredicate(args.dbProvider, args.patchListCache, file)); + return new EqualsFilePredicate(args.dbProvider, args.patchListCache, file); } else { throw error("regular expression not permitted here: file:" + file); } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java index 361377a026..6760b048dd 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java @@ -43,11 +43,14 @@ public class ChangeQueryRewriter extends QueryRewriter { null, null, null, null, null), null)); private final Provider dbProvider; + private final IndexRewrite indexRewrite; @Inject - ChangeQueryRewriter(Provider dbProvider) { + ChangeQueryRewriter(Provider dbProvider, + IndexRewrite indexRewrite) { super(mydef); this.dbProvider = dbProvider; + this.indexRewrite = indexRewrite; } @Override @@ -60,6 +63,11 @@ public class ChangeQueryRewriter extends QueryRewriter { return hasSource(l) ? new OrSource(l) : super.or(l); } + @Override + public Predicate rewrite(Predicate in) { + return super.rewrite(indexRewrite.rewrite(in)); + } + @Rewrite("-status:open") @NoCostComputation public Predicate r00_notOpen() { diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java index 4a4093b2b2..dd8c970f1b 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java @@ -54,4 +54,9 @@ class EqualsFilePredicate extends IndexPredicate { public int getCost() { return 1; } + + @Override + public boolean isIndexOnly() { + return true; + } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IndexRewrite.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IndexRewrite.java new file mode 100644 index 0000000000..f3bca643cf --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IndexRewrite.java @@ -0,0 +1,37 @@ +// Copyright (C) 2013 The Android Open Source Project +// +// Licensed 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 com.google.gerrit.server.git; + +package com.google.gerrit.server.query.change; + +import com.google.gerrit.server.query.Predicate; + +public interface IndexRewrite { + /** Instance indicating secondary index is disabled. */ + public static final IndexRewrite DISABLED = new IndexRewrite() { + @Override + public Predicate rewrite(Predicate in) { + return in; + } + }; + + /** + * Rewrite a predicate to push as much boolean logic as possible into the + * secondary index query system. + * + * @param in predicate to rewrite. + * @return a predicate with some subtrees replaced with predicates that are + * also sources that query the index directly. + */ + public Predicate rewrite(Predicate in); +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IndexRewriteImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IndexRewriteImpl.java new file mode 100644 index 0000000000..c0e5483cb7 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IndexRewriteImpl.java @@ -0,0 +1,171 @@ +// Copyright (C) 2013 The Android Open Source Project +// +// Licensed 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 com.google.gerrit.server.git; + +package com.google.gerrit.server.query.change; + +import com.google.common.collect.Lists; +import com.google.gerrit.server.index.ChangeIndex; +import com.google.gerrit.server.index.IndexPredicate; +import com.google.gerrit.server.index.PredicateWrapper; +import com.google.gerrit.server.query.AndPredicate; +import com.google.gerrit.server.query.NotPredicate; +import com.google.gerrit.server.query.OrPredicate; +import com.google.gerrit.server.query.Predicate; +import com.google.gerrit.server.query.QueryParseException; +import com.google.inject.Inject; + +import java.util.BitSet; +import java.util.List; + +/** Rewriter that pushes boolean logic into the secondary index. */ +public class IndexRewriteImpl implements IndexRewrite { + private final ChangeIndex index; + + @Inject + IndexRewriteImpl(ChangeIndex index) { + this.index = index; + } + + @Override + public Predicate rewrite(Predicate in) { + Predicate out = rewriteImpl(in); + if (out == null) { + return in; + } else if (out == in) { + return wrap(out); + } else { + return out; + } + } + + /** + * Rewrite a single predicate subtree. + * + * @param in predicate to rewrite. + * @return {@code null} if no part of this subtree can be queried in the + * index directly. {@code in} if this subtree and all its children can be + * queried directly in the index. Otherwise, a predicate that is + * semantically equivalent, with some of its subtrees wrapped to query the + * index directly. + */ + private Predicate rewriteImpl(Predicate in) { + if (in instanceof IndexPredicate) { + return in; + } + if (!isRewritePossible(in)) { + return null; + } + int n = in.getChildCount(); + BitSet toKeep = new BitSet(n); + BitSet toWrap = new BitSet(n); + BitSet rewritten = new BitSet(n); + List> newChildren = Lists.newArrayListWithCapacity(n); + for (int i = 0; i < n; i++) { + Predicate c = in.getChild(i); + Predicate nc = rewriteImpl(c); + if (nc == null) { + toKeep.set(i); + newChildren.add(c); + } else if (nc == c) { + toWrap.set(i); + newChildren.add(nc); + } else { + rewritten.set(i); + newChildren.add(nc); + } + } + if (toKeep.cardinality() == n) { + return null; // Can't rewrite any children. + } + if (rewritten.cardinality() == n) { + // All children were partially, but not fully, rewritten. + return in.copy(newChildren); + } + if (toWrap.cardinality() == n) { + // All children can be fully rewritten, push work to parent. + return in; + } + return partitionChildren(in, newChildren, toWrap); + } + + + private Predicate partitionChildren(Predicate in, + List> newChildren, BitSet toWrap) { + if (toWrap.cardinality() == 1) { + int i = toWrap.nextSetBit(0); + newChildren.set(i, wrap(newChildren.get(i))); + return in.copy(newChildren); + } + + // Group all toWrap predicates into a wrapped subtree and place it as a + // sibling of the non-/partially-wrapped predicates. Assumes partitioning + // the children into arbitrary subtrees of the same type is logically + // equivalent to having them as siblings. + List> wrapped = Lists.newArrayListWithCapacity( + toWrap.cardinality()); + List> all = Lists.newArrayListWithCapacity( + newChildren.size() - toWrap.cardinality() + 1); + for (int i = 0; i < newChildren.size(); i++) { + Predicate child = newChildren.get(i); + if (toWrap.get(i)) { + wrapped.add(child); + if (allNonIndexOnly(child)) { + // Duplicate non-index-only predicate subtrees alongside the wrapped + // subtrees so they can provide index hints to the DB-based rewriter. + all.add(child); + } + } else { + all.add(child); + } + } + all.add(wrap(in.copy(wrapped))); + return in.copy(all); + } + + private static boolean allNonIndexOnly(Predicate p) { + if (p instanceof IndexPredicate) { + return !((IndexPredicate) p).isIndexOnly(); + } + if (p instanceof AndPredicate + || p instanceof OrPredicate + || p instanceof NotPredicate) { + for (int i = 0; i < p.getChildCount(); i++) { + if (!allNonIndexOnly(p.getChild(i))) { + return false; + } + } + return true; + } else { + return true; + } + } + + private PredicateWrapper wrap(Predicate p) { + try { + return new PredicateWrapper(index, p); + } catch (QueryParseException e) { + throw new IllegalStateException( + "Failed to convert " + p + " to index predicate", e); + } + } + + private static boolean isRewritePossible(Predicate p) { + if (p.getClass() != AndPredicate.class + && p.getClass() != OrPredicate.class + && p.getClass() != NotPredicate.class) { + return false; + } + return p.getChildCount() > 0; + } +} diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/IndexRewriteTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/IndexRewriteTest.java new file mode 100644 index 0000000000..fd3b695c5d --- /dev/null +++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/IndexRewriteTest.java @@ -0,0 +1,181 @@ +// Copyright (C) 2013 The Android Open Source Project +// +// Licensed 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 com.google.gerrit.server.git; + +package com.google.gerrit.server.query.change; + +import com.google.common.collect.ImmutableList; +import com.google.gerrit.server.index.ChangeIndex; +import com.google.gerrit.server.index.PredicateWrapper; +import com.google.gerrit.server.query.AndPredicate; +import com.google.gerrit.server.query.OrPredicate; +import com.google.gerrit.server.query.Predicate; +import com.google.gerrit.server.query.QueryParseException; +import com.google.gwtorm.server.OrmException; +import com.google.gwtorm.server.ResultSet; + +import junit.framework.TestCase; + +import java.io.IOException; + +@SuppressWarnings("unchecked") +public class IndexRewriteTest extends TestCase { + private static class DummyIndex implements ChangeIndex { + @Override + public void insert(ChangeData cd) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public void replace(ChangeData cd) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public ChangeDataSource getSource(Predicate p) + throws QueryParseException { + return new Source(); + } + } + + private static class Source implements ChangeDataSource { + @Override + public int getCardinality() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean hasChange() { + throw new UnsupportedOperationException(); + } + + @Override + public ResultSet read() throws OrmException { + throw new UnsupportedOperationException(); + } + } + + private DummyIndex index; + private ChangeQueryBuilder queryBuilder; + private IndexRewrite rewrite; + + @Override + public void setUp() throws Exception { + super.setUp(); + index = new DummyIndex(); + queryBuilder = new ChangeQueryBuilder( + new ChangeQueryBuilder.Arguments(null, null, null, null, null, null, + null, null, null, null, null, null), + null); + + rewrite = new IndexRewriteImpl(index); + } + + public void testIndexPredicate() throws Exception { + Predicate in = parse("file:a"); + assertEquals(wrap(in), rewrite(in)); + } + + public void testNonIndexPredicate() throws Exception { + Predicate in = parse("branch:a"); + assertSame(in, rewrite(in)); + } + + public void testIndexPredicates() throws Exception { + Predicate in = parse("file:a file:b"); + assertEquals(wrap(in), rewrite(in)); + } + + public void testNonIndexPredicates() throws Exception { + Predicate in = parse("branch:a OR branch:b"); + assertSame(in, rewrite(in)); + } + + public void testOneIndexPredicate() throws Exception { + Predicate in = parse("branch:a file:b"); + Predicate out = rewrite(in); + assertSame(AndPredicate.class, out.getClass()); + assertEquals(ImmutableList.of(in.getChild(0), wrap(in.getChild(1))), + out.getChildren()); + } + + public void testThreeLevelTreeWithAllIndexPredicates() throws Exception { + Predicate in = + parse("-status:abandoned (status:open OR status:merged)"); + assertEquals(wrap(in), rewrite.rewrite(in)); + } + + public void testThreeLevelTreeWithSomeIndexPredicates() throws Exception { + Predicate in = parse("-branch:a (file:b OR file:c)"); + Predicate out = rewrite(in); + assertEquals(AndPredicate.class, out.getClass()); + assertEquals(ImmutableList.of(in.getChild(0), wrap(in.getChild(1))), + out.getChildren()); + } + + public void testMultipleIndexPredicates() throws Exception { + Predicate in = + parse("file:a OR branch:b OR file:c OR branch:d"); + Predicate out = rewrite(in); + assertSame(OrPredicate.class, out.getClass()); + assertEquals(ImmutableList.of( + in.getChild(1), in.getChild(3), + wrap(Predicate.or(in.getChild(0), in.getChild(2)))), + out.getChildren()); + } + + public void testDuplicateSimpleNonIndexOnlyPredicates() throws Exception { + Predicate in = parse("status:new project:p file:a"); + Predicate out = rewrite(in); + assertSame(AndPredicate.class, out.getClass()); + assertEquals(ImmutableList.of( + in.getChild(0), in.getChild(1), + wrap(Predicate.and(in.getChild(0), in.getChild(2)))), + out.getChildren()); + } + + public void testDuplicateCompoundNonIndexOnlyPredicates() throws Exception { + Predicate in = + parse("(status:new OR status:draft) project:p file:a"); + Predicate out = rewrite(in); + assertSame(AndPredicate.class, out.getClass()); + assertEquals(ImmutableList.of( + in.getChild(0), in.getChild(1), + wrap(Predicate.and(in.getChild(0), in.getChild(2)))), + out.getChildren()); + } + + public void testDuplicateCompoundIndexOnlyPredicates() throws Exception { + Predicate in = + parse("(status:new OR file:a) project:p file:b"); + Predicate out = rewrite(in); + assertSame(AndPredicate.class, out.getClass()); + assertEquals(ImmutableList.of( + in.getChild(1), + wrap(Predicate.and(in.getChild(0), in.getChild(2)))), + out.getChildren()); + } + + private Predicate parse(String query) throws QueryParseException { + return queryBuilder.parse(query); + } + + private Predicate rewrite(Predicate in) { + return rewrite.rewrite(in); + } + + private PredicateWrapper wrap(Predicate p) + throws QueryParseException { + return new PredicateWrapper(index, p); + } +}