Merge "Implement generic matching for integer fields in ChangePredicate"
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHook.java b/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHook.java
index 6c1f097..5e951d3 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHook.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHook.java
@@ -27,9 +27,9 @@
 import com.google.gerrit.server.git.HookUtil;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangePredicates;
 import com.google.gerrit.server.query.change.ChangeStatusPredicate;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gerrit.server.query.change.OwnerPredicate;
 import com.google.gerrit.server.query.change.ProjectPredicate;
 import com.google.gerrit.server.util.MagicBranch;
 import com.google.inject.Provider;
@@ -118,7 +118,7 @@
                   Predicate.and(
                       new ProjectPredicate(projectName.get()),
                       ChangeStatusPredicate.open(),
-                      new OwnerPredicate(user)))) {
+                      ChangePredicates.owner(user)))) {
         PatchSet ps = cd.currentPatchSet();
         if (ps != null) {
           // Ensure we actually observed a patch set ref pointing to this
diff --git a/java/com/google/gerrit/server/index/change/ChangeIndex.java b/java/com/google/gerrit/server/index/change/ChangeIndex.java
index 49d0d4e..b8a5cd9 100644
--- a/java/com/google/gerrit/server/index/change/ChangeIndex.java
+++ b/java/com/google/gerrit/server/index/change/ChangeIndex.java
@@ -19,7 +19,7 @@
 import com.google.gerrit.index.IndexDefinition;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.query.change.LegacyChangeIdPredicate;
+import com.google.gerrit.server.query.change.ChangePredicates;
 import com.google.gerrit.server.query.change.LegacyChangeIdStrPredicate;
 
 /**
@@ -32,7 +32,7 @@
   @Override
   default Predicate<ChangeData> keyPredicate(Change.Id id) {
     return getSchema().useLegacyNumericFields()
-        ? new LegacyChangeIdPredicate(id)
+        ? ChangePredicates.id(id)
         : new LegacyChangeIdStrPredicate(id);
   }
 }
diff --git a/java/com/google/gerrit/server/query/change/AssigneePredicate.java b/java/com/google/gerrit/server/query/change/AssigneePredicate.java
deleted file mode 100644
index 2bd0e51..0000000
--- a/java/com/google/gerrit/server/query/change/AssigneePredicate.java
+++ /dev/null
@@ -1,36 +0,0 @@
-// Copyright (C) 2016 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.gerrit.entities.Account;
-import com.google.gerrit.server.index.change.ChangeField;
-
-public class AssigneePredicate extends ChangeIndexPredicate {
-  protected final Account.Id id;
-
-  public AssigneePredicate(Account.Id id) {
-    super(ChangeField.ASSIGNEE, id.toString());
-    this.id = id;
-  }
-
-  @Override
-  public boolean match(ChangeData object) {
-    if (id.get() == ChangeField.NO_ASSIGNEE) {
-      Account.Id assignee = object.change().getAssignee();
-      return assignee == null;
-    }
-    return id.equals(object.change().getAssignee());
-  }
-}
diff --git a/java/com/google/gerrit/server/query/change/AttentionSetPredicate.java b/java/com/google/gerrit/server/query/change/AttentionSetPredicate.java
deleted file mode 100644
index 5cfc1c9..0000000
--- a/java/com/google/gerrit/server/query/change/AttentionSetPredicate.java
+++ /dev/null
@@ -1,36 +0,0 @@
-// Copyright (C) 2020 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 static com.google.gerrit.server.util.AttentionSetUtil.additionsOnly;
-
-import com.google.gerrit.entities.Account;
-import com.google.gerrit.server.index.change.ChangeField;
-
-/** Simple predicate for searching by attention set. */
-public class AttentionSetPredicate extends ChangeIndexPredicate {
-  protected final Account.Id id;
-
-  AttentionSetPredicate(Account.Id id) {
-    super(ChangeField.ATTENTION_SET_USERS, id.toString());
-    this.id = id;
-  }
-
-  @Override
-  public boolean match(ChangeData changeData) {
-    return additionsOnly(changeData.attentionSet()).stream()
-        .anyMatch(update -> update.account().equals(id));
-  }
-}
diff --git a/java/com/google/gerrit/server/query/change/ChangeIndexPredicate.java b/java/com/google/gerrit/server/query/change/ChangeIndexPredicate.java
index 6499c98..28bfb0b 100644
--- a/java/com/google/gerrit/server/query/change/ChangeIndexPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ChangeIndexPredicate.java
@@ -14,13 +14,16 @@
 
 package com.google.gerrit.server.query.change;
 
+import com.google.common.primitives.Ints;
 import com.google.gerrit.index.FieldDef;
+import com.google.gerrit.index.FieldType;
 import com.google.gerrit.index.query.IndexPredicate;
 import com.google.gerrit.index.query.Matchable;
 import com.google.gerrit.index.query.Predicate;
+import java.util.Objects;
 
 /** Predicate that is mapped to a field in the change index. */
-public abstract class ChangeIndexPredicate extends IndexPredicate<ChangeData>
+public class ChangeIndexPredicate extends IndexPredicate<ChangeData>
     implements Matchable<ChangeData> {
   /**
    * Returns an index predicate that matches no changes in the index.
@@ -43,7 +46,30 @@
   }
 
   @Override
+  public boolean match(ChangeData cd) {
+    if (getField().isRepeatable()) {
+      Iterable<Object> values = (Iterable<Object>) getField().get(cd);
+      for (Object v : values) {
+        if (matchesSingleObject(v)) {
+          return true;
+        }
+      }
+      return false;
+    } else {
+      return matchesSingleObject(getField().get(cd));
+    }
+  }
+
+  @Override
   public int getCost() {
     return 1;
   }
+
+  private boolean matchesSingleObject(Object fieldValueFromObject) {
+    String fieldTypeName = getField().getType().getName();
+    if (fieldTypeName.equals(FieldType.INTEGER.getName())) {
+      return Objects.equals(fieldValueFromObject, Ints.tryParse(value));
+    }
+    throw new UnsupportedOperationException("match function must be provided in subclass");
+  }
 }
diff --git a/java/com/google/gerrit/server/query/change/ChangePredicates.java b/java/com/google/gerrit/server/query/change/ChangePredicates.java
new file mode 100644
index 0000000..568916d
--- /dev/null
+++ b/java/com/google/gerrit/server/query/change/ChangePredicates.java
@@ -0,0 +1,122 @@
+// 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.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.index.query.Predicate;
+import com.google.gerrit.server.index.change.ChangeField;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/** Predicates that match against {@link ChangeData}. */
+public class ChangePredicates {
+  private ChangePredicates() {}
+
+  /**
+   * Returns a predicate that matches changes where the provided {@link Account.Id} is in the
+   * attention set.
+   */
+  public static Predicate<ChangeData> attentionSet(Account.Id id) {
+    return new ChangeIndexPredicate(ChangeField.ATTENTION_SET_USERS, id.toString());
+  }
+
+  /**
+   * Returns a predicate that matches changes that are assigned to the provided {@link Account.Id}.
+   */
+  public static Predicate<ChangeData> assignee(Account.Id id) {
+    return new ChangeIndexPredicate(ChangeField.ASSIGNEE, id.toString());
+  }
+
+  /**
+   * Returns a predicate that matches changes that are a revert of the provided {@link Change.Id}.
+   */
+  public static Predicate<ChangeData> revertOf(Change.Id revertOf) {
+    return new ChangeIndexPredicate(ChangeField.REVERT_OF, revertOf.toString());
+  }
+
+  /**
+   * Returns a predicate that matches changes that have a comment authored by the provided {@link
+   * Account.Id}.
+   */
+  public static Predicate<ChangeData> commentBy(Account.Id id) {
+    return new ChangeIndexPredicate(ChangeField.COMMENTBY, id.toString());
+  }
+
+  /**
+   * Returns a predicate that matches changes where the provided {@link Account.Id} has a pending
+   * change edit.
+   */
+  public static Predicate<ChangeData> editBy(Account.Id id) {
+    return new ChangeIndexPredicate(ChangeField.EDITBY, id.toString());
+  }
+
+  /**
+   * Returns a predicate that matches changes where the provided {@link Account.Id} has a pending
+   * draft comment.
+   */
+  public static Predicate<ChangeData> draftBy(Account.Id id) {
+    return new ChangeIndexPredicate(ChangeField.DRAFTBY, id.toString());
+  }
+
+  /**
+   * Returns a predicate that matches changes that were reviewed by any of the provided {@link
+   * Account.Id}.
+   */
+  public static Predicate<ChangeData> reviewedBy(Collection<Account.Id> ids) {
+    List<Predicate<ChangeData>> predicates = new ArrayList<>(ids.size());
+    for (Account.Id id : ids) {
+      predicates.add(new ChangeIndexPredicate(ChangeField.REVIEWEDBY, id.toString()));
+    }
+    return Predicate.or(predicates);
+  }
+
+  /** Returns a predicate that matches changes that were not yet reviewed. */
+  public static Predicate<ChangeData> unreviewed() {
+    return Predicate.not(
+        new ChangeIndexPredicate(ChangeField.REVIEWEDBY, ChangeField.NOT_REVIEWED.toString()));
+  }
+
+  /** Returns a predicate that matches the change with the provided {@link Change.Id}. */
+  public static Predicate<ChangeData> id(Change.Id id) {
+    return new ChangeIndexPredicate(
+        ChangeField.LEGACY_ID, ChangeQueryBuilder.FIELD_CHANGE, id.toString());
+  }
+
+  /** Returns a predicate that matches changes owned by the provided {@link Account.Id}. */
+  public static Predicate<ChangeData> owner(Account.Id id) {
+    return new ChangeIndexPredicate(ChangeField.OWNER, id.toString());
+  }
+
+  /**
+   * Returns a predicate that matches changes that are a cherry pick of the provided {@link
+   * Change.Id}.
+   */
+  public static Predicate<ChangeData> cherryPickOf(Change.Id id) {
+    return new ChangeIndexPredicate(ChangeField.CHERRY_PICK_OF_CHANGE, id.toString());
+  }
+
+  /**
+   * Returns a predicate that matches changes that are a cherry pick of the provided {@link
+   * PatchSet.Id}.
+   */
+  public static Predicate<ChangeData> cherryPickOf(PatchSet.Id psId) {
+    return Predicate.and(
+        cherryPickOf(psId.changeId()),
+        new ChangeIndexPredicate(ChangeField.CHERRY_PICK_OF_PATCHSET, String.valueOf(psId.get())));
+  }
+}
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 6e2f49c..7ebaec7 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -533,7 +533,7 @@
       Integer id = Ints.tryParse(query);
       if (id != null) {
         return args.getSchema().useLegacyNumericFields()
-            ? new LegacyChangeIdPredicate(Change.id(id))
+            ? ChangePredicates.id(Change.id(id))
             : new LegacyChangeIdStrPredicate(Change.id(id));
       }
     } else if (PAT_CHANGE_ID.matcher(query).matches()) {
@@ -551,7 +551,7 @@
   @Operator
   public Predicate<ChangeData> status(String statusName) {
     if ("reviewed".equalsIgnoreCase(statusName)) {
-      return IsReviewedPredicate.create();
+      return ChangePredicates.unreviewed();
     }
     return ChangeStatusPredicate.parse(statusName);
   }
@@ -576,7 +576,7 @@
     }
 
     if ("edit".equalsIgnoreCase(value)) {
-      return new EditByPredicate(self());
+      return ChangePredicates.editBy(self());
     }
 
     if ("unresolved".equalsIgnoreCase(value)) {
@@ -610,11 +610,11 @@
     }
 
     if ("reviewed".equalsIgnoreCase(value)) {
-      return IsReviewedPredicate.create();
+      return ChangePredicates.unreviewed();
     }
 
     if ("owner".equalsIgnoreCase(value)) {
-      return new OwnerPredicate(self());
+      return ChangePredicates.owner(self());
     }
 
     if ("reviewer".equalsIgnoreCase(value)) {
@@ -653,11 +653,11 @@
     }
 
     if ("assigned".equalsIgnoreCase(value)) {
-      return Predicate.not(new AssigneePredicate(Account.id(ChangeField.NO_ASSIGNEE)));
+      return Predicate.not(ChangePredicates.assignee(Account.id(ChangeField.NO_ASSIGNEE)));
     }
 
     if ("unassigned".equalsIgnoreCase(value)) {
-      return new AssigneePredicate(Account.id(ChangeField.NO_ASSIGNEE));
+      return ChangePredicates.assignee(Account.id(ChangeField.NO_ASSIGNEE));
     }
 
     if ("submittable".equalsIgnoreCase(value)) {
@@ -1060,7 +1060,7 @@
   }
 
   private Predicate<ChangeData> draftby(Account.Id who) {
-    return new HasDraftByPredicate(who);
+    return ChangePredicates.draftBy(who);
   }
 
   @Operator
@@ -1119,9 +1119,9 @@
   }
 
   private Predicate<ChangeData> owner(Set<Account.Id> who) {
-    List<OwnerPredicate> p = Lists.newArrayListWithCapacity(who.size());
+    List<Predicate<ChangeData>> p = Lists.newArrayListWithCapacity(who.size());
     for (Account.Id id : who) {
-      p.add(new OwnerPredicate(id));
+      p.add(ChangePredicates.owner(id));
     }
     return Predicate.or(p);
   }
@@ -1146,7 +1146,7 @@
   }
 
   private Predicate<ChangeData> attention(Set<Account.Id> who) {
-    return Predicate.or(who.stream().map(AttentionSetPredicate::new).collect(toImmutableSet()));
+    return Predicate.or(who.stream().map(ChangePredicates::attentionSet).collect(toImmutableSet()));
   }
 
   @Operator
@@ -1156,9 +1156,9 @@
   }
 
   private Predicate<ChangeData> assignee(Set<Account.Id> who) {
-    List<AssigneePredicate> p = Lists.newArrayListWithCapacity(who.size());
+    List<Predicate<ChangeData>> p = Lists.newArrayListWithCapacity(who.size());
     for (Account.Id id : who) {
-      p.add(new AssigneePredicate(id));
+      p.add(ChangePredicates.assignee(id));
     }
     return Predicate.or(p);
   }
@@ -1177,9 +1177,9 @@
     }
 
     Set<Account.Id> accounts = getMembers(groupId);
-    List<OwnerPredicate> p = Lists.newArrayListWithCapacity(accounts.size());
+    List<Predicate<ChangeData>> p = Lists.newArrayListWithCapacity(accounts.size());
     for (Account.Id id : accounts) {
-      p.add(new OwnerPredicate(id));
+      p.add(ChangePredicates.owner(id));
     }
     return Predicate.or(p);
   }
@@ -1275,9 +1275,9 @@
   }
 
   private Predicate<ChangeData> commentby(Set<Account.Id> who) {
-    List<CommentByPredicate> p = Lists.newArrayListWithCapacity(who.size());
+    List<Predicate<ChangeData>> p = Lists.newArrayListWithCapacity(who.size());
     for (Account.Id id : who) {
-      p.add(new CommentByPredicate(id));
+      p.add(ChangePredicates.commentBy(id));
     }
     return Predicate.or(p);
   }
@@ -1337,7 +1337,7 @@
   @Operator
   public Predicate<ChangeData> reviewedby(String who)
       throws QueryParseException, IOException, ConfigInvalidException {
-    return IsReviewedPredicate.create(parseAccount(who));
+    return ChangePredicates.reviewedBy(parseAccount(who));
   }
 
   @Operator
@@ -1420,8 +1420,11 @@
 
   @Operator
   public Predicate<ChangeData> revertof(String value) throws QueryParseException {
+    if (value == null || Ints.tryParse(value) == null) {
+      throw new QueryParseException("'revertof' must be an integer");
+    }
     if (args.getSchema().hasField(ChangeField.REVERT_OF)) {
-      return new RevertOfPredicate(value);
+      return ChangePredicates.revertOf(Change.id(Ints.tryParse(value)));
     }
     throw new QueryParseException("'revertof' operator is not supported by change index version");
   }
@@ -1440,13 +1443,11 @@
     if (args.getSchema().hasField(ChangeField.CHERRY_PICK_OF_CHANGE)
         && args.getSchema().hasField(ChangeField.CHERRY_PICK_OF_PATCHSET)) {
       if (Ints.tryParse(value) != null) {
-        return new CherryPickOfChangePredicate(value);
+        return ChangePredicates.cherryPickOf(Change.id(Ints.tryParse(value)));
       }
       try {
         PatchSet.Id patchSetId = PatchSet.Id.parse(value);
-        return Predicate.and(
-            new CherryPickOfChangePredicate(patchSetId.changeId().toString()),
-            new CherryPickOfPatchSetPredicate(patchSetId.getId()));
+        return ChangePredicates.cherryPickOf(patchSetId);
       } catch (IllegalArgumentException e) {
         throw new QueryParseException(
             "'"
diff --git a/java/com/google/gerrit/server/query/change/CherryPickOfChangePredicate.java b/java/com/google/gerrit/server/query/change/CherryPickOfChangePredicate.java
deleted file mode 100644
index 07492aa..0000000
--- a/java/com/google/gerrit/server/query/change/CherryPickOfChangePredicate.java
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright (C) 2019 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.gerrit.server.index.change.ChangeField;
-
-public class CherryPickOfChangePredicate extends ChangeIndexPredicate {
-  public CherryPickOfChangePredicate(String cherryPickOfChange) {
-    super(ChangeField.CHERRY_PICK_OF_CHANGE, cherryPickOfChange);
-  }
-
-  @Override
-  public boolean match(ChangeData cd) {
-    if (cd.change().getCherryPickOf() == null) {
-      return false;
-    }
-    return Integer.toString(cd.change().getCherryPickOf().changeId().get()).equals(value);
-  }
-}
diff --git a/java/com/google/gerrit/server/query/change/CherryPickOfPatchSetPredicate.java b/java/com/google/gerrit/server/query/change/CherryPickOfPatchSetPredicate.java
deleted file mode 100644
index f62eab9..0000000
--- a/java/com/google/gerrit/server/query/change/CherryPickOfPatchSetPredicate.java
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright (C) 2019 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.gerrit.server.index.change.ChangeField;
-
-public class CherryPickOfPatchSetPredicate extends ChangeIndexPredicate {
-  public CherryPickOfPatchSetPredicate(String cherryPickOfPatchSet) {
-    super(ChangeField.CHERRY_PICK_OF_PATCHSET, cherryPickOfPatchSet);
-  }
-
-  @Override
-  public boolean match(ChangeData cd) {
-    if (cd.change().getCherryPickOf() == null) {
-      return false;
-    }
-    return cd.change().getCherryPickOf().getId().equals(value);
-  }
-}
diff --git a/java/com/google/gerrit/server/query/change/CommentByPredicate.java b/java/com/google/gerrit/server/query/change/CommentByPredicate.java
deleted file mode 100644
index ce578c6..0000000
--- a/java/com/google/gerrit/server/query/change/CommentByPredicate.java
+++ /dev/null
@@ -1,49 +0,0 @@
-// Copyright (C) 2015 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.gerrit.entities.Account;
-import com.google.gerrit.entities.ChangeMessage;
-import com.google.gerrit.entities.HumanComment;
-import com.google.gerrit.server.index.change.ChangeField;
-import java.util.Objects;
-
-public class CommentByPredicate extends ChangeIndexPredicate {
-  protected final Account.Id id;
-
-  public CommentByPredicate(Account.Id id) {
-    super(ChangeField.COMMENTBY, id.toString());
-    this.id = id;
-  }
-
-  Account.Id getAccountId() {
-    return id;
-  }
-
-  @Override
-  public boolean match(ChangeData cd) {
-    for (ChangeMessage m : cd.messages()) {
-      if (Objects.equals(m.getAuthor(), id)) {
-        return true;
-      }
-    }
-    for (HumanComment c : cd.publishedComments()) {
-      if (Objects.equals(c.author.getId(), id)) {
-        return true;
-      }
-    }
-    return false;
-  }
-}
diff --git a/java/com/google/gerrit/server/query/change/CommentPredicate.java b/java/com/google/gerrit/server/query/change/CommentPredicate.java
index 92ab7643..0abe45d 100644
--- a/java/com/google/gerrit/server/query/change/CommentPredicate.java
+++ b/java/com/google/gerrit/server/query/change/CommentPredicate.java
@@ -37,7 +37,7 @@
       Predicate<ChangeData> p =
           Predicate.and(
               index.getSchema().useLegacyNumericFields()
-                  ? new LegacyChangeIdPredicate(id)
+                  ? ChangePredicates.id(id)
                   : new LegacyChangeIdStrPredicate(id),
               this);
       for (ChangeData cData : index.getSource(p, IndexedChangeQuery.oneResult()).read()) {
diff --git a/java/com/google/gerrit/server/query/change/ConflictsPredicate.java b/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
index 16f85b1..80e3cb9 100644
--- a/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
@@ -90,7 +90,7 @@
     and.add(
         Predicate.not(
             args.getSchema().useLegacyNumericFields()
-                ? new LegacyChangeIdPredicate(c.getId())
+                ? ChangePredicates.id(c.getId())
                 : new LegacyChangeIdStrPredicate(c.getId())));
     and.add(Predicate.or(filePredicates));
 
diff --git a/java/com/google/gerrit/server/query/change/EditByPredicate.java b/java/com/google/gerrit/server/query/change/EditByPredicate.java
deleted file mode 100644
index 439d3a5..0000000
--- a/java/com/google/gerrit/server/query/change/EditByPredicate.java
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright (C) 2015 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.gerrit.entities.Account;
-import com.google.gerrit.server.index.change.ChangeField;
-
-public class EditByPredicate extends ChangeIndexPredicate {
-  protected final Account.Id id;
-
-  public EditByPredicate(Account.Id id) {
-    super(ChangeField.EDITBY, id.toString());
-    this.id = id;
-  }
-
-  @Override
-  public boolean match(ChangeData cd) {
-    return cd.editsByUser().contains(id);
-  }
-}
diff --git a/java/com/google/gerrit/server/query/change/FuzzyTopicPredicate.java b/java/com/google/gerrit/server/query/change/FuzzyTopicPredicate.java
index a3de17d..47652b8 100644
--- a/java/com/google/gerrit/server/query/change/FuzzyTopicPredicate.java
+++ b/java/com/google/gerrit/server/query/change/FuzzyTopicPredicate.java
@@ -45,7 +45,7 @@
     try {
       Predicate<ChangeData> thisId =
           index.getSchema().useLegacyNumericFields()
-              ? new LegacyChangeIdPredicate(cd.getId())
+              ? ChangePredicates.id(cd.getId())
               : new LegacyChangeIdStrPredicate(cd.getId());
       Iterable<ChangeData> results =
           index.getSource(and(thisId, this), IndexedChangeQuery.oneResult()).read();
diff --git a/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java b/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java
deleted file mode 100644
index cd05399..0000000
--- a/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright (C) 2010 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.gerrit.entities.Account;
-import com.google.gerrit.server.index.change.ChangeField;
-
-public class HasDraftByPredicate extends ChangeIndexPredicate {
-  protected final Account.Id accountId;
-
-  public HasDraftByPredicate(Account.Id accountId) {
-    super(ChangeField.DRAFTBY, accountId.toString());
-    this.accountId = accountId;
-  }
-
-  @Override
-  public boolean match(ChangeData cd) {
-    return cd.draftsByUser().contains(accountId);
-  }
-}
diff --git a/java/com/google/gerrit/server/query/change/InternalChangeQuery.java b/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
index e0f7d91..ed0f237 100644
--- a/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
+++ b/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
@@ -99,7 +99,7 @@
     predicateFactory =
         (id) ->
             schema().useLegacyNumericFields()
-                ? new LegacyChangeIdPredicate(id)
+                ? ChangePredicates.id(id)
                 : new LegacyChangeIdStrPredicate(id);
   }
 
diff --git a/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java b/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java
deleted file mode 100644
index 1f55cd2..0000000
--- a/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java
+++ /dev/null
@@ -1,54 +0,0 @@
-// Copyright (C) 2015 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 static com.google.gerrit.server.index.change.ChangeField.REVIEWEDBY;
-
-import com.google.gerrit.entities.Account;
-import com.google.gerrit.index.query.Predicate;
-import com.google.gerrit.server.index.change.ChangeField;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-import java.util.Set;
-
-public class IsReviewedPredicate extends ChangeIndexPredicate {
-  protected static final Account.Id NOT_REVIEWED = Account.id(ChangeField.NOT_REVIEWED);
-
-  public static Predicate<ChangeData> create() {
-    return Predicate.not(new IsReviewedPredicate(NOT_REVIEWED));
-  }
-
-  public static Predicate<ChangeData> create(Collection<Account.Id> ids) {
-    List<Predicate<ChangeData>> predicates = new ArrayList<>(ids.size());
-    for (Account.Id id : ids) {
-      predicates.add(new IsReviewedPredicate(id));
-    }
-    return Predicate.or(predicates);
-  }
-
-  protected final Account.Id id;
-
-  private IsReviewedPredicate(Account.Id id) {
-    super(REVIEWEDBY, Integer.toString(id.get()));
-    this.id = id;
-  }
-
-  @Override
-  public boolean match(ChangeData cd) {
-    Set<Account.Id> reviewedBy = cd.reviewedBy();
-    return !reviewedBy.isEmpty() ? reviewedBy.contains(id) : id.equals(NOT_REVIEWED);
-  }
-}
diff --git a/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java b/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java
deleted file mode 100644
index dfcf308..0000000
--- a/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright (C) 2010 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 static com.google.gerrit.server.index.change.ChangeField.LEGACY_ID;
-
-import com.google.gerrit.entities.Change;
-
-/** Predicate over change number (aka legacy ID or Change.Id). */
-public class LegacyChangeIdPredicate extends ChangeIndexPredicate {
-  protected final Change.Id id;
-
-  public LegacyChangeIdPredicate(Change.Id id) {
-    super(LEGACY_ID, ChangeQueryBuilder.FIELD_CHANGE, id.toString());
-    this.id = id;
-  }
-
-  @Override
-  public boolean match(ChangeData object) {
-    return id.equals(object.getId());
-  }
-}
diff --git a/java/com/google/gerrit/server/query/change/MessagePredicate.java b/java/com/google/gerrit/server/query/change/MessagePredicate.java
index 8e9aa00..caf751e 100644
--- a/java/com/google/gerrit/server/query/change/MessagePredicate.java
+++ b/java/com/google/gerrit/server/query/change/MessagePredicate.java
@@ -36,7 +36,7 @@
       Predicate<ChangeData> p =
           Predicate.and(
               index.getSchema().useLegacyNumericFields()
-                  ? new LegacyChangeIdPredicate(object.getId())
+                  ? ChangePredicates.id(object.getId())
                   : new LegacyChangeIdStrPredicate(object.getId()),
               this);
       for (ChangeData cData : index.getSource(p, IndexedChangeQuery.oneResult()).read()) {
diff --git a/java/com/google/gerrit/server/query/change/OwnerPredicate.java b/java/com/google/gerrit/server/query/change/OwnerPredicate.java
deleted file mode 100644
index 410d431..0000000
--- a/java/com/google/gerrit/server/query/change/OwnerPredicate.java
+++ /dev/null
@@ -1,38 +0,0 @@
-// Copyright (C) 2010 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.gerrit.entities.Account;
-import com.google.gerrit.entities.Change;
-import com.google.gerrit.server.index.change.ChangeField;
-
-public class OwnerPredicate extends ChangeIndexPredicate {
-  protected final Account.Id id;
-
-  public OwnerPredicate(Account.Id id) {
-    super(ChangeField.OWNER, id.toString());
-    this.id = id;
-  }
-
-  protected Account.Id getAccountId() {
-    return id;
-  }
-
-  @Override
-  public boolean match(ChangeData object) {
-    Change change = object.change();
-    return change != null && id.equals(change.getOwner());
-  }
-}
diff --git a/java/com/google/gerrit/server/query/change/RevertOfPredicate.java b/java/com/google/gerrit/server/query/change/RevertOfPredicate.java
deleted file mode 100644
index 927f9ce..0000000
--- a/java/com/google/gerrit/server/query/change/RevertOfPredicate.java
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright (C) 2017 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.gerrit.server.index.change.ChangeField;
-
-public class RevertOfPredicate extends ChangeIndexPredicate {
-  public RevertOfPredicate(String revertOf) {
-    super(ChangeField.REVERT_OF, revertOf);
-  }
-
-  @Override
-  public boolean match(ChangeData cd) {
-    if (cd.change().getRevertOf() == null) {
-      return false;
-    }
-    return cd.change().getRevertOf().toString().equals(value);
-  }
-}
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteDraftComments.java b/java/com/google/gerrit/server/restapi/account/DeleteDraftComments.java
index ec82e1a..d225798 100644
--- a/java/com/google/gerrit/server/restapi/account/DeleteDraftComments.java
+++ b/java/com/google/gerrit/server/restapi/account/DeleteDraftComments.java
@@ -41,8 +41,8 @@
 import com.google.gerrit.server.change.ChangeJson;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangePredicates;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
-import com.google.gerrit.server.query.change.HasDraftByPredicate;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gerrit.server.restapi.change.CommentJson;
 import com.google.gerrit.server.restapi.change.CommentJson.HumanCommentFormatter;
@@ -147,7 +147,7 @@
 
   private Predicate<ChangeData> predicate(Account.Id accountId, DeleteDraftCommentsInput input)
       throws BadRequestException {
-    Predicate<ChangeData> hasDraft = new HasDraftByPredicate(accountId);
+    Predicate<ChangeData> hasDraft = ChangePredicates.draftBy(accountId);
     if (CharMatcher.whitespace().trimFrom(Strings.nullToEmpty(input.query)).isEmpty()) {
       return hasDraft;
     }