Merge branch 'stable-3.6' into stable-3.7

* stable-3.6:
  Cache repository locations in LocalDiskRepositoryManager
  Don't always rewrite And/OrPredicate to And/OrSource
  Improve performance of parentof operator
  AndSource: consider cardinality while choosing the data source
  Ensure reindex can see all data
  Update git submodules
  Update git submodules
  Skip detecting content renames for binary files

Release-Notes: skip
Change-Id: I452141f72d2299fafb2ab083a3ac206ef68a9d78
diff --git a/java/com/google/gerrit/index/query/AndSource.java b/java/com/google/gerrit/index/query/AndSource.java
index 2a04051..f7feaa0 100644
--- a/java/com/google/gerrit/index/query/AndSource.java
+++ b/java/com/google/gerrit/index/query/AndSource.java
@@ -58,20 +58,27 @@
     this.indexConfig = indexConfig;
 
     int c = Integer.MAX_VALUE;
-    DataSource<T> s = null;
-    int minCost = Integer.MAX_VALUE;
+    Predicate<T> selectedSource = null;
+    int minCardinality = Integer.MAX_VALUE;
     for (Predicate<T> p : getChildren()) {
       if (p instanceof DataSource) {
-        c = Math.min(c, ((DataSource<?>) p).getCardinality());
+        DataSource<T> source = (DataSource<T>) p;
+        int cardinality = source.getCardinality();
+        c = Math.min(c, source.getCardinality());
 
-        int cost = p.estimateCost();
-        if (cost < minCost) {
-          s = toPaginatingSource(p);
-          minCost = cost;
+        if (selectedSource == null
+            || cardinality < minCardinality
+            || (cardinality == minCardinality
+                && p.estimateCost() < selectedSource.estimateCost())) {
+          selectedSource = p;
+          minCardinality = cardinality;
         }
       }
     }
-    this.source = s;
+    if (selectedSource == null) {
+      throw new IllegalArgumentException("No DataSource Found");
+    }
+    this.source = toPaginatingSource(selectedSource);
     this.cardinality = c;
   }
 
diff --git a/java/com/google/gerrit/pgm/util/BatchProgramModule.java b/java/com/google/gerrit/pgm/util/BatchProgramModule.java
index 18919ed..05b50da 100644
--- a/java/com/google/gerrit/pgm/util/BatchProgramModule.java
+++ b/java/com/google/gerrit/pgm/util/BatchProgramModule.java
@@ -29,6 +29,7 @@
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.InternalUser;
 import com.google.gerrit.server.LibModuleLoader;
 import com.google.gerrit.server.LibModuleType;
 import com.google.gerrit.server.ModuleOverloader;
@@ -154,7 +155,7 @@
     bind(Realm.class).to(FakeRealm.class);
     bind(IdentifiedUser.class).toProvider(Providers.of(null));
     bind(EmailNewPatchSet.Factory.class).toProvider(Providers.of(null));
-    bind(CurrentUser.class).to(IdentifiedUser.class);
+    bind(CurrentUser.class).to(InternalUser.class);
     factory(PatchSetInserter.Factory.class);
     factory(RebaseChangeOp.Factory.class);
 
diff --git a/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java b/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
index daa2e09..57d37fa 100644
--- a/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
+++ b/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
@@ -34,8 +34,10 @@
 import java.nio.file.attribute.BasicFileAttributes;
 import java.util.Collections;
 import java.util.EnumSet;
+import java.util.Map;
 import java.util.NavigableSet;
 import java.util.TreeSet;
+import java.util.concurrent.ConcurrentHashMap;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ConfigConstants;
@@ -109,6 +111,7 @@
   }
 
   private final Path basePath;
+  private final Map<Project.NameKey, FileKey> fileKeyByProject = new ConcurrentHashMap<>();
 
   @Inject
   LocalDiskRepositoryManager(SitePaths site, @GerritServerConfig Config cfg) {
@@ -153,17 +156,23 @@
 
   @Override
   public Repository openRepository(Project.NameKey name) throws RepositoryNotFoundException {
-    return openRepository(getBasePath(name), name);
-  }
+    FileKey cachedLocation = fileKeyByProject.get(name);
+    if (cachedLocation != null) {
+      try {
+        return RepositoryCache.open(cachedLocation);
+      } catch (IOException e) {
+        fileKeyByProject.remove(name, cachedLocation);
+      }
+    }
 
-  private Repository openRepository(Path path, Project.NameKey name)
-      throws RepositoryNotFoundException {
     if (isUnreasonableName(name)) {
       throw new RepositoryNotFoundException("Invalid name: " + name);
     }
-    FileKey loc = FileKey.lenient(path.resolve(name.get()).toFile(), FS.DETECTED);
+    FileKey location = FileKey.lenient(getBasePath(name).resolve(name.get()).toFile(), FS.DETECTED);
     try {
-      return RepositoryCache.open(loc);
+      Repository repo = RepositoryCache.open(location);
+      fileKeyByProject.put(name, location);
+      return repo;
     } catch (IOException e) {
       throw new RepositoryNotFoundException("Cannot open repository " + name, e);
     }
diff --git a/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java b/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java
index a1756bf..e64d1fe 100644
--- a/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java
+++ b/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java
@@ -47,6 +47,7 @@
 import java.util.BitSet;
 import java.util.EnumSet;
 import java.util.List;
+import java.util.Optional;
 import java.util.Set;
 import org.eclipse.jgit.util.MutableInteger;
 
@@ -285,9 +286,17 @@
 
   private Predicate<ChangeData> copy(Predicate<ChangeData> in, List<Predicate<ChangeData>> all) {
     if (in instanceof AndPredicate) {
-      return new AndChangeSource(all, config);
+      Optional<Predicate<ChangeData>> atLeastOneChangeDataSource =
+          all.stream().filter(p -> (p instanceof ChangeDataSource)).findAny();
+      if (atLeastOneChangeDataSource.isPresent()) {
+        return new AndChangeSource(all, config);
+      }
     } else if (in instanceof OrPredicate) {
-      return new OrSource(all);
+      Optional<Predicate<ChangeData>> nonChangeDataSource =
+          all.stream().filter(p -> !(p instanceof ChangeDataSource)).findAny();
+      if (!nonChangeDataSource.isPresent()) {
+        return new OrSource(all);
+      }
     }
     return in.copy(all);
   }
diff --git a/java/com/google/gerrit/server/patch/diff/ModifiedFilesCacheImpl.java b/java/com/google/gerrit/server/patch/diff/ModifiedFilesCacheImpl.java
index 9954d96..41f2fed 100644
--- a/java/com/google/gerrit/server/patch/diff/ModifiedFilesCacheImpl.java
+++ b/java/com/google/gerrit/server/patch/diff/ModifiedFilesCacheImpl.java
@@ -82,7 +82,7 @@
             .valueSerializer(GitModifiedFilesCacheImpl.ValueSerializer.INSTANCE)
             .maximumWeight(10 << 20)
             .weigher(ModifiedFilesWeigher.class)
-            .version(3)
+            .version(4)
             .loader(ModifiedFilesLoader.class);
       }
     };
diff --git a/java/com/google/gerrit/server/patch/gitdiff/GitModifiedFilesCacheImpl.java b/java/com/google/gerrit/server/patch/gitdiff/GitModifiedFilesCacheImpl.java
index 51a592d..b70f6e1 100644
--- a/java/com/google/gerrit/server/patch/gitdiff/GitModifiedFilesCacheImpl.java
+++ b/java/com/google/gerrit/server/patch/gitdiff/GitModifiedFilesCacheImpl.java
@@ -82,7 +82,7 @@
             .weigher(GitModifiedFilesWeigher.class)
             // The cache is using the default disk limit as per section cache.<name>.diskLimit
             // in the cache documentation link.
-            .version(1)
+            .version(2)
             .loader(GitModifiedFilesCacheImpl.Loader.class);
       }
     };
@@ -131,6 +131,8 @@
           df.setDetectRenames(true);
           df.getRenameDetector().setRenameScore(key.renameScore());
         }
+        // Skip detecting content renames for binary files.
+        df.getRenameDetector().setSkipContentRenamesForBinaryFiles(true);
         // The scan method only returns the file paths that are different. Callers may choose to
         // format these paths themselves.
         return df.scan(key.aTree().equals(ObjectId.zeroId()) ? null : key.aTree(), key.bTree());
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index e99306a..1c6235b 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -26,6 +26,7 @@
 import com.google.common.base.Splitter;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.entities.Account;
@@ -109,6 +110,8 @@
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
 
 /** Parses a query string meant to be applied to change objects. */
 public class ChangeQueryBuilder extends QueryBuilder<ChangeData, ChangeQueryBuilder> {
@@ -796,11 +799,28 @@
     List<ChangeData> changes = parseChangeData(value);
     List<Predicate<ChangeData>> or = new ArrayList<>(changes.size());
     for (ChangeData c : changes) {
-      or.add(new ParentOfPredicate(value, c, args.repoManager));
+      for (RevCommit revCommit : getParents(c)) {
+        or.add(ChangePredicates.commitPrefix(revCommit.getId().getName()));
+      }
     }
     return Predicate.or(or);
   }
 
+  private Set<RevCommit> getParents(ChangeData change) {
+    PatchSet ps = change.currentPatchSet();
+    try (Repository repo = args.repoManager.openRepository(change.project());
+        RevWalk walk = new RevWalk(repo)) {
+      RevCommit c = walk.parseCommit(ps.commitId());
+      return Sets.newHashSet(c.getParents());
+    } catch (IOException e) {
+      throw new StorageException(
+          String.format(
+              "Loading commit %s for ps %d of change %d failed.",
+              ps.commitId(), ps.id().get(), ps.id().changeId().get()),
+          e);
+    }
+  }
+
   @Operator
   public Predicate<ChangeData> parentproject(String name) {
     return new ParentProjectPredicate(args.projectCache, args.childProjects, name);
diff --git a/java/com/google/gerrit/server/query/change/OrSource.java b/java/com/google/gerrit/server/query/change/OrSource.java
index 83535c9..f2de536 100644
--- a/java/com/google/gerrit/server/query/change/OrSource.java
+++ b/java/com/google/gerrit/server/query/change/OrSource.java
@@ -18,7 +18,6 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.entities.Change;
-import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.query.FieldBundle;
 import com.google.gerrit.index.query.LazyResultSet;
 import com.google.gerrit.index.query.OrPredicate;
@@ -35,16 +34,15 @@
 
   public OrSource(Collection<? extends Predicate<ChangeData>> that) {
     super(that);
+    Optional<Predicate<ChangeData>> nonChangeDataSource =
+        getChildren().stream().filter(p -> !(p instanceof ChangeDataSource)).findAny();
+    if (nonChangeDataSource.isPresent()) {
+      throw new IllegalArgumentException("No ChangeDataSource: " + nonChangeDataSource.get());
+    }
   }
 
   @Override
   public ResultSet<ChangeData> read() {
-    Optional<Predicate<ChangeData>> nonChangeDataSource =
-        getChildren().stream().filter(p -> !(p instanceof ChangeDataSource)).findAny();
-    if (nonChangeDataSource.isPresent()) {
-      throw new StorageException("No ChangeDataSource: " + nonChangeDataSource.get());
-    }
-
     // ResultSets are lazy. Calling #read here first and then dealing with ResultSets only when
     // requested allows the index to run asynchronous queries.
     List<ResultSet<ChangeData>> results =
diff --git a/java/com/google/gerrit/server/query/change/ParentOfPredicate.java b/java/com/google/gerrit/server/query/change/ParentOfPredicate.java
deleted file mode 100644
index e48d586..0000000
--- a/java/com/google/gerrit/server/query/change/ParentOfPredicate.java
+++ /dev/null
@@ -1,62 +0,0 @@
-// Copyright (C) 2021 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.query.change;
-
-import com.google.common.collect.Sets;
-import com.google.gerrit.entities.PatchSet;
-import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.index.query.Matchable;
-import com.google.gerrit.index.query.OperatorPredicate;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import java.io.IOException;
-import java.util.Set;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevWalk;
-
-public class ParentOfPredicate extends OperatorPredicate<ChangeData>
-    implements Matchable<ChangeData> {
-  protected final Set<RevCommit> parents;
-
-  public ParentOfPredicate(String value, ChangeData change, GitRepositoryManager repoManager) {
-    super(ChangeQueryBuilder.FIELD_PARENTOF, value);
-    this.parents = getParents(change, repoManager);
-  }
-
-  @Override
-  public boolean match(ChangeData changeData) {
-    return changeData.patchSets().stream().anyMatch(ps -> parents.contains(ps.commitId()));
-  }
-
-  @Override
-  public int getCost() {
-    return 1;
-  }
-
-  protected Set<RevCommit> getParents(ChangeData change, GitRepositoryManager repoManager) {
-    PatchSet ps = change.currentPatchSet();
-    try (Repository repo = repoManager.openRepository(change.project());
-        RevWalk walk = new RevWalk(repo)) {
-      RevCommit c = walk.parseCommit(ps.commitId());
-      return Sets.newHashSet(c.getParents());
-    } catch (IOException e) {
-      throw new StorageException(
-          String.format(
-              "Loading commit %s for ps %d of change %d failed.",
-              ps.commitId(), ps.id().get(), ps.id().changeId().get()),
-          e);
-    }
-  }
-}
diff --git a/javatests/com/google/gerrit/acceptance/ssh/QueryIT.java b/javatests/com/google/gerrit/acceptance/ssh/QueryIT.java
index 72498c0..912c464 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/QueryIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/QueryIT.java
@@ -24,12 +24,24 @@
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.SshSession;
 import com.google.gerrit.acceptance.UseSsh;
+import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.ReviewerInput;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.client.Side;
+import com.google.gerrit.index.query.FieldBundle;
+import com.google.gerrit.index.query.ListResultSet;
+import com.google.gerrit.index.query.Matchable;
+import com.google.gerrit.index.query.OperatorPredicate;
+import com.google.gerrit.index.query.Predicate;
+import com.google.gerrit.index.query.QueryParseException;
+import com.google.gerrit.index.query.ResultSet;
 import com.google.gerrit.server.data.ChangeAttribute;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeDataSource;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
 import com.google.gson.Gson;
+import com.google.inject.AbstractModule;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -301,6 +313,74 @@
     }
   }
 
+  @Test
+  public void chooseCheapestDatasource() throws Exception {
+    try (AutoCloseable ignored = installPlugin("myplugin", SamplePluginModule.class)) {
+      for (int i = 0; i < 5; i++) {
+        createChange();
+      }
+      List<ChangeAttribute> changes =
+          executeSuccessfulQuery("status:open " + CheapSource.FIELD + "_myplugin:foo");
+      assertThat(changes).hasSize(0);
+    }
+  }
+
+  protected static class SamplePluginModule extends AbstractModule {
+    @Override
+    public void configure() {
+      bind(ChangeQueryBuilder.ChangeOperatorFactory.class)
+          .annotatedWith(Exports.named(CheapSource.FIELD))
+          .to(CheapSourceOperator.class);
+    }
+  }
+
+  protected static class CheapSourceOperator implements ChangeQueryBuilder.ChangeOperatorFactory {
+    @Override
+    public Predicate<ChangeData> create(ChangeQueryBuilder builder, String value)
+        throws QueryParseException {
+      return new CheapSource(value);
+    }
+  }
+
+  protected static class CheapSource extends OperatorPredicate<ChangeData>
+      implements Matchable<ChangeData>, ChangeDataSource {
+    public static final String FIELD = "cheapsource";
+
+    public CheapSource(String value) {
+      super(FIELD, value);
+    }
+
+    @Override
+    public int getCardinality() {
+      return 1;
+    }
+
+    @Override
+    public ResultSet<ChangeData> read() {
+      return new ListResultSet<>(new ArrayList<>());
+    }
+
+    @Override
+    public ResultSet<FieldBundle> readRaw() {
+      return null;
+    }
+
+    @Override
+    public boolean match(ChangeData object) {
+      return true;
+    }
+
+    @Override
+    public int getCost() {
+      return 1;
+    }
+
+    @Override
+    public boolean hasChange() {
+      return false;
+    }
+  }
+
   private List<ChangeAttribute> executeSuccessfulQuery(String params, SshSession session)
       throws Exception {
     String rawResponse = session.exec("gerrit query --format=JSON " + params);
diff --git a/javatests/com/google/gerrit/index/query/AndPredicateTest.java b/javatests/com/google/gerrit/index/query/AndPredicateTest.java
index 01fa99b..860a9fd 100644
--- a/javatests/com/google/gerrit/index/query/AndPredicateTest.java
+++ b/javatests/com/google/gerrit/index/query/AndPredicateTest.java
@@ -29,8 +29,8 @@
 public class AndPredicateTest extends PredicateTest {
   @Test
   public void children() {
-    final TestPredicate a = f("author", "alice");
-    final TestPredicate b = f("author", "bob");
+    final TestPredicate<String> a = f("author", "alice");
+    final TestPredicate<String> b = f("author", "bob");
     final Predicate<String> n = and(a, b);
     assertEquals(2, n.getChildCount());
     assertSame(a, n.getChild(0));
@@ -39,8 +39,8 @@
 
   @Test
   public void childrenUnmodifiable() {
-    final TestPredicate a = f("author", "alice");
-    final TestPredicate b = f("author", "bob");
+    final TestPredicate<String> a = f("author", "alice");
+    final TestPredicate<String> b = f("author", "bob");
     final Predicate<String> n = and(a, b);
 
     assertThrows(UnsupportedOperationException.class, () -> n.getChildren().clear());
@@ -60,18 +60,18 @@
 
   @Test
   public void testToString() {
-    final TestPredicate a = f("q", "alice");
-    final TestPredicate b = f("q", "bob");
-    final TestPredicate c = f("q", "charlie");
+    final TestPredicate<String> a = f("q", "alice");
+    final TestPredicate<String> b = f("q", "bob");
+    final TestPredicate<String> c = f("q", "charlie");
     assertEquals("(q:alice q:bob)", and(a, b).toString());
     assertEquals("(q:alice q:bob q:charlie)", and(a, b, c).toString());
   }
 
   @Test
   public void testEquals() {
-    final TestPredicate a = f("author", "alice");
-    final TestPredicate b = f("author", "bob");
-    final TestPredicate c = f("author", "charlie");
+    final TestPredicate<String> a = f("author", "alice");
+    final TestPredicate<String> b = f("author", "bob");
+    final TestPredicate<String> c = f("author", "charlie");
 
     assertTrue(and(a, b).equals(and(a, b)));
     assertTrue(and(a, b, c).equals(and(a, b, c)));
@@ -84,9 +84,9 @@
 
   @Test
   public void testHashCode() {
-    final TestPredicate a = f("author", "alice");
-    final TestPredicate b = f("author", "bob");
-    final TestPredicate c = f("author", "charlie");
+    final TestPredicate<String> a = f("author", "alice");
+    final TestPredicate<String> b = f("author", "bob");
+    final TestPredicate<String> c = f("author", "charlie");
 
     assertTrue(and(a, b).hashCode() == and(a, b).hashCode());
     assertTrue(and(a, b, c).hashCode() == and(a, b, c).hashCode());
@@ -95,11 +95,11 @@
 
   @Test
   public void testCopy() {
-    final TestPredicate a = f("author", "alice");
-    final TestPredicate b = f("author", "bob");
-    final TestPredicate c = f("author", "charlie");
-    final List<TestPredicate> s2 = ImmutableList.of(a, b);
-    final List<TestPredicate> s3 = ImmutableList.of(a, b, c);
+    final TestPredicate<String> a = f("author", "alice");
+    final TestPredicate<String> b = f("author", "bob");
+    final TestPredicate<String> c = f("author", "charlie");
+    final List<TestPredicate<String>> s2 = ImmutableList.of(a, b);
+    final List<TestPredicate<String>> s3 = ImmutableList.of(a, b, c);
     final Predicate<String> n2 = and(a, b);
 
     assertNotSame(n2, n2.copy(s2));
diff --git a/javatests/com/google/gerrit/index/query/AndSourceTest.java b/javatests/com/google/gerrit/index/query/AndSourceTest.java
index 3ae48fb..8b95bff 100644
--- a/javatests/com/google/gerrit/index/query/AndSourceTest.java
+++ b/javatests/com/google/gerrit/index/query/AndSourceTest.java
@@ -14,20 +14,35 @@
 
 package com.google.gerrit.index.query;
 
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
 import com.google.common.collect.Lists;
+import com.google.gerrit.server.query.change.ChangeData;
 import org.junit.Test;
 
 public class AndSourceTest extends PredicateTest {
   @Test
   public void ensureLowerCostPredicateRunsFirst() {
-    TestMatchablePredicate p1 = new TestMatchablePredicate("predicate1", "foo", 10);
-    TestMatchablePredicate p2 = new TestMatchablePredicate("predicate2", "foo", 1);
+    TestDataSourcePredicate p1 = new TestDataSourcePredicate("predicate1", "foo", 10, 10);
+    TestDataSourcePredicate p2 = new TestDataSourcePredicate("predicate2", "foo", 1, 10);
     AndSource<String> andSource = new AndSource<>(Lists.newArrayList(p1, p2), null);
     andSource.match("bar");
     assertFalse(p1.ranMatch);
     assertTrue(p2.ranMatch);
   }
+
+  @Test
+  public void ensureAtLeastOneChildIsADataSource() {
+    TestMatchablePredicate<ChangeData> p1 = new TestMatchablePredicate<>("predicate1", "foo", 1);
+    TestMatchablePredicate<ChangeData> p2 = new TestMatchablePredicate<>("predicate2", "foo", 1);
+
+    IllegalArgumentException thrown =
+        assertThrows(
+            IllegalArgumentException.class,
+            () -> new AndSource<>(Lists.newArrayList(p1, p2), null));
+    assertThat(thrown).hasMessageThat().contains("No DataSource Found");
+  }
 }
diff --git a/javatests/com/google/gerrit/index/query/NotPredicateTest.java b/javatests/com/google/gerrit/index/query/NotPredicateTest.java
index 3d1839d..c476bc9 100644
--- a/javatests/com/google/gerrit/index/query/NotPredicateTest.java
+++ b/javatests/com/google/gerrit/index/query/NotPredicateTest.java
@@ -30,7 +30,7 @@
 public class NotPredicateTest extends PredicateTest {
   @Test
   public void notNot() {
-    final TestPredicate p = f("author", "bob");
+    final TestPredicate<String> p = f("author", "bob");
     final Predicate<String> n = not(p);
     assertTrue(n instanceof NotPredicate);
     assertNotSame(p, n);
@@ -39,7 +39,7 @@
 
   @Test
   public void children() {
-    final TestPredicate p = f("author", "bob");
+    final TestPredicate<String> p = f("author", "bob");
     final Predicate<String> n = not(p);
     assertEquals(1, n.getChildCount());
     assertSame(p, n.getChild(0));
@@ -47,7 +47,7 @@
 
   @Test
   public void childrenUnmodifiable() {
-    final TestPredicate p = f("author", "bob");
+    final TestPredicate<String> p = f("author", "bob");
     final Predicate<String> n = not(p);
 
     assertThrows(UnsupportedOperationException.class, () -> n.getChildren().clear());
@@ -88,10 +88,10 @@
   @Test
   @SuppressWarnings({"rawtypes", "unchecked"})
   public void testCopy() {
-    final TestPredicate a = f("author", "alice");
-    final TestPredicate b = f("author", "bob");
-    final List<TestPredicate> sa = Collections.singletonList(a);
-    final List<TestPredicate> sb = Collections.singletonList(b);
+    final TestPredicate<String> a = f("author", "alice");
+    final TestPredicate<String> b = f("author", "bob");
+    final List<TestPredicate<String>> sa = Collections.singletonList(a);
+    final List<TestPredicate<String>> sb = Collections.singletonList(b);
     final Predicate n = not(a);
 
     assertNotSame(n, n.copy(sa));
diff --git a/javatests/com/google/gerrit/index/query/OrPredicateTest.java b/javatests/com/google/gerrit/index/query/OrPredicateTest.java
index 4d6c6e1..e5c9672 100644
--- a/javatests/com/google/gerrit/index/query/OrPredicateTest.java
+++ b/javatests/com/google/gerrit/index/query/OrPredicateTest.java
@@ -29,8 +29,8 @@
 public class OrPredicateTest extends PredicateTest {
   @Test
   public void children() {
-    final TestPredicate a = f("author", "alice");
-    final TestPredicate b = f("author", "bob");
+    final TestPredicate<String> a = f("author", "alice");
+    final TestPredicate<String> b = f("author", "bob");
     final Predicate<String> n = or(a, b);
     assertEquals(2, n.getChildCount());
     assertSame(a, n.getChild(0));
@@ -39,8 +39,8 @@
 
   @Test
   public void childrenUnmodifiable() {
-    final TestPredicate a = f("author", "alice");
-    final TestPredicate b = f("author", "bob");
+    final TestPredicate<String> a = f("author", "alice");
+    final TestPredicate<String> b = f("author", "bob");
     final Predicate<String> n = or(a, b);
 
     assertThrows(UnsupportedOperationException.class, () -> n.getChildren().clear());
@@ -60,18 +60,18 @@
 
   @Test
   public void testToString() {
-    final TestPredicate a = f("q", "alice");
-    final TestPredicate b = f("q", "bob");
-    final TestPredicate c = f("q", "charlie");
+    final TestPredicate<String> a = f("q", "alice");
+    final TestPredicate<String> b = f("q", "bob");
+    final TestPredicate<String> c = f("q", "charlie");
     assertEquals("(q:alice OR q:bob)", or(a, b).toString());
     assertEquals("(q:alice OR q:bob OR q:charlie)", or(a, b, c).toString());
   }
 
   @Test
   public void testEquals() {
-    final TestPredicate a = f("author", "alice");
-    final TestPredicate b = f("author", "bob");
-    final TestPredicate c = f("author", "charlie");
+    final TestPredicate<String> a = f("author", "alice");
+    final TestPredicate<String> b = f("author", "bob");
+    final TestPredicate<String> c = f("author", "charlie");
 
     assertTrue(or(a, b).equals(or(a, b)));
     assertTrue(or(a, b, c).equals(or(a, b, c)));
@@ -84,9 +84,9 @@
 
   @Test
   public void testHashCode() {
-    final TestPredicate a = f("author", "alice");
-    final TestPredicate b = f("author", "bob");
-    final TestPredicate c = f("author", "charlie");
+    final TestPredicate<String> a = f("author", "alice");
+    final TestPredicate<String> b = f("author", "bob");
+    final TestPredicate<String> c = f("author", "charlie");
 
     assertTrue(or(a, b).hashCode() == or(a, b).hashCode());
     assertTrue(or(a, b, c).hashCode() == or(a, b, c).hashCode());
@@ -95,11 +95,11 @@
 
   @Test
   public void testCopy() {
-    final TestPredicate a = f("author", "alice");
-    final TestPredicate b = f("author", "bob");
-    final TestPredicate c = f("author", "charlie");
-    final List<TestPredicate> s2 = ImmutableList.of(a, b);
-    final List<TestPredicate> s3 = ImmutableList.of(a, b, c);
+    final TestPredicate<String> a = f("author", "alice");
+    final TestPredicate<String> b = f("author", "bob");
+    final TestPredicate<String> c = f("author", "charlie");
+    final List<TestPredicate<String>> s2 = ImmutableList.of(a, b);
+    final List<TestPredicate<String>> s3 = ImmutableList.of(a, b, c);
     final Predicate<String> n2 = or(a, b);
 
     assertNotSame(n2, n2.copy(s2));
diff --git a/javatests/com/google/gerrit/index/query/OrSourceTest.java b/javatests/com/google/gerrit/index/query/OrSourceTest.java
new file mode 100644
index 0000000..8c8d09a
--- /dev/null
+++ b/javatests/com/google/gerrit/index/query/OrSourceTest.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2022 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.index.query;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.common.collect.Lists;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.OrSource;
+import org.junit.Test;
+
+public class OrSourceTest extends PredicateTest {
+  @Test
+  public void ensureAllChildrenAreDataSources() {
+    TestMatchablePredicate<ChangeData> p1 = new TestMatchablePredicate<>("predicate1", "foo", 10);
+    TestMatchablePredicate<ChangeData> p2 = new TestMatchablePredicate<>("predicate2", "foo", 1);
+
+    IllegalArgumentException thrown =
+        assertThrows(
+            IllegalArgumentException.class, () -> new OrSource(Lists.newArrayList(p1, p2)));
+    assertThat(thrown).hasMessageThat().contains("No ChangeDataSource");
+  }
+}
diff --git a/javatests/com/google/gerrit/index/query/PredicateTest.java b/javatests/com/google/gerrit/index/query/PredicateTest.java
index 8cb8d17..b8e0fbb 100644
--- a/javatests/com/google/gerrit/index/query/PredicateTest.java
+++ b/javatests/com/google/gerrit/index/query/PredicateTest.java
@@ -19,8 +19,33 @@
 @Ignore
 public abstract class PredicateTest {
   @SuppressWarnings("ProtectedMembersInFinalClass")
-  protected static final class TestMatchablePredicate extends TestPredicate
-      implements Matchable<String> {
+  protected static class TestDataSourcePredicate extends TestMatchablePredicate<String>
+      implements DataSource<String> {
+    protected final int cardinality;
+
+    protected TestDataSourcePredicate(String name, String value, int cost, int cardinality) {
+      super(name, value, cost);
+      this.cardinality = cardinality;
+    }
+
+    @Override
+    public int getCardinality() {
+      return cardinality;
+    }
+
+    @Override
+    public ResultSet<String> read() {
+      return null;
+    }
+
+    @Override
+    public ResultSet<FieldBundle> readRaw() {
+      return null;
+    }
+  }
+
+  protected static class TestMatchablePredicate<T> extends TestPredicate<T>
+      implements Matchable<T> {
     protected int cost;
     protected boolean ranMatch = false;
 
@@ -30,7 +55,7 @@
     }
 
     @Override
-    public boolean match(String object) {
+    public boolean match(T object) {
       ranMatch = true;
       return false;
     }
@@ -41,13 +66,13 @@
     }
   }
 
-  protected static class TestPredicate extends OperatorPredicate<String> {
-    private TestPredicate(String name, String value) {
+  protected static class TestPredicate<T> extends OperatorPredicate<T> {
+    protected TestPredicate(String name, String value) {
       super(name, value);
     }
   }
 
-  protected static TestPredicate f(String name, String value) {
-    return new TestPredicate(name, value);
+  protected static TestPredicate<String> f(String name, String value) {
+    return new TestPredicate<>(name, value);
   }
 }
diff --git a/javatests/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java b/javatests/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java
index 3d0fd25..cd3958d 100644
--- a/javatests/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java
+++ b/javatests/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java
@@ -19,7 +19,6 @@
 import static com.google.gerrit.entities.Change.Status.MERGED;
 import static com.google.gerrit.entities.Change.Status.NEW;
 import static com.google.gerrit.index.query.Predicate.and;
-import static com.google.gerrit.index.query.Predicate.or;
 import static com.google.gerrit.server.index.change.IndexedChangeQuery.convertOptions;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static org.junit.Assert.assertEquals;
@@ -28,13 +27,14 @@
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.index.IndexConfig;
 import com.google.gerrit.index.QueryOptions;
+import com.google.gerrit.index.query.AndPredicate;
+import com.google.gerrit.index.query.OrPredicate;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.server.query.change.AndChangeSource;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
 import com.google.gerrit.server.query.change.ChangeStatusPredicate;
-import com.google.gerrit.server.query.change.OrSource;
 import java.util.Arrays;
 import java.util.EnumSet;
 import java.util.Set;
@@ -116,8 +116,8 @@
 
     assertThat(out.getChild(0)).isEqualTo(query(firstIndexedSubQuery));
 
-    assertThat(out.getChild(1).getClass()).isSameInstanceAs(OrSource.class);
-    OrSource indexedSubTree = (OrSource) out.getChild(1);
+    assertThat(out.getChild(1).getClass()).isSameInstanceAs(OrPredicate.class);
+    OrPredicate indexedSubTree = (OrPredicate) out.getChild(1);
 
     Predicate<ChangeData> secondIndexedSubQuery = parse("foo:a OR file:b");
     assertThat(indexedSubTree.getChildren())
@@ -144,13 +144,10 @@
 
   @Test
   public void multipleIndexPredicates() throws Exception {
-    Predicate<ChangeData> in = parse("file:a OR foo:b OR file:c OR foo:d");
+    Predicate<ChangeData> in = parse("file:a OR file:b OR file:c");
     Predicate<ChangeData> out = rewrite(in);
-    assertThat(out.getClass()).isSameInstanceAs(OrSource.class);
-    assertThat(out.getChildren())
-        .containsExactly(
-            query(or(parse("file:a"), parse("file:c"))), parse("foo:b"), parse("foo:d"))
-        .inOrder();
+    assertThat(out.getClass()).isSameInstanceAs(IndexedChangeQuery.class);
+    assertEquals(query(in), out);
   }
 
   @Test
@@ -198,7 +195,7 @@
     int n = 3;
     Predicate<ChangeData> f = parse("file:a");
     Predicate<ChangeData> l = parse("limit:" + n);
-    Predicate<ChangeData> in = andSource(f, l);
+    Predicate<ChangeData> in = AndPredicate.and(f, l);
     assertThat(rewrite.rewrite(in, options(0, n))).isEqualTo(andSource(query(f, 3), l));
     assertThat(rewrite.rewrite(in, options(1, n))).isEqualTo(andSource(query(f, 4), l));
     assertThat(rewrite.rewrite(in, options(2, n))).isEqualTo(andSource(query(f, 5), l));